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的子类。

  1. AbstractContextLoaderInitializer:调用createRootApplicationContext()方法创建根容器
  2. AbstractDispatcherServletInitializer:调用createServletApplicationContext()创建IOC容器,调用createDispatcherServlet()创建DispatcherServlet,调用addServlet()将DispatcherServlet添加到容器中,getServletMappings()获取请求映射,并添加到容器中
  3. AbstractAnnotationConfigDispatcherServletInitializer:调用createRootApplicationContext()方法创建根容器,调用getRootConfigClasses()方法获取配置类,调用createServletApplicationContext()方法创建IOC容器,调用getServletConfigClasses()方法获取配置类。

总结:以注解方式启动Spring MVC,继承AbstractAnnotationConfigDispatcherServletInitializer,实现抽象方法,指定DispatcherServlet的配置信息。

5.Spring MVC-整合

参考官方文档:https://docs.spring.io/spring/docs/5.3.0-SNAPSHOT/spring-framework-reference/web.html#mvc-servlet-context-hierarchy

创建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对象就可以感知到,于是,将结果一并返回了。