第16章_注解
1、注解及注解的使用
注解的分类
Java的注解可以分为两种:
- 第一种是元注解
- 第二种是自定义注解
一般我们把元注解理解为描述注解的注解,把元数据理解为描述数据的数据,把元类理解为描述类的类。。。
在Java中有五个元注解:@Target
、@Retention
、@Documented
、@Inherited
、@Repeatable
(JDK1.8新增)
注解的定义和使用
我们接下来看一下在Java中如何定义和使用一个注解。
JDK中提供的内置注解 @Override 的定义方式的源码如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
可以看到,在定义注解时,使用关键字@interface表示这是一个注解类型,@interface和class、enum、interface等都是关键字。
定义好注解之后,想要使用这个注解,只需要在声明方法和类时引入该注解即可,比如在java.lang.Double的Hash方法中就用到了@Override 注解:
@Override
public int hashCode() {
return double.hashCode(value);
}
以上的@Override注解其实就是一个自定义注解,可以看到,在定义这个注解时,用到了另外两个注解,分别是@Target和@Retention,这两个注解就是元注解。
2、Java中的五个元注解
2.1 @Target
@Target注解用来指定一个注解的范围,表示被描述的注解可以放在什么地方。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
该注解中有一个成员变量value,它的类型是ElementType 数组,这就说明一个注解可以同时指定多个ElementType。
ElementType是个枚举类,其中列举了可以使用注解的元素类型,主要有以下几个枚举项,如表所示。
名称 | 说明 |
---|---|
TYPE | 用于类、接口、及枚举 |
FIELD | 用于成员变量(包含枚举常量) |
METHOD | 用于方法 |
PARAMETER | 用于形式参数 |
CONSTRUCTOR | 用于构造函数 |
LOCAL_VARIABLE | 用于局部变量 |
ANNOTATION_TYPE | 用于注解类型 |
PACKAGE | 用于包 |
TYPE_PARAMETER | 用于类型参数(JDK1.8新增) |
TYPE_USE | 用于类型使用(JDK1.8新增) |
MOUDLE | 用于模块(JDK9新增) |
@Target是最基础的一个元注解,想要让一个注解可以被使用,就必须使用@Target来标注它的使用范围。
以下是我们定义的一个注解,并且制定了其只能用在类型和方法上:
@Target({
ElementType.TYPE, ElementType.METHOD
}
)
public @interface MyTarget {
}
我们可以在测试类中的指定位置使用该注解:
@MyTarget
public class TargetTest {
private String name;
@MyTarget
private String getName() {
return name;
}
}
但我们尝试在name这个成员变量上使用@MyTarget 注解时,会在编译期报错:
javac TargetTest.java
TargetTest.java:7: 错误:注释类型不适用于该类型的声明
@MyTarget
^
1个错误
2.2 @Documented
使用@Documented注解修饰的注解类会被JavaDoc工具提取成文档,
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
默认情况下,JavaDoc中是不含注解的,如果定义注解时指定了@Documented,则表明这个注解的信息需要包含在JavaDoc中。
下面通过一个示例演示以下,创建一个注解类,先不用@Documented修饰:
@Target(ElementType.TYPE)
public @interface MyDocumented {
String value() default "this is MyDocumented";
}
然后定义测试类
@MyDocumented
public class DocumentedTest {
private String print(){
return "ToBeTopJavaer@xxxx";
}
}
之后,我们尝试生成DocumentedTest 的JavaDoc,执行以下命令:
javadoc -d mydoc DocumentedTest.java
执行上述命令后,回在目录中生成一个mydoc文件夹,打开文件中的DocumentedTest.html
即可看到以下内容:
可以看到没有任何关于注解的信息。
接下来修改 MyDocumented,改为以下形式:
@Documented
@Target(ElementType.TYPE)
public @interface MyDocumented {
String value() default "this is MyDocumented";
}
之后,在重新执行命令,生成新的JavaDoc,对于DocumentedTest的描述中保留了MyDocumented 注解信息。
2.3 @Retention
@Retention注解用于描述注解的保留策略,表示在什么级别保存该注解信息。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
其中有一个RetentionPolicy类型的成员变量,用来指定保留策略。
RetentionPolicy同样是一个枚举类,其中有以下几个枚举项,如表16-2所示。
名称 | 说明 |
---|---|
SOURCE | 注解将被编译器丢弃,即只在原文件中保留 |
CLASS | 编译器将注解记录在Class文件中,但不需要在运行时由虚拟机保留。这是所有注解的默认保留策略 |
RUNTIME | 注解将由编译器记录在Class 文件中,并在运行时由虚拟机保留,因此可以以反射方式读取它们 |
需要注意的是,如果我们定义的一个注解需要在运行期通过反射读取,那么就需要把RetentionPolicy
设置成RUNTIME
。
2.4 @Inherited
@Inherited注解用来指定该注解可以被继承。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
当我们使用@Inherited定义了一个@MyInherited之后,使用@MyInherited修饰A类,这时A的子类B也会自动具有该注解。
下面举个例子来说明这个元注解的用法,先定义一个注解,并且没有使用@Inherited修饰:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInherited {
}
定义InheritedTestA,并使用@MyInherited修饰:
@MyInherited
public class InheritedTestA {
}
定义InheritedTestB,继承自InheritedTestA,并执行以下测试代码:
public class InheritedTestB extends InheritedTestA {
public static void main(String[] args){
System.out.println("InheritedTestA has MyInherited ? " +InheritedTestA.class.isAnnotationPresent(MyInherited.class));
System.out.println("InheritedTestB has MyInherited ? " +InheritedTestB.class.isAnnotationPresent(MyInherited.class));
}
}
输出结果如下:
InheritedTestA has MyInherited ? true InheritedTestB has MyInherited ? false
修改@MyInherited的代码,使用@Inherited修饰后重新测试:
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInherited {
}
得到的结果如下:
InheritedTestA has MyInherited ? true InheritedTestB has MyInherited ? true
在定义MyInherited注解时,我们还用到了前面介绍的@Retention注解,并且把保留策略设置为RUNTIME,因为只有这样,我们才能在运行期得到类上面的注解描述。
需要注意的是,@Inherited只会影响类上面的注解,而方法和属性等上面的注解的继承性是不受@Inherited影响的。而声明在方法、成员变量等处的注解,即使该注解没有使用@Retention标注,默认都是可以被继承的,除非子类重写了父类的方法或者覆盖了父类中的成员变量。
2.5 @Repeatable
@Repeatable注解是Java 8新增加的一个元注解,使用该注解来标识允许一个注解在一个元素上使用多次。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
/**
* Indicates the <em>containing annotation type</em> for the
* repeatable annotation type.
* @return the containing annotation type
*/
Class<? extends Annotation> value();
}
默认情况下,我们不能在同一个元素上多次使用同一个注解,比如定义一个@MyRepeatable注解,并且不使用@Repeatable修饰:
@Target(ElementType.METHOD)
public @interface MyRepeatable {
}
这时我们是没有办法直接用以下方式定义一个方法的:
public class RepeatableTest {
@MyRepeatable
@MyRepeatable
public void test(){
}
}
以上代码会编译报错:
javac RepeatableTest.java
Repeatableest:java 错误:PiyRepeatable不是可重复的注释类型
@MyRepeatable
1个错误
在Java 8之前,想要解决这个问题,需要自己定义注解容器,不是很方便,Java 8中新增了@Repeatable注解后,就相对简单了。 我们修改以上注解:
@Target(ElementType.METHOD)
@Repeatable(MyRepeatables.class)
public @interface MyRepeatable {
}
并且定义一个@MyRepeatables注解即可:
@Target(ElementType.METHOD)
public @interface MyRepeatables {
MyRepeatable[] value();
}
3、注解的继承与组合
@interface这种类型可以继承吗?
其实,注解类是不能继承其他类也不能实现其他接口的。但是,注解和注解之间是可以建立组合关系的。
为了方便记录方法的入参和出参的日志,我们定义了@Log注解:
@Target({ElementType.METHOD,ElementType.TYPE))
@Retention(RetentionPolicy.RUNTIME)
public @interface Logger {
String methodName() default "";
}
为了做方法的幂等处理,我们定义一个@Idempotent注解:
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String idempotentNo() default "";
}
为了使异常可以被正常处理,我们定义了@ExceptionCatch注解:
@Target({ElementType.METHOD,ElementTypeTYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExceptionCatch{
}
这时我们有一个要对外部提供RPC的接口,我们需要同时让这个接口具有日志记录、方法幂等和异常处理等功能,该怎么办?
最简单的办法就是在这个接口的方法上分别使用以上三个注解。
但是,还有一个好办法,那就是通过组合的方式把这个三个注解组合到一起。例如,定义一个RpcMethod注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Idempotent
@Exceptioncatch
@Logger
public @interface RpcMethod {
String idempotentNo();
String methodName();
}
这种组合注解在Spring中随处可见,比如Spring Boot中的@SpringBootApplication这个注解,就是通过组合多个注解实现的:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters ={CFilter(type = FilterType.CUSTOM,classes =TypeExcludeFilter.class),@Filter(type = FilterType.CUSTO, classes = AutoConfigurationExcludeFilter.class)})
public @interface SpringBootApplication
}
而且,注解的组合层数是没有限制的,可以无限组合。
但是,组合注解有一个小问题需要注意,就是当我们通过反射获取一个类的注解时,只能获取组合注解,无法获取被组合的注解,需要通过组合注解的二次解析才能得到。
当然,如果在开发中使用Spring,这个问题就迎刃而解了,Spring中的AnnotatedElementUtils
的getMergedAnnotation
方法可以获取被组合的注解。
4、注解与反射的结合
本节简单介绍如何通过反射判断类、方法等是否有某个注解,以及如何获取注解的值。
前面提过,如果想在运行期获取注解,那么这个注解的RetentionPolicy
必须是 RUNTIME
, 否则这个注解是无法保留到运行期的。
而反射的执行,必然是发生在运行期的。所以通过反射获取的注解,其RetentionPolicy必然是RUNTIME。
我们先定义一个@MyAnnotation注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface MyAnnotation {
String value();
}
接下来写一段反射的代码,内容如下:
@MyAnnotation("java")
public class AnnotationTest {
public static void main(String[] args) {
Class clazz = AnnotationTest.class;
MyAnnotation classAnnotation = (MyAnnotation) clazz.getAnnotation(MyAnnotation.class);
if (classAnnotation != null) {
System.out.println("get value from class annotation:" + classAnnotation.value());
}
try {
Method method = clazz.getMethod("author");
MyAnnotation methodAnnotation = method.getAnnotation(MyAnnotation.class);
System.out.println("get value from method annotation:" + methodAnnotation.value());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
@MyAnnotation("gong_yz")
public void author() {
}
}
运行以上代码后的输出结果如下:
get value from class annotation:java get value from method annotation:gong_yz
可以看到,我们通过反射技术在运行期获取了标注在类和方法上的注解及注解中成员变量的值。
因为有反射+注解的完美结合,所以我们可以利用这两个技术做很多事情,下一节将介绍几种实际应用的场景。无论场景如何多变,基础的原理都是利用了反射技术+自定义注解。
5、日常开发中的常用注解
5.1 使用自定义注解做日志记录
不知道读者有没有遇到类似的诉求,就是希望在一个方法的入口处或者出口处做统一的日志处理,比如记录入参、出参和方法执行的时间等。
如果在每一个方法中都编写这样的代码,那么一方面会有很多代码重复,另一方面也容易使这段逻辑被遗漏。
在这种场景下,就可以使用自定义注解+切面实现这个功能了。
假设我们想要在一些Web请求的方法上记录本次操作具体做了什么事情,比如新增了一条记录或者删除了一条记录等。
首先自定义一个注解:
/**
*Operate Log的自定义注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OpLog {
/**
* 申业务类型,如新增、删除、修改
*/
public OpType opType();
/**
*业务对象名称,如订单、库存、价格
*/
public String opItem();
/**
*业务对象编号表达式,描述了如何获取订单号
*/
public String opItemIdExpression();
}
因为我们不仅要在日志中记录本次操作了什么,还需要知道被操作的对象的唯一性标识,如订单号信息。
但每一个接口方法的参数类型肯定是不一样的,很难有一个统一的标准,这时我们可以借助SpeL表达式,即在表达式中指明如何获取对应的对象的唯一性标识。
有了上面的注解,接下来就可以写切面了。主要代码如下:
5.2 使用自定义注解做前置检查
当对外提供接口时,会对其中的部分参数有一定的要求,比如某些参数值不能为空等。多数情况下我们都需要主动进行校验,判断对方传入的值是否合理。
下面推荐一个使用Hibernate Validator+自定义注解+AOP实现参数校验的方式。
首先定义一个具体的入参类。
6、不要过度依赖注解
6.1 什么是编程式事务
基于底层的API,如PlatformTransactionManager、TransactionDefinition和TransactionTemplate等核心接口,开发者完全可以通讨编程的方式进行事务管理。
编程式事务需要开发者在代码中手动管理事务的开启、提交和回滚等操作。例如:
public void test(){
TransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
//事务操作
//事务提交
transactionManager.commit(status);
}catch (DataAccessException e){
//事务提交
transactionManager.rollback(status);
throw e;
}
}
开发者可以通过API自己控制事务。
6.2 什么是声明式事务
声明式事务管理方法允许开发者在配置的帮助下来管理事务,而不需要依赖底层API进行硬编码。开发者可以只使用注解或基于配置的XML来管理事务。例如:
@Transactional
public void test(){
//事务操作
}
使用@Transactional即可给test方法增加事务控制。
当然,上面的代码只是简化后的,想要便用事务还需要一些配置内容。这里就不详细阐述了。
这两种事务有合目的优畎点,那么这两种事务有哪些各自适用的场景呢?为什么有人会拒绝使用声明式事务呢?
6.3 声明式事务的优点
声明式事务帮助我们节省了很多代码,它会自动进行事务的开启、提交和回滚等操作。声明式事务的管理是使用AOP实现的,本质上就是在目标方法执行前后进行拦截。在目标方法执行前加人或创建一个事务,在目标方法执行后,根据实际情况选择提交或回滚事务。
使用这种方式,对代码没有侵入性,在方法内只需要编写业务逻辑即可。
6.4 声明式事务的粒度问题
6.5 声明式事务用不对容易失效
6.6 小结
注解虽好,但还是要谨慎使用,不要过度依赖注解。