8.spring系列- java注解

2023-10-26

问题

  1. 注解是干什么的?
  2. 一个注解可以使用多次吗?如何使用?
  3. @Inherited是做什么的?
  4. @Target中的TYPE_PARAMETER和TYPE_USER用在什么地方?
  5. 泛型中如何使用注解?
  6. 注解定义可以实现继承吗?
  7. spring对注解有哪些增强?@Aliasfor注解是干什么的?

什么是注解

代码中注释大家都熟悉吧,注释是给开发者看的,可以提升代码的可读性和可维护性,但是对于java编译器和虚拟机来说是没有意义的,编译之后的字节码文件中是没有注释信息的;而注解和注释有点类似,唯一的区别就是注释是给人看的,而注解是给编译器和虚拟机看的,编译器和虚拟机在运行的过程中可以获取注解信息,然后可以根据这些注解的信息做各种想做的事情。比如:大家对@Override应该比较熟悉,就是一个注解,加在方法上,标注当前方法重写了父类的方法,当编译器编译代码的时候,会对@Override标注的方法进行验证,验证其父类中是否也有同样签名的方法,否则报错,通过这个注解是不是增强了代码的安全性。

总的来说:注解是对代码的一种增强,可以在代码编译或者程序运行期间获取注解的信息,然后根据这些信息做各种牛逼的事情。

注解语法

public @interface MyAnnotation {
}

注解中定义的参数

public @interface 注解名称{
    [public] 参数类型 参数名称1() [default 参数默认值];
    [public] 参数类型 参数名称2() [default 参数默认值];
    [public] 参数类型 参数名称n() [default 参数默认值];
}

注解中可以定义多个参数,参数的定义有以下特点:

  1. 访问修饰符必须为public,不写默认为public
  2. 该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型(体现了注解的嵌套效果)以及上述类型的一位数组
  3. 该元素的名称一般定义为名词,如果注解中只有一个元素,请把名字起为value(后面使用会带来便利操作)
  4. 参数名称后面的()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法
  5. default代表默认值,值必须和第2点定义的类型一致
  6. 如果没有默认值,代表后续使用注解时必须给该类型元素赋值

@Target 指定注解的使用范围

看一下@Target源码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

继续看一下ElementType源码:

public enum ElementType {
    /*类、接口、枚举、注解上面*/
    TYPE,
    /*字段上*/
    FIELD,
    /*方法上*/
    METHOD,
    /*方法的参数上*/
    PARAMETER,
    /*构造函数上*/
    CONSTRUCTOR,
    /*本地变量上*/
    LOCAL_VARIABLE,
    /*注解上*/
    ANNOTATION_TYPE,
    /*包上*/
    PACKAGE,
    /*类型参数上*/
    TYPE_PARAMETER,
    /*类型名称上*/
    TYPE_USE
}

指定注解的保留策略:@Retention

java程序的3个过程

  1. 源码阶段
  2. 源码被编译为字节码之后变成class文件
  3. 字节码被虚拟机加载然后运行

那么自定义注解会保留在上面的哪个阶段,则是有@Retention注解来指定。

看下@Retention源码

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}

RetentionPolicy源码:

public enum RetentionPolicy {
    /*注解只保留在源码中,编译为字节码之后就丢失了,也就是class文件中就不存在了*/
    SOURCE,
    /*注解只保留在源码和字节码中,运行阶段会丢失*/
    CLASS,
    /*源码、字节码、运行期间都存在*/
    RUNTIME
}

综合案例

@Target(value = {
        ElementType.TYPE,
        ElementType.METHOD,
        ElementType.FIELD,
        ElementType.PARAMETER,
        ElementType.CONSTRUCTOR,
        ElementType.LOCAL_VARIABLE
})
@Retention(RetentionPolicy.RUNTIME)
@interface Ann6 {
    String value();

    ElementType elementType();
}

