SpringBoot系列之攔截器注入Bean的幾種姿勢

小灰灰Blog發表於2021-11-15

之前介紹過一篇攔截器的基本使用姿勢: 【WEB系列】SpringBoot之攔截器Interceptor使用姿勢介紹

在SpringBoot中,通過實現WebMvcConfigureraddInterceptors方法來註冊攔截器,那麼當我們的攔截器中希望使用Bean時,可以怎麼整?

<!-- more -->

I. 專案搭建

本專案藉助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA進行開發

開一個web服務用於測試

<dependencies>
    <!-- 郵件傳送的核心依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

II.攔截器

實現攔截器比較簡單,實現HandlerInterceptor介面就可以了,比如我們實現一個基礎的許可權校驗的攔截器,通過從請求頭中獲取引數,當滿足條件時表示通過

0.安全校驗攔截器

@Slf4j
public class SecurityInterceptor implements HandlerInterceptor {
    /**
     * 在執行具體的Controller方法之前呼叫
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 一個簡單的安全校驗,要求請求頭中必須包含 req-name : yihuihui
        String header = request.getHeader("req-name");
        if ("yihuihui".equals(header)) {
            return true;
        }

        log.info("請求頭錯誤: {}", header);
        return false;
    }

    /**
     * controller執行完畢之後被呼叫,在 DispatcherServlet 進行檢視返回渲染之前被呼叫,
     * 所以我們可以在這個方法中對 Controller 處理之後的 ModelAndView 物件進行操作。
     * <p>
     * preHandler 返回false,這個也不會執行
     *
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("執行完畢!");
        response.setHeader("res", "postHandler");
    }


    /**
     * 方法需要在當前對應的 Interceptor 類的 preHandle 方法返回值為 true 時才會執行。
     * 顧名思義,該方法將在整個請求結束之後,也就是在 DispatcherServlet 渲染了對應的檢視之後執行。此方法主要用來進行資源清理。
     *
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("回收");
    }
}

接下來是這個攔截器的註冊

@RestController
@SpringBootApplication
public class Application implements WebMvcConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/**");
    }

    @GetMapping(path = "show")
    public String show() {
        return UUID.randomUUID().toString();
    }
}

接下來問題來了,我們希望這個用於校驗的值放在配置檔案中,不是在程式碼中寫死,可以怎麼整?

1. 指定配置

在專案資原始檔中,新增一個配置用於表示校驗的請求頭

application.yml

security:
  check: yihuihui

配置的讀取,可以使用 Envrioment.getProperty(),也可以使用 @Value註解

但是注意上面的攔截器註冊,直接構造的一個方法,新增到InterceptorRegistry,在攔截器中,即使新增@Value@Autowired註解也不會生效(歸根結底就是這個攔截器並沒有受Spring上下文管理)

2. 攔截器注入Bean

那麼在攔截器中如果想使用Spring容器中的bean物件,可以怎麼整?

2.1 新增靜態的ApplicationContext容器類

一個可行的方法就是在專案中維護一個工具類,其內部持有ApplicationContext的引用,通過這個工具類來訪問bean物件

@Component
public class SpringUtil implements ApplicationContextAware, EnvironmentAware {
    private static ApplicationContext applicationContext;
    private static Environment environment;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtil.applicationContext = applicationContext;
    }

    @Override
    public void setEnvironment(Environment environment) {
        SpringUtil.environment = environment;
    }

    public static <T> T getBean(Class<T> clz) {
        return applicationContext.getBean(clz);
    }

    public static String getProperty(String key) {
        return environment.getProperty(key);
    }
}

基於此,在攔截器中,如果想要獲取配置,直接改成下面這樣既可

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 一個簡單的安全校驗,要求請求頭中必須包含 req-name : yihuihui
        String header = request.getHeader("req-name");
        if (Objects.equals(SpringUtil.getProperty("security.check"), header)) {
            return true;
        }

        log.info("請求頭錯誤: {}", header);
        return false;
    }

這種方式來訪問bean,優點就是通用性更強,適用範圍廣

2.2 攔截器註冊為bean

上面的方法雖然可行,但是看起來總歸不那麼優雅,那麼有辦法直接將攔截器宣告為bean物件,然後直接使用@Autowired註解來注入依賴的bean麼

當然是可行的,注意bean註冊的幾種姿勢,我們這裡採用下面這種方式來註冊攔截器

@Bean
public SecurityInterceptor securityInterceptor() {
    return new SecurityInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(securityInterceptor()).addPathPatterns("/**");
}

上面通過配置類的方式來宣告bean,然後在註冊攔截器的地方,不直接使用構造方法來建立例項;上面的用法表示是使用spring的bean容器來註冊,基於這種方式來實現攔截器的bean宣告

因此在攔截器中就可以注入其他依賴了

測試就比較簡單了,如下

yihui@M-162D9NNES031U:SpringBlog git:(master) $ curl 'http://127.0.0.1:8080/show' -H 'req-name:yihuihui' -i
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 36
Date: Mon, 15 Nov 2021 10:56:30 GMT

6610e593-7c60-4dab-97b7-cc671c27762d%

3. 小結

本文雖說介紹的是如何在攔截器中注入bean,實際上的知識點依然是建立bean物件的幾種姿勢;上面提供了兩種常見的方式,一個SpringUtil持有SpringContext,然後藉助這個工具類來訪問bean物件,巧用它可以省很多事;

另外一個就是將攔截器宣告為bean,這種方式主要需要注意的點是攔截器的註冊時,不能直接new 攔截器;當然bean的建立,除了上面這個方式之外,還有其他的case,有興趣的小夥伴可以嘗試一下

III. 不能錯過的原始碼和相關知識點

0. 專案

相關博文:

專案原始碼:

1. 微信公眾號: 一灰灰Blog

盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛

一灰灰blog

相關文章