【轉載】【SSM】SpringBoot 統一功能處理,(*Spring 攔截器實現與原理)

✨1.用戶登錄權限效驗

1.1 最初用戶登錄驗證

1.2 Spring AOP 用戶統一登錄驗證的問題

1.3 Spring 攔截器

1. 自定義攔截器

2. 將自定義攔截器加入到系統配置

1.4 攔截器實現原理

1. 實現原理源碼分析

2. 攔截器小結

✨2. 統一異常處理

✨3. 統一數據返回格式

3.1 爲什麼需要統一數據返回格式?

3.2 統一數據返回格式的實現


✨1.用戶登錄權限效驗

用戶登錄權限的發展從之前每個方法中自己驗證用戶登錄權限,到現在統一的用戶登錄驗證處理,它是一個逐漸完善和逐漸優化的過程。

1.1 最初用戶登錄驗證

最初用戶登錄驗證的實現方法:

@RestController
public class IndexController {
    @RequestMapping("/method1")
    public String method1(HttpServletRequest request){
        User user = null;
        HttpSession session = request.getSession();
        if(session != null){
            user = (User) session.getAttribute("currentUser");
        }
        if(user == null){
            return "redirect:/login.html";
        }
        return "redirect:/";
    }
    @RequestMapping("/method2")
    public String method2(HttpServletRequest request){
        User user = null;
        HttpSession session = request.getSession();
        if(session != null){
            user = (User) session.getAttribute("currentUser");
        }
        if(user == null){
            return "redirect:/login.html";
        }
        return "redirect:/";
    }
}

每個方法中都有相同的用戶登錄驗證權限,它的缺點是:

  1. 每個方法中都要單獨寫用戶登錄驗證的方法,即使封裝成公共方法,也一樣要傳參調用和在方法中 進行判斷。
  2. 添加控制器越多,調用用戶登錄驗證的方法也越多,這樣就增加了後期的修改成本和維護成本。
  3. 這些用戶登錄驗證的方法和接下來要實現的業務幾何沒有任何關聯,但每個方法中都要寫一遍。 所以提供一個公共的 AOP 方法來進行統一的用戶登錄權限驗證迫在眉睫。

1.2 Spring AOP 用戶統一登錄驗證的問題

我們想到的另一個實現方案是 Spring AOP 前置通知或環繞通知來實現,具體實現代碼如下:

@Aspect
@Component
public class UserAspect {

    @Pointcut("execution(* com.example.demo.controller..*.*(..))")
    public void pointCut(){}
    
     // 前置方法
    @Before("pointcut()")
        public void doBefore(){
    }
    
    // 環繞方法
    @Around("pointCut()")
    public Object doAround(ProceedingJoinPoint joinPoint){
        User user = null;
        System.out.println("Around 方法開始執行");
        try {
            // 執行攔截方法
            user = (User) joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("Around 方法結束執行");
        return user;
    }
}

如果要在 Spring AOP 的切面中實現用戶登錄權限效驗的功能,有以下兩個問題:

  1. 沒辦法獲取到 HttpSession 對象。
  2. 我們要對一部分方法進行攔截,而另一部分方法不攔截,如註冊方法和登錄方法是不攔截的,這樣 的話排除方法的規則很難定義,甚至沒辦法定義。 

1.3 Spring 攔截器

對於以上問題 Spring 中提供了具體的實現攔截器:HandlerInterceptor,攔截器的實現分爲以下兩個步驟:

  1. 創建自定義攔截器,實現 HandlerInterceptor 接口的 preHandle(執行具體方法之前的預處理) 方法。
  2. 將自定義攔截器加入 WebMvcConfigurer 的 addInterceptors 方法中。

1. 自定義攔截器

接下來使用代碼來實現一個用戶登錄的權限效驗,自定義攔截器是一個普通類,具體實現代碼如下: 

@Slf4j
@Component  // 攔截器,如果用戶未登錄,則直接重定向到登錄頁,不會走到後續操作
public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("MyInterceptor.preHandle()");
        User currentUser = null;
        HttpSession session = request.getSession();
        if(session != null){
            currentUser = (User)session.getAttribute("currentUser");
        }
        if(currentUser == null){
            // 說明用戶未登錄
            response.sendRedirect("/login.html");
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("MyInterceptor.postHandle()");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("MyInterceptor.afterCompletion()");
    }
}

