迁移到SpringBoot 03 - Servlet和Filter

原先云认证中有几个特殊加载的组件:

  1. checkingServlet:一个Servlet,用于检查系统的状态,主要用于负载均衡LB检测服务是否正常使用。该Servlet定义了了一组URL,可以用来检查系统的状态,例如:/checking/database(检查数据库是否可以访问),/checking/version(返回系统版本号),/checking(总体检查系统状态)等。这些功能理论上不适用Servlet也能够实现,但是由于历史原因采用了Servlet,这次一直也不考虑重新编写。
  2. ClientCertificateFromProxyFilter:一个Filter,用于检查负载均衡转发过来的客户端证书,配合Spring Security完成双向SSL的认证工作。

1. Spring中的处理

之前为了能够使用Spring的注入功能,将参数自动注入Servlet中,还特地写了一个辅助类,模仿Spring中的DelegatingFilterProxy实现的。有了这个辅助类,可以将Servlet定义成普通的Spring Bean,从而使用Spring带来的便利。这个类的代码大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.eveus.website.servlet.support;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.*;
import java.io.IOException;

/**
* Servlet代理。
* 将一个Spring管理的Bean作为Servlet的代理实现。该Bean需要extends HttpServlet,并实现相应的方法。
*
* @author limin.zhang
* @version $Id: $Id
*/
public class DelegatingServletProxy extends GenericServlet {

private static final long serialVersionUID = 6485296276572157986L;
private String targetBean;
private Servlet proxy;

/** {@inheritDoc} */
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
proxy.service(req, res);
}

/** {@inheritDoc} */
@Override
public void init() throws ServletException {
this.targetBean = getServletName();
getServletBean();
proxy.init(getServletConfig());
}

private void getServletBean() {
WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
this.proxy = (Servlet) wac.getBean(targetBean);
}
}

DelegatingServletProxy的作用作为一个代理类,会自动通过WebApplicationContextUtils工具类获取同名的Spring对象。然后将请求代理给这个对象。
为了能够正常工作,需要初始化两个地方:

1.1 web.xml

1
2
3
4
5
6
7
8
9
<servlet>
<servlet-name>checkingServlet</servlet-name>
<servlet-class>com.eveus.website.servlet.support.DelegatingServletProxy</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>checkingServlet</servlet-name>
<url-pattern>/checking/*</url-pattern>
</servlet-mapping>

通过web.xml加载DelegatingServletProxy辅助类,然后做好mapping。

1.2 spring bean配置

1
2
3
4
<!-- Servlet Bean definition. Will be delegated by DelegatingServletProxy -->
<!-- CheckingServlet, used to show server version & check system status -->
<bean id="checkingServlet" class="com.eveus.website.servlet.CheckingServlet">
</bean>

然后把Bean定义成普通的Spring Bean就可以使用了。

2. SpringBoot改造

在Spring boot项目中,通常是不存在web.xml的。因此希望能够不使用web.xml完成配置。通过从网上搜索资料,找到了以下几个方案。

2.1 WebApplicationInitializer / SpringBootServletInitializer

最先找到的方法是WebApplicationInitializer,这是Servlet 3.0规范引入的扩展点。这是Spring 4之后新增的一个接口,其官方文档介绍如下:

Interface to be implemented in Servlet 3.0+ environments in order to configure the ServletContext programmatically – as opposed to (or possibly in conjunction with) the traditional web.xml-based approach.
Implementations of this SPI will be detected automatically by SpringServletContainerInitializer, which itself is bootstrapped automatically by any Servlet 3.0 container.

简单来说,就是实现该接口可以在Servlet 3.0+环境中以编程方式配置ServletContext,而不是(或结合)传统的基于web.xml文件配置。

通常的说法是通过实现WebApplicationInitializer接口的onStart方法,可以添加Filter、Servlet等的定义。类似这样:

1
2
3
4
5
6
// 注册Filter
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.addFilter("clientCertificateFromProxyFilter", new DelegatingFilterProxy("clientCertificateFromProxyFilter"))
.addMappingForUrlPatterns(null, true, "/api/*");
}

但是经过测试发现这种方法在springboot中无效。SpringBootServletInitializer按照官方文档来说,是用于替代WebApplicationInitializer的,应该起到同样的作用。

后来经过查看官方文档,确认这个回调点在嵌入容器的模式下是无效的,被SpringBoot有意关闭了:

Embedded servlet containers do not directly execute the Servlet 3.0+ javax.servlet.ServletContainerInitializer interface or Spring’s org.springframework.web.WebApplicationInitializer interface. This is an intentional design decision intended to reduce the risk that third party libraries designed to run inside a war may break Spring Boot applications.

If you need to perform servlet context initialization in a Spring Boot application, you should register a bean that implements the org.springframework.boot.web.servlet.ServletContextInitializer interface. The single onStartup method provides access to the ServletContext and, if necessary, can easily be used as an adapter to an existing WebApplicationInitializer.

关闭的原因是防止第三方库实现这个接口后自动执行破坏打包后的SpringBoot应用的启动。当然这个接口对war包模式还是有效的。但是鉴于我们现在用的是嵌入容器的模式,打包成jar包,所以还必须找其他的方案解决。

2.2 直接声明Bean

实际上Spring boot针对Servlet、Filter类型的Bean进行了特殊处理。会自动进行注册:

To add a Servlet, Filter, or Servlet *Listener by using a Spring bean, you must provide a @Bean definition for it. Doing so can be very useful when you want to inject configuration or dependencies. However, you must be very careful that they do not cause eager initialization of too many other beans, because they have to be installed in the container very early in the application lifecycle. (For example, it is not a good idea to have them depend on your DataSource or JPA configuration.) You can work around such restrictions by initializing the beans lazily when first used instead of on initialization.

In the case of Filters and Servlets, you can also add mappings and init parameters by adding a FilterRegistrationBean or a ServletRegistrationBean instead of or in addition to the underlying component.

简单来说就是通过把Servlet, Filter, 或者Servelt *Listener顶一个成Java Bean,SpringBoot可以加载。当然也可以通过定义FilterRegistrationBeanServletRegistrationBean来对Filter、Servlet进行一定的初始化。

所以最后的解决方案如下(使用ServletRegistrationBean、FilterRegistrationBean):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Configuration
public class BeanConfig{

@Bean(name="clientCertificateFromProxyFilter")
public ClientCertificateFromProxyFilter clientCertificateFromProxyFilter() {
return new ClientCertificateFromProxyFilter();
}

@Bean(name="checkingServlet")
public CheckingServlet checkingServlet() {
return new CheckingServlet();
}

///////////////////// Servlet ////////////////////////
@Bean
public ServletRegistrationBean cxfServlet() {
final ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new CXFServlet(), "/api/*");
servletRegistrationBean.setLoadOnStartup(1);
return servletRegistrationBean;
}

@Bean(name="checking")
public ServletRegistrationBean checking() {
ServletRegistrationBean registration = new ServletRegistrationBean(checkingServlet());
registration.addUrlMappings("/checking/*");
return registration;
}

////////////////////// Filter ///////////////////////////
@Bean(name="certProxy")
public FilterRegistrationBean certProxy() {
FilterRegistrationBean registration = new FilterRegistrationBean(clientCertificateFromProxyFilter());
registration.addUrlPatterns("/api/*");
return registration;
}
}

附录、参考资料

热评文章