深入理解Spring注解机制:合并注解的合成

深入理解Spring注解机制:合并注解的合成,第1张

众所周知, spring 从 25 版本以后开始支持使用注解代替繁琐的 xml 配置,到了 springboot 更是全面拥抱了注解式配置。平时在使用的时候,点开一些常见的等注解,会发现往往在一个注解上总会出现一些其他的注解,比如 @Service :

大部分情况下,我们可以将 @Service 注解等同于 @Component 注解使用,则是因为 spring 基于其 JDK 对 元注解的机制 进行了扩展。

在 java 中,元注解是指可以注解在其他注解上的注解,spring 中通过对这个机制进行了扩展,实现了一些原生 JDK 不支持的功能,比如允许在注解中让两个属性互为别名,或者将一个带有元注解的子注解直接作为元注解看待,或者在这个基础上,通过 @AliasFor 或者同名策略让子注解的值覆盖元注解的值。

本文将基于 spring 源码 52x 分支,解析 spring 如何实现这套功能的。

这是系列的第三篇文章,将详细介绍 Spring 是如何在经过搜索与属性映射后,将处理后的注解合成为合并注解的。

相关文章:

我们在前文了解用于搜索注解的合并注解聚合 MergedAnnotations 与用于完成注解属性映射的 AnnotationTypeMappings 和 AnnotationTypeMapping ,现在我们需要知道在 MergedAnnotations 这个容器中, AnnotationTypeMappings 和 AnnotationTypeMapping 是如何转为一个我们所需要的合并注解 MergedAnnotation 的。

与前文一样,我们以 AnnotatedElementUtilsfindMergedAnnotations 方法作为入口:

我们在上文顺着 MergedAnnotationsget 一路找到 TypeMappedAnnotationsMergedAnnotationFinder 的 process 方法,在这里我们目睹了一个普通的注解的元注解被解析为 AnnotationTypeMappings 与 AnnotationTypeMapping 的过程:

该方法是 AnnotationTypeMapping 转为 MergedAnnotation 的关键。

TypeMappedAnnotation 是 MergedAnnotation 一个通用实现,在大部分情况下,我们所说的合并注解其实指的就是这个类。

通过它的构造方法我们得以了解其创建过程:

可以看得出, TypeMappedAnnotation 基本可以认为是 AnnotationTypeMapping 的包装类,它以一个 AnnotationTypeMapping 实例作为数据源,从而提供一些关于映射后的属性的相关功能。

回到 AnnotatedElementUtilsfindMergedAnnotations ,我们可以看到,在通过 MergedAnnotations 获得了一个 MergedAnnotation 对象——实际上是 TypeMappedAnnotation 对象——之后,又调用了 MergedAnnotationsynthesize 方法,将 MergedAnnotation 转成了一个调用方指定类型的注解对象。

该方法先调用了 AbstractMergedAnnotation 的 synthesize 方法:

随后再调用了实现类 TypeMappedAnnotation 的 synthesize 方法:

继续点开 createSynthesized :

而 SynthesizedMergedAnnotationInvocationHandler 是一个用于 JDK 动态代理的 InvocationHandler ,我们不需要完全站看,仅需看看它的构造函数与 InvocationHandlerinvoke 就能明白它的运作机制了:

至此,合并注解的合成机制已经很明确了:

承接上文,当我们使用 MergedAnnotationsynthesize 方法时,我们可能会得到两种对象:

而通过注解代理对象取值时,这些方法会被代理到 SynthesizedMergedAnnotationInvocationHandler 中存放的 MergedAnnotation 对象上,从而让这个代理对象通过原始注解的属性,获得与原始注解不一样的属性值。

当我们调用代理对象的属性值时,它会在 SynthesizedMergedAnnotationInvocationHandler 中,通过 invoke 代理到对应的方法上:

这里我们代理对象是如何获取注解属性值的:

这里的 MergedAnnotationgetValue 最终在经过多次跳转后,调到 TypeMappedAnnotationgetAttributeValue 上:

而这边的 getValue 方法就是真正要获取属性值的地方。

