Spring-web对Servlet3.0的应用#
先上一张继承体系图,下面围绕这张图片展开
@HandlesTypes({WebApplicationInitializer.class}) public class SpringServletContainerInitializer implements ServletContainerInitializer { public SpringServletContainerInitializer() { } public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException { List<WebApplicationInitializer> initializers = new LinkedList(); Iterator var4; if (webAppInitializerClasses != null) { var4 = webAppInitializerClasses.iterator(); while(var4.hasNext()) { Class<?> waiClass = (Class)var4.next(); if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) { try { initializers.add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance()); } catch (Throwable var7) { throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7); } } } } ... }
可以看到,Spring应用一启动就会加载WebApplicationInitializer
接口下的所有组件,并且,只要这些组件不是接口,不是抽象类,Spring就为它们创建实例
更进一步看一下上下文中WebApplicationInitializer
接口的实现类
AbstractContextLoaderInitializer#
看他对onstart()
方法的重写, 主要干了什么呢? 注册了一个上下文的监听器(借助这个监听器读取SpringMvc的配置文件),初始化应用的上下文
public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer { protected final Log logger = LogFactory.getLog(this.getClass()); public AbstractContextLoaderInitializer() { } public void onStartup(ServletContext servletContext) throws ServletException { this.registerContextLoaderListener(servletContext); } 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"); } }
AbstractDispatcherServletInitializer#
见名知意,他是DispatcherServlet
的初始化器,他主要做了什么事呢?
- 上面看了,它的父类初始化上下文,于是它调用父类的构造,往上传递web环境的上下文
- 紧接着添加
DispatcherServlet
public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer { public static final String DEFAULT_SERVLET_NAME = "dispatcher"; public AbstractDispatcherServletInitializer() { } public void onStartup(ServletContext servletContext) throws ServletException { super.onStartup(servletContext); this.registerDispatcherServlet(servletContext); } protected void registerDispatcherServlet(ServletContext servletContext) { String servletName = this.getServletName(); Assert.hasLength(servletName, "getServletName() must not return null or empty"); // 可以看一下,它创建的是web的容器 WebApplicationContext servletAppContext = this.createServletApplicationContext(); Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null"); // 创建负责调度的 DispatcherServlet FrameworkServlet dispatcherServlet = this.createDispatcherServlet(servletAppContext); Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null"); dispatcherServlet.setContextInitializers(this.getServletApplicationContextInitializers()); // 添加Servlet 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 { // 添加servlet的mapping信息 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); } ... }
AbstractAnnotationConfigDispatcherServletInitializer#
createRootApplicationContext
重写了父类的创建上下文的方法,我觉得这算是一个高潮吧, 因为啥呢,AnnotationConfigWebApplicationContext是SpringMvc使用的应用的上下文,怎么创建的源码在下面,其实我有在Spring源码阅读中写过这个方面的笔记,下面仅仅是将配置类传递给Spring的bean工厂,并没有对配置类进行其他方面的解析,或者是扫描包啥的
createServletApplicationContext()
重写了它父类的创建serlvet上下文的方法,
有个点,大家有没有发现,SpringMvc的上下文和Servlet的上下文是同一个对象,都是AnnotationConfigWebApplicationContext
,不同点就是添加了if-else分支判断,防止重复创建
public abstract class AbstractAnnotationConfigDispatcherServletInitializer extends AbstractDispatcherServletInitializer { public AbstractAnnotationConfigDispatcherServletInitializer() { } @Nullable protected WebApplicationContext createRootApplicationContext() { Class<?>[] configClasses = this.getRootConfigClasses(); if (!ObjectUtils.isEmpty(configClasses)) { AnnotationConfigWebApplicationContext context = new 有没有大神了解这个情况, SpringMvc的应用上下文和Servlet应用上下文竟然是同一个(); context.register(configClasses); return context; } else { return null; } } protected WebApplicationContext createServletApplicationContext() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); Class<?>[] configClasses = this.getServletConfigClasses(); if (!ObjectUtils.isEmpty(configClasses)) { context.register(configClasses); } return context; }
对比官网推荐的启动案例:#
下面的是Spring官网推荐是通过注解的配置方法,仔细看看,其实和上面的Spring-Web模块的做法是一样的
public class MyWebApplicationInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletCxt) { // Load Spring web application configuration AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext(); ac.register(AppConfig.class); ac.refresh(); // Create and register the DispatcherServlet DispatcherServlet servlet = new DispatcherServlet(ac); ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet); registration.setLoadOnStartup(1); registration.addMapping("/app/*"); } }
基于Servlet3.0全注解方式整合SpringMvc#
经过前面的分析,第一个结论是:服务器一启动,经过自上而下的继承体系AbstractAnnotationConfigDispacherServletInitializer
会被加载执行,所以,当我们想使用全注解方式完成继承SpringMVC时,继承AbstractAnnotationConfigDispacherServletInitializer
就好
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { // 获取Spring容器的配置类 @Override protected Class<?>[] getRootConfigClasses() { return new Class[]{RootConfig.class}; } // 获取web容器的配置类 @Override protected Class<?>[] getServletConfigClasses() { return new Class[]{WebConfig.class}; } /** * 获取DispatcherSerlvet的映射信息 * / : 表示拦截所有请求(包含静态资源 XXX.js XXX.jpg) 但是不包含 XXX.jsp * /* : 表示拦截所有请求(包含静态资源 XXX.js XXX.jpg) 包含 XXX.jsp * @return */ @Override protected String[] getServletMappings() { return new String[]{"/"}; } }
下面的两个配置类, 按照他的意思,分成了两个配置类,一个是web上下文中的配置类,另一个是Spring原生环境的配置类
但是吧,看看下面的配置真的是特别麻烦,一个得排除@Controller
,完事另一个得包含@Controller
,其实Spring原生上下文都认识这些通用注解,倒不如直接就一个配置类,还省事
@ComponentScan(value = "com.changwu",includeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class}) },useDefaultFilters = false) public class WebConfig { } @ComponentScan(value = "com.changwu",excludeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class}) }) public class RootConfig { }
Servlet3.0 异步请求处理器#
早前的前后端请求响应的模型是怎样的呢? 用户发送的请求经过网络传输到Tomcat,Tomcat中存在一个线程池,这时Tomcat会从线程池中取出一条线程专门处理这个请求,一直到处理完毕,给了用户响应之后才将此线程回收到线程池,但是线程池中的线程终究是有限的,一旦同时好几百的连接进来,Tomcat的压力骤然上升,难免会出现阻塞的现象
Serlet3.0引入的异步处理,让主线程拥有非阻塞的特性,这样tomcat接收请求访问的吞吐量就会增加
示例:
// 启动异步 @WebServlet(value = "/async",asyncSupported = true) public class MyAsyncServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req,resp); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 开启异步处理 AsyncContext asyncContext = req.startAsync(); asyncContext.start(new Runnable() { @Override public void run() { // do other things // 结束 asyncContext.complete(); // 响应 ServletResponse response = asyncContext.getResponse(); try { response.getWriter().write("123"); } catch (IOException e) { e.printStackTrace(); } } }); } }
SpringMvc的异步任务#
针对Servlet3.0的异步特性,SpringMvc相关的支持是提供了异步线程池
DeferredResult#
我觉得这个异步的实现方式简直是无与伦比!!!无法言表!!!
@GetMapping("/quotes") @ResponseBody public DeferredResult<String> quotes() { DeferredResult<String> deferredResult = new DeferredResult<String>(); // Save the deferredResult somewhere.. return deferredResult; } // From some other thread... deferredResult.setResult(result);
Callable#
- 方法的最后将
Callable
返回 call()
方法中做写需要异步处理器的逻辑
执行流程:
- SpringMvc会将这个Callable放到一个叫TaskExcutor中执行
- DispatcherSerlvet和所有的Filter退出web容器,但是Response保持打开状态
- SpringMvc会将Callable的返回结果重写派发给setlvet恢复之前的处理
- 根据Callable返回的结果SpringMvc进行渲染
@PostMapping public Callable<String> processUpload(final MultipartFile file) { return new Callable<String>() { public String call() throws Exception { // ... return "someView"; } }; }
Request#
请求方式与很多种,get post head trace options 还有put delete
其中get post put delete 是RestfulAPI中推荐,也是现在盛行使用的四种请求方法
get: 最为简单的请求方式,一般数据添加在url后面一般这样写username?张三&password?123123
, 由于URL的长度有限制,故能传输的数据一般在1M左右, 而且数据明文传输,像上面那样,村咋存在安全隐患
post的数据存放在请求体中,一般没有大小限制,相对于get而言,post的安全性更好一点
继承图如下:#
上图中我们最常使用的HttpServletRequest
竟然是个接口,当时一开始学web的时候确实觉得很奇怪,但是现在想想其实也还好了,因为Tomcat提供了实现类org.apache.catalina.connector.RequestFacade
获取请求行数据-GET#
请求行: GET /test/app?name=zhangsan http/1.1
- 获取请求方法: GET
String getMethod()
- 获取虚拟路径(项目路径): /test
String getContextPath()
- 获取Servlet路径; /app
String getServletPath()
- 获取get请求的请求参数: name=zhangsan
String getQueryString()
- 获取URI : /test/app
String getRequestURI()
- 获取URL : http:localhost/test/app
String getRequestURL()
- 获取协议版本
String getProtocol()
- 获取远程主机地址
String getRemoteAddr()
获取请求头数据#
- 根据名称获取请求头
String getHeader(String name);
- 获取所有的请求头
Enumertion<String> getHeaderNames(); 获取所有请求头的名称
获取请求体数据#
仅仅有post方式,才会有请求体:使用它分成两步:
- 从request中获取流对象
BufferReader getReader(); // 获取字符输入流 ServletInputStream getInputStrream(); // 获取字节输入流
- 从流对象中获取到需要的数据
通用的方法#
- 根据参数名获取参数值
String getParamter(String name); 根据参数名获取参数值
- 根据参数名,获取参数值数组
String [] getParameterValues(String name)
- 获取所有请求的参数名称
Enumeration<String> getParamerterNames()
- 获取所有参数键值对形式的map集合
Map<String,String[]) getParamerterMap()
有了通用的方法特性之后,我们就不跟针对doGet,doPost两种方式写两份代码, 只要在doGet()或者doPost()中调用另外一个就ok,因为方法针对两者通用
解决中文乱码#
首先: Tomcat8自身解决了中文乱码问题
Post方式提交数据依然存在乱码问题,像下面这样先设置编码再使用 req
request.setCharacterEncoding("utf-8")
request的请求转发#
当用户的某一个请求需要通过多个Servlet协作完成时,请求在Servlet之间跳转,这种资源跳转的方式称为请求转发
使用方法:通过当前的request获取出RequestDispacher对象,通过这个对象的forward(req,res)进行转发的动作
RequestDispatcher getRequestDispacher(String path) // path是另一个Servlet的url-pattern forward(currentReq,currentRes);
特点:
- 浏览器地址栏路径没有发生变化
- 服务器内部官网的资源跳转,不能跳往别的站点
- 一次转发,对浏览器来说,仅仅发送了一次请求
因为我们没让浏览器发送两次请求,在服务端完成了请求转发,所以上面的path仅仅是servlet-url-pattern,而不包含项目路径
域对象-共享数据#
域对象: request域, 既然是域对象,他就有自己的作用范围,request的作用范围是什么呢? 就是一次请求,每次请求都是一个域, 换句话说,如果说客户端的一次请求经过了AServlet,然后AServlet将请求转发到了BServlet,name AServlet BServlet就在一个域中,也就可以共享彼此的数据
怎么玩?
在AServlet setAttribute(String name,Object obj); 请求转发到BServlet 在BServlet getAttribute(String name); 移除 removeAttribute(String name);