目錄
在使用Spring MVC開發RESTful API的時候,我們經常會使用Java的攔截機制來處理請求,Filter是Servlet Api過濾器,Interceptor則是Spring自帶的攔截器,而Aspect切面是Spring AOP一個概念,主要的使用場景有:日誌記錄、事務控制和異常處理,該篇文章主要說說它們是如何實現的以及他們之間的差別,在這過程中也會探討全局異常處理機制的原理以及異常處理過程。
Filter
我對Filter過濾器做了以下總結:
- 介紹:
java的過濾器,依賴於Sevlet,和框架無關的,是所有過濾組件中最外層的,從粒度來說是最大的,它主要是在過濾器中修改字符編碼(CharacterEncodingFilter)、過濾掉沒用的參數、簡單的安全校驗(比如登錄不登錄之類)
- 實現和配置方式
- 直接實現Filter接口+@Component
- @Bean+@Configuration(第三方Filter)
- web.xml配置方式
Filter的實現方式
@Component
public class TimeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("初始化TimeFilter...");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
System.out.println("-------TimeFilter Start--------");
long start = new Date().getTime();
filterChain.doFilter(request, response);
System.out.println("TimeFilter執行耗時:" + (new Date().getTime() - start));
System.out.println("-------TimeFilter End--------");
}
@Override
public void destroy() {
System.out.println("銷燬TimeFilter...");
}
}
注意:
關於filterChain.doFilter(request,response,filterChain),執行filterChain.doFilter的意思是將請求轉發給過濾器鏈上的下一個對象,如果沒有filter那就是你請求的資源。一般filter都是一個鏈,web.xml 裏面配置了幾個就有幾個。一個一個的連在一起這裏指的是下一個Filter, request->filter1->filter2->filter3->...->response。
我們定義完Filter之後,如果我們不使用@Component註解注入,可以使用另一種方式將Filter注入到我們的容器中,這裏使用@Bean的形式定義,通過自定義配置類WebConfig實現配置,最後返回registrationBean,這個方法主要有兩個好處就是第一我們可以通過registrationBean.setUrlPatterns(urls)來指明filter在哪些路徑下起作用,第二我們可以使用該方法去注入第三方的filter,原因的很多地方的filter其實並不是以@Component注入方式(也就是沒有標註@Component註解),這時候我們就只能使用第二種方式來實現了。
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
TimeInterceptor timeInterceptor;
@Bean
public FilterRegistrationBean charsetFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
TimeFilter timeFilter = new TimeFilter();
CharsetFilter charsetFilter = new CharsetFilter();
registrationBean.setFilter(charsetFilter);
registrationBean.setFilter(timeFilter);
//相當於@webFilter的@WebInitParam()註解的作用
Map<String,String> paramMap = new HashMap<>();
paramMap.put("charset","utf-8");
registrationBean.setInitParameters(paramMap);
//相當於@webFilter的 urlPatterns = "/*"的作用
List<String> urls = new ArrayList<>();
urls.add("/*");
//urls.add("/user/*");
registrationBean.setUrlPatterns(urls);
return registrationBean;
}
我們在controller中定義一個getInfo()方法:
//請求路徑的{id}回傳到方法裏也就是傳到(@PathVariable String id)的id裏
@RequestMapping(value = "/user/{id:\\d+}",method = RequestMethod.GET)
@JsonView(User.UserDetailView.class) //這裏因爲UserDetailView繼承了UserSimpleView所有會返回username和password
@ApiOperation("獲取用戶信息")
public User getInfo(@PathVariable Integer id) {
// throw new UserNotExistException(id);
System.out.println("進入getInfo()服務");
User user = new User();
user.setId(1);
user.setUsername("jacklin");
user.setPassword("123");
return user;
}
當我們調用controller中的getInfo()方法的時候,看看請求響應是否成以及控制檯的輸出:
GET請求發送成功,返回200,控制檯輸出如下:
從上述結果,我們可以分析得出,當客戶端發送請求,到達Controller方法之前,先執行Filter初始化操作,接着進入Controller的方法體,最後執行完成,通過分析我們明白了Filter的工作原理和方法的執行順序!
Interceptor
我對Interceptor過濾器做了以下總結(導圖中加粗部分是重點):
- 簡介:
spring框架的攔截器,主要依賴於Spring MVC框架,它是在 service 或者一個方法調用前,調用一個方法,或者在方法調用後,調用一個方法。
- 實現和配置方式:
實現HandlerInterceptor接口看,並重寫preHandle、postHandle、afterCompletion方法。
- 解釋說明:
- SpringMVC中的Interceptor是鏈式的調用的,在一個應用中或者是在一個請求中可以同時存在多個Interceptor,每個Inteceptor的調用都會按照它的聲明順序依次執行,而且最先執行的Intecptor的preHandler方法,所以可以在這個方法中進行一些前置初始化操作或者是對當前請求的一個預處理,也可以在這個方法中進行一些判斷是否要繼續進行下去。
- 該方法的返回值是Boolean類型的,當它返回爲false時,表示請求結束,後續的Interceptor和Controller都不會再執行;
- 當返回值爲true 時就會繼續調用下一個Interceptor的preHandle方法,如果已經是最後一個Interceptor的時候就會是調用當前請求的Controller方法。
Interceptor攔截器的實現方式
/**
* @Author 林必昭
* @Date 2019/7/4 13:15
*/
@Component
public class TimeInterceptor implements HandlerInterceptor {
/**
* preHandle方法的返回值是boolean值,當返回的是false時候,不會進入controller裏的方法
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("------->preHandle");
System.out.println("控制器類名:" + ((HandlerMethod) handler).getBean().getClass().getName()); //獲取類名
System.out.println("控制器中的對應的方法名:" + ((HandlerMethod) handler).getMethod().getName()); //獲取類中方法名
request.setAttribute("startTime", new Date().getTime());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("------->postHandle");
Long start = (Long) request.getAttribute("startTime");
System.out.println("TimeInterceptor 執行耗時:" + " " + (new Date().getTime() - start));
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) throws Exception {
System.out.println("------->afterCompletion");
Long start = (Long) request.getAttribute("startTime");
System.out.println("TimeInterceptor 執行耗時:" + " " + (new Date().getTime() - start));
System.out.println("Exception is " + e);
}
}
注意:我們使用@Component定義Interceptor之後,還不能起作用,好要進行下一步配置,我們在之前定義的WebConfig配置類繼承抽象類WebMvcConfigurerAdapter,將Interceptor注入容器中:
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
TimeInterceptor timeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInterceptor);
}
}
這樣Interceptor就起作用了,同樣,我們通過發送請求,觀察控制檯的輸出,來分析結果:
從TimeInterceptor攔截器結果,我們可以分析得出,當客戶端發送請求,到達Controller方法之前,先執行Interceptor的preHandler方法,接着進入Controller的方法體,通過Interceptor我們可以獲取到對應的Controller和執行的方法名,接着執行postHandler方法,最後執行afterCompletion方法,如何結果出現異常,也會執行afterCompletion,這裏沒有異常,所以Exception爲空。
那麼當控制層中拋出異常,如果沒有使用全局異常處理,在攔截器上也能捕獲到異常信息,我們可以嘗試一下,在Controller拋出一個RuntimeException,RuntimeException並沒有在全局異常處理中被處理,Controller修改如下:
@RequestMapping(value = "/user/{id:\\d+}",method = RequestMethod.GET)
@JsonView(User.UserDetailView.class) //這裏因爲UserDetailView繼承了UserSimpleView所有會返回username和password
@ApiOperation("獲取用戶信息")
public User getInfo(@PathVariable Integer id) {
/**
* 當拋出UserNotExistException異常的時候,會跳到ControllerExceptionHandler的handleUserNotExistException方法
* 進行相應的處理
*/
throw new RuntimeException("user not exist!!"); //這裏拋出一個RuntimeException
// System.out.println("進入getInfo()服務");
// User user = new User();
// user.setId(1);
// user.setUsername("jacklin");
// user.setPassword("123");
// return user;
}
觀察控制檯輸出:
結果很明顯了,當控制層出現異常的時候,異常沒有被全局處理器處理,到達攔截器,攔截器會捕獲到異常,這時候只執行了preHandle和afterCompletionn方法,並沒有執行postHandle方法,控制檯也輸出了異常信息。
想想,如果拋出我們自定義異常,而且自定義異常被全局處理器攔截處理,異常還會到達我們的攔截器嗎,我們來自定義一個異常UserNotExistException,如下:
public class UserNotExistException extends RuntimeException {
private static final long serialVersionUID = -9136501205369741760L;
private String id;
public UserNotExistException(String id){
super("user is not exist...");
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
接着,定義全局異常處理器GlobalExceptionHandler,使用@ControllerAdvice修飾:
/**
* 全局異常處理,負責處理controller拋出的異常
*
* @Author 林必昭
* @Date 2019/7/4 11:31
*/
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //服務器內部錯誤
public Map<String, Object> handleUserNotExistException(UserNotExistException ex) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("id", ex.getId());
resultMap.put("message", ex.getMessage());
return resultMap;
}
}
然後,我們再在UserController中拋出我們的自定義異常UserNotExistException,觀察控制檯的輸出,來分析結果:
public User getInfo(@PathVariable Integer id) {
/**
* 當拋出UserNotExistException異常的時候,會跳到ControllerExceptionHandler的handleUserNotExistException方法
* 進行相應的處理
*/
//throw new RuntimeException("user not exist!!");
throw new UserNotExistException("user not exist!!")
}
從結果看出,異常是空的,證明我們定義的異常處理器已經生效,UserNotExistException在GlobalExceptionHandler已經被處理了,所有異常沒有到達我們的攔截器,到這裏我們可以得出異常的處理順序了,在文末會給出。
Aspect
我對Aspect過濾器做了以下總結:
在使用Spring AOP切面前,我們需要導入pom依賴:
<!-- 切面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
切面攔截的實現方式
@Aspect
@Component
public class TimeAspect {
/**
* 切入點
*/
@Around("execution(* com.lbz.web.controller.UserController.*(..))") //UserController下的任何方法被調用都會執行這個切片
public Object handleControllerMethod(ProceedingJoinPoint point) throws Throwable {
System.out.println("TimeAspect start");
long start = new Date().getTime();
Object object = point.proceed(); //proceed中文意思是繼續的意思,也就是切入,相當於filterChain.doFilter()
Object[] args = point.getArgs(); //與Filter和Interceptor的區別是,可以獲取到UserController裏方法的參數
for (Object arg : args) {
System.out.println("控制層的方法對應參數是:" + arg);
}
System.out.println("TimeAspect執行耗時:" + (new Date().getTime() - start));
System.out.println("TimeAspect end");
return object;
}
}
這裏的point.proceed()是繼續的意思,也就是切入,相當於filterChain.doFilter(),與Filter和Interceptor不同的是,我們可以通過point.getArgs();拿到對應方法的參數,我們通過遍歷把參數打印看一下。
從結果看出,我們可以看到我們拿到方法對應的參數,爲1,也就是我們請求:http://localhost:8060/user/1 傳入的id的值;
總結:
1、過濾器可以拿到原始方法的Http的請求和響應信息,拿不到對應方法的詳細信息,
攔截器既可以拿到原始方法的Http請求和響應信息,也能拿到對應方法的詳細信息,但是拿不到被調用方法對應參數的值,
而切面可以拿到被調用方法傳遞過來參數的值,但卻拿不到原始的Http請求和響應對象。
2、Controller方法拋出異常之後,最先捕獲到異常的是切片,如果你定義了全局異常處理器並聲明瞭ControllerAdvice,切片捕獲到異常往外拋,就輪到全局異常處理器處理,接着到攔截器,再到過濾器,也就是:
攔截作用順序:Aspect->全局處理器->攔截器->過濾器->Tomcat
最後,我完成了對Filter、Interceptor、Aspect三種攔截方式的實現和過程分析,通過本次的學習,我也掌握了很多的知識,包括攔截器的工作原理,異常被處理的順序,全局異常處理機制,掌握如何實現請求的攔截和處理,我個人覺得多看不如一寫,多寫寫加以思考總會有收穫