# [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,所以會被拒絕存取。

4. 那我們就來使用 `generate-token` 產生一個 token,

5. 把這個 token 複製下來,然後到右上角,會看到一個 `Authorize` 的按鈕,點進去之後,把 token 貼上去,然後按下 `Authorize`。
這樣我們就會在接下來的 Request 的 Header 裡面帶上一個叫做 `Authorization` 的 Header,裡面的值就是我們剛剛貼上去的 token。

6. 接著我們就可以使用 `protected-resource` 來存取我們的資源。

## 實作
其實我在每一個 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");
}
```