这一步有点复杂,主要是根据不同的情况,通过 AnnotationTypeMapping 中的几个属性映射数组,包括 aliasMappings 、 conventionMappings , annotationValueMappings 与 annotationValueSource 来确定最终用于取值的 AnnotationTypeMapping 对象与调用的方法在 AttributeMethods 中的下标:

至此,获取属性值的方法流程也走完了。

在这一章,我们了解了当通过 MergedAnnotations 获得注解并解析得到 AnnotationTypeMapping 后, AnnotationTypeMapping 是如何再转为我们所需的 MergedAnnotation ,以及在此之后, MergedAnnotation 又是如何生成我们最终所需要的代理注解的。

简而言之,当解析注解的元注解获得所需的 AnnotationTypeMapping 后, MergedAnnotation 会判断 AnnotationTypeMapping 是否发生过属性映射,如果没有则返回该映射对象对应的原始注解,否则就通过 SynthesizedMergedAnnotationInvocationHandler 生成一个对应类型的 JDK 动态代理对象。

当我们通过代理对象去调用注解的方法,获取注解的属性的时候, SynthesizedMergedAnnotationInvocationHandler 会把方法代理到对应的内部方法中,而获取属性时,还会通过 MergedAnnotationgetValue ,最终绕到 AnnotationTypeMapping 中获取被映射后的属性值。

1、注解就是配置文件的另一种表现形式,Spring通过注解也可以创建对象。步骤如下:

(1) 在源代码加入注解,例如@Component

(2) 在spring的配置文件,加入组件扫描器的标签

2、 创建对象的注解

(1)@Component 普通java对象

@Repository : 放在dao接口的实现类上面,表示创建dao对象,持久层对象,能访问数据库

@Service : 放在业务层接口的实现类上面, 表示创建业务层对象, 业务层对象有事务的功能

@Controller:放在控制器类的上面,表示创建控制器对象。 属于表示层对象。 控制器对象能接受请求,把请求的处理结果显示给用户。

以上四个注解都能创建对象,但是@Repository @Service @Controller有角色说明, 表示对象是分层的。

3、 简单类型属性赋值

@Value: 简单类型属性赋值

属性:value 简单类型属性值

位置:1)在属性定义的上面 ,无需set方法,推荐使用

2)在set方法的上面

4、引用类型赋值注解

(1)@Autowired:spring框架提供的,给引用类型赋值的,使用自动注入原理。支持byName,byType。默认是byType。

如果想@Autowired注解按对象名称注入需要在属性的上面再添加一个注解@Qualifier;

(2)@Resource: 来自jdk中,给引用类型赋值的,支持byName,byType默认是byName,spring支持这个注解的使用。

说明,使用jdk18带有@Resource注解, 高于jdk18没有这个@Resource, 需要加入一个依赖。

springboot的优点就是简化配置,,没有了xml,基本都是一个配置(applicationproperties)+注解来实现springboot的构建

那么都有哪些注解咧说一下我在工作中常用的注解

1:##@SpringBootApplication

标识该类为SpringBoot项目启动类。并且让SpringBoot自动给程序进行必要的配置,等同于@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan这三个注解

(1):@SpringBootConfiguration表示的是该类会作为Springboot的一个配置类,

(2):@EnableAutoConfiguration表示开启自动配置功能,里面也实现了自动配置原理

@Configuration会把组件会装配到实体类上封装为一个bean,AutoConfigurationImportSelector的selectImports()这个方法,找到所有JavaConfig自动配置类的全限定名对应的class,然后将所有自动配置类加载到Spring容器中。bean有了,配置有了,相当于对象也有了,这就是自动配置

(3):@ComponentScan用来将包加入SpringIOC的包扫描,

2: @RestController 和@Controller

@RestController相当于@Controller+@ResponseBody,

@RestController加在类上面的注解,使得类里面的每个方法都将json/xml返回数据加返回到前台页面中。

比如return "abc" 前端会展示abc三个字母

@Controller加在类上面的注解,使得类里面的每个方法都返回一个视图页面。

比如return "abc" 前端会展示静态资源中的的abchtml里面的内容

3: @component和@configuration

