• 欢迎光临~

【Spring注解驱动】(三)servlet3.0

开发技术 开发技术 2022-07-21 次浏览

前言

今天是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 注解开发

【Spring注解驱动】(三)servlet3.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-INFservicesjavax.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,而根容器扫描业务层持久层控制数据源事务相关的组件。

【Spring注解驱动】(三)servlet3.0

如下面的项目中,使用的是listener配合xml的方式构建子父容器,Spring父容器创建

【Spring注解驱动】(三)servlet3.0

时触发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容器的实例,相当于原来配置的spring.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

【Spring注解驱动】(三)servlet3.0

基本思路是通过消息中间件来起到缓冲的作用,来处理异步请求。如应用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注解驱动】(三)servlet3.0
喜欢 (0)