本文转载自微信公众号「JavaFish」bynasus。转载本文请联系爪哇鱼公众号。大家好,我是郭大哥。随着开发经验的积累,越来越觉得基础真的很重要。例如:大多数框架(如Spring)都使用注解来简化代码,提高编码效率。掌握注解是一个JAVA程序员必备的技能。但是我发现很多工作2、3年的同学还没有写过自定义注解,被问到注解的原理时一头雾水。我惊呆了,你是怎么看懂代码的?基于此,今天我们就一起来学习一下注解。国际惯例,先上脑图:01什么是标注?Java注解(Annotation),相信你没用过也见过。在我的理解中,注解是代码中的特殊标签,可以在编译、类加载、运行时读取,进行相应的处理。注解和注解很像,不同的是注解是给人看的(想想遇到那些没有半句注解的业务代码,是不是还不舒服?);而注解是供程序阅读的,它可以被编译器Pick阅读。1.1注解的作用大多数时候,注解与反射或AOP方面结合使用。它们具有许多功能,例如标记和检查。最重要的一点是简化代码,降低耦合,提高执行效率。比如我们公司通过自定义注解和AOP切面的结合,解决了写接口重复提交的问题。简述我司防止重复提交注解的逻辑:请求写接口提交参数-拼接参数生成MD5码-将MD5码和用户信息拼接成key,设置Redis分布式锁,能顺利提交获取不到(分布式锁默认3秒过期),获取不到则重复提交,报错。如果每增加一个写接口都要写多个逻辑,程序员会抓狂的。于是,一些大佬通过注解+AOP方面的方式解决了这个问题。只需要在写接口的controller方法上加上这个注解就可以解决问题,也方便维护。1.2注解语法引入注解语法,使用我司自定义注解,防止重复提交。其定义如下://declareNoRepeatSubmit注解@Target(ElementType.METHOD)//meta-annotation@Retention(RetentionPolicy.RUNTIME)//meta-annotationpublic@interfaceNoRepeatSubmit{/***锁定时间,默认单位(秒))*/longlockTime()default3L;}Java注解都是用@interface修饰的,我们的NoRepeatSubmit注解也不例外。此外,还使用了两个元注释。其中,@Target注解传入ElementType.METHOD参数表示@NoRepeatSubmit只能用在方法上,@Retention(RetentionPolicy.RUNTIME)用于表示注解的生命周期为runtime。从代码上看,注解的定义和接口非常相似,编译后也会生成NoRepeatSubmit.class的定义。1.3注释元素在注释中定义的变量称为元素。注释可能有也可能没有元素。就像@Override是一个没有元素的注解,@SuppressWarnings是一个有元素的注解。@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public@interfaceOverride{}@Target({TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE,MODULE})@Retention(RetentionPolicy.SOURCE)public@interfaceSuppressWarnings{String[]value();}带有元素的自定义注解:@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceNoRepeatSubmit{/***锁定时间,默认单位(秒)*/longlockTime()default2L;}1.3.1注释元素格式//基本格式数据类型元素名();//带默认值的数据类型元素名()default默认值;1.3.2数据类型注解元素支持如下数据类型:所有基本类型(int、float、boolean、byte、double、char、long、short)StringClassenumAnnotation以上类型的数组在注解元素声明时可以使用基本类型允许使用任何包装器类型,注解也可以作为元素的类型,即嵌套注解。1.3.3编译器对元素默认值的限制遵循以下规则:元素要么有默认值,要么在使用注解时提供元素的值。对于非基本类型的元素,无论是在源码中声明还是在注解接口中定义为默认值,都不能使用null作为取值。1.4注解的使用注解在代码中以@注解名的形式使用,例如:以下常见的用法。publicclassTestController{//NoRepeatSubmit注解修改save方法防止重复提交@NoRepeatSubmitpublicstaticvoidsave(Objecto){//保存逻辑}//一个方法上可以有多个不同的注解@Deprecated@SuppressWarnings("uncheck")publicstaticvoidgetDate(){}}在保存方法上使用@NoRepeatSubmit(我们公司的自定义注解)。添加后,编译期会自动识别注解,并执行注解处理器的方法,防止重复提交;而对于@Deprecated和@SuppressWarnings("uncheck"),这是Java内置的注解,前者表示该方法已过时,后者则忽略指定的异常检查。02Java注解的分类上面介绍了注解的语法和用法。我们遇到过@Target和@Retention等我们以前从未见过的注解。你可能有点困惑。不过没关系,听我说。Java中内置了@Override、@Deprecated、@SuppressWarnings等注解;还有@Target、@Retention、@Documented、@Inherited等修改注解的注解,称为元注解。2.1内置注解Java自己定义了一套注解,作用于代码:@Override——检查方法是否为重写方法。如果发现父类或者引用的接口没有这个方法,就会报编译错误。@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public@interfaceOverride{}@Deprecated-标记已弃用的方法。如果使用这种方式,会报编译警告。@Documented@Retention(RetentionPolicy.RUNTIME)@Target(value={CONSTRUCTOR,FIELD,LOCAL_VARIABLE,METHOD,PACKAGE,PARAMETER,TYPE})public@interfaceDeprecated{}@SuppressWarnings-用于有选择地关闭类和方法的编译器,成员变量和变量初始化警告。@Target({TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE})@Retention(RetentionPolicy.SOURCE)public@interfaceSuppressWarnings{String[]value();}JDK7多加了3个,这些的用法,我也用得很小的。不做过多介绍,有兴趣的朋友分别百度:@SafeVarargs-Java7开始支持,忽略任何使用参数作为泛型变量的方法或构造函数调用产生的警告。@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.CONSTRUCTOR,ElementType.METHOD})public@interfaceSafeVarargs{}@FunctionalInterface-Java8开始支持,标识匿名函数或功能接口。@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public@interfaceFunctionalInterface{}@Repeatable-Java8支持,标记注解可以在同一条语句上多次使用。@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public@interfaceRepeatable{Classvalue();}2.2元注解元注解是修改注解的注解,分别为:2.2.1@Target指定注解的范围(如方法、类或字段),其中ElementType为枚举类型,定义如下,也代表可能的取值范围publicenumElementType{/**表示注解可以应用于classesandinterfaces(includingannotationstype)orenumdeclaration*/TYPE,/**表示注解可以应用于字段(domain)声明,包括enum实例*/FIELD,/**表示注解可以应用于methoddeclarations*/METHOD,/**表示Thisannotation可以应用于参数声明*/PARAMETER,/**表示annotation可以应用于constructordeclarations*/CONSTRUCTOR,/**表示annotations可应用于局部变量声明*/LOCAL_VARIABLE,/**表示注解可应用于注解声明(应用于另一个注解)*/ANNOTATION_TYPE,/**表示注解可应用于包声明*/PACKAGE,/***表示注解可以应用于类型参数声明(1.8新增)*@since1.8*/TYPE_PARAMETER,/***类型使用声明(1.8新增)*@since1.8*/TYPE_USE}PS:如果@Target没有指定作用域,默认可以作用于任意元素。等价于:@Target(value={CONSTRUCTOR,FIELD,LOCAL_VARIABLE,METHOD,PACKAGE,PARAMETER,TYPE})2.2.2@Retention用于指定注解的生命周期,有三个值,分别对应三个枚举RetentionPolicy中的值有:源码级别(source)、类文件级别(class)或运行时级别(runtime)SOURCE:只在源代码中可用typeofannotation信息会保留在源代码和class文件中,执行时不会加载到虚拟机中),PS:当annotation没有定义Retention值时,默认值为CLASS,如Java内置注解,@Override、@Deprecated、@SuppressWarnning等。都有注释告知ation),比如SpringMvc中的@Controller、Autowired中的@、@RequestMapping等。另外,我们自定义的注解也大多在这一层。2.2.2.1理解@Retention这里先介绍一下正题。要想理解@Retention,就必须理解从java文件到class文件,再到class被jvm加载的过程。下图描述了从.java文件到编译成class文件的过程:有一个注解抽象语法树的链接,其实就是解析注解然后做相应的处理。所以重点是,如果想在编译期做一些基于注解的处理,就需要继承Java的抽象注解处理器AbstractProcessor,重写里面的process()方法。一般来说,只要注解的@Target作用域是SOURCE或者CLASS,我们就得继承;因为这两个生命周期级别的注解在加载到JVM后会被抹掉。比如lombok继承了AnnotationProcessor的AbstractProcessor来实现编译时处理。这就是为什么我们可以使用@Data来实现get和set方法。2.2.3@Documented执行javadoc时,标记生成的用户文档中是否包含这些注解。2.2.4@Inherited标记该注解是继承的。比如A类用@Table标注,@Table注解用@Inherited(继承)声明;从A继承的子类也继承@Table注解。//Table注解的声明,带继承@Inherited@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public@interfaceTable{}03自定义注解好了,理论说了这么多。大家都听腻了,我也说腻了。那么如何自定义一个注解并让它发挥作用呢?下面我就带大家看看我们公司的注解防止重复提交是如何实现的?当然,由于内部设计,我只会写伪代码。思路前面已经介绍过了,为了方便阅读,我记下了,大家可以理解。要求是:如果同一用户在三秒内重复提交相同的参数,则报异常,防止重复提交,否则写入请求正常提交处理。3.1定义注解首先,定义注解必须被@interface修饰;其次,有四点需要考虑:注解生命周期@Retention,一般为RUNTIME运行时。注解@Target作用的范围是写请求,即控制器方法。是否需要元素,用分布式锁实现,必须有锁的过期时间。给定默认值,也支持自定义。是否生成javadoc@Documented,想都没想加这个注解就对了。基于此,我们公司自定义的防止重复提交的注解就出来了:@Documented@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceBanReSubmitLock{/***锁定时间,默认单位(秒)default时间(3秒)*/longlockTime()default3L;}3.2AOP切面处理@Aspect@ComponentpublicclassBanRepeatSubmitAop{@AutowiredprivatefinalRedisUtilsredisUtils;@Pointcut("@annotation(com.nasus.framework.web.annotation.BanReSubmitLock)")privatevoidbanReSubmitLockAop(){}@Around("banReSubmitLockAop()")publicObjectaroundApi(ProceedingJoinPointpoint)throwsThrowable{//获取AOP切面方法签名MethodSignaturesignature=(MethodSignature)point.getSignature();//方法Methodmethod=signature.getMethod();//获取BanRepeatSubmitLock注解onthetargetmethodBanReSubmitLockbanReSubmitLock=method.getAnnotation(BanReSubmitLock.class);//根据用户信息和提交参数,创建Redis分布式的keylockStringlockKey=createReSumbitLockKey(point,method);//根据key获取分布式锁对象Locklock=redisUtils.getReSumbitLock(lockKey);//Lockbooleanresult=lock.tryLock();//加锁失败,抛异常if(!result){thrownewException("请不要重复请求");}//其他处理...}/***生成密钥*/privateStringcreateReSumbitLockKey(ProceedingJoinPointpoint,Methodmethod){//拼接用户信息&请求参数...//MD5处理...//返回}}是看到这里使用AOP切面方法获取@NoReSubmitLock修饰的方法,并获取切点的参数(注解修饰的方法),用户信息等,通过MD5处理,最后尝试加锁。3.3使用publicclassTestController{//NoReSubmitLock注解修改save方法防止重复提交@NoReSubmitLockpublicbooleansave(Objecto){//保存逻辑}}使用起来也很简单,只需要一个注解就可以完成大部分逻辑;如果不使用注解,每个写接口的方法都要写防止重复提交的逻辑,代码非常繁琐,难以维护。通过这个例子,相信大家也看到了注解的作用。04小结本文介绍注解的作用主要是标记、校验和解耦;介绍注释的语法;介绍注解的元素和传值方式;一个例子,介绍注解是如何工作的?注解是代码的特殊标记,可以在程序编译、类加载和运行时读取和处理。对应RetentionPolicy中的三个枚举,其中SOURCE和CLASS需要继承AbstractProcessor(注解抽象处理器),实现process()方法来处理我们自定义的注解。RUNTIME级别是我们常用的级别。结合Java的反射机制,可以在很多场景下优化代码。05参考链接bilibili.com/video/BV1p4411P7V3mp.weixin.qq.com/s/BPKvLbdCyuWijkD-si75Dwblog.csdn.net/javazejian/article/details/71860633