虽然Component注解也会当做配置类,但是并不会为其生成CGLIB代理Class,所以在生成Driver对象时和生成Car对象时调用car()方法执行了两次new *** 作,所以是不同的对象。当时Configuration注解时,生成当前对象的子类Class,并对方法拦截,第二次调用car()方法时直接从BeanFactory之中获取对象,所以得到的是同一个对象。

4: @Autowired 与@Resource

@Resource和@Autowired都是做bean的注入时使用,其实@Resource并不是Spring的注解,它的包是javaxannotationResource,需要导入,但是Spring支持该注解的注入。

@Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。如果我们想使用按照名称(byName)来装配,可以结合@Qualifier注解一起使用

@Resource默认按照ByName自动注入,由J2EE提供,需要导入包javaxannotationResource。

使用注解

在一般的Java开发中,最常接触到的可能就是@Override和@SupressWarnings这两个注解了。使用@Override的时候只需要一个简单的声明即可。这种称为标记注解(marker annotation ),它的出现就代表了某种配置语义。而其它的注解是可以有自己的配置参数的。配置参数以名值对的方式出现。使用 @SupressWarnings的时候需要类似@SupressWarnings({"uncheck", "unused"})这样的语法。在括号里面的是该注解可供配置的值。由于这个注解只有一个配置参数,该参数的名称默认为value,并且可以省略。而花括号则表示是数组类型。在JPA中的@Table注解使用类似@Table(name = "Customer", schema = "APP")这样的语法。从这里可以看到名值对的用法。在使用注解时候的配置参数的值必须是编译时刻的常量。

从某种角度来说,可以把注解看成是一个XML元素,该元素可以有不同的预定义的属性。而属性的值是可以在声明该元素的时候自行指定的。在代码中使用注解,就相当于把一部分元数据从XML文件移到了代码本身之中,在一个地方管理和维护。

开发注解

在一般的开发中,只需要通过阅读相关的API文档来了解每个注解的配置参数的含义,并在代码中正确使用即可。在有些情况下,可能会需要开发自己的注解。这在库的开发中比较常见。注解的定义有点类似接口。下面的代码给出了一个简单的描述代码分工安排的注解。通过该注解可以在源代码中记录每个类或接口的分工和进度情况。

@Retention(RetentionPolicyRUNTIME)

@Target(ElementTypeTYPE)

public @interface Assignment {

    String assignee();

    int effort();

    double finished() default 0;

}

@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型。可以通过default来声明参数的默认值。在这里可以看到@Retention和@Target这样的元注解,用来声明注解本身的行为。@Retention用来声明注解的保留策略,有CLASS、RUNTIME和SOURCE这三种,分别表示注解保存在类文件、JVM运行时刻和源代码中。只有当声明为RUNTIME的时候,才能够在运行时刻通过反射API来获取到注解的信息。@Target用来声明注解可以被添加在哪些类型的元素上,如类型、方法和域等。

处理注解

在程序中添加的注解,可以在编译时刻或是运行时刻来进行处理。在编译时刻处理的时候,是分成多趟来进行的。如果在某趟处理中产生了新的Java源文件,那么就需要另外一趟处理来处理新生成的源文件。如此往复,直到没有新文件被生成为止。在完成处理之后,再对Java代码进行编译。JDK 5中提供了apt工具用来对注解进行处理。apt是一个命令行工具,与之配套的还有一套用来描述程序语义结构的Mirror API。Mirror API(comsunmirror)描述的是程序在编译时刻的静态结构。通过Mirror API可以获取到被注解的Java类型元素的信息,从而提供相应的处理逻辑。具体的处理工作交给apt工具来完成。编写注解处理器的核心是AnnotationProcessorFactory和AnnotationProcessor两个接口。后者表示的是注解处理器,而前者则是为某些注解类型创建注解处理器的工厂。

以上面的注解Assignment为例,当每个开发人员都在源代码中更新进度的话,就可以通过一个注解处理器来生成一个项目整体进度的报告。 首先是注解处理器工厂的实现。

public class AssignmentApf implements AnnotationProcessorFactory {  

    public AnnotationProcessor getProcessorFor(Set<AnnotationTypeDeclaration> atds, AnnotationProcessorEnvironment env) {

        if (atdsisEmpty()) {

           return AnnotationProcessorsNO_OP;

        }

        return new AssignmentAp(env); //返回注解处理器

    } 

