Spring註解驅動-Servlet與Spring MVC(九)

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對象就可以感知到,於是,將結果一併返回了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章