@Ann6(value = "我用在类上", elementType = ElementType.TYPE)
public class UseAnnotation6 {
    @Ann6(value = "我用在字段上", elementType = ElementType.FIELD)
    private String a;

    @Ann6(value = "我用在构造方法上", elementType = ElementType.CONSTRUCTOR)
    public UseAnnotation6(@Ann6(value = "我用在方法参数上", elementType = ElementType.PARAMETER) String a) {
        this.a = a;
    }

    @Ann6(value = "我用在了普通方法上面", elementType = ElementType.METHOD)
    public void m1() {
        @Ann6(value = "我用在了本地变量上", elementType = ElementType.LOCAL_VARIABLE) String a;
    }
}

@Target(ElementType.TYPE_PARAMETER)

@Target(value = {
        ElementType.TYPE_PARAMETER
})
@Retention(RetentionPolicy.RUNTIME)
@interface Ann7 {
    String value();
}

public class UseAnnotation7<@Ann7("T0是在类上声明的一个泛型类型变量") T0, @Ann7("T1是在类上声明的一个泛型类型变量") T1> {

    public <@Ann7("T2是在方法上声明的泛型类型变量") T2> void m1() {
    }

    public static void main(String[] args) throws NoSuchMethodException {
        for (TypeVariable typeVariable : UseAnnotation7.class.getTypeParameters()) {
            print(typeVariable);
        }

        for (TypeVariable typeVariable : UseAnnotation7.class.getDeclaredMethod("m1").getTypeParameters()) {
            print(typeVariable);
        }
    }

    private static void print(TypeVariable typeVariable) {
        System.out.println("类型变量名称:" + typeVariable.getName());
        Arrays.stream(typeVariable.getAnnotations()).forEach(System.out::println);
    }
}

运行效果:

类型变量名称:T0
@com.javacode2018.lesson001.demo18.Ann7(value=T0是在类上声明的一个泛型类型变量)
类型变量名称:T1
@com.javacode2018.lesson001.demo18.Ann7(value=T1是在类上声明的一个泛型类型变量)
类型变量名称:T2
@com.javacode2018.lesson001.demo18.Ann7(value=T2是在方法上声明的泛型类型变量)

AnnotatedElement常用方法

 // 该元素如果存在指定类型的注解,则返回这些注解
 <T extends Annotation> T getAnnotation(Class<T> annotationClass);
 // 返回此元素上存在的所有注解,包含从父类继承的
 Annotation[] getAnnotations();
 // 判断此元素上是否存在指定类型的注解
 default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        return getAnnotation(annotationClass) != null;
  }
// 返回直接存在于此元素上的所有注解,不包括父类的注解
 Annotation[] getDeclaredAnnotations();

案例

两个自定义注解:

@Target({ElementType.PACKAGE,
        ElementType.TYPE,
        ElementType.FIELD,
        ElementType.CONSTRUCTOR,
        ElementType.METHOD,
        ElementType.PARAMETER,
        ElementType.TYPE_PARAMETER,
        ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Ann1 {

    public String value();
}
@Target({ElementType.PACKAGE,
        ElementType.TYPE,
        ElementType.FIELD,
        ElementType.CONSTRUCTOR,
        ElementType.METHOD,
        ElementType.PARAMETER,
        ElementType.TYPE_PARAMETER,
        ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Ann2 {

    public int value();
}

注解标注的类:

@Ann1("用在类上")
@Ann2(0)
public class UseAnnotation1<@Ann1("用在变量类型") T0, @Ann2(12) T1> {

    @Ann1("用在字段上")
    @Ann2(2)
    private String name;

    private Map<@Ann1("用在泛型上,String") @Ann2(3) String, @Ann1("用在泛型上,Integer") @Ann2(4) Integer> map;

    @Ann1("用在构造器上")
    @Ann2(5)
    public UseAnnotation1() {

    }

    @Ann1("用在了返回值上")
    @Ann2(6)
    public String m1(@Ann1("用在了参数上") @Ann2(7) String name) {
        return null;
    }
}

测试方法:

@Test
    public void testAnnotation() throws NoSuchFieldException, NoSuchMethodException {
        Annotation[] annotations = UseAnnotation1.class.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }

        System.out.println("------------------");

        TypeVariable<Class<UseAnnotation1>>[] typeParameters = UseAnnotation1.class.getTypeParameters();
        for (TypeVariable<Class<UseAnnotation1>> typeParameter : typeParameters) {
            Annotation[] annotations1 = typeParameter.getAnnotations();
            for (Annotation annotation : annotations1) {
                System.out.println(annotation);
            }
        }

        System.out.println("------------------");

        Field name = UseAnnotation1.class.getDeclaredField("name");
        Annotation[] annotations1 = name.getAnnotations();
        for (Annotation annotation : annotations1) {
            System.out.println(annotation);
        }

        System.out.println("-----------------");

        Field field = UseAnnotation1.class.getDeclaredField("map");
        Type genericType = field.getGenericType();
        Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments();
        AnnotatedType annotatedType = field.getAnnotatedType();
        AnnotatedType[] annotatedActualTypeArguments = ((AnnotatedParameterizedType) annotatedType).getAnnotatedActualTypeArguments();
        int i = 0;
        for (AnnotatedType actualTypeArgument : annotatedActualTypeArguments) {
            Type actualTypeArgument1 = actualTypeArguments[i++];
            String typeName = actualTypeArgument1.getTypeName();
            System.out.println(typeName);
            for (Annotation annotation : actualTypeArgument.getAnnotations()) {
                System.out.println(annotation);
            }
        }

        System.out.println("-----------------");

        Constructor<?> constructor = UseAnnotation1.class.getConstructors()[0];
        for (Annotation annotation : constructor.getAnnotations()) {
            System.out.println(annotation);
        }

        System.out.println("-----------------");

        Method method = UseAnnotation1.class.getMethod("m1", String.class);
        for (Annotation annotation : method.getAnnotations()) {
            System.out.println(annotation);
        }

        System.out.println("-----------------");

        Parameter[] parameters = method.getParameters();
        for (Parameter parameter : parameters) {
            Annotation[] annotations2 = parameter.getAnnotations();
            for (Annotation annotation : annotations2) {
                System.out.println(annotation);
            }
        }
    }

运行结果:

@com.spring.annotation.Ann1(value=用在类上)
@com.spring.annotation.Ann2(value=0)
------------------
@com.spring.annotation.Ann1(value=用在变量类型)
@com.spring.annotation.Ann2(value=12)
------------------
@com.spring.annotation.Ann1(value=用在字段上)
@com.spring.annotation.Ann2(value=2)
-----------------
java.lang.String
@com.spring.annotation.Ann1(value=用在泛型上,String)
@com.spring.annotation.Ann2(value=3)
java.lang.Integer
@com.spring.annotation.Ann1(value=用在泛型上,Integer)
@com.spring.annotation.Ann2(value=4)
-----------------
@com.spring.annotation.Ann1(value=用在构造器上)
@com.spring.annotation.Ann2(value=5)
-----------------
@com.spring.annotation.Ann1(value=用在了返回值上)
@com.spring.annotation.Ann2(value=6)
-----------------
@com.spring.annotation.Ann1(value=用在了参数上)
@com.spring.annotation.Ann2(value=7)

@Inherited : 实现类之间的注解继承

看一下这个注解的源码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

我们通过@Target元注解的属性值可以看出,这个@Inherited 是专门修饰注解的。
作用:让子类可以继承父类中被@Inherited修饰的注解,注意是继承父类中的,如果接口中的注解也使用@Inherited修饰了,那么接口的实现类是无法继承这个注解的

案例

public class InheritAnnotationTest {

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @interface A1{}

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @interface A2{}

    @A1
    interface I1{}

    @A2
    class C1{}

    class M extends C1 implements I1{}

    public static void main(String[] args) {
        Annotation[] annotations = M.class.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }
    }
}

运行结果:

@com.spring.annotation.InheritAnnotationTest$A2()

从输出中可以看出类可以继承父类上被@Inherited修饰的注解,而不能继承接口上被@Inherited修饰的注解,这个一定要注意

@Repeatable重复使用注解

如果我们想重复使用注解的时候,需要用到@Repeatable注解

使用步骤

  1. 先定义容器注解
@Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.FIELD})
    @Repeatable(A1s.class)
    @interface A1 {
        String name();
    }
  1. 为注解指定容器
