【Spring注解驱动】(三)Servlet 3.0

2023-06-28,,

前言

今天是7.21日,终于是看完了。。暑假在家学习是真的差点意思

1 Servlet 3.0简介

Servlet 2.0是在web.xml中配置servlet filterlistenerDispatcherServlet等等,而在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容器只扫描controllerview 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

【Spring注解驱动】(三)Servlet 3.0的相关教程结束。

《【Spring注解驱动】(三)Servlet 3.0.doc》

下载本文的Word格式文档,以方便收藏与打印。