    public Collection<String> supportedAnnotationTypes() {

        return CollectionsunmodifiableList(ArraysasList("annotationAssignment"));

    }

    public Collection<String> supportedOptions() {

        return CollectionsemptySet();

    }

}

AnnotationProcessorFactory接口有三个方法:getProcessorFor是根据注解的类型来返回特定的注解处理器;supportedAnnotationTypes是返回该工厂生成的注解处理器所能支持的注解类型;supportedOptions用来表示所支持的附加选项。在运行apt命令行工具的时候,可以通过-A来传递额外的参数给注解处理器,如-Averbose=true。当工厂通过 supportedOptions方法声明了所能识别的附加选项之后,注解处理器就可以在运行时刻通过AnnotationProcessorEnvironment的getOptions方法获取到选项的实际值。注解处理器本身的基本实现如下所示。

public class AssignmentAp implements AnnotationProcessor { 

    private AnnotationProcessorEnvironment env;

    private AnnotationTypeDeclaration assignmentDeclaration;

    public AssignmentAp(AnnotationProcessorEnvironment env) {

        thisenv = env;

        assignmentDeclaration = (AnnotationTypeDeclaration) envgetTypeDeclaration("annotationAssignment");

    }

    public void process() {

        Collection<Declaration> declarations = envgetDeclarationsAnnotatedWith(assignmentDeclaration);

        for (Declaration declaration : declarations) {

           processAssignmentAnnotations(declaration);

        }

    }

    private void processAssignmentAnnotations(Declaration declaration) {

        Collection<AnnotationMirror> annotations = declarationgetAnnotationMirrors();

        for (AnnotationMirror mirror : annotations) {

            if (mirrorgetAnnotationType()getDeclaration()equals(assignmentDeclaration)) {

                Map<AnnotationTypeElementDeclaration, AnnotationValue> values = mirrorgetElementValues();

                String assignee = (String) getAnnotationValue(values, "assignee"); //获取注解的值

            }

        }

    }   

}

注解处理器的处理逻辑都在process方法中完成。通过一个声明(Declaration)的getAnnotationMirrors方法就可以获取到该声明上所添加的注解的实际值。得到这些值之后,处理起来就不难了。

在创建好注解处理器之后,就可以通过apt命令行工具来对源代码中的注解进行处理。 命令的运行格式是apt -classpath bin -factory annotationaptAssignmentApf src/annotation/work/java,即通过-factory来指定注解处理器工厂类的名称。实际上,apt工具在完成处理之后,会自动调用javac来编译处理完成后的源代码。

JDK 5中的apt工具的不足之处在于它是Oracle提供的私有实现。在JDK 6中,通过JSR 269把自定义注解处理器这一功能进行了规范化,有了新的javaxannotationprocessing这个新的API。对Mirror API也进行了更新,形成了新的javaxlangmodel包。注解处理器的使用也进行了简化,不需要再单独运行apt这样的命令行工具,Java编译器本身就可以完成对注解的处理。对于同样的功能,如果用JSR 269的做法,只需要一个类就可以了。

@SupportedSourceVersion(SourceVersionRELEASE_6)

@SupportedAnnotationTypes("annotationAssignment")

public class AssignmentProcess extends AbstractProcessor {

    private TypeElement assignmentElement; 

    public synchronized void init(ProcessingEnvironment processingEnv) {

        superinit(processingEnv);

        Elements elementUtils = processingEnvgetElementUtils();

        assignmentElement = elementUtilsgetTypeElement("annotationAssignment");

    } 

    public boolean process(Set< extends TypeElement> annotations, RoundEnvironment roundEnv) {

        Set< extends Element> elements = roundEnvgetElementsAnnotatedWith(assignmentElement);

        for (Element element : elements) {

            processAssignment(element);

        }

    }

    private void processAssignment(Element element) {

        List< extends AnnotationMirror> annotations = elementgetAnnotationMirrors();

        for (AnnotationMirror mirror : annotations) {

            if (mirrorgetAnnotationType()asElement()equals(assignmentElement)) {

                Map< extends ExecutableElement,  extends AnnotationValue> values = mirrorgetElementValues();

                String assignee = (String) getAnnotationValue(values, "assignee"); //获取注解的值

            }

        }

    } 

}