//容器注解中必须有个value类型的参数,参数类型为子注解类型的数组。
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.FIELD})
    @interface A1s {
        A1[] value();
    }
  1. 使用注解
@A1(name = "我是类上的第一个A1注解")
    @A1(name = "我是类上的第二个A1注解")
    public class RepeatableTest {

        @A1s(
                {@A1(name = "我是变量上的第一个A1S注解"),
                        @A1(name = "我是变量上的第一个A1S注解")
                })
        private String V1;
    }

测试:

public static void main(String[] args) throws NoSuchFieldException {
        Annotation[] annotations = RepeatableTest.class.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }

        System.out.println("------------------");

        Field name = RepeatableTest.class.getDeclaredField("V1");
        Annotation[] annotations1 = name.getAnnotations();
        for (Annotation annotation : annotations1) {
            System.out.println(annotation);
        }
    }

运行结果:

@com.spring.annotation.RepeatableAnnTest$A1s(value=[@com.spring.annotation.RepeatableAnnTest$A1(name=我是类上的第一个A1注解), @com.spring.annotation.RepeatableAnnTest$A1(name=我是类上的第二个A1注解)])
------------------
@com.spring.annotation.RepeatableAnnTest$A1s(value=[@com.spring.annotation.RepeatableAnnTest$A1(name=我是变量上的第一个A1S注解), @com.spring.annotation.RepeatableAnnTest$A1(name=我是变量上的第一个A1S注解)])
Disconnected from the target VM, address: '127.0.0.1:59466', transport: 'socket'

我们先看一个案例

public class AliasForAnnTest {

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface A1 {
        String value() default "a";
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @A1
    @interface B1 {
        String value() default "b";
    }

    @B1("b1")
    public class Ann {
    }

    public static void main(String[] args) {
        A1 mergedAnnotation = AnnotatedElementUtils.getMergedAnnotation(Ann.class, A1.class);
        System.out.println(mergedAnnotation);
        
        System.out.println("------------");
        
        B1 mergedAnnotation1 = AnnotatedElementUtils.getMergedAnnotation(Ann.class, B1.class);
        System.out.println(mergedAnnotation1);
    }
}

运行结果:

@com.spring.annotation.AliasForAnnTest$A1(value=a)
------------
@com.spring.annotation.AliasForAnnTest$B1(value=b1)

代码很简单,没有什么问题。如果我想在Ann类上给注解A1设置值怎么办呢?@Aliasfor可以实现

@Aliasfor 案例

在上面的案例上直接修改B1:

@Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @A1
    @interface B1 {
        String value() default "b";
		//添加一个@AliasFor 注解,指定注解类和注解属性名
        @AliasFor(annotation = A1.class, value = "value")
        String a1Value();
    }

Ann类修改:

@B1(value = "b1" , a1Value = "a1")
    public class Ann {
    }

其他不变,运行结果:

@com.spring.annotation.AliasForAnnTest$A1(value=a1)
------------
@com.spring.annotation.AliasForAnnTest$B1(a1Value=a1, value=b1)

在来一个案例

public class AliasForAnn {

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.FIELD})
    @interface A1 {

        @AliasFor(value = "v2")
        String v1() default "";

        @AliasFor(value = "v1")
        String v2() default "";
    }

    @A1(v1 = "类 v1")
    public class Test {

        @A1(v2 = "属性 v2")
        private String name;
    }

    public static void main(String[] args) throws NoSuchFieldException {
        A1 mergedAnnotation = AnnotatedElementUtils.getMergedAnnotation(Test.class, A1.class);
        System.out.println(mergedAnnotation);

        System.out.println("--------");

        A1 name = AnnotatedElementUtils.getMergedAnnotation(Test.class.getDeclaredField("name"), A1.class);
        System.out.println(name);
    }
}

