自定義註解示例

自定義註解在項目開發過程中非常有用,當框架提供的註解無法滿足我們的業務邏輯需求時會需要我們自定義註解,瞭解自定義註解之前需要先了解元註解,即所謂註解的註解,本文不詳聊元註解的概念,簡單粗暴上示例代碼演示幾種常見的自定義註解方式,想了解元註解的可以查看JAVA編程思想第四版第二十章註解一章,或者直接網上找博客內容會有很多,下面開始正文。

Controller層註解-結合spring攔截器自定義註解

針對controller層的註解,我們一般可以採用自定義註解結合spring攔截器的方式:

1. 自定義註解

首先你需要自定義一個註解,註解的定義採用關鍵字@interface來定義,如下

//關於元註解的知識可以網上查看資料
@Retention(RetentionPolicy.RUNTIME)
//該註解只用在方法上,用在其他地方的可以查看元註解的其他枚舉值,不贅述
@Target(ElementType.METHOD)
public @interface MyselfAnnotion {
    //註解的變量支持基本數據類型,字符串,枚舉,以及對應的數組類型
    String name() default "guanyu";
    int age() default 18;
    boolean isHero() default true;
    String[] bros() default {};
}

2. 自定義攔截器處理註解

當你定義好了註解後,你需要自定義處理該註解的攔截器,在攔截器中進行業務的處理,自定義攔截器實現spring提供的接口HandlerInterceptor即可,如下

spring提供的攔截器接口如下:

public interface HandlerInterceptor {

  //請求發送到Controller之前調用
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        return true;
    }
    //請求發送到Controller之後調用
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable ModelAndView modelAndView) throws Exception {
    }

    //完成請求的處理的回調方法
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable Exception ex) throws Exception {
    }

}

可以看到spring提供的攔截器接口主要有三個方法,分別用來處理不同階段的請求,按照自己項目的需要重寫其中的方法即可,比如有時候在controller你需要自定義一個註解來檢測用戶是否登錄,登錄之後將用戶信息存在threadLocal中供後續調用,那麼你可以在preHandle方法中實現相應的業務邏輯,當一次請求完成之後,你又需要將threadlocal中的信息remove掉,那麼你可以在afterCompletion方法中進行釋放,本例子中展示簡單的在請求發送到controller之前的處理,因此只重寫preHandle方法,如下:

@Component
@Slf4j
public class MyAnnotionInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod){
            HandlerMethod method = (HandlerMethod) handler;
            //1獲取方法上的註解
            MyselfAnnotion methodAnnotation = method.getMethodAnnotation(MyselfAnnotion.class);
            //2獲取類上的註解
            //MyselfAnnotion annotation = method.getBeanType().getAnnotation(MyselfAnnotion.class);
            //3獲取類中屬性的註解
            /*Field[] declaredFields = method.getBeanType().getDeclaredFields();
            for (Field field:declaredFields){
                field.setAccessible(true);
                MyselfAnnotion annotation = field.getAnnotation(MyselfAnnotion.class);
            }*/
            if (null == methodAnnotation){
                return true;
            }
            if (MyAnnotionEnum.GUANYU.getName().equals(methodAnnotation.name())){
                System.out.println("攔截器檢測到是五虎上將" + methodAnnotation.name());
            }else {
                System.out.println("攔截器檢測到不是關羽,是"+ methodAnnotation.name() +"不能通關");
                return false;
            }
            if (MyAnnotionEnum.GUANYU.getAge().equals(methodAnnotation.age())){
                System.out.println("攔截器檢測到這是18歲的關羽,威猛,過關");
                return true;
            }else {
                System.out.println("攔截器檢測到這個關羽老了,不能通關");
                return false;
            }
       }
        return false;
    }
}
//攔截器中用到的枚舉類
@Getter
@AllArgsConstructor
public enum MyAnnotionEnum {
    LIUBEI("liubei",48),
    ZHANGFEI("zhangfei",24),
    GUANYU("guanyu",18);
    
    private String name;
    private Integer age;
}

上面自定義的攔截器中,在preHandle方法中有如下的邏輯,首先獲取判定該handler是否是handlermethod實例,關於handlermethod可以理解爲存儲着controller中每個@RequestMapping註解方法的對象,可以上官網瞭解下:springmvc,這裏簡單說下理解:

