【Spring注解驱动】(三)Servlet 3.0
前言
今天是7.21日,终于是看完了。。暑假在家学习是真的差点意思
1 Servlet 3.0简介
Servlet 2.0
是在web.xml中配置servlet filter
、listener
、DispatcherServlet
等等,而在Servlet 3.0
中,Spring则为我们提供了一系列注解实现了上面的配置。
Servlet 3.0需要tomcat 7.0及以上版本
2 Servlet 3.0 注解开发
首先创建一个servlet3.0的项目,idea中动态网页的创建是JavaEnterprise选项,project template选择web application选项,会自动创建servlet、index.jsp以及web.xml(这里不需要再使用了)
2.1 @WebServlet
在servlet上添加注解,既可实现脱离web.xml配置Servlet
@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.getWriter().write("Hello!");
}
public void destroy() {
}
}
2.2 Shared libraries / runtimes pluggability 运行时插件能力
Servlet容器启动会扫描当前应用的每一个Jar包中,是否包含ServletContainerInitializer的实现类。而一般情况下会将实现类的指向
(即其全类名)放置在META-INF/services文件夹下的javax.servlet.ServletContainerInitializer文件中,文件内容指向实现类。
总结:容器在启动应用的时候,会扫描当前应用的每一个Jar包里面(Idea应该放在Resource下面)META-INF/services/javax.servlet.ServletContainerInitialize指定的实现类
,启动并运行这个实现类方法
。
①创建一个ServletContainerInitializer的自定义实现类MyServletContainerInitializer:
public class MyServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
}
}
②在Resource/META-INF/services/下创建文件javax.servlet.ServletContainerInitialize并添加实现类全类名
com.hikaru.servlet3.MyServletContainerInitializer
如此一来,当servlet容器启动的时候,就会扫描到实现类,执行实现类的onStartup方法。
2.3 @HandlesTypes注解指定感兴趣类型
容器启动的时候,会将@HandlesType指定的这个类型下面的子类(不包括这个类型)(实现类、接口等)传递过来。而onStartup方法的两个参数:
①set:感兴趣的所有子类型
@HandlesTypes(value = {HelloService.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
System.out.println("感兴趣的类型:");
for(Class<?> clax:set) {
System.out.println(clax);
}
}
}
输出结果:
感兴趣的类型:
class com.hikaru.service.HelloServiceImpl
class com.hikaru.service.AbstractHelloService
interface com.hikaru.service.HelloServiceExt
2.4 使用ServletContext注册Web组件(Servlet、Filter、Listener)
onStartup方法的第两个参数
②ServletContext:代表web应用的ServletContext,一个web应用包含一个。
2.4.1 创建并注册Servlet
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doGet(req, resp);
resp.getWriter().write("Jay");
}
}
注册组件:
// 注册组件
ServletRegistration.Dynamic userServlet = servletContext.addServlet("userServlet", new UserServlet());
// 配置servlet的配置信息
userServlet.addMapping("/user");
2.4.2 创建并注册Filter
public class UserFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("UserFilter doFilter");
filterChain.doFilter(servletRequest, servletResponse);
}
}
注册组件:
// 注册Filter组件
FilterRegistration.Dynamic userFilter = servletContext.addFilter("userFilter", UserFilter.class);
// 配置组件配置信息
userFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
2.4.3 创建并注册Listener
public class UserListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("UserListener contextInitialized");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("UserListener contextDestroyed");
}
}
注册组件
// 注册Listener组件
servletContext.addListener(UserListener.class);
注册组件只需提供组件的class即可,当然也可以进行new创建
综上,MyServletContainerInitializer如下:
@HandlesTypes(value = {HelloService.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
System.out.println("感兴趣的类型:");
for(Class<?> clax:set) {
System.out.println(clax);
}
// 注册Servlet组件
ServletRegistration.Dynamic userServlet = servletContext.addServlet("userServlet", new UserServlet());
// 配置servlet的配置信息
userServlet.addMapping("/user");
// 注册Listener组件
servletContext.addListener(new UserListener());
// 注册Filter组件
FilterRegistration.Dynamic userFilter = servletContext.addFilter("userFilter", UserFilter.class);
// 配置组件配置信息
userFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
}
}
3 Servlet 3.0 整合SpringMVC
①使用maven-war-plugin
插件
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
②导入spring-webmvc
依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.1</version>
</dependency>
导入spring-webmvc之后,会自动导入其依赖的依赖,如aop、ioc(core,beans,context,experssion)以及webmvc的依赖
③导入servlet-api
依赖
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
tomcat服务器中也会存在servlet-api的jar包,因此在打成war包的时候不应该包括这一部分,否则会造成冲突
scope的各种取值详解
scope取值 | 有效范围(compile, runtime, test) | 依赖传递 | 例子 |
---|---|---|---|
compile | all | 是 | spring-core |
provided | compile, test | 否 | servlet-api |
runtime | runtime, test | 是 | JDBC驱动 |
test | test | 否 | JUnit |
system | compile, test | 是 |
3.1 Spring启动SpringMVC web容器分析
①上面有讲过,web容器在启动的时候,会扫描每个jar包下的\META-INF\services\javax.servlet.ServletContainerInitializer,而我们导入的spring-web是存在这个文件的,可以看到其内容指向SpringServletContainerInitializer
org.springframework.web.SpringServletContainerInitializer
②因此接下来会加载这个文件内容指向的类,该类(Spring应用)通过@HandlesTypes({WebApplicationInitializer.class}),一启动就会加载WebApplicationInitializer
接口下的所有组件,并为非接口、抽象类的组件创建实例对象。
③而WebApplicationInitializer
有三个实现类或者子类:
第一个是实现类 AbstractContextLoaderInitializer
:通过createRootApplicationContext()方法创建根组件,将listener加入到servlet web容器,以此利用Listener创建根容器,实现容器之间的子父关系。
protected void registerContextLoaderListener(ServletContext servletContext) {
WebApplicationContext rootAppContext = this.createRootApplicationContext();
if (rootAppContext != null) {
ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
listener.setContextInitializers(this.getRootApplicationContextInitializers());
servletContext.addListener(listener);
} else {
this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not return an application context");
}
}
主要思想是通过在父容器中注册一个ContextLoaderListener,这样在父容器启动的时候会触发监听器回调,然后将根容器添加到父容器中。
第二个是实现类AbstractContextLoaderInitializer
的子类AbstractDispatcherServletInitializer
,首先通过createServletApplicationContext()方法创建了一个web的IOC容器,然后通过createDispatcherServlet(servletAppContext)创建了一个DispatcherServlet
,最后通过servletContext.addServlet(servletName, dispatcherServlet)将其添加到web容器
并设置一系列映射、启动配置。
protected void registerDispatcherServlet(ServletContext servletContext) {
String servletName = this.getServletName();
Assert.hasLength(servletName, "getServletName() must not return null or empty");
WebApplicationContext servletAppContext = this.createServletApplicationContext();
Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
FrameworkServlet dispatcherServlet = this.createDispatcherServlet(servletAppContext);
Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
dispatcherServlet.setContextInitializers(this.getServletApplicationContextInitializers());
Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
if (registration == null) {
throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. Check if there is another servlet registered under the same name.");
} else {
registration.setLoadOnStartup(1);
registration.addMapping(this.getServletMappings());
registration.setAsyncSupported(this.isAsyncSupported());
Filter[] filters = this.getServletFilters();
if (!ObjectUtils.isEmpty(filters)) {
Filter[] var7 = filters;
int var8 = filters.length;
for(int var9 = 0; var9 < var8; ++var9) {
Filter filter = var7[var9];
this.registerServletFilter(servletContext, filter);
}
}
this.customizeRegistration(registration);
}
}
第三个是实现类子类AbstractDispatcherServletInitializer
的子类AbstractAnnotationConfigDispatcherServletInitializer
,这个类实现了注解方式的Dispatcher注册,首先通过抽象方法createRootApplicationContext()
创建根容器
,然后通过AnnotationConfigWebApplicationContext创建一个web Ioc容器
,最后在容器中注册根容器,以此实现两个容器的子父类关系。
总结:以注解方式启动MVC,只需要继承AbstractAnnotationConfigDispatcherServletInitializer,实现其抽象方法指定DispatcherServlet的配置信息即可不必使用web.xml配置。
3.2 以前Spring整合SpringMVC的做法
Spring官网中使用了父子容器的形式,即父容器web容器和子容器根容器,web容器只扫描controller
、view resolvers
以及其他和web组件相关的bean
,而根容器扫描业务层
和持久层
、控制数据源
、事务
相关的组件。
如下面的项目中,使用的是listener配合xml的方式构建子父容器,Spring父容器创建
时触发Listener回调通过xml文件将service、事务等组件读取到Spring IOC容器,再建立Spring容器和SpringMVC容器的子父类关系。
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-web-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-persist-*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
3.3 基于注解的Spring SpringMVC整合
①创建AbstractAnnotationConfigDispatcherServletInitializer
类的自定义实现类MyWebAppInitializer,相当于原来的web.xml
,其中的三个实现方法:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{RootConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{AppConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
1)getRootConfigClasses:返回父容器
即根容器
的实例,相当于原来的配置Spring 容器的spring.xml
配置文件
2) getServletConfigClasses:返回子容器即web容器的实例,相当于原来配置的springMVC.xml
。
3)getServletMappings:配置DispatcherServlet
的配置信息,"/"表示拦截除jsp外的所有请求,"/*"表示拦截所有包括jsp的请求,而jsp页面是由tomcatjsp引擎解析的,所以这里应该使用"/"。
②创建父子容器的配置类
AppConfig
:只扫描注解为Config组件
@Configuration
@ComponentScan(value = "com.hikaru", includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
}, useDefaultFilters = false)
public class AppConfig {
}
includeFilters想要生效必须禁用默认的过滤规则
RootConfig
@Configuration
@ComponentScan(value = "com.hikaru", excludeFilters = {
@ComponentScan.Filter(type=FilterType.ANNOTATION, classes = {Controller.class})
})
public class RootConfig {
}
③创建相应组件
UserController
@Controller
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/user")
@ResponseBody
public String user() {
return userService.getUser();
}
}
UserServiceImpl
@Service
public class UserServiceImpl implements UserService{
@Override
public String getUser() {
return "Jay";
}
}
如此一来,Controller的组件和Service的组件都在Spring父容器中了,便完成了Spring SpringMVC的整合
3.4 通过WebMvcConfigurer接口定制与接管SpringMVC
@Configuration
@ComponentScan(value = "com.hikaru", includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
}, useDefaultFilters = false)
public class AppConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
}
@Override
public void addFormatters(FormatterRegistry registry) {
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
}
@Override
public void addCorsMappings(CorsRegistry registry) {
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
@Override
public Validator getValidator() {
return null;
}
@Override
public MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
如configureDefaultServletHandling()实现方法就是用来开启静态资源,开启方法直接调用参数的enable方法即可。相当于原xml实现中的<mvc:default-servlet-handler>
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
java 8开始支持接口默认实现了,即实现一个接口不需要实现它的所有方法,所以这里不需要再使用WebMvcConfigurerAdapter直接使用WebMvcConfigurer接口即可。
3.4.1 configureViewResolvers()配置视图解析器
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// registry.jsp();
registry.jsp("/WEB-INF/views", ".jsp");
}
如果采用默认的写法,会是下面的配置:
public UrlBasedViewResolverRegistration jsp() {
return this.jsp("/WEB-INF/", ".jsp");
}
然后配置servlet进行测试,但不知道为什么我这里一直报500视图解析器没有启动。。找了半天也没有找到哪里写的不对,以后有机会再回来看吧。。不过前后端分离这种写法基本也用不到了。
后面的静态资源、拦截器也就先不看了
4 servlet 3.0 异步请求
4.1 原生Servlet实现异步处理
在Servlet 3.0之前,servlet采用Thread-Per-Request方式处理请求,即每一个Http请求都由某一个线程从头到尾负责处理。
如果一个请求需要进行IO操作,比如访问数据库,调用第三方服务接口等,那么其所对应的线程会同步地等待IO操作完成,而IO操作是非常慢的,所以此时的线程并不能及时放回线程池以供后续使用,在并发量越来越大的情况下,这将带来严重的性能问题,即便是像Spring、Struts这样高层的框架也脱离不了这样的桎梏,因为他们都是建立在Servlet上的,为了解决这样的问题,Servlet3.0引入了异步处理
,然后在Servlet3.1中又引入了非阻塞IO
进一步增强异步处理的性能。
测试
@WebServlet(value = "/hello-servlet", asyncSupported = true)
public class HelloServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
System.out.println("主线程开始:" + Thread.currentThread());
// 开启异步模式
AsyncContext asyncContext = request.startAsync();
// 业务逻辑进行异步处理;开始异步处理
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
System.out.println("副线程开始");
sayHello();
// 通知异步完成
asyncContext.complete();
// 获取异步上下文
AsyncContext asyncContext1 = request.getAsyncContext();
// 获取响应
ServletResponse response1 = asyncContext1.getResponse();
response1.getWriter().write("hello async");
System.out.println("副线程结束");
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}
}
});
System.out.println("主线程结束:" + Thread.currentThread());
}
public void sayHello() throws InterruptedException {
System.out.println(Thread.currentThread() + "ing...");
Thread.sleep(3000);
}
public void destroy() {
}
}
4.2 SpringMVC实现异步处理
4.2.1 通过返回Callable
借助Callable实现异步处理:
①控制器返回一个Callable
②Spring异步处理,将Callable提交到TaskExecutor 使用一个隔离的线程进行执行
③DispatcherServlet将所有的Filter退出web容器的线程,但是response保持打开状态,此时还能向浏览器进行响应
④Callable返回结果,SpringMVC将请求重新派发给容器,恢复之前的处理。
⑤根据Callable的返回结果,SpringMVC继续进行视图渲染流程(从收请求到视图渲染)
@Controller
public class AsyncController {
@RequestMapping("/async")
@ResponseBody
public Callable<String> async01() {
System.out.println("主线程开始" + Thread.currentThread());
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("副线程开始:" + Thread.currentThread());
Thread.sleep(2000);
System.out.println("副线程结束:" + Thread.currentThread());
return null;
}
};
System.out.println("主线程结束" + Thread.currentThread());
return callable;
}
}
输出结果:
主线程开始Thread[http-nio-8080-exec-6,5,main]
主线程结束Thread[http-nio-8080-exec-6,5,main]
副线程开始:Thread[MvcAsync1,5,main]
副线程结束:Thread[MvcAsync1,5,main]
4.2.2 通过返回DefferredResult
基本思路是通过消息中间件来起到缓冲的作用,来处理异步请求。如应用1只能获取响应创建订单请求,但是只有应用2能进行创建订单,那么应用1就可以把创建订单的消息放在消息中间件中,应用2负责监听消息中间件,并创建订单后将处理结果返回放在消息中间件中,线程2负责监听消息中间件的返回结果,并最终响应给客户端。
①首先创建一个DeferredQueue队列,用来存储和获取请求消息
②再创建一个createOrder请求,并将消息存储到DeferredQueue队列,一旦另一个create请求完成则此createOrder请求也进行响应
@ResponseBody
@RequestMapping("/createOrder")
public DeferredResult<Object> createOrder() {
DeferredResult<Object> deferredResult =
new DeferredResult((long) 3000, "create fail");
DeferredQueue.save(deferredResult);
return deferredResult;
}
③最后创建一个create请求通过获取DeferredQueue队列消息,用于监听createOrder请求实际创建订单,创建完成之后将结果返回
@RequestMapping("/create")
@ResponseBody
public String create() {
String s = UUID.randomUUID().toString();
DeferredResult<Object> deferredResult = DeferredQueue.get();
deferredResult.setResult(deferredResult);
return "success ===>" + s;
}
结果:
http://localhost:8080/SpringMVC_Async_war_exploded/createOrder
30616c4d-d58c-4909-991c-9bcc6cef5d58
http://localhost:8080/SpringMVC_Async_war_exploded/create
success ===>30616c4d-d58c-4909-991c-9bcc6cef5d58