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