2. 將自定義攔截器加入到系統配置

將上一步中的自定義攔截器加入到系統配置信息中,具體實現代碼如下:

// 1. 必須是一個 Spring bean(否則沒有機會調用)
// 2. 必須實現了 WebMvcConfigurer 接口
@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final MyInterceptor myInterceptor;

    @Autowired
    public WebConfig(MyInterceptor myInterceptor) {
        this.myInterceptor = myInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 可以添加多個攔截器,先添加的攔截器,先被應用
        registry.addInterceptor(myInterceptor)
                .addPathPatterns("/**")               // 應用到所有 URL 上
                .excludePathPatterns("/error")        // 只要有錯誤,都會到這
                .excludePathPatterns("/login.html")   // 但不包括login.html
                .excludePathPatterns("/login.do");    // 但不包括login.do
    }
}
  • addPathPatterns:表示需要攔截的 URL,“**”表示攔截任意方法(也就是所有方法)。
  • excludePathPatterns:表示需要排除的 URL。

說明:以上攔截規則可以攔截此項目中的使用 URL,包括靜態文件(圖片文件、JS 和 CSS 等文件)。

1.4 攔截器實現原理

正常情況下的調用順序:

 

 

然而有了攔截器之後,會在調用 Controller 之前進行相應的業務處理,執行的流程如下圖所示:

 

 

 

1. 實現原理源碼分析

  1. 所有的 Controller 執行都會通過一個調度器 DispatcherServlet 來實現 。
  2. 在開始執行 Controller 之前,會先調用 預處理方法 applyPreHandle。
  3. 在 applyPreHandle 中會獲取所有的攔截器 HandlerInterceptor 並執行攔截器中 的 preHandle 方法。

2. 攔截器小結

通過上面的源碼分析,我們可以看出,Spring 中的攔截器也是通過動態代理和環繞通知的思想實現的, 大體的調用流程如下:

 

 


✨2. 統一異常處理

統一異常處理使用的是 @ControllerAdvice + @ExceptionHandler 來實現的

  • @ControllerAdvice 表示 控制器通知類
  • @ExceptionHandler 是異常處理器

兩個結合表示當出現異常的時候執行某個通知,也 就是執行某個方法事件,具體實現代碼如下: 

@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
    @ExceptionHandler(Exception.class)
    public Object exceptionAdvice(Exception e){
        HashMap<String, Object> result = new HashMap<>();
        result.put("success", -1);
        result.put("message", "總的異常信息:" + e.getMessage());
        result.put("data", null);
        return result;
    }
    @ExceptionHandler(NullPointerException.class)
    public Object NullPointerExceptionAdvice(NullPointerException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("success", -1);
        result.put("message", "空指針異常:" + e.getMessage());
        result.put("data", null);
        return result;
    }
}
  • 如果出現了異常就返回給前端一個 HashMap 的對象,其中包含的字段如代碼中定義的那樣。
  • 我們可以針對不同的異常,返回不同的結果。
  • 當有多個異常通知時,匹配順序爲當前類及其子類向上依次匹配。

✨3. 統一數據返回格式

3.1 爲什麼需要統一數據返回格式?

統一數據返回格式的優點有很多,比如以下幾個:

  1. 方便前端程序員更好的接收和解析後端數據接口返回的數據。
  2. 降低前端程序員和後端程序員的溝通成本,按照某個格式實現就行了,因爲所有接口都是這樣返回 的。
  3. 有利於項目統一數據的維護和修改。
  4. 有利於後端技術部門的統一規範的標準制定,不會出現稀奇古怪的返回內容。 

3.2 統一數據返回格式的實現

統一的數據返回格式可以使用 @ControllerAdvice + ResponseBodyAdvice 的方式實現,具體實現代碼如下: 

@ControllerAdvice
public class ResBodyAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        // 內容是否需要重寫(通過此方法可以選擇性部分控制器和方法進行重寫)
        // 返回 true 表示重寫
        return false;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 構造統一返回對象
        HashMap<String, Object> result = new HashMap<>();
        result.put("success", 1);
        result.put("message", "");
        result.put("data", body);
        return result;
    }
}

轉載:https://www.lmlphp.com/user/610796/article/item/10630125

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