# [Java]Customize Annotation In Spring Boot 平常很常使用 Spring boot 提供給我們的 Annotation,實在是有夠方便。 如果也可以自己做一些 Annotation,就可以幫我做完一堆事情,感覺就很潮! 那到底該怎麼做呢? ## 情境 這邊我用 JWT 當作一個情境, 當我以下兩隻 API, ```java @GetMapping("/generate-token") public String generateToken() { return jwtUtil.generateToken("user123"); } @GetMapping("/protected-resource") public String protectedResource() { return "This is a protected resource"; } ``` 而我想要產生一個 token 時,我希望不要驗證我有沒有 token (啊我就要產生,你還要驗證我幹嘛!!), 而當我要存取 protected-resource 時,我希望我有 token 才能存取。 那麼這兩隻的 API 需求就不一樣, 如果我使用 OncePerRequestFilter 來做的話,那麼所有的 Request 都會被抓去驗證。 有沒有辦法在我需要被驗證的 API 上面加上一個特定 Annotation,他就會需要驗證 token才可以存取,而不需要被驗證的 API 不加這個特定的 Annotation 就不會被驗證。 ## 測試專案 1. 先把我的 [專案](https://github.com/mister33221/customize-annotation-practice.git) clone 下來。 直接給他跑起來! 2. 然後在瀏覽器出入 `http://localhost:8080/swagger-ui.html` 來開啟我們的 Swagger UI。 3. 第一步直接點擊 `protected-resource` 這個 API,會發現我們沒有 token,所以會被拒絕存取。 ![image](https://hackmd.io/_uploads/rk9h4GOu0.png) 4. 那我們就來使用 `generate-token` 產生一個 token, ![image](https://hackmd.io/_uploads/rJMAEzO_A.png) 5. 把這個 token 複製下來,然後到右上角,會看到一個 `Authorize` 的按鈕,點進去之後,把 token 貼上去,然後按下 `Authorize`。 這樣我們就會在接下來的 Request 的 Header 裡面帶上一個叫做 `Authorization` 的 Header,裡面的值就是我們剛剛貼上去的 token。 ![image](https://hackmd.io/_uploads/HJQOSfuO0.png) 6. 接著我們就可以使用 `protected-resource` 來存取我們的資源。 ![image](https://hackmd.io/_uploads/HyMcSzuu0.png) ## 實作 其實我在每一個 class 裡面都已經寫了非常詳細該怎麼自己製作 Annotation,所以 README 裡面我就不寫得太詳細囉! 我甚至也順便把 OpenApi Swagger 的也寫了! 有興趣的就到我的 github [專案](https://github.com/mister33221/customize-annotation-practice.git) 看看吧! 主要的程式碼如下 - RequireJwt ```java /* @Target註解指定了RequireJwt註解可以應用的Java元素類型。 在這個例子中,它被設置為ElementType.METHOD, 這意味著RequireJwt註解只能被應用於方法上。 ElementType是一個枚舉,它還包括了其他值, 如TYPE(可以應用於類或接口)、FIELD(可以應用於字段)等。 */ /* @Retention註解指定了RequireJwt註解的保留策略, 即這個註解在什麼時候有效。在這個例子中, 它被設置為RetentionPolicy.RUNTIME, 這意味著RequireJwt註解將在運行時保留, 因此可以通過反射在運行時被訪問。 其他的RetentionPolicy值包括 SOURCE(註解只在源碼中保留,並在編譯時被丟棄)和 CLASS(註解在編譯時被保留,但在運行時不可用)。 */ /* interface 是用來定義一個介面,裡面可以包含方法、屬性、類等等。可以實作,也可以不實作, 別的物件可以 implements 這個介面,然後實作裡面的方法。 @interface 是專門用來定義 Annotation 的, */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireJwt { } /* 當你在寫 Annotation 的時候,有些 Annotation 可以填入一些參數來讓使用者可以自定義某些行為。 不過在我們的範例裡面沒有用到,所以才是空白的。 我們可以定義以下這些東西來讓使用者可以自定義 Annotation 的行為: 基本類型:如int, float, boolean等。 String:字符串類型。 Class:類字面量,如MyClass.class。 枚舉:枚舉類型的值。 註解:其他註解。 以上類型的數組:例如int[], String[], MyEnum[]等。 以下是一個範例 // 定義一個枚舉類型作為註解的一部分 enum Status { ACTIVE, INACTIVE, PENDING } // 定義另一個註解類型作為註解的一部分 @interface AdditionalInfo { String description() default "No description"; } // 定義ComplexAnnotation註解 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) // 假設這個註解是用於類型上 public @interface ComplexAnnotation { // 基本類型 int intValue() default 0; float floatValue() default 0.0f; boolean booleanValue() default true; // String String stringValue() default "Default"; // Class Class<?> classValue() default Object.class; // 枚舉 Status statusValue() default Status.ACTIVE; // 註解 AdditionalInfo additionalInfo() default @AdditionalInfo; // 以上類型的數組 int[] intArray() default {}; String[] stringArray() default {}; Status[] statusArray() default {}; Class<?>[] classArray() default {}; } 而當別人要使用這個 Annotation 的時候,可以這樣使用: // 假設有一個類,用於演示如何使用ComplexAnnotation註解 @ComplexAnnotation( intValue = 10, floatValue = 5.5f, booleanValue = true, stringValue = "Example", classValue = MyClass.class, statusValue = Status.ACTIVE, additionalInfo = @AdditionalInfo(description = "This is an example class"), intArray = {1, 2, 3}, stringArray = {"one", "two", "three"}, statusArray = {Status.ACTIVE, Status.INACTIVE}, classArray = {MyClass.class, YourClass.class} ) public class MyClass { // 類的實現細節 } */ ``` - WebMvcConfig ```java /* WebMvcConfigurer 它提供了一系列的回調方法(callback methods), 允許開發者對 Spring MVC 的配置進行細微的調整和擴展, 而不需要完全替換或修改現有的配置。 配置視圖解析器 (configureViewResolvers): 允許開發者自定義視圖解析的方式,從而控制如何將視圖名稱映射到具體的視圖實現上。 添加靜態資源處理器 (addResourceHandlers): 用於指定靜態資源的位置和如何處理它們,例如圖片、CSS 和 JavaScript 文件。 添加攔截器 (addInterceptors): 提供了一種方式來添加自定義攔截器,這些攔截器可以在請求處理之前或之後執行特定的操作。 配置跨域請求 (addCorsMappings): 允許為應用配置跨源資源共享(CORS)策略,從而控制哪些跨域請求是被允許的。 配置內容協商 (configureContentNegotiation): 使開發者能夠自定義內容協商的策略,以支援不同的媒體類型。 添加格式化器和轉換器 (addFormatters): 允許添加自定義的格式化器和轉換器,用於數據綁定和類型轉換。 配置異步支持 (configureAsyncSupport): 提供了配置異步請求處理的選項,例如設置默認的超時時間或自定義 Callable 和 DeferredResult 的處理方式。 */ @Configuration @AllArgsConstructor public class WebMvcConfig implements WebMvcConfigurer { private JwtInterceptor jwtInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 把我們自製的攔截器加入到攔截器鏈中 registry.addInterceptor(jwtInterceptor); } } ``` - JwtInterceptor ```java /* HandlerInterceptor接口用於攔截處理器的執行。 可以在控制器(Controller)處理請求之前、之後以及完成請求處理後(即渲染視圖之後)進行自定義的操作。 這對於實現如日誌記錄、身份驗證、授權等跨切面功能非常有用。 HandlerInterceptor接口定義了三個方法: preHandle(HttpServletRequest request, HttpServletResponse response, Object handler): 在控制器處理請求之前調用。如果返回true,則繼續向下執行(到下一個攔截器或處理器);如果返回false,則中斷執行流程。 postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView): 在控制器處理請求之後、渲染視圖之前調用。 afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex): 在完成請求處理後調用,這時可以進行資源清理等操作。 */ @Component @AllArgsConstructor public class JwtInterceptor implements HandlerInterceptor { private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { /* 這邊在確認 handler Object 是否是 HandlerMethod 的 instance, 但為什麼要做這樣的確認? 因為 HandlerMethod 是一個 Spring MVC 的類別, 它包含了一個方法的所有資訊,包括方法名稱、參數、返回值等等。 如果 handler 不是 HandlerMethod 的 instance, 那就代表這個請求可能不是映射到Controller中的一個方法, 而是其他類型的請求(例如對靜態資源的請求),則直接返回true, 表示不對這類請求進行後續的攔截處理。 */ if (!(handler instanceof HandlerMethod handlerMethod)) { return true; } /* 從 handlerMethod 中取得方法上的 RequireJwt annotation, */ RequireJwt requireJwt = handlerMethod.getMethodAnnotation(RequireJwt.class); if (requireJwt != null) { // 從 Headers 中取得 Authorization 的值 String token = request.getHeader("Authorization"); // 如果 Authorization 的值不存在或不是以 Bearer 開頭,則回傳 401 if (token == null || !token.startsWith("Bearer ")) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "No JWT token found in request headers"); return false; } // 取得 token 的值,去掉 Bearer 字串並對其進行各種驗證 token = token.substring(7); if (!jwtUtil.validateToken(token)) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token"); return false; } } return true; } } ``` - ApiController ```java @GetMapping("/protected-resource") @RequireJwt public String protectedResource() { return "This is a protected resource"; } @GetMapping("/generate-token") public String generateToken() { return jwtUtil.generateToken("user123"); } ```