Spring實戰 七 SpringMVC的高階技術
概述
先自己搭個專案回顧一下子上一章的內容,我這裡採用的是Java方式配置SpringMVC並且使用了thymeleaf模板技術展示一個簡單的首頁。
DispatcherServlet高階配置
AbstractAnnotationConfigDispatcherServletInitializer`來進行自動配置,這可以應付大部分情況下的應用了,但是總有特殊情況。
通過重寫三個抽象方法之外的其他的方法,我們可以完成更加細緻的配置。
在Spring初始化,註冊DispatcherServlet到Servlet容器後,就會呼叫customizeRegistration
方法,傳入一個ServletRegistration.Dynamic
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/useruploads/"));
registration.setLoadOnStartup(1);
}
如上,我們通過customizeRegistration
設定了多檔案上傳、DispatcherServlet的啟動時載入,通過這個方法,我們還可以其它的一些配置。
新增其他Servlet和Filter
SpringMVC接管了大部分我們該做的事,而且能做的很好,但有時候我們也需要建立自己的Servlet和Filter。
實現WebApplicationInitializer介面
EMMMM...第一個辦法就是實現WebApplicationInitializer
介面。在第五章的筆記中介紹過這個介面。總之,這是Spring提供的一個介面,實現了這個介面的類會在Servlet容器啟動時被掃描到並自動呼叫其中的onStartup
方法,我們之前繼承的AbstractAnnotationConfigDispatcherServletInitializer
也是這個介面的實現類。
我們可以隨便建立一個初始化器然後在onStartup
中呼叫servletContext
中的addServlet
或者addFilter
來實現新增filter和servlet。
public class ServletAndFilterInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.addServlet("myServlet", MyServlet.class)
.addMapping("/myservlet");
servletContext.addFilter("myFilter", MyFilter.class)
.addMappingForUrlPatterns(null,false,"/myservlet");
}
}
下面貼上MyServlet和MyFilter程式碼:
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("MyServlet");
}
}
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init....");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.getWriter().println("MyFilter");
filterChain.doFilter(servletRequest,servletResponse);
}
}
使用這個辦法創建出來的Servlet和Filter就相當於獨立於Spring的DispatcherServlet工作了。大部分時候我們還是要工作在Spring中,只是想註冊一個對映到DispatcherServlet上的Filter的話,不用這麼大動作。只需要回到AbstractAnnotationConfigDispatcherServletInitializer
中,重寫內部的getServletFilters
方法即可。
重寫getServletFilters
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Filter[] getServletFilters() {
return new Filter[]{new DispatcherServletFilter()};
}
// ...
}
public class DispatcherServletFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
((HttpServletResponse) response).getWriter().println("Hello, DispatcherServletFilter!");
chain.doFilter(request,response);
}
}
這樣你就可以把任意多個Filter對映到DispatcherServlet上了。
其他方法的說明
其實我試了在web.xml
下宣告Servlet或者開啟容器的自動掃描基於註解配置的Servlet,也都是可以正常配置的,但是我不知道和在Spring中配置有啥區別,在WebApplicationInitializer
中的ServletContext
和直接註冊時的是一個物件,所以也不存在Spring會包裝了那個Context物件在我們通過WebApplicationInitializer
物件註冊時會做一些額外操作的事。
Web.xml中配置DispatcherServlet
如果我們必須在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" metadata-complete="false">
<!-- 設定根上下文配置檔案位置 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
像在Java中配置一樣ContextLoaderListener和DispatcherServlet各需要一個配置,經過如上設定,ContextLoaderListener會載入/WEB-INF/spring/root-context.xml
,從這裡讀取Bean定義。
而DispatcherServlet需要從servlet定義的init-param
中設定這個檔案的位置,要不然預設是從/WEB-INF/{Servlet名字}-context.xml
中讀取,這個示例中也就是/WEB-INF/appServlet-context.xml
。
經過如下設定,DispatcherServlet就會從/WEB-INF/spring/appServlet/servlet-context.xml
中讀取定義。
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
我們還是想通過Java來進行Bean的定義,而不是XML,我們只用XML來註冊DispatcherServlet和ContextLoaderListener。
如下我們通過指定contextClass
和contextConfigLocation
來進行這項操作,把之前的兩個配置檔案WebConfig.java
和RootConfig.java
給使用起來。
<?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" metadata-complete="false">
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>io.lilpig.springlearn.springlearn04.config.RootConfig</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>io.lilpig.springlearn.springlearn04.config.WebConfig</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
行,刪了,換Java配置。。。。
處理檔案上傳
文字型別的表單通常有限制,遇到上傳檔案的需求就沒有辦法了,multipart型別是一個解決辦法。
如下是一個multipart型別的資料示例
每個------WebKitFormBoundary...
是一個欄位,下面一行是欄位名啥的,然後一個空行,再往下一行是這個欄位的資料。
如果這個欄位是一個檔案型別或者其他型別的資料,就會有一個Content-Type
頭,這個頭指定了它的型別,伺服器端會根據這個頭來取資料。
設定MultipartResolver
Spring提供了對應的Resolver來解析multipart型別的欄位。
CommonsMultipartResolver
:使用Jakarta Commons FileUpload解析multipart請求StandardServletMultipartResolver
:依賴於Spring3.0對multipart請求的支援(始於Spring3.1)
我們使用第二種方案來處理檔案上傳
在WebConfig中建立這樣一個Bean。
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
那麼使用者上傳的檔案該儲存到哪裡呢?大小、格式限制在哪裡做呢?
如果是XML方式,需要配置DispatcherServlet
中的一部分來指定如何處理檔案。
如果是實現WebApplicationInitializer則可以在DispatcherServlet的Servlet Registration上呼叫setMutipartConfig
進行設定。
對於大多數情況下的繼承AbstractAnnotationConfigDispatcherServletInitializer
,我們只需要重寫其中的customizeRegistration
方法來設定引數就好了。
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/uploads")
);
}
我們只使用了一個引數的構造器來指定上傳的暫存路徑,還可以有其它的構造器用來指定更多的引數。
假如我們不希望使用者上傳大於2MB的檔案,不希望整個請求的資料高達4MB,所有檔案都寫入到磁碟中,那麼可以使用如下設定。
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/uploads", 2097152, 4194304, 0)
);
}
XML配置如下
處理Multipart請求
下面就是編寫Controller來處理Multipart請求了
我先寫一個表單頁面
下面使用enctype
指定請求體格式為multipart/form-data
,然後使用了一個file型別的input,除此之外沒啥好說的。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post" th:object="${user}" enctype="multipart/form-data">
username: <input type="text" th:field="*{username}" name="username"> <br>
age: <input type="number" th:field="*{age}" name="age"> <br>
icon: <input type="file" name="profilePicture" accept="image/gif,image/jpeg,image/png"><br>
<button type="submit">註冊</button>
</form>
</body>
</html>
下面就是Controller,我們使用了@RequestPart
註解,並通過一個byte陣列來接收檔案。使用者上傳的檔案先會被伺服器儲存到之前配置的暫存目錄下,然後再將檔案的位元組碼傳遞給我們。
@Controller
@RequestMapping("/user")
public class UserController {
@GetMapping("/register")
public String register(Model model) {
model.addAttribute("user", new User());
return "register";
}
@PostMapping("/register")
public String processRegistration(
@RequestPart("profilePicture") byte[] profilePicture,
@Valid User user,
Errors errors
) {
if (errors.hasErrors()) {
return "register";
}else{
return "redirect:/";
}
}
}
使用byte[]
這種原始的資料型別,一切都得我們自己來處理,比如儲存檔案。
使用Spring提供的MultipartFile介面,我們可以更加方便地對使用者上傳的資料進行操作。
如下是MultipartFile提供的方法,我們可以獲取原始檔名,型別,大小,甚至可以用transferTo
直接以檔案的形式儲存到本地。
@PostMapping("/register")
public String processRegistration(
@RequestPart("profilePicture")MultipartFile multipartFile,
@Valid User user,
Errors errors
) {
try {
multipartFile.transferTo(
new File("D:/tmp/springlearn/userpictures/"+multipartFile.getOriginalFilename()));
} catch (IOException e) {
e.printStackTrace();
}
return "redirect:/";
}
還可以使用Part
型別來接收檔案,它更簡單,甚至無需配置MultipartResolver
。
異常處理
在這之前我們處理異常都是通過在Controller中使用try-catch
語句然後返回不同的邏輯檢視名。這樣Controller中就會產生不同的實際檢視路徑。如下:
try{
doSomething();
return "successed";
} catch(SomeException e){
return "redirect:/someErrorPage";
}
如果我們把這部分內容從Controller中剝離,讓Controller就關注成功的狀態,讓失敗的狀態由其他模組處理,會更好一些。
ResponseStatus
對於未捕獲的異常,預設情況下Spring會將其捕獲然後返回500狀態碼,並列印所有的異常堆疊。
這樣不僅對使用者不友好,而且將系統的異常堆疊暴露給使用者對系統來說也很不安全。
如果我們在異常上使用@ResponseStatus
註解,那麼我們可以繫結一個錯誤程式碼和錯誤訊息。
@ResponseStatus(
value = HttpStatus.NOT_FOUND,
reason = "user not found"
)
public class UserNotFoundException extends Exception{
}
然後在controller的方法中丟擲對應異常
@GetMapping("/{username}")
public String getUser(@PathVariable String username) throws UserNotFoundException {
// 就是假裝沒找到
throw new UserNotFoundException();
}
編寫處理異常的方法
大部分發生異常的時候我們都要進行處理然後返回一個自定義頁面,而不只是返回一個預設頁面給使用者。
在Controller中定義一個方法,方法上宣告一個@ExceptionHandler
的註解並指定要捕獲的異常型別。
@ExceptionHandler(UserNotFoundException.class)
public String userNotFoundHandler() {
return "errors/user-not-found";
}
這個方法和普通的控制器方法沒什麼區別,也可以返回一個邏輯檢視名,主要是該控制器內所有未捕獲的UserNotFoundException
異常都會被這個函式處理。
如下, 我們定義的異常頁面被渲染。
控制器通知
如果很多個控制器中都可能會出現同種型別的異常,那麼把處理異常的程式碼放到控制器中就不合適了。
控制器通知可以將這種異常的處理抽離出來,我們先寫一個看看。
@ControllerAdvice
public class UserExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public String userNotFoundHandler() {
return "errors/user-not-found";
}
}
@ControllerAdvice
上被聲明瞭一個@Component
註解,也就是說它也可以被Spring掃描。接下來只要是控制器中未處理的@ExcepitonHandler
中定義的異常都會執行對應的方法。
跨重定向請求資料
之前如果有重定向並傳遞資料的需求,我們都是通過"redirect:/url/"+引數
來傳遞的。
這樣傳遞有一些限制,首先它不安全,直接拼接URL可能被使用者注入一些惡意程式碼,其次它只能儲存字串。
Spring提供了兩個辦法來解決這個問題。
URL模板
我們可以通過URL模板來解決掉直接拼接引數產生的不安全問題,URL模板會對拼接的值進行轉義。
@PostMapping("/register")
public String processRegistration(
User user,
Model model
) {
model.addAttribute("username",user.getUsername());
return "redirect:/user/showuser/{username}";
}
將model中的屬性放到返回的邏輯檢視名中。
然後跳轉到的處理器方法可以通過PathVariable接收它
@GetMapping("/showuser/{username}")
public String showUser(@PathVariable String username, Model model) {
model.addAttribute("username",username);
return "showuser";
}
flash屬性
flash屬性解決了傳遞的值只能是字串的問題。
flash使用session技術實現,但它被取出之後就不在session中存在了,也許是因為轉瞬即逝,所以叫flash????
使用flash屬性就不能使用普通的Model
了,需要RedirectAttributes
,下面是使用案例。
@PostMapping("/register")
public String processRegistration(
User user,
RedirectAttributes model
) {
model.addAttribute("username",user.getUsername());
model.addFlashAttribute("user", user);
return "redirect:/user/showuser/{username}";
}
flash屬性會被傳到轉發請求的模型中,並且從session中移除。下面的控制器方法檢測了模型裡是否存在對應物件,如果存在就直接返回檢視名,然後檢視進行渲染。
@GetMapping("/showuser/{username}")
public String showUser(@PathVariable String username, Model model) throws Exception {
if(!model.containsAttribute("user"))
throw new Exception("bad paramter");
return "showuser";
}