【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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步