目录
1、什么是注解?
Annontation是Java5开始引入的新特征,中文名称叫注解。
它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观、更明了的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。Annontation像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。
Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。
2、注解的用处:
- 生成文档。这是最常见的,也是java 最早提供的注解。常用的有@param @return 等
- 跟踪代码依赖性,实现替代配置文件功能。比如Dagger 2 依赖注入,未来java 开发,将大量注解配置,具有很大用处;
- 在编译时进行格式检查。如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。
3、注解的原理
注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。
3.1、元注解
所有元注解定义在java.lang.annotation包下面,其中Annotation是注解的基本接口,所有的注解都继承这个接口。
java.lang.annotation 提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解):
1、@Documented:指定被标注的注解会包含在javadoc中。
2、@Retention: 指定注解的生命周期(源码、class文件、运行时),其参考值见类的定义:java.lang.annotation.RetentionPolicy
● RetentionPolicy.SOURCE : 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。@Override, @SuppressWarnings都属于这类注解。
● RetentionPolicy.CLASS : 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式。
● RetentionPolicy.RUNTIME : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。
3、@Target:指定注解使用的目标范围(类、方法、字段等),其参考值见类的定义:java.lang.annotation.ElementType
● ElementType.CONSTRUCTOR :用于描述构造器。
● ElementType.FIELD :成员变量、对象、属性(包括enum实例)。
● ElementType.LOCAL_VARIABLE: 用于描述局部变量。
● ElementType.METHOD : 用于描述方法。
● ElementType.PACKAGE :用于描述包。
● ElementType.PARAMETER :用于描述参数。
● ElementType.ANNOTATION_TYPE:用于描述参数
● ElementType.TYPE :用于描述类、接口(包括注解类型) 或enum声明。
4、@Inherited:指定子类可以继承父类的注解,只能是类上的注解,方法和字段的注解不能继承。即如果父类上的注解是@Inherited修饰的就能被子类继承。
jdk1.8又提供了以下两个元注解
5、@Native:指定字段是一个常量,其值引用native code。
6、@Repeatable:注解上可以使用重复注解,即可以在一个地方可以重复使用同一个注解,像spring中的包扫描注解就使用了这个。
7、使用@interface关键词来定义注解。
3.2、常见标准的Annotation
1、@Override
java.lang.Override 是一个标记类型注解,它被用作标注方法。它说明了被标注的方法重写了父类的方法,起到了断言的作用。如果我们使用了这种注解在一个没有覆盖父类方法的方法时,java 编译器将以一个编译错误来警示。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Override是给javac(java编译器)看的,编译完以后就@Override注解就没有价值了,@Override注解在源代码中有用,编译成.class文件后@Override注解就没有用了,因此@Override的Retention的属性值是RetentionPolicy.SOURCE。
2、@Deprecated
Deprecated 也是一种标记类型注解。当一个类型或者类型成员使用@Deprecated 修饰的话,编译器将不鼓励使用这个被标注的程序元素。所以使用这种修饰具有一定的“延续性”:如果我们在代码中通过继承或者覆盖的方式使用了这个过时的类型或者成员,虽然继承或者覆盖后的类型或者成员并不是被声明为@Deprecated,但编译器仍然要报警。
3、@SuppressWarnings
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
@SuppressWarnings是给javac(java编译器)看的,编译器编译完java文件后,@SuppressWarnings注解就没有用了,所以@SuppressWarnings的Retention的属性值是RetentionPolicy.SOURCE。
3.3、注解处理器类库
Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口。除此之外,Java在java.lang.reflect 包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:
Class:类定义
Constructor:构造器定义
Field:累的成员变量定义
Method:类的方法定义
Package:类的包定义
java.lang.reflect 包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射API扩充了读取运行时Annotation信息的能力。当一个Annotation类型被定义为运行时的Annotation后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的如下方法来访问Annotation信息:
方法1:<T extends Annotation> T getAnnotation(Class<T> annotationClass): 返回改程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。
方法2:Annotation[] getAnnotations():返回该程序元素上存在的所有注解。
方法3:boolean isAnnotationPresent(Class<?extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false.
方法4:Annotation[] getDeclaredAnnotations(Class<T> annotationClass):返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。
4、自定义注解
自定义注解类编写的一些规则:
- Annotation 类型定义为@interface, 所有的Annotation 会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口。
- 参数成员只能用public 或默认(default) 这两个访问权修饰。语法:类型 属性名() [default 默认值]; default表示默认值 ,也可以不编写默认值的.
- 参数成员只能用基本类型byte、short、char、int、long、float、double、boolean八种基本数据类型和String、Enum、Class、annotations等数据类型,以及这一些类型的数组.
- 要获取类方法和字段的注解信息,必须通过Java的反射技术来获取 Annotation 对象,因为你除此之外没有别的获取注解对象的方法。
- 注解也可以没有定义成员,,不过这样注解就没啥用了。
注意: 自定义注解需要使用到元注解。
- 注解方法不能有参数。
- 注解方法的返回类型局限于原始类型,字符串,枚举,注解,或以上类型构成的数组。
- 注解方法可以包含默认值。
5、自定义注解使用:
标记注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String color() default "blue";// 为属性指定缺省值
String product() default "001";
}
@MyAnnotation
public class UserAnnotation {
@MyAnnotation(color = "colortest2 method")
public static void oldMethod() {
System.out.println("old method, don't use it.");
}
@MyAnnotation(color="colortest",product="producttest")
public static void genericsTest() throws FileNotFoundException {
List<String> l = new ArrayList<>();
l.add("abc");
oldMethod();
}
}
public class AnnotationParsing {
public static void main(String[] args) {
try {
Class<?> loadClass = AnnotationParsing.class
.getClassLoader()
.loadClass("com.lys.myannotation.UserAnnotation");
if (loadClass.isAnnotationPresent(MyAnnotation.class)) {
Annotation[] declaredAnnotations = loadClass.getDeclaredAnnotations();
for (Annotation annotation : declaredAnnotations) {
System.out.println("Annotation in class '" + annotation);
}
}
for (Method method : loadClass.getMethods()) {
// checks if MethodInfo annotation is present for the method
if (method.isAnnotationPresent(MyAnnotation.class)) {
try {
// iterates all the annotations available in the method
for (Annotation anno : method.getDeclaredAnnotations()) {
System.out.println("Annotation in Method '"
+ method + "' : " + anno);
}
MyAnnotation methodAnno = method.getAnnotation(MyAnnotation.class);
if (methodAnno.product().equals("001")) {
System.out.println("Method with product is 001 = "+ method);
}
} catch (Throwable ex) {
ex.printStackTrace();
}
}
}
} catch (SecurityException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
6、条件注解
6.1、@Conditional注解
Conditional 是由 SpringFramework 提供的一个注解,位于 org.springframework.context.annotation 包内,定义如下。
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
Conditional 注解类里只有一个 value 属性,需传入一个 Condition 类型的数组,我们先来看看这个 Condition 接口长什么样。
package org.springframework.context.annotation;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.core.type.AnnotatedTypeMetadata;
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
其中,matches() 方法传入的参数 ConditionContext 是专门为 Condition 而设计的一个接口类,可以从中获取到Spring容器的以下对象信息:
当一个 Bean 被 Conditional 注解修饰时,Spring容器会对数组中所有 Condition 接口的 matches() 方法进行判断,只有当其中所有 Condition 接口的 matches()方法都为 ture 时,才会创建 Bean 。
6.2、自定义Conditional
接下来,我们将以一个国际化 I18n Bean 动态创建为例(根据配置中的 i18n.lang 属性值来动态地创建国际化 I18n Bean),对如何使用 Conditional 注解进行简单举例:
- 当 i18n.lang=zh_CN 就创建中文 I18nChs Bean,
- 当 i18n.lang=en_US 就创建英文 I18nEng Bean。
创建好的两个 Condition 实现类 I18nChsCondition 和 I18nEngCondition 代码如下
package com.lys.myannotation;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class I18nChsCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String lang = context.getEnvironment().getProperty("i18n.lang");
ConditionOutcome outCome = new ConditionOutcome("zh_CN".equals(lang), "i18n.lang=" + lang);
return outCome;
}
}
package com.lys.myannotation;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class I18nEngCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String lang = context.getEnvironment().getProperty("i18n.lang");
ConditionOutcome outCome = new ConditionOutcome("en_US".equals(lang), "i18n.lang=" + lang);
return outCome;
}
}
I18n 接口定义:
package com.lys.myannotation;
public interface I18n {
// 获取 name 属性的值
String i18n(String name);
}
I18n 接口的两个实现类 I18nChs 和 I18nEng 定义如下。
package com.lys.myannotation;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
@Component
@Conditional(I18nChsCondition.class)
public class I18nChsImpl implements I18n {
Map<String, String> map = new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("lang", "中文");
}
};
@Override
public String i18n(String name) {
return map.get(name);
}
}
package com.lys.myannotation;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
@Component
@Conditional(I18nEngCondition.class)
public class I18nEngImpl implements I18n {
Map<String, String> map = new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("lang", "English");
}
};
@Override
public String i18n(String name) {
return map.get(name);
}
}
配置 application.properties 内容如下:
# language : zh_CN/Chinese,en_US/America
i18n.lang=zh_CN
测试代码如下:
package com.lys;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.lys.bean.Person;
import com.lys.myannotation.I18n;
@RunWith(SpringRunner.class)
@SpringBootTest
public class LysStartTests {
@Autowired
I18n i18n;
@Test
public void testConditional() {
System.out.println(i18n.getClass().getName());
System.out.println(i18n.i18n("lang"));
}
}
运行testConditional()测试方法,打印结果:
配置 application.properties 内容如下:
# language : zh_CN/Chinese,en_US/America i18n.lang=en_US
再次运行程序,打印结果:
为了书写和调用方便,我们还可以把上面的条件定义成注解,以 I18nChsCondition 为例,定义代码如下。
package com.lys.myannotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(I18nChsCondition.class)
public @interface I18nChs {
}
将 I18nChs 注解添加到 I18nChsImpl 上。
package com.lys.myannotation;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
@Component
@I18nChs
public class I18nChsImpl implements I18n {
Map<String, String> map = new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("lang", "中文");
}
};
@Override
public String i18n(String name) {
return map.get(name);
}
}
6.3、SpringBoot 扩展注解
从上面的示例不难看出,如果要使用我们自定义条件类实现起来还是有点小麻烦的,不过比较庆幸的是, SpringBoot 在 Conditional 注解的基础上已经提前为我们定义好了一系列功能丰富的注解,我们可以直接使用。
接下来我们使用 ConditionalOnProperty 注解来实现上面的国际化示例。
仅需修改 I18nChsImpl 和 I18nEngImpl 两个实现组件类,其他代码不变,程序执行结果与之前相同。
@Component
@ConditionalOnProperty(name = "i18n.lang", havingValue = "zh_CN", matchIfMissing = true)
public class I18nChsImpl implements I18n {//内容同上,此处省略}
@Component
@ConditionalOnProperty(name = "i18n.lang", havingValue = "en_US", matchIfMissing = false)
public class I18nEngImpl implements I18n {//内容同上,此处省略}