阅读 383

【进阶之路】自定义注解介绍与实战

在使用spring框架的时候,我们经常会感叹注解式编程真是大大简化了开发的时间,几个小小的注解,就能解决一系列的配置问题,让写代码像写诗一样轻松明快。

我们都知道,在spring框架的前期,大多使用XML配置进行开发。XML配置起来有时候冗长,如实体类的映射,使用XML进行开发会显得十分复杂。同时注解在处理一些不变的元数据时有时候比XML方便的多,比如spring 声明式事务管理,如果用XML写的代码会多的多。注解与Java Bean紧密结合,既大大减少了配置文件的体积,又增加了Java Bean的可读性与内聚性

当然,不管使用注解还是XML,满足需求的前提下,采用最简单的方法才是最合适的。

今天我就以一个简单的例子来给大家讲解,如何进行自定义注解,帮助我们使用注解开发项目。

一、元注解

首先,我们定义一个类需要用到元注解。

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface NotifyMonitor {
    String value() default "";
}
复制代码

1、@Target

@Target注解,是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的。它使用一个枚举类型定义如下:


public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    /** 类,接口(包括注解类型)或枚举的声明 */
    TYPE,

    /** Field declaration (includes enum constants) */
    /** 属性的声明 */
    FIELD,

    /** Method declaration */
    /** 方法的声明 */
    METHOD,

    /** Formal parameter declaration */
    /** 方法形式参数声明 */
    PARAMETER,

    /** Constructor declaration */
    /** 构造方法的声明 */
    CONSTRUCTOR,

    /** Local variable declaration */
    /** 局部变量声明 */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    /** 注解类型声明 */
    ANNOTATION_TYPE,

    /** Package declaration */
    /** 包的声明 */
    PACKAGE,
}
复制代码

就像我们之前定义的,@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD,ElementType.TYPE})就是可以运用在注解、方法和类上。

2、@Retention

@Retention注解,翻译为持久力、保持力。即用来修饰自定义注解的生命力。 注解的生命周期有三个阶段:

  • 1、Java源文件阶段;
  • 2、编译到.class文件阶段;
  • 3、运行期阶段。
public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     (注解将被编译器忽略掉)
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     (注解将被编译器记录在class文件中,但在运行时不会被虚拟机保留,这是一个默认的行为)
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *(注解将被编译器记录在.class文件中,而且在运行时会被虚拟机保留,因此它们能通过反射被读取到)
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}
复制代码

我们使用的@Retention(RetentionPolicy.RUNTIME) 是让注解将被编译器记录在.class文件中,而且在运行时会被虚拟机保留,所以它能通过反射被读取到。

3、@Inherited

在注解上使用@Inherited 表示该注解会被子类继承,注意,仅针对类,成员属性、方法并不受此注释的影响。

对于类来说,子类要继承父类的注解需要该注解被 @Inherited 标识。

对于成员属性和方法来说,非重写的都会保持和父类一样的注解,而被实现的抽象方法,被重写的方法都不会有父类的注解。

当@NotifyMonitor注解加在某个类A上时,假如类B继承了A,则B也会带上该注解。

我们可以看到,在springboot,很多类也加上了这个注解。

4、@Documented

除了我们在注解类上应用到的之外,@Documented注解的作用是在使用 javadoc 工具为类生成帮助文档时保留其注解信息。

如果去掉了这个注解,那么在生成的工具文档上就不会出现这个注解,对于一些内部工具类注解来说可有无可。

二、利用AOP实现自定义注解

我们来实现下面这个场景,执行一个任务,如果任务报错,我们就通过钉钉通知指定的人员让他进行处理。

要实现这个功能,我们可能会想到try-catch方式。当然,没有什么不对,但是如果要在一百个不同的方法中加入这个逻辑,岂不是要实现100次?于是乎,使用自定义注解的方式或许是不错的主意。

我写了一个类来实现上诉方法:

@Slf4j
@Aspect
@Component
public class NotifyMonitorAspect {
    @Autowired
    private DingDingOpe dingDingOpe;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
	
    //相比大家对aop都不会陌生
    @Pointcut("@annotation(com.nanju.aop.NotifyMonitor)")
    private void monitor() {}

    /**
     * 处理任务
     point.proceed()是用来执行原来的任务
     dingDingOpe.sendRobotMsg  是自定了一个方法用来通知钉钉
     *
     * @param point
     */
    @Around("monitor()")
    public Object doAround(ProceedingJoinPoint point) {
        String jobName = getJobName(point);
        Object object = null;
        try {
             object = point.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            String url = getUrl(jobName);
            dingDingOpe.sendRobotMsg(url, "任务处理失败:"+"{"+throwable.getMessage()+"}", false);
        }
        return object;
    }


 /**
     * 获取Job名称,这个方法就是利用了NotifyMonitor中的value值,根据不同的方法使用不同的通知
     * @param point 切点
     */
    private String getJobName(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        NotifyMonitor jobs = method.getAnnotation(NotifyMonitor.class);
        if ("".equals(jobs.value())){
            return null;
        }
        return jobs.value();
    }
    
     /**
     * 根据Job名称获取通知地址,使用了stringRedisTemplate,提前将输入埋入redis,也可以放在数据库里,配置通知地址
     * @param notifyMonitor
     *
     */
    private String getUrl(String notifyMonitor) {
        if (Objects.isNull(notifyMonitor)){
            return TextUtils.dealNull(stringRedisTemplate.opsForValue().get("warn:dingdingUrl:"));
        }else{
            return  TextUtils.dealNull(stringRedisTemplate.opsForValue().get("warn:dingdingUrl:"+notifyMonitor));
        }
    }
}
复制代码

我们测试一下

1、在方法上加上注解@NotifyMonitor

2、调用方法

3、执行成功

我们还可以尝试一下,在@NotifyMonitor加上value(因为只有一个属性,所以value="xxx" 与 "xxx" 等价)

4、执行结果

这样,一个注解式的任务处理、通知功能就完成了。自定义注解不仅能够在方法执行前后进行扩展、获取到实现注解的方法、所在类等信息、修改参数和返回值,还能够实现包括线程池、分布式锁、类数据校验等等你能想到的大部分操作,我在工作中也实现了其中一些功能,减少了大量的重复代码,也让代码的可读性提高了。

了解到这里,不妨你也自己动手来写一个自定义注解来简化我们的项目吧。

大家好,我是练习java两年半时间的南橘,下面是我的微信,需要之前的导图或者想互相交流经验的小伙伴可以一起互相交流哦。

文章分类
后端
文章标签