1.Servlet-简介&测试
Servlet相关文档可以去这里下载:https://www.jcp.org/en/jsr/summary?id=servlet,目前已经出来Servlet 4.0了。
新建一个Java EE的Web Application项目,添加一个Servlet。通过浏览器发送/hello请求测试,页面可以输出“hello”字符串。
package com.atguigu.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
// 用来映射/hello请求
@WebServlet("/hello")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().write("hello");
}
}
2.Servlet-ServletContainerInitializer
Servlet容器启动后,会扫描当前应用里每一个jar包下的ServletContainerInitializer的实现类。这个实现类必须绑定在META-INF/services/javax.servlet.ServletContainerInitializer下,文件的内容就是ServletContainerInitializer实现类的全类名。
创建一个自定义的MyServletContainerInitializer,在src下创建META-INF/services文件夹,并加入javax.servlet.ServletContainerInitializer文件,文件内容是ServletContainerInitializer实现类的全类名。
package com.atguigu.servlet;
import com.atguigu.service.HelloService;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;
import java.util.Arrays;
import java.util.Set;
// 容器启动的时候会将HandlesTypes指定类型下面的子类(实现类,子接口等)传递过来
@HandlesTypes(value = {HelloService.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {
/**
* 在应用启动的时候,会调用onStartup()方法
* @param set @HandlesTypes传入类型的子类
* @param servletContext 代表当前Web应用的ServletContext
* @throws ServletException
*/
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
System.out.println("MyServletContainerInitializer.onStartup");
System.out.println("感兴趣的类:" + Arrays.asList(set));
// 拿到感兴趣的类以后,可以对这些类做一些操作,比如创建对象,执行方法等
}
}
创建HelloSerice.java接口、HelloServiceExt.java接口继承HelloSerice.java接口、AbstractHelloService.java抽象类实现HelloSerice.java接口、创建HelloServiceImpl.java实现HelloSerice.java接口。启动Tomcat,在控制台可以看到把HelloService的子类都打印出来了,这些都是我们感兴趣的类。
3.Servlet-ServletContext注册三大组件
我们可以利用ServletContext对象,使用编码的方式,向容器中添加Servlet、Listener、Filter等外部组件。我们创建自定义的Servlet、Listener、Filter做测试。
package com.atguigu.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().write("I am UserServlet");
}
}
package com.atguigu.servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class UserListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
System.out.println("UserListener.contextInitialized");
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
// 获取ServletContext容器
ServletContext servletContext = servletContextEvent.getServletContext();
// 既然这里可以获取到ServletContext的对象,那么可以在这里通过ServletContext对象添加自定义组件
System.out.println("UserListener.contextDestroyed");
}
}
package com.atguigu.servlet;
import javax.servlet.*;
import java.io.IOException;
public class UserFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("UserFilter.init");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("UserFilter.doFilter");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("UserFilter.destroy");
}
}
然后在MyServletContainerInitializer的onStartup()方法中通过编码的方式,在程序启动的时候, 添加三大组件。
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
System.out.println("MyServletContainerInitializer.onStartup");
System.out.println("感兴趣的类:" + Arrays.asList(set));
// 拿到感兴趣的类以后,可以对这些类做一些操作,比如创建对象,执行方法等
// 模拟注册外部的Servlet、Listener、Filter,在项目启动的时候,给ServletContext中添加组件
// 添加外部Servlet组件
ServletRegistration.Dynamic userServlet = servletContext.addServlet("userServlet", new UserServlet());
// 指定映射
userServlet.addMapping("/user");
// 添加外部Listener
servletContext.addListener(UserListener.class);
// 添加外部Filter
FilterRegistration.Dynamic userFilter = servletContext.addFilter("userFilter", UserFilter.class);
// 指定映射
userFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
}
除了在onStartup()方法里可以拿到ServletContext,还可以通过Listener中ServletContextEvent对象获取ServletContext对象。从而实现在程序启动的时候,加入外部组件的效果。
4.Servlet-与SpringMVC整合分析
新建一个maven项目,修改pom.xml配置文件,添加相关依赖。在Project Structure的Libraries中,加入Tomcat的servlet-api.jar。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu</groupId>
<artifactId>springmvc-annotation</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.11.RELEASE</version>
</dependency>
</dependencies>
<!--保证没有web.xml的时候不报错-->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.3</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
Web容器启动时候,扫描每个jar包下的META-INF/services/javax.servlet.ServletContainerInitializer,加载这个文件中指定的SpringServletContainerInitializer类。SpringServletContainerInitializer类上带有@HandlesTypes注解,于是,在项目启动的时候,会加载HandlesTypes指定的WebApplicationInitializer的之类。查看WebApplicationInitializer的子类。
- AbstractContextLoaderInitializer:调用createRootApplicationContext()方法创建根容器
- AbstractDispatcherServletInitializer:调用createServletApplicationContext()创建IOC容器,调用createDispatcherServlet()创建DispatcherServlet,调用addServlet()将DispatcherServlet添加到容器中,getServletMappings()获取请求映射,并添加到容器中
- AbstractAnnotationConfigDispatcherServletInitializer:调用createRootApplicationContext()方法创建根容器,调用getRootConfigClasses()方法获取配置类,调用createServletApplicationContext()方法创建IOC容器,调用getServletConfigClasses()方法获取配置类。
总结:以注解方式启动Spring MVC,继承AbstractAnnotationConfigDispatcherServletInitializer,实现抽象方法,指定DispatcherServlet的配置信息。
5.Spring MVC-整合
创建MyWebAppInitializer继承AbstractAnnotationConfigDispatcherServletInitializer,重写相关方法。
package com.atguigu;
import com.atguigu.config.RootConfig;
import com.atguigu.config.ServletConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// 获取根容器的配置类,等于Spring的配置文件,作为父容器
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}
// 获取Web容器的配置类,等于Spring MVC的配置文件,作为子容器
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{ServletConfig.class};
}
// 获取DispatcherServlet的映射信息
@Override
protected String[] getServletMappings() {
// /:拦截所有请求,包括静态资源,不包括*.jsp
// /*:拦截所有请求,包括静态资源,包括*.jsp
return new String[]{"/"};
}
}
创建RootConfig.java和ServletConfig.java配置类,形成父子容器的效果,其中ServletConfig用来扫描Controller、ViewResolver、HandlerMapping,RootConfig用来扫描Service、Repository等。
package com.atguigu.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
// RootConfig不扫描带有Controller注解的类
@ComponentScan(value = "com.atguigu", excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})})
public class RootConfig {
}
package com.atguigu.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
// ServletConfig只扫描带有Controller注解的类
// useDefaultFilters = false禁用默认规则,否则includeFilters不生效
@ComponentScan(value = "com.atguigu", includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})}, useDefaultFilters = false)
public class ServletConfig {
}
创建HelloController和HelloService。
package com.atguigu.controller;
import com.atguigu.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
@Autowired
HelloService helloService;
@ResponseBody
@RequestMapping(value = "/hello")
public String hello() {
return helloService.hello("王劭阳");
}
}
package com.atguigu.service;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
public String hello(String text) {
return "hello, " + text;
}
}
发送/hello请求测试,发现中文乱码了,这里暂且不管,下一节会说明怎么解决。
6.Spring MVC-定制与接管Spring MVC
有xml配置文件的时候,我们会在配置文件中写一些配置,现在没有配置文件了,需要使用Java代码或者注解的方式实现配置的功能。
在ServletConfig上标注@EnableWebMvc注解,并让ServletConfig继承WebMvcConfigurerAdapter类。如果需要定制,就重写里面的方法。上面提到的中文乱码问题,就是因为StringHTTPMessageConverter类默认使用的字符编码是ISO-8859-1,所以出现了乱码,我们需要创建一个StringHTTPMessageConverter,并指定UTF-8编码,添加到配置中去,此时再去页面查看,中文乱码问题就解决了。
package com.atguigu.config;
import com.atguigu.interceptor.MyInterceptor;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.nio.charset.StandardCharsets;
import java.util.List;
// ServletConfig只扫描带有Controller注解的类
// useDefaultFilters = false禁用默认规则,否则includeFilters不生效
@ComponentScan(value = "com.atguigu", includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})}, useDefaultFilters = false)
@EnableWebMvc
public class ServletConfig extends WebMvcConfigurerAdapter {
// 配置静态资源访问
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
// 相当于在配置文件中写了<mvc:default-servlet-handler/>
configurer.enable();
}
// 配置字符编码转换器
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
}
// 配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
}
}
具体有哪些方法,可以参考WebMvcConfigurer接口或WebMvcConfigurerAdapter抽象类里的方法。这些方法和xml里配置的对应关系,参考官方文档:https://docs.spring.io/spring/docs/5.3.0-SNAPSHOT/spring-framework-reference/web.html#mvc-config。
7.Servlet-异步请求
假设有一个业务方法执行非常耗时,我们在访问这个请求的时候,主线程会卡住,当主线程一直不能被释放,达到线程池的最大值之后,主线程就不能接收请求了,这不是我们想看到的。创建MyAsyncServlet实现异步处理,将主线程和子线程分开,保证主线程不阻塞,耗时逻辑交给子线程执行。
package com.atguigu.servlet;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
// asyncSupported的默认值是false,需要开启异步请求支持
@WebServlet(value = "/async", asyncSupported = true)
public class MyAsyncServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("主线程开始:" + Thread.currentThread());
// 开启异步模式
AsyncContext asyncContext = request.startAsync();
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
System.out.println("子线程开始:" + Thread.currentThread());
slow();
asyncContext.complete();
// 获取响应体
ServletResponse asyncContextResponse = asyncContext.getResponse();
asyncContextResponse.getWriter().write("this is a async operation");
System.out.println("子线程结束:" + Thread.currentThread());
} catch (Exception e) {
e.printStackTrace();
}
}
});
System.out.println("主线程结束:" + Thread.currentThread());
}
public void slow() throws Exception {
Thread.sleep(10000);
}
}
8.Spring MVC-异步请求-返回Callable
控制器的方法返回Callable对象:Spring执行异步处理,将Callable提交到TaskExecutor,使用一个隔离的线程进行执行,当主线程执行完之后,DispatcherServlet和所有的Filter都退出Web容器,但是Response依旧保持打开状态,等待Callable返回结果时,Spring MVC会再次将请求派发给容器,恢复之前的处理,根据Callable的结果,Spring MVC继续执行流程(从接收请求到视图渲染)。
接收请求的Controller可以不用持续等待,从而可以继续接收其他请求,但是在浏览器端,页面还是加载中的。这有利于缓解服务端的请求压力。
package com.atguigu.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.concurrent.Callable;
@Controller
public class AsyncController {
@RequestMapping("/async01")
@ResponseBody
public Callable<String> async01() {
System.out.println("主线程开始:" + Thread.currentThread() + ":时间:" + System.currentTimeMillis());
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("子线程开始:"+Thread.currentThread()+":时间:"+ System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("子线程结束:"+Thread.currentThread()+":时间:"+ System.currentTimeMillis());
return "async01";
}
};
System.out.println("主线程结束:" + Thread.currentThread() + ":时间:" + System.currentTimeMillis());
return callable;
}
}
开启一个Interceptor,就可以监听到两次preHandle()的调用。第一次是直接调用,第二次是再次派发请求的结果。
9.Spring MVC-异步请求-返回DeferredResult
当一个请求到达Controller方法的时候,如果方法的返回值是DeferredResult,在没有超时或者没有setResult的时候,主线程会结束,DeferredResult会另启动线程处理业务逻辑,在浏览器端看到的效果是加载中,不过此时主线程已经可以继续接收请求了。当setResult()执行或者超时之后,将结果进行返回。
@RequestMapping("/createOrder")
@ResponseBody
public DeferredResult<Object> createOrder() {
// 如果3秒钟没有拿到订单信息,就提示创建订单错误
DeferredResult<Object> deferredResult = new DeferredResult<>(3000L, "create order fail");
// 暂时保存起来,模拟将deferredResult发送到了MQ的场景
DeferredResultQueue.save(deferredResult);
// 当监听到deferredResult.setResult()方法执行后,进行返回
return deferredResult;
}
@RequestMapping("/create")
@ResponseBody
public String create() {
// 实际创建订单业务
String order = UUID.randomUUID().toString();
DeferredResult<Object> deferredResult = DeferredResultQueue.get();
deferredResult.setResult(order);
return "success..." + order;
}
/createOrder请求用来获取订单,/create请求用来产生订单,它们之间通过一个Queue模拟消息队列。
package com.atguigu.service;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class DeferredResultQueue {
private static Queue<DeferredResult<Object>> queue = new ConcurrentLinkedQueue<DeferredResult<Object>>();
public static void save(DeferredResult<Object> deferredResult) {
queue.add(deferredResult);
}
public static DeferredResult<Object> get() {
return queue.poll();
}
}
A系统把DeferredResult对象放进队列,/create请求产生订单并执行setResult()方法,将订单信息set进去,此时A系统的DeferredResult对象就可以感知到,于是,将结果一并返回了。