仔细比较上面两段代码,可以发现它们的基本结构是类似的。不同之处在于JDK 6中通过元注解@SupportedAnnotationTypes来声明所支持的注解类型。另外描述程序静态结构的javaxlangmodel包使用了不同的类型名称。使用的时候也更加简单,只需要通过javac -processor annotationpapAssignmentProcess Demo1java这样的方式即可。

上面介绍的这两种做法都是在编译时刻进行处理的。而有些时候则需要在运行时刻来完成对注解的处理。这个时候就需要用到Java的反射API。反射API提供了在运行时刻读取注解信息的支持。不过前提是注解的保留策略声明的是运行时。Java反射API的AnnotatedElement接口提供了获取类、方法和域上的注解的实用方法。比如获取到一个Class类对象之后,通过getAnnotation方法就可以获取到该类上添加的指定注解类型的注解。

实例分析

下面通过一个具体的实例来分析说明在实践中如何来使用和处理注解。假定有一个公司的雇员信息系统,从访问控制的角度出发,对雇员的工资的更新只能由具有特定角色的用户才能完成。考虑到访问控制需求的普遍性,可以定义一个注解来让开发人员方便的在代码中声明访问控制权限。

@Retention(RetentionPolicyRUNTIME)

@Target(ElementTypeMETHOD)

public @interface RequiredRoles {

    String[] value();

}

下一步则是如何对注解进行处理,这里使用的Java的反射API并结合动态代理。下面是动态代理中的InvocationHandler接口的实现。

public class AccessInvocationHandler<T> implements InvocationHandler {

    final T accessObj;

    public AccessInvocationHandler(T accessObj) {

        thisaccessObj = accessObj;

    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        RequiredRoles annotation = methodgetAnnotation(RequiredRolesclass); //通过反射API获取注解

        if (annotation != null) {

            String[] roles = annotationvalue();

            String role = AccessControlgetCurrentRole();

            if (!ArraysasList(roles)contains(role)) {

                throw new AccessControlException("The user is not allowed to invoke this method");

            }

        }

        return methodinvoke(accessObj, args);

    } 

}

在具体使用的时候,首先要通过ProxynewProxyInstance方法创建一个EmployeeGateway的接口的代理类,使用该代理类来完成实际的 *** 作。

定义:注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK15及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

你好,可以通过java的反射获取。代码如下:

public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {

try {

//TDict是实体对象

Class<TDict> c = TDictclass;

//获取对象中所有的方法

Method[] methods = cgetMethods();

//进行循环

for (Method method : methods) {

//判断方法上是否存在Column这个注解

if (methodisAnnotationPresent(Columnclass)) {

//methodinvoke(bus, new Object[]{});

Column col = methodgetAnnotation(Columnclass);

//Systemoutprintln(col);

//下面是取值

Systemoutprint("方法名称:"+methodgetName());

Systemoutprint("\tname---"+colname());

Systemoutprint("\tnullable---"+colnullable());

Systemoutprintln("\tlength---"+collength());

}

}

} catch (SecurityException e) {

// TODO Auto-generated catch block

eprintStackTrace();

}

}

需要引用

import javalangreflectInvocationTargetException;

        import javalangreflectMethod;

        import javaxpersistenceColumn;

        

        测试结果:

        方法名称:getId name---id nullable---false length---255

        方法名称:getDictDescribe name---dict_describe nullable---true length---50

        方法名称:getDictType name---dict_type nullable---true length---20

        方法名称:getDictSort name---dict_sort nullable---true length---255

        方法名称:getDictRemark name---dict_remark nullable---true length---50

        方法名称:getDictCtime name---dict_ctime nullable---true length---50

        望采纳,

以上就是关于深入理解Spring注解机制:合并注解的合成全部的内容,包括:深入理解Spring注解机制:合并注解的合成、Spring注解总结、Springboot(四):springboot的注解有哪些注解等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/9680828.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-30
下一篇2023-04-30

发表评论

登录后才能评论

评论列表(0条)

    保存