32.1 Java进阶之注解概念,工作原理
本篇文章学习总结自《Java核心技术卷二》
1.什么是注解?
如果使用过Spring做开发,相信对注解并不陌生,相信大家都用过@Resource,@Autowired注解,通过将@Resource这个注解修饰一个类,Spring会自动这个类进行对象创建,通过@Autowired修饰一个变量,Spring会将这个变量赋值为相应的类的对象引用。
这样我们就能将类的对象的管理交给Spring来进行管理,方便自己的开发工作。
所以,我们可以把注解看作一种标记工具,通过这种工具,我们能开发出一些框架级别的东西来帮助我们提高编程效率。 我们可以拿它和泛型进行类比。
首先我们要知道和泛型一样,注解不会改变程序的编译方式,也就是Java编译器对于包含注解和不包含注解的代码会产生相同的虚拟机指令。但通过注解我们可以对一些开发进行简化。
1.1.如何才能使用注解?
为了我们能使用注解,我们需要选择一个处理工具,然后向我们的处理工具可以理解的代码中插入注解,之后运用该处理工具处理代码。例如Spring框架就是一个处理工具。
所以,简单来说,注解就是那些插入到源代码中使用其他工具可以对其进行处理的标签。
使用注解很简单,真正具有挑战性的是自己开发注解。
2.注解的基本概念
在Java中,注解是被当作一个修饰符来使用的,它被置于被注解项之前,中间没有分号,每一个注解的名称前面都加上了一个@符号,注解可以作为代码的一部分。 但是如果没有正确的工具去处理它,注解是不能生效的。
例如:
public class MyClass{
@Test
public void checkRandomInsertions();
}
上面代码中的 @Test自身不会做任何事,它需要工具的支持才会有用。(后面会讲到)
2.1 包含元素的注解
除了类似于@Test,有的注解中也允许包含参数,我们可以传入参数的值来控制注解的处理逻辑。
例如:
@Test(timeout="10000")
包含的元素可以被读取这些注解的工具来处理,元素也允许有多种形式。
2.2 注解的作用范围
除方法外,我们还可以注解类,成员变量以及局部变量,我们还可以注解包,参数变量,类型参数以及类型用法,这些注解可以存在于任何可以放置像public ,static修饰符的地方。
我们可以把注解合理的理解为一个修饰符。
2.3 注解的定义
每个注解都必须通过一个注解接口进行定义,这些接口中的方法与注解中的元素相对应。
例如:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestSelf{
long timeout() default 0L;
}
其中 @Target 和 @Retention 是元注解,它们注解了TestSelf注解。(@Target将TestSelf注解标识成一个只能运用到方法上的注解,@Retention将TestSelf标识为当类文件载入到虚拟机的时候,仍可以保留下来)
@interface 声明创建一个Java注解接口,处理注解的工具将接受那些实现这个注解接口的对象,这类工具可以调用timeout方法来检索某个特定Test注解的timeout元素。
2.4 元注解
元注解能让我们定义注解的作用范围,从上面我们可以知道的元注解有两个:@Target和@Retention
2.4.1 @Target
可以知道,它是定义注解的作用范围是方法级别,还是其他级别的,我们看一下ElementType类:
public enum ElementType {
TYPE, //用于类、接口(包括注解类型)或 enum 声明 -ok
FIELD, //用于成员变量(包括枚举常量)-ok
METHOD, //用于方法 -ok
PARAMETER, //用于参数 -ok
CONSTRUCTOR, // 用于构造方法 -ok
LOCAL_VARIABLE, // 用于局部变量 -ok
ANNOTATION_TYPE, // 用于注解类型 -ok
PACKAGE, // 用于包,不常用
TYPE_PARAMETER, //用于类型参数 @since 1.8 -ok
TYPE_USE //使用一种类型 @since 1.8 -ok
}
可以为指定注解定义多个作用范围,例如:
@Target({ElementType.METHOD})
@Target({ElementType.TYPE,ElementType.METHOD})
具体实例如下:
//考虑到代码篇幅较大,完整代码在文章底部链接中
@TypeTest
@TypeUseTest
public class GoodTest<@TypeParameterTest V> {
@TypeUseTest
@FiledTest
private String field;
@MethodTest
public void doSomething(){
System.out.println("哈哈哈");
}
public void doSomething1(@TypeUseTest @ParameterTest String param1){
System.out.println("呵呵呵");
}
@TypeUseTest
@ConstructorTest
public GoodTest(){
System.out.println("这是构造方法");
}
public void doSomething3(){
@TypeUseTest
@LocalVariableTest
String local;
}
@TypeUseTest
@AnnotationTypeTest
private @interface test{
}
@TypeUseTest
public int test(){
return 1;
}
}
2.4.2 @Retention
它是用来定义注解信息的保留级别,用于描述注解的生命周期,也就是该注解被保留的时间长短。@Retention 注解中的成员变量(value)用来设置保留策略,value 是 java.lang.annotation.RetentionPolicy 枚举类型,RetentionPolicy 有 3 个枚举常量,如下所示:
public enum RetentionPolicy {
SOURCE, //在源文件中有效(即源文件保留)
CLASS, //在 class 文件中有效(即 class 保留)
RUNTIME //在运行时有效(即运行时保留)
}
上面三者生命周期大小排序为 SOURCE < CLASS < RUNTIME。
这个需要注解根据我们自定义注解的性质去使用:
如果需要在运行时去动态获取注解信息,那只能用 RUNTIME
如果只要在编译时进行一些预处理操作,比如生成一些辅助代码(比如一些脚本信息),就用 CLASS ;
如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 。
例如下面定义了一个需要在程序运行时去获取注解信息的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestSelf{
long timeout() default 0L;
}
2.4.3 @Documented
这个注解用来指定修饰的注解类会被javaDoc工具提取为文档。默认情况下javaDoc是不会将注解接口抽取为文档的。
例如:
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface MyDocumented {
public String value() default "这是@Documented注解";
}
@MyDocumented
public class DocumentedTest {
/**
* 测试Documented注解知否生效
* @return
*/
@MyDocumented
public String test(){
return "测试";
}
}
2.4.4 @Inherited
@Inherited 是一个标记注解,用来指定该注解可以被继承。使用 @Inherited 注解的 Class 类,表示这个注解可以被用于该 Class 类的子类。就是说如果某个类使用了被 @Inherited 修饰的注解,则其子类将自动具有该注解。
实例:
public class InheritedTest {
/**
* 使用元注解Inherited
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
private @interface WithInherited {
}
/**
* 不使用元注解Inherited
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
private @interface NoInherited {
}
@WithInherited
@NoInherited
public static class SuperClass {
}
public static class ChildClass extends SuperClass {
}
public static void main(String[] args) {
AnnotatedElement element = SuperClass.class;
System.out.println("SuperClass 是否被 @WithInherited 注解? " + element.isAnnotationPresent(WithInherited.class));
System.out.println("SuperClass 是否被 @NoInherited 注解? " + element.isAnnotationPresent(NoInherited.class));
AnnotatedElement childElement = ChildClass.class;
System.out.println("ChildClass 是否继承 SuperClass 中的 @WithInherited 注解? " + childElement.isAnnotationPresent(WithInherited.class));
System.out.println("ChildClass 是否继承 SuperClass 中的 @NoInherited 注解? " + childElement.isAnnotationPresent(NoInherited.class));
}
}
2.4.5 @Repeatable
@Repeatable 注解是 Java 8 新增加的,它允许在相同的程序元素中重复注解,在需要对同一种注解多次使用时,往往需要借助 @Repeatable 注解。Java 8 版本以前,同一个程序元素前最多只能有一个相同类型的注解,如果需要在同一个元素前使用多个相同类型的注解,则必须使用注解“容器”。
例如:
/**
* 注解类型 Repeatable 用于指明它作为元注解所声明的注解类型是可重复的.
* 也就是用 @Repeatable 声明的注解 A, 可以作为容器注解, A 保证了被其注解的注解可以重复使用.
*/
public class RepeatableStudy {
/**
* 定义容器注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
private @interface Container {
//这里存储注解Element的参数组
Element[] value();
}
/**
* 定义可复用注解并指向容器注解
*/
@Repeatable(Container.class)
private @interface Element {
String value() default "";
}
/**
* 这里的可复用注解的注解信息会被注解Container接收
* 我们可以通过得到Container信息来获取这里的注解信息
*/
@Element("开始")
@Element("准备")
@Element("出发")
@Element("到达目的地")
private class Running {
}
public static void main(String[] args) {
//判断Running是否存储了Container注解信息
if(Running.class.isAnnotationPresent(Container.class)) {
Container c = Running.class.getAnnotation(Container.class);
System.out.println("Running's Element:");
//遍历实际的Element注解中的信息
for(Element e: c.value()){
System.out.println(e.value());
}
}
}
}
2.4.6 @Native
使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可。
3.了解注解基本工作原理
3.1 下面我们通过一个例子来学习注解的基本工作原理
下面有一个程序:
3.1.1 首先定义注解ActionListernFor.java
/**
* 定义一个ActionListenerFor注解,用于监听方法的执行
* 这个注解只能修饰方法且能在运行时获取其信息
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor {
String source();
}
3.1.2 然后编写注解处理工具类(这是自定义注解的关键,没有它注解就是个摆设)
ActionListenerInstaller.java
/**
* ActionListener注解处理工具类
*/
public class ActionListenerInstaller {
/**
* 这个方法为注解生效做好了前提条件
* 主要分为两步:1.扫描出注解修饰的方法 2.将指定方法和注解生效的对象进行绑定
* @param obj 目标对象
*/
public static void processAnnotations(Object obj) {
try {
Class<?> cl = obj.getClass();//获取目标对象的Class对象
/*
遍历其方法信息,处理那些被ActionListener修饰的方法
*/
for (Method m : cl.getDeclaredMethods()) {
//检查使用ActionListenerFor注解的方法并将其加入监听列表中
ActionListenerFor a = m.getAnnotation(ActionListenerFor.class);
if (a != null) {
//获取Class中名为source注解值的Field对象
Field f = cl.getDeclaredField(a.source());
f.setAccessible(true);
//将方法加入监听中
addListener(f.get(obj), obj, m);
}
}
} catch(ReflectiveOperationException e){
e.printStackTrace();
}
}
/**
* 此方法将指定的方法和指定的组件进行绑定,已达到监听的效果
* 注意:此方法的实现用到了jdk的动态代理机制,看不懂的同学可以学习下动态代理原理
* @param source 属性对象
* @param param 目标对象
* @param m 方法对象
* @throws ReflectiveOperationException
*/
public static void addListener(Object source, final Object param, final Method m) throws ReflectiveOperationException {
//定义一个前置处理器,让按钮在执行完点击事件后执行指定的方法
InvocationHandler handler = new InvocationHandler() {
final Method realM = m;
final Object realProxy = param;
public Object invoke(Object proxy, Method m1, Object[] args) throws Throwable {
System.out.println(m1.getName());
if(m1.getName().equals("actionPerformed")){
System.out.println("执行actionPerformed!!");
}
//这个操作就是将param参数传入并执行m方法
return realM.invoke(realProxy);
}
};
// 获取监听器代理对象
Object listenerProxy = Proxy.newProxyInstance(null,new Class[] { ActionListener.class }, handler);
// 获取指定组件的指定方法对象
Method adder = source.getClass().getMethod("addActionListener", ActionListener.class);
//给指定按钮组件加入监听器
adder.invoke(source, listenerProxy);
}
}
3.1.3 应用代码
ButtonFrame.java
/**
* 一个验证ActionListenerFor注解的UI面板
*/
public class ButtonFrame extends JFrame {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;
private JPanel panel;
private JButton yellowButton;
private JButton blueButton;
private JButton redButton;
public ButtonFrame() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
panel = new JPanel();
add(panel);
yellowButton = new JButton("Yellow");
blueButton = new JButton("Blue");
redButton = new JButton("Red");
panel.add(yellowButton);
panel.add(blueButton);
panel.add(redButton);
//在这里使用注解处理器让被ActionListenerFor修饰的方法生效
ActionListenerInstaller.processAnnotations(this);
}
/**
* 这里使用注解方法的方式为名为yellowButton的按钮添加监听器
*/
@ActionListenerFor(source = "yellowButton")
public void yellowBackground() {
panel.setBackground(Color.YELLOW);
}
@ActionListenerFor(source = "blueButton")
public void blueBackground() {
panel.setBackground(Color.BLUE);
}
@ActionListenerFor(source = "redButton")
public void redBackground() {
panel.setBackground(Color.RED);
}
}
3.1.4 测试代码
/**
* 自定义注解效果测试
*/
public class ButtonTest {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
ButtonFrame frame = new ButtonFrame();
frame.setTitle("ButtonTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
我们首先来运行一下上面程序可以看到下面结果:
我们可以很清楚的知道上面的程序中主要有一个图形化的界面和三个按钮,我们每点击一个按钮界面的背景颜色会发生相应的改变。
我们普通的做法是给每个按钮都注册一个事件监听器,但上面却不是,它是利用注解来完成事件响应的。
3.2 代码解析
下面我们分析下它是如何完成的
3.2.1 首先我们要定义一个注解
在上面程序中定义了一个名为ActionListenerFor的注解
package annotations;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor{
String source();
}
处理注解的工具可以调用source()方法来检索注解中的source元素值做处理。
3.2.2 定义处理注解工具
当我们定义好一个注解后,它本身是没任何功能的,它就是一个可以标注的注解。 但是如果我们编写了针对这个注解的处理工具,我们就可以让使用工具让这个注解发挥出它的作用。
在上面的例子中ActionListenerInstaller.java 就是针对ActionListenerFor注解的一种处理工具。
它的主要作用就是让注解发挥作用,它的真正的功能其实就是让注解了的方法和按钮事件相对应,从而使我们点击按钮会有效果。
下面我们结合上面的使用注解的地方逐步分析:
/**
* 这里使用注解方法的方式为名为yellowButton的按钮添加监听器
*/
@ActionListenerFor(source = "yellowButton")
public void yellowBackground() {
panel.setBackground(Color.YELLOW);
}
我们可以看到,上面的例子中,我们使用了ActionListenerFor修饰了方法yellowBacgroud,在这里我们只是标注了这个方法,那么如何让注解生效呢?
通过分析代码我们可以知道,真正让注解生效的是:
ActionListenerInstaller.processAnnotations(this);
**processAnnotations()方法的主要作用就是获取上面的三个注解了的方法,并且将这几个方法自动的注册到监听器中,从而实现了三个按钮的监听。**具体细节大家可以仔细看一下代码。
下图展示了上面例子中注解是如何被处理的:
可能大家不容易get到上面图的重点,下面是我针对上面图中程序运用API,反射机制处理注解的详细流程解释:
4 完整代码地址
Java基础学习/src/main/java/Progress/exa32_1 · 严家豆/Study - 码云 - 开源中国 (gitee.com)
作者:small-water
出处:https://www.cnblogs.com/small-water/p/17870000.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· 字符编码:从基础到乱码解决
· Open-Sora 2.0 重磅开源!