运行结果:

@com.spring.annotation.AliasForAnn$A1(v1=类 v1, v2=类 v1)
--------
@com.spring.annotation.AliasForAnn$A1(v1=属性 v2, v2=属性 v2)

我们看下@AliasFor的源码:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface AliasFor {

    @AliasFor("attribute")
    String value() default "";

    @AliasFor("value")
    String attribute() default "";

    Class<? extends Annotation> annotation() default Annotation.class;

}

AliasFor注解中value和attribute互为别名,随便设置一个,同时会给另外一个设置相同的值。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

8.spring系列- java注解 的相关文章

随机推荐

  • CIMCO Edit2022安装教程(非常详细)从零基础入门到精通,看完这一篇就够了(附安装包)

    软件下载 软件 CIMCO Edit 版本 2022 语言 简体中文 大小 251 79M 安装环境 Win11 Win10 Win8 Win7 硬件要求 CPU 2 0GHz 内存 4G 或更高 下载通道 百度网盘丨64位下载链接 htt
  • CommonJS是啥东西嘞

    AMD AMD要用define定一个模块 define dep1 dep2 function dep1 dep2 return function 包目录 package json包 bin用于可的目 lib用于JavaScript的目 do
  • sqli-libs基础篇总结(1-22)

    1 关于sqli labs 这个是sql注入的靶场 可以在git上下载 2 题目简介 前面的1 22题都是sql注入的基础题目 覆盖范围很广 不过都是针对mysql数据库的 1 4题 union注入 5 8题 布尔盲注 9 10题 延时盲注
  • sql server备份及导出表数据和结构

    一 备份表数据及结构 select into new table name from old tablename 二 导出表数据及结构 1 选中要导出的数据库 gt 任务 gt 生成脚本 或者在任务里面有生成脚本这个选项 好好找找能找到的
  • 高清变脸更快更逼真!比GAN更具潜力的可逆生成模型来了

    昨天上市即破发的小米 今天上午股价大涨近10 这下雷军要笑了 而且可以笑得更灿烂 更灿烂是什么样 来 我们用OpenAI刚刚发布的人工智能技术 给大家展示一下 当然这个最新的技术 不止这点本事 它的 想象力 很强大的说 比如 留胡子的硬汉版
  • 关于eclipse项目栏关闭项目不想再看到

    前言 如果你用是什么IntelliJ IDEA我这篇文章你就不用看了 我的建议还是用IDEA我也喜欢用 但是因为我们老师电脑卡的原因 这个编辑器比较吃配置所以用的eclipse 以前还用的myeclipse虽然我对编辑器没什么要求 但是我用
  • Jmeter常用线程组设置策略

    一 前言 在JMeter压力测试中 我们时常见到的几个场景有 单场景基准测试 单场景并发测试 单场景容量测试 混合场景容量测试 混合场景并发测试以及混合场景稳定性测试 在本篇文章中 我们会用到一些插件 在这边先给大家列出 Custom Th
  • Java多线程详解(线程同步)

    嗨喽 小伙伴们我来了 上一章 我们通过几个例子 点击跳转 介绍了线程安全问题 而说到线程安全就不得不提到线程同步 它是解决线程安全的一种重要方法 本章就来简单地介绍一下线程同步 从上一章的学习我们知道 当多个线程操作一个资源的时候 有可能由
  • 远程代码执行

    远程代码执行 远程代码执行 Remote Code Execute 远程命令执行 Remote Command Execute 1 为啥要远程执行代码 路由器 防火墙 入侵检测等设备的web管理界面 自动化运维的管理系统 提供给用户一个接口
  • antV实现离线中国2D地图并叠加拓扑(一)

    业务背景 中国地图铺满屏幕 屏幕的中间部分动态展示当前区域地图 当前区域有可能是省 市 县等 需要在当前区域展示拓扑站点 并实时弹出小面板展示当前站点详情 实现方式 antv G6实现拓扑图 antv L7绘制地图 本身L7是可以实现动态标
  • java生成6位随机数

    生成6位随机数 不会是5位或者7位 仅只有6位 System out println int Math random 9 1 100000 同理 生成5位随机数 System out println int Math random 9 1
  • 雷军也入局了...

    风口理论的发明者雷总最近也杀入大模型 AI领域了 早在10多天前雷军在微博就发过一段话 这段话其实已经暗示了雷军和他的小米已经在研发大模型产品了 相信要不了多久小米的大模型产品就会面世 这下国内几乎所有互联网巨头都杀入了大模型领域 同时还有
  • 2011年,移动互联网加速蔓延 – 来自2011移动开发者大会

    2011移动开发者大会 这是第二届移动开发者大会了 这一年来移动互联网各个领域蔓延开来 蔓延这个词是开复老师演讲的主题 从事塞班开发的请举手 举手者寥寥 记得在去年移动开发者大会上 举手者还有一些 经过一年的蔓延 塞班虽然仍然占有较大的份额
  • hadoop之YARN

    在YARN中 资源调度器 Scheduler 是ResourceManager中的重要组件 主要负责对整个集群 CPU 内存 的资源进行分配和调度 分配以资源Container的形式分发到各个应用程序中 如MapReduce作业 应用程序与
  • 一种消息和任务队列——beanstalkd

    beanstalkd 是一个轻量级消息中间件 其主要特性 基于管道 tube 和任务 job 的工作队列 work queue d 管道 tube tube类似于消息主题 topic 在一个beanstalkd中可以支持多个tube 每个t
  • 《现代加工技术》第1章 绪论

    1 1 加工技术发展简史 文章目录 1 1 加工技术发展简史 1 2 现代加工技术的地位与分类 1 3 现代加工技术的发展趋势 加工技术历史悠久 可以说它伴随着人类的诞生而出现 伴随着人类的进步而发展 人类与猿相分离 是由于人学会了双足行走
  • 第2章 应用层-计算机网络

    目录 学习目的 相关概念理解和主流应用层协议学习 目录 应用层协议原理 一些网络应用 了解应用层大概的应用 研发网络应用程序 研发网络应用程序的核心 基于运输层提供的服务 实现可以相互通信的端系统 没有应用程序软件运行在网络核心设备上 网络
  • 大数据专业到底有多火?

    2017 2018大数据为什么这么火热 从以下方面来看 人民日报官方微信公众平台发布了一篇文章 公布已有35所高校获批 数据科学与大数据技术 专业 使大数据受到更多家长的关注 大数据也被越来越多的人重视 高校开办相关专业也不能缓解大数据人才
  • MySQL安装出现环境问题(缺少Microsoft Visual C++ 2019 Redistributable Package)

    在安装MySQL时 选择安装类型 Setup Type 后 需要检查底层环境 一般来说 电脑的环境都是满足要求的 部分电脑可能会存在环境缺失的情况 不同安装模式和版本的环境要求不同 有的一个两个 有的多个 当相关环境缺失时 环境会报错 并显
  • 8.spring系列- java注解

    问题 注解是干什么的 一个注解可以使用多次吗 如何使用 Inherited是做什么的 Target中的TYPE PARAMETER和TYPE USER用在什么地方 泛型中如何使用注解 注解定义可以实现继承吗 spring对注解有哪些增强 A