Spring MVC應用啓動時會蒐集並分析每個Web控制器方法,從中提取對應的"<請求匹配條件,控制器方法>"映射關係,形成一個映射關係表保存在一個RequestMappingHandlerMapping bean中。然後在客戶請求到達時,再使用RequestMappingHandlerMapping中的該映射關係表找到相應的控制器方法去處理該請求。在RequestMappingHandlerMapping中保存的每個”<請求匹配條件,控制器方法>"映射關係對兒中,"請求匹配條件"通過RequestMappingInfo包裝和表示,而"控制器方法"則通過HandlerMethod來包裝和表示。(想了解這部分內容的可以查看spring技術內幕-計文柯第二版中p166,第4.4.4小節 Mvc處理HTTP分發請求這一小節,也可以看看spring源碼深度解析-郝佳一書中p291,第11章 springmvc)

一個HandlerMethod對象,可以認爲是對如下信息的一個包裝 :
Object bean Web控制器方法所在的Web控制器bean。可以是字符串,代表bean的名稱;也可以是bean實例對象本身。
Class beanType Web控制器方法所在的Web控制器bean的類型,如果該bean被代理,這裏記錄的是被代理的用戶類信息
Method method Web控制器方法
Method bridgedMethod 被橋接的Web控制器方法
MethodParameter[] parameters Web控制器方法的參數信息:所在類所在方法,參數,索引,參數類型
HttpStatus responseStatus 註解@ResponseStatus的code屬性
String responseStatusReason 註解@ResponseStatus的reason屬性

如果該handler是handlermethod實例,則判斷是否有自定義註解MyselfAnnotion在方法上,如果沒有直接返回true放行,如果有繼續判斷註解中name是否關羽,是否年齡18,兩者都滿足就放行,不滿足則不放行。

3. 將自定義攔截器註冊進webMvc攔截器鏈並定義攔截路由

/**
 * 註冊攔截器,攔截特定請求
 */
@Configuration
public class MyAnnotionInterceptorConfig extends WebMvcConfigurationSupport {
    //注入自定義的攔截器
    @Autowired
    private MyAnnotionInterceptor myAnnotionInterceptor;
    //註冊攔截器並定義攔截路由
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(myAnnotionInterceptor).addPathPatterns("/testMyselfAnno/**");
        super.addInterceptors(registry);
    }
}

至此,結合攔截器自定義的註解已經完成,可以編寫測試類controller測試下,如下:

4. 編寫測試Controller類

 	@RequestMapping("/testMyselfAnno/no")
    @MyselfAnnotion(name = "guanyu", age = 50)
    public void testMyAnnotion1(){
        System.out.println("雖然是關羽,但是年齡大了,沒通過校驗");
    }

    @RequestMapping("/yes")
    @MyselfAnnotion(name = "guanyu", age = 50)
    public void testMyAnnotion2(){
        System.out.println("雖然有註解,但是路徑不屬於攔截範圍,通過校驗,進入方法體");
    }

    @RequestMapping("/testMyselfAnno/liubei/no")
    @MyselfAnnotion(name = "liubei", age = 48)
    public void testMyAnnotion3(){
        System.out.println("不是關羽,無法通過校驗");
    }

    @RequestMapping("/testMyselfAnno/defaultyes")
    @MyselfAnnotion
    public void testMyAnnotion4(){
        System.out.println("註解默認值是關羽,而且很年輕,通過校驗,進入方法體");
    }

我們依次在postman上訪問對應的接口(或者通過spring-test包的mockmvc去mock)查看對應結果:

1)訪問方法testMyAnnotion1上的路由 /testMyselfAnno/no

首先該路徑能夠匹配上我們在配置中配的要攔截的路徑,且該方法上含有@MyselfAnnotion自定義註解,其中name的確是關羽,所以在攔截器處理時會打印出"攔截器檢測到是五虎上將guanyu",之後判定年齡,由於註解中年齡爲50,攔截器處理時會打印出"攔截器檢測到這個關羽老了,不能通關",之後返回false,不會走進方法中的具體打印。
顯示

其他例子也可自行分析後自測體驗下。

Service層註解-結合SpringAOP自定義註解

1. 自定義註解

註解還是採用上文中的註解@MyselfAnnotion

2.自定義切面處理註解

在自定義切面之前,先在之前的Controller層新增加一個接口如下:

@RestController
public class MyAnnotionTestController{
    
    @Autowired
    private MyAnnotionTestService myAnnotionTestService;

    @RequestMapping("/test/aop")
    @MyselfAnnotion
    public void testMyAnnotion5(){
        System.out.println("我來自Controller層,我用來測試自定義註解");
        myAnnotionTestService.testAopAnnotion();
    }

}

之後定義service層接口及實現類如下:

//接口
public interface MyAnnotionTestService {
    void testAopAnnotion();
}
//實現類
@Service
public class MyAnnotionTestServiceImpl implements MyAnnotionTestService {
    @Override
    @MyselfAnnotion
    public void testAopAnnotion() {
        System.out.println("我來自service層,我用來測試自定義註解");
    }
}

由於本案例展示service層的註解與AOP切面的結合,所以暫時在示例時指定切點表達式具體到service層,下面定義切面,如下

@Aspect
@Component
public class MyselfAnnotionAspect {
    //通過切點表達式定義切點
    @Pointcut("execution(* com.enjoyican.demo.selfannotion.service.impl..*(..))")
    public void myPointCut(){};
    @Pointcut("@annotation(MyselfAnnotion)")
    public void myAnnoCut(){};

    //定義方法增強類型(本例子採用環繞增強)
    @Around("myAnnoCut()&&myPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        System.out.println("AOP切面在執行service方法之前增強");
        point.proceed();
        System.out.println("AOP切面在執行service方法之後增強");
        return null;
    }
}

首先上面的切面定義了兩個切點,一個是定義到service.impl包下,另一個定義成帶有註解MyselfAnnotion的類,然後定義一個環繞增強,切點表達式採用兩者結合即可定位到對應的service層下面打了@MyselfAnnotion註解的類中(本例子之所以這麼定義是爲了稍後演示方便,切點表達式也可以合成一個,後面我會專門說下spring中切點表達式的案例的)

現在的IDEA智能提示非常方便,當你的切面定義好後,在增強方法對應地方光標會顯示增強了哪些方法,比如按照我上面的寫法,會出現如下提示:
2
IDEA提示

3. 測試註解

如上面切面定義好了,在切面中我們做了一件事就是在執行service方法的前後打印了兩句話(在實際業務中可以是在執行service方法前後進行一些處理,比如打印入參,返回值或其他功能),接下來通過postman跑下對應的接口,得到如下響應:
註解響應
可以看到雖然我們在controller層和service層都加了@MyselfAnnotion註解,但是我們切面只處理service層的,所以在打印controller層的“我來自Controller層,我用來測試自定義註解”這句話的前後並沒有進行增強,而service按照我們想的進行了方法增強。

上面舉的例子比較簡單,實際業務中將註解用到service層某些方法之上來實現我們的業務邏輯的情況很常見,相比僅僅用切面去控制,通過一個註解控制的力度更細更靈活一些,也更方便操作。

另外本例中雖然以在service層通過註解加AOP的形式來自定義註解處理業務邏輯,但實際上controller層的有些場景也可以用aop來控制,並不是必須要採用攔截器,這個要看你具體想獲取的是哪些信息以及需要在哪個階段增強有關,如果用aop控制的話也就是切點表達式怎麼寫的問題。

比如,在上面例子中,加入將環繞增強變成如下形式:

//定義方法增強類型(本例子採用環繞增強)
    @Around("myAnnoCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {

        System.out.println("AOP切面在執行service方法之前增強");
        point.proceed();
        System.out.println("AOP切面在執行service方法之後增強");
        return null;
    }

此時我們的增強對象是所有打了@MyselfAnnotion註解的方法,因此這個情況下controller層的方法也會被增強,此時IDEA的提示也會顯示出來,如下圖:

IDEA提示

因爲此時被增強的方法多了,所以點擊時候會顯示都有哪些被增強。此時我們再訪問剛纔的接口,得到的響應如下:

註解響應

可看到在controller和service層方法執行前後,都被增強了(忽略上面AOP提示中都是顯示service增強

參數校驗型自定義註解

在開發過程中,還有一類註解不得不提,就是通常用來對方法入參進行校驗的註解。對方法入參進行校驗,當然也可以通過aop來處理,通過獲取屬性上的註解,進行判定。在springmvc中,我們藉助常用的hibernate-validator即可實現大部分入參的校驗,關於使用hibernate-validator進行校驗的知識,可以參考官方文檔或者從網上搜索相關資源即可,本小結不對此做特別說明,也可以看下如下博客基於註解校驗入參spring組件參數校驗

有時候我們的需求在現有的註解中可能沒找到合適的,這時候可能需要我們自定義註解 ,本例子中說明一個自定義校驗入參中枚舉值是否符合現有枚舉值的自定義註解,前置知識可以看下這篇博客

1.自定義註解

@Documented
//注意這個註解
@Constraint(validatedBy = {ValidEnumValueValidator.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
public @interface ValidEnumValue {

    String message() default "不是有效的枚舉值";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
	//這裏的ValidityInterpretable接口在後面定義
    Class<? extends ValidityInterpretable> enumType();

}

2.定義@Constraint中用到的校驗規則類

在本例中即爲ValidEnumValueValidator,注意需要實現ConstraintValidator<ValidEnumValue, Integer>接口,泛型接口中的兩個參數一個是自定義的註解ValidEnumValue,另外一個爲自定義的校驗註解中校驗值的類型,根據實際業務需要確定

定義如下:

public class ValidEnumValueValidator implements ConstraintValidator<ValidEnumValue, Integer> {

    private ValidityInterpretable validityInterpretable;

    private boolean isEmpty;

    @Override
    public void initialize(ValidEnumValue constraintAnnotation) {
        //初始化加載所有枚舉類
        ValidityInterpretable[] enumConstants = constraintAnnotation.enumType().getEnumConstants();
        if (enumConstants.length == 0) {
            isEmpty = true;
        }
        validityInterpretable = enumConstants[0];
    }

    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        //判斷有效性的具體邏輯由子類重寫
        if (value == null) {
            return true;
        }
        if (isEmpty) {
            return false;
        }
        return validityInterpretable.isValid(value);
    }

}

3.定義對應的子類重寫父類中的方法,即相關的校驗邏輯

首先定義註解中用到的ValidityInterpretable接口

public interface ValidityInterpretable {
    /**
     * 判斷value對該enum而言是否是有效的枚舉值
     */
    boolean isValid(Integer value);

}

其次定義一個子類實現該接口,重寫校驗的方法,該子類爲我們枚舉值定義的類:

@AllArgsConstructor
@Getter
public enum IdolOrderEnum implements ValidityInterpretable {

    /**
     * 蔡徐坤
     */
    CAI_XU_KUN(1),

    /**
     * 陳立農
     */
    CHEN_LI_NONG(2),

    /**
     * 範丞丞
     */
    FAN_CHEN_CHEN(3);

    private Integer value;

    @Override
    public boolean isValid(Integer value) {
        return Arrays.stream(values()).anyMatch(one -> one.getValue().equals(value));
    }
}

在上面的枚舉我們定義了一個偶像排名的枚舉類(此處引用偶像練習生ninepercent出道排名,沒別的原因,僅僅是今天微博熱搜上看到的),其中提供了一個isValid方法用來校驗所有的枚舉值value中有沒有與我傳入的value相同的,有說明該入參符合規範,沒有說明入參不符合規矩,下面編寫一個測試類來說明

4.註解的使用

註解的使用,在入參dto中需要校驗字段有效性的地方,打上自定義的註解,來判斷前端或者api調用中對方傳來的參數是否符合要求:

首先定義一個入參DTO對象:

@Data
@ToString
public class MyRequest {
    @NotNull
    //此處打上對應的註解,註明校驗的枚舉類
    @ValidEnumValue(enumType = IdolOrderEnum.class)
    private Integer order;

    private String name;
}

之後依然採用之前的MyAnnotionTestController在其中添加一個方法如下:

@RequestMapping("/test/validator")
    public void testMyAnnotion6(@RequestBody @Valid MyRequest request, BindingResult result){
        if (result.hasErrors()){
            List<FieldError> fieldErrors = result.getFieldErrors();
            for (FieldError error : fieldErrors) {
                //可以返回具體的錯誤異常
                System.out.println(error.getField()+error.getDefaultMessage());
            }
        }
        System.out.println("測試入參校驗自定義註解,入參:"+request.toString());
    }

之後用postman訪問,加入我們傳入一個枚舉中沒有的值,order=4,就會無法通過校驗,如下:

註解響應

如果我們傳入正確的值,就會通過校驗,執行方法後面的代碼:

註解響應

總結

以上總結了結合攔截器,aop,以及@Constraint註解來處理自定義註解的案例,在實際開發中自定義註解是比較有用的,可以方便我們開發。需要注意的是,自定義註解的使用並不是一定要結合上面三種情況,我們知道註解通過反射可以拿到,那麼有時候我們在類屬性字段上的註解只需要通過反射獲取之後,進行對應的判定和業務邏輯處理即可。

如有疑問,可在我的個人博客spring自定義註解下留言或者在CSDN留言即可

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