「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」
前言
哈喽大家好,我是卡诺,一名致力于成为全栈的全粘工程师!
通过new对象不行吗?为什么要用反射?、Java反射极简操作手册两章内容的讲解,相信大家对反射运用一定是如鱼得水!谈到反射,我们不得不提及与之息息相关的另一个知识点 “注解” 。
自JDK5开始,Java新增了注解(元数据)支持。作为一名javaer,注解是我们日常开发使用最必不可少功能之一,比如:Spring中的@Bean
、@Controller
、@Service
等。除此之外我们还常常会自定义一些注解以解决业务中遇到的问题,比如:简化对象属性拷贝,日志记录等,本章我们将由浅入深学习注解的相关知识。
本文已加入 【再学一次Java】专栏,该专栏旨在重温Java知识,夯实基础,包含:Lambda、反射、注解、多线程等进阶知识。如果有需要的小伙伴可以关注一下,专栏持续更新ing!
简介
官方描述👉访问地址:Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.
Annotations have a number of uses, among them:
- Information for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings.
- Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth.
- Runtime processing — Some annotations are available to be examined at runtime. 谷歌翻译一下:注解,一种元数据形式,提供有关程序的数据,这些数据不属于程序本身。注解对其标记的代码的操作没有直接影响。 注释有许多用途,其中包括:
- 为编译器提供信息 —— 编译器可以使用注解来检测错误或抑制警告。
- 编译时和部署时处理 —— 软件工具可以处理注释信息以生成代码、XML 文件等。
- 运行时处理 —— 一些注解可以在运行时检查。
简单来说,注解可以看作一个标记,它不会改变被标记的代码本身。当使用注解时,必须有相关的代码针对该注解进行处理,否则无任何效果。注解可以标记在类、方法、属性、方法参数等上,可以在编译、类加载、运行时被读取,并对其进行相关操作。
初识注解
声明
public @interface 注解名 {
// 属性列表
类型 属性();
// or
类型 属性() default 默认值;
}
属性类型
案例
public @interface Name {
String value();
int index() default 0;
}
注解看起来接口定义有些类似,只不过是在interface
关键字前增加了@符号,所以注解本质上是不是也是一个接口?我们来看看Name.java
的字节码文件。
注解到底是什么?
进入Name.java
所在文件目录依次执行:javac Name.java
、javap -verbose Name.class > Name.txt
,即可得到如下代码:
Classfile /Users/uu/IdeaProjects/uu-study/uu-java-study/thinking-java/advance-java-example/src/main/java/com/uucoding/advance/annoation/Name.class
Last modified 2022-2-9; size 254 bytes
MD5 checksum 551d1075da4faa2f1700b77eb1e7652f
Compiled from "Name.java"
public interface com.uucoding.advance.annoation.Name extends java.lang.annotation.Annotation
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
#1 = Class #12 // com/uucoding/advance/annoation/Name
#2 = Class #13 // java/lang/Object
#3 = Class #14 // java/lang/annotation/Annotation
#4 = Utf8 value
#5 = Utf8 ()Ljava/lang/String;
#6 = Utf8 index
#7 = Utf8 ()I
#8 = Utf8 AnnotationDefault
#9 = Integer 0
#10 = Utf8 SourceFile
#11 = Utf8 Name.java
#12 = Utf8 com/uucoding/advance/annoation/Name
#13 = Utf8 java/lang/Object
#14 = Utf8 java/lang/annotation/Annotation
{
public abstract java.lang.String value();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC, ACC_ABSTRACT
public abstract int index();
descriptor: ()I
flags: ACC_PUBLIC, ACC_ABSTRACT
AnnotationDefault:
default_value: I#9}
SourceFile: "Name.java"
上述字节码,如我们所想,注解的确是一个接口,继承了java.lang.annotation.Annotation
(为注解提供库支持)。
使用
@Name(value = "name")
@Test
public void testName(){
System.out.println("test @Name");
}
如果注解的属性有value,且使用时只显示设置value属性,可以简写为@Name("name")
,如果value的值为数组类型【String[] value();】那么可以简写为@Name({"name1", "name2"})
,但如果显示设置多个属性则需要完整书写,如下:
@Name(value = "name", index = 1)
@Test
public void testName1(){
System.out.println("test @Name");
}
执行上述测试用例,使用@Name
标记不会影响程序的运行,也无任何作用。想让注解用起来,请继续向下看!
元注解
元注解也是注解,不过它是一种特殊的注解,专门用来标记普通注解,对普通注解进行解释说明、标记其作用范围。开发一个可用注解,你必须得使用元注解对其进行标记。元注解包含:@Retention
、@Target
、@Documented
、@Inherited
、@Repeatable
,其中@Retention
、@Target
是标记注解的必要元注解。
@Retention
标记注解的保留策略
可用策略
-
@Retention(value = RetentionPolicy.SOURCE)
- 注解在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
-
@Retention(value = RetentionPolicy.CLASS)
- 注解在class文件中保留,不会被加载到 JVM 中,默认值。
-
@Retention(value = RetentionPolicy.RUNTIME)
- 注解保留到程序运行的时候,会被加载进入到 JVM 中,该策略能被反射读取。
示例
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
}
日常开发中一般是使用@Retention(RetentionPolicy.RUNTIME)
策略,Java内置的一些告警注解【@Deprecated、@Override等】,Lombok的注解用的是@Retention(RetentionPolicy.SOURCE)
策略,@Retention(RetentionPolicy.CLASS)
策略可以用于在编译期生成代理类(没用过,也没见过在用的框架,小伙伴们如果有用到的可以留言讨论讨论)。
@Target
标记注解使用的位置,注解可以标记在类、方法、属性、方法参数等上,是通过@Target
元注解来进行控制。@Target
的value属性是数组类型,可以支持设置多个值,表示支持多个位置,比如:Spring中的@RequestMapping
注解。
位置可用值
- @Target(ElementType.TYPE)
- 表示注解可以在接口、类、枚举、注解上使用 【常用】
- @Target(ElementType.FIELD)
- 字段、枚举的常量 【常用】
- @Target(ElementType.METHOD)
- 注解可以用在方法上 【常用】
- @Target(ElementType.PARAMETER)
- 注解可以用在方法参数 【常用】
- @Target(ElementType.CONSTRUCTOR)
- 注解可以用在构造函数
- @Target(ElementType.LOCAL_VARIABLE)
- 注解可以用在局部变量上
- @Target(ElementType.ANNOTATION_TYPE)
- 表示注解可以用在注解上
- @Target(ElementType.PACKAGE)
- 注解可以用在包上
- @Target(ElementType.TYPE_PARAMETER)
- Java8新增,任何声明类型的地方
- @Target(ElementType.TYPE_USE)
- Java8新增,任意使用类型的地方
示例1
@Target({
ElementType.TYPE,
ElementType.FIELD,
ElementType.METHOD,
ElementType.PARAMETER,
ElementType.CONSTRUCTOR,
ElementType.LOCAL_VARIABLE,
ElementType.ANNOTATION_TYPE,
ElementType.PACKAGE
})
public @interface TargetTest {
}
// 示例1
@TargetTest
public void testTarget(@TargetTest String name ){
// 示例2、ElementType.LOCAL_VARIABLE
@TargetTest int age = 0;
}
// 示例3 - 新建一个package-info.java文件
// ElementType.PACKAGE
@TargetTest package com.uucoding.advance.annoation;
示例2
// 测试 @Target(ElementType.TYPE_USE)
public void testTargetTypeUseTest(){
throw new @TargetTypeUseTest RuntimeException();
}
// 测试 @Target(ElementType.TYPE_PARAMETER)
public class TypeParameter<@TargetTypeParameterTest T> {
}
在 Java8 发布之前,注解只能应用于声明,Java8新增类型注释TYPE_USE
和TYPE_PARAMETER
,类型注释是为了支持改进的 Java 程序分析,让其在编译期间发现错误,以确保更强的类型检查。这俩我们开发很少用,这里就不做赘述,感兴趣的小伙伴可以看看如下两个链接的介绍!
@Documented
使用@Documented元注解定义的注解,注解将会被生成到javadoc中,如果不需要生成javadoc,此注解不需要使用。
@Inherited
标记子类可以继承父类中的该注解
示例
// 有@Inherited元注解
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface InheritedTest {
}
// 无@Inherited元注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface NoInheritedTest {
}
// 父类
@NoInheritedTest
@InheritedTest
public class InheritedSuper {
}
// 子类
public class InheritedChild extends InheritedSuper{
}
// 测试子类是否含有@InheritedTest注解
@Test
public void testInherited(){
Class<InheritedChild> inheritedChildClass = InheritedChild.class;
// 借助反射,获取InheritedChild类上的所有注解信息
Annotation[] annotations = inheritedChildClass.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation);
}
// 输出:@com.uucoding.advance.annoation.InheritedTest()
}
@Repeatable
在Java8之前,如果想要标记在同一个地方标记多个注解,我们通常这样写:
// 需要在类上标记多个@MapperScan
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface MapperScan {
String[] value();
}
// 准备一个存储@MapperScan的容器注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface MapperScans {
MapperScan[] value();
}
// 对类进行标记
@MapperScans({
@MapperScan("com.kanuo"), @MapperScan("com.uucoding")
})
public class MapperScansTest {
}
对于上述的代码,我们更希望更加语义化,表现为如下形式:
@MapperScan("com.kanuo")
@MapperScan("com.uucoding")
public class MapperScansTest {
}
Java8后,我们只需要在@MapperScan
注解上新增一个@Repeatable
即可实现上述需求,代码更改如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
+ @Repeatable(MapperScans.class)
public @interface MapperScan {
String[] value();
}
获取注解的方式是没有任何变化的,Java8也依然通过获取MapperScans
,通过MapperScan
获取值为null,获取类上注解方式如下:
@Test
@DisplayName("测试Repeatable")
public void testRepeatable(){
//java 8之前
Class<MapperScansTest> mapperScansTestClass = MapperScansTest.class;
MapperScans annotation1 = mapperScansTestClass.getAnnotation(MapperScans.class);
// @com.uucoding.advance.annoation.MapperScans(value=[@com.uucoding.advance.annoation.MapperScan(value=[com.kanuo]), @com.uucoding.advance.annoation.MapperScan(value=[com.uucoding])])
System.out.println(annotation1);
//java 8之后
Class<MapperScanTest> mapperScanTestClass = MapperScanTest.class;
MapperScans annotation2 = mapperScanTestClass.getAnnotation(MapperScans.class);
// @com.uucoding.advance.annoation.MapperScans(value=[@com.uucoding.advance.annoation.MapperScan(value=[com.kanuo]), @com.uucoding.advance.annoation.MapperScan(value=[com.uucoding])])
System.out.println(annotation2);
}
注:如果
MapperScansTest
类上只有一个@MapperScan
注解,使用getAnnotation(MapperScan.class)
获取,如果是多个@MapperScan
注解,则需要使用getAnnotation(MapperScans.class)
常用内置注解
Java为我们提供了一些开箱即用的内置注解,常用的注解如下:
@Override
覆写父类方法时候加在子类覆写的方法上,不加会有警告信息Missing '@Override' annotation on 'test()' ”
。
@Deprecated
将代码标记为过时,不建议使用。 如果使用编译器在会发出警告,现代编辑器中表现为entity.setId()
。
@SafeVarargs
参数安全类型注解,构造或方法是可变参数时,使用该注解会阻止编译器产生unchecked的警告。
@SuppressWarnings
抑制编译器警告,比如在调用@Deprecated标记的方法时候,加上该注解,编译器则忽略警告。
@FunctionalInterface
JDK8提供的注解,用以标记函数式接口,这个我们在「Lambda必知必会」一章有案例介绍。
上述几个注解,实际开发中可以不使用,但是一种规范。
自定义注解
声明注解
public @interface First {
}
设置注解保留策略
@Retention(RetentionPolicy.RUNTIME)
public @interface First {
}
设置注解使用位置
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface First {
}
可选操作
- 给注解加个属性
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface First {
String value();
// 设置默认值
String name() default "first";
}
- 让注解打包到javadoc中
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
public @interface First {
String value();
String name() default "first";
}
- 设置为可继承的注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Inherited
public @interface First {
String value();
String name() default "first";
}
- 设置为可重复注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Inherited
@Repeatable(First.List.class)
public @interface First {
String value();
String name() default "first";
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Inherited
public @interface List {
First[] value();
}
}
可重复注解这里需要注意:@List
的元注解必须和@First
的元注解保持一致!
使用注解
@First("value first")
public class FirstDemo {
}
获取注解
前文我们在@Inherited
和@Repeatable
元注解讲解的时候已经简单的了解通过反射获取注解的示例,接下来将基于前文自定义的一些注解,将业务开发中反射对注解的常用操作通过代码逐一演示。
- 新增一个子类继承
FirstDemo
, 并对其标记@NoInheritedTest
注解,新增一个@FieldOrMethod
注解,支持标记方法和属性。代码如下
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface FieldOrMethod {
boolean isMethod() default false;
}
@NoInheritedTest
public class FirstChildDemo extends FirstDemo{
@FieldOrMethod
private String name;
@FieldOrMethod(isMethod = true)
public String getName() {
return name;
}
}
- 类上注解操作
@Test
public void testAnnotation(){
Class<FirstChildDemo> firstChildDemoClass = FirstChildDemo.class;
// getDeclaredAnnotations 只获取自身的类注解
Annotation[] declaredAnnotations = firstChildDemoClass.getDeclaredAnnotations();
for (Annotation declaredAnnotation : declaredAnnotations) {
System.out.println(declaredAnnotation); // @com.uucoding.advance.annoation.NoInheritedTest()
}
// getAnnotations 获取自身和父类的所有注解(父类的只有具有可继承性@Inherited修饰的注解,才可以被获取)
declaredAnnotations = firstChildDemoClass.getAnnotations();
for (Annotation declaredAnnotation : declaredAnnotations) {
System.out.println(declaredAnnotation);
// @com.uucoding.advance.annoation.First(name=first, value=value first)
//@com.uucoding.advance.annoation.NoInheritedTest()
}
// getDeclaredAnnotation获取自身的类指定注解
NoInheritedTest noInheritedTest = firstChildDemoClass.getDeclaredAnnotation(NoInheritedTest.class);
System.out.println(noInheritedTest);
// getAnnotation获取自身的类指定注解
First first = firstChildDemoClass.getAnnotation(First.class);
System.out.println(first); // @com.uucoding.advance.annoation.First(name=first, value=value first)
// 判断是否存在某个注解
boolean annotationPresent = firstChildDemoClass.isAnnotationPresent(First.class);
System.out.println(annotationPresent); // true
}
- 方法属性上注解操作
@Test
public void testFieldOrMethod() throws NoSuchFieldException, NoSuchMethodException {
Class<FirstChildDemo> firstChildDemoClass = FirstChildDemo.class;
Field name = firstChildDemoClass.getDeclaredField("name");
FieldOrMethod annotation = name.getAnnotation(FieldOrMethod.class);
System.out.println(annotation);// @com.uucoding.advance.annoation.FieldOrMethod(isMethod=false)
Method getName = firstChildDemoClass.getDeclaredMethod("getName");
FieldOrMethod getNameAnnotation = getName.getAnnotation(FieldOrMethod.class);
System.out.println(getNameAnnotation);// @com.uucoding.advance.annoation.FieldOrMethod(isMethod=true)
}
源码
总结
- 本章主要通过概念和案例对注解进行全面的概括,包括:元注解、自定义注解、反射与注解;
@Retention
和@Target
这两个元注解是注解开发中最常用的两个,也是注解生效的必要注解;- 当需要使用反射进行操作时,
@Retention
的值必须为@Retention(value = RetentionPolicy.RUNTIME)
最后
- 感谢铁子们耐心看到最后,如果大家感觉本文有所帮助,麻烦给个赞👍或关注➕;
- 由于本人技术有限,文章和代码可能存在错误,希望大家评论指出,万分感激🙏;
- 同时也欢迎大家V我(uu2coding)一起讨论学习前端、Java知识,一起卷一起进步。