自定义注解在项目开发过程中非常有用,当框架提供的注解无法满足我们的业务逻辑需求时会需要我们自定义注解,了解自定义注解之前需要先了解元注解,即所谓注解的注解,本文不详聊元注解的概念,简单粗暴上示例代码演示几种常见的自定义注解方式,想了解元注解的可以查看JAVA编程思想第四版第二十章注解一章,或者直接网上找博客内容会有很多,下面开始正文。


文章目录

  • Controller层注解-结合spring拦截器自定义注解
  • 1. 自定义注解
  • 2. 自定义拦截器处理注解
  • 3. 将自定义拦截器注册进webMvc拦截器链并定义拦截路由
  • 4. 编写测试Controller类
  • Service层注解-结合SpringAOP自定义注解
  • 1. 自定义注解
  • 2.自定义切面处理注解
  • 3. 测试注解
  • 参数校验型自定义注解
  • 1.自定义注解
  • 2.定义@Constraint中用到的校验规则类
  • 3.定义对应的子类重写父类中的方法,即相关的校验逻辑
  • 4.注解的使用
  • 总结


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,不会走进方法中的具体打印。

自定义注解basePackages无效果_spring

其他例子也可自行分析后自测体验下。

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智能提示非常方便,当你的切面定义好后,在增强方法对应地方光标会显示增强了哪些方法,比如按照我上面的写法,会出现如下提示:

自定义注解basePackages无效果_spring_02


自定义注解basePackages无效果_自定义注解_03

3. 测试注解

如上面切面定义好了,在切面中我们做了一件事就是在执行service方法的前后打印了两句话(在实际业务中可以是在执行service方法前后进行一些处理,比如打印入参,返回值或其他功能),接下来通过postman跑下对应的接口,得到如下响应:

自定义注解basePackages无效果_拦截器_04


可以看到虽然我们在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的提示也会显示出来,如下图:

自定义注解basePackages无效果_拦截器_05

因为此时被增强的方法多了,所以点击时候会显示都有哪些被增强。此时我们再访问刚才的接口,得到的响应如下:

自定义注解basePackages无效果_spring_06

可看到在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,就会无法通过校验,如下:

自定义注解basePackages无效果_拦截器_07

如果我们传入正确的值,就会通过校验,执行方法后面的代码:

自定义注解basePackages无效果_java_08

总结

以上总结了结合拦截器,aop,以及@Constraint注解来处理自定义注解的案例,在实际开发中自定义注解是比较有用的,可以方便我们开发。需要注意的是,自定义注解的使用并不是一定要结合上面三种情况,我们知道注解通过反射可以拿到,那么有时候我们在类属性字段上的注解只需要通过反射获取之后,进行对应的判定和业务逻辑处理即可。