mercyblitz / thinking-in-spring-boot-samples

小马哥书籍《Spring Boot 编程思想》示例工程
Apache License 2.0
1.9k stars 793 forks source link

「属性别名」 和 「属性覆盖」 理解有误 #61

Open justmehyp opened 5 years ago

justmehyp commented 5 years ago

小马哥这本书写的很好,使我收益匪浅,同时也很钦佩小马哥技术造诣之高。

最近看到 第 7 章「走向注解驱动编程(Annotation-Driven)」,很多之前稀里糊涂的地方,在书的指引下,加上动手试验和阅读相关资料之后,豁然开朗。

好了,开始提 Bug 吧 :》 我手上的书版次是:2019年3月第1版,印次是:2019年4月第2次印刷。

第 211 页第 1 段,关于“显性覆盖”,文档中并没有确认指出属性A和B是否在同一个注解中 是理解有误的,官方原文如下:

Explicit Overrides: if attribute A is declared as an alias for attribute B in a meta-annotation via @AliasFor, then A is an explicit override for B.

可以看到,属性 B 在 meta-annotation 中,所以 A 和 B 不在同一个注解中。 假如 A 和 B 在同一个注解,那么不能单向 A @AliasFor B,还需要 B @AliasFor A,但此时, A 和 B 之间不是"显性覆盖"的关系,而是 显性属性别名(Explicit Aliases) 的关系。

第 215 页末尾 和 第 216 页开头的示例,无法重现,我试了 Spring Boot 版本从 1.0.0.RELEASE 到 2.1.7.RELEASE 的几个版本,都无法重现。

书上的运行结果是不会报错,会输出:

Bean 名称: txManager, 对象: thinking.in.spring.boot.samples.spring5.bean.TransactionalServiceBean@52e6fdee
保存...
txManager2: 事务提交...

但我却抛异常了:

beanName: transactionalServiceBean, bean: com.example.springboot.annotationattribute.TransactionalServiceBean@1672fe87  
Exception in thread "main" org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [org.springframework.transaction.PlatformTransactionManager] is defined: expected single matching bean but found 2: txManager,txManager2

我的代码如下:

TransactionalServiceBootstrap.java

@Configuration
@ComponentScan
@EnableTransactionManagement
public class TransactionalServiceBootstrap {

    public static void main(String[] args) {

        ApplicationContext ac = new AnnotationConfigApplicationContext(TransactionalServiceBootstrap.class);
        ac.getBeansOfType(TransactionalServiceBean.class).forEach((beanName, bean) -> {
            System.out.println("beanName: " + beanName + ", bean: " + bean);
            bean.save();
        });
    }

    @Bean("txManager")
    public PlatformTransactionManager txManager() {
        return new PlatformTransactionManager() {
            @Override
            public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
                return new SimpleTransactionStatus();
            }

            @Override
            public void commit(TransactionStatus status) throws TransactionException {
                System.out.println("txManager commit.");
            }

            @Override
            public void rollback(TransactionStatus status) throws TransactionException {
                System.out.println("txManager rollback.");
            }
        };
    }

    @Bean("txManager2")
    public PlatformTransactionManager txManager2() {
        return new PlatformTransactionManager() {
            @Override
            public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
                return new SimpleTransactionStatus();
            }

            @Override
            public void commit(TransactionStatus status) throws TransactionException {
                System.out.println("txManager2 commit.");
            }

            @Override
            public void rollback(TransactionStatus status) throws TransactionException {
                System.out.println("txManager2 rollback.");
            }
        };
    }
}

@TransactionalService
class TransactionalServiceBean {
    public void save() {
        System.out.println("saving...");
    }
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Transactional
@Service("transactionalService")
@interface TransactionalService {
    String name() default "";

    String value() default "txManager";
}

实际上,无法重现,是符合我的预期的,因为 Spring 对 属性 value 做了特殊对待,「隐性覆盖」对 value 不起作用, Spring 源码位置见org.springframework.core.annotation.AnnotatedElementUtils.MergedAnnotationAttributesProcessor#postProcess 方法,摘取如下:

// Implicit annotation attribute override based on convention
else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) {
    overrideAttribute(element, annotation, attributes, attributeName, attributeName);
}

第 220 页第 1 段 末尾 因此@TransactionalService.name()与@Service.value()之间的关系被"Attribute Aliases and Overrides"一节定义为"隐性别名" 理解有误,官方对于"隐性别名"的说明是:

Implicit Aliases: if two or more attributes in one annotation are declared as explicit overrides for the same attribute in a meta-annotation via @AliasFor, they are implicit aliases.

可以看到,in one annotation 才是属性别名,@TransactionalService.name()与@Service.value() 显然不在同一个注解中。

我对「属性别名」和「属性覆盖」的理解总结如下: -「属性别名」只能发生在同一个注解内部 -「属性覆盖」只能发生在注解之间

另外,我还将这段时间的学习和理解记录在这篇文章中https://github.com/justmehyp/note-spring-boot/blob/master/note/Annotation-Programming-Model.md,不一定都正确,如有错误,还望帮忙提醒纠正,多谢多谢!

mercyblitz commented 5 years ago

感谢反馈,确认中...

wanglforever commented 5 years ago

原文的Bean是txManager和txManager2,并非txManager1和txManager2,名称有误,所以复现不了,我的是19年第四次印刷,不确定是否修改过。

justmehyp commented 5 years ago

@wanglforever 已修改 txManager1 为 txManager,结果还是一样的,无法重现,原因是 @TransactionalService.value() 不会隐式覆盖 @Service.value()

wanglforever commented 5 years ago

@justmehyp 您说的没错,@TransactionalService.value() 不会隐式覆盖 @Service.value(),但是SpringFramework5.0.6,AnnotationBeanNameGenerator#generateBeanName获取Bean的beanName的方式是如果低层注解上有value属性,那么直接返回value属性的值,而不是去取顶层@Component的value属性值,所以会有这种现象 Bean 名称 : txManager , 对象 : thinking.in.spring.boot.samples.spring5.bean.TransactionalServiceBean@7283d3eb 而beanName为txManager的PlatformTransactionManager就会被忽略掉,因此这种现象是没有问题的,您是否使用的不是5.0.6的版本,因为小马哥的配套代码也是基于5.0.6的,而且我这里是可以复现的。

justmehyp commented 5 years ago

@wanglforever 多谢,看源码确实如此。在我的 mac上,还会出现 TransactionServiceBean bean 被 注解方法 txManager 生成的 bean 覆盖的情况。可能是不同操作系统上 bean 的扫描加载顺序不一致吧,所以我这边切到 5.0.6 版本依然无法重现:

21:03:23.448 [main] INFO ... - Overriding bean definition for bean 'txManager' with a different definition: replacing [Generic bean: class [... TransactionServiceBean];  ... with [ ... factoryMethodName=txManager; ...]

@mercyblitz 由此可见,Spring 对 Annotation 的 value属性做了一些不少特殊处理 ,此处选择 value 属性作为示例不妥,干扰因素太多。 目前发现: 1) 隐式覆盖不适用于 value 属性, 特殊处理逻辑在org.springframework.core.annotation.AnnotatedElementUtils.MergedAnnotationAttributesProcessor#postProcess 2) 如果注解有元注解 @Component, 那么在 AnnotationBeanNameGenerator#determineBeanNameFromAnnotation 方法中,会取其 value 属性值作为 bean name

DamianSheldon commented 2 years ago

觉得还可以增加隐性覆盖和显性覆盖共存情况的讨论,例如:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
@Transactional
public @interface TransactionalService {

    String name() default "nameValue";

    String value() default "value";

    String transactionManager() default "transactionManager";

    @AliasFor(annotation = Transactional.class, attribute = "value")
    String txManager() default "txManager";

}

5.0.6 版本现在是报错:Caused by: org.springframework.core.annotation.AnnotationConfigurationException: In AnnotationAttributes for annotation [org.springframework.transaction.annotation.Transactional] declared on class thinking.in.spring.boot.samples.spring5.bean.TransactionalServiceBean, attribute 'transactionManager' and its alias 'value' are declared with values of [transactionManager] and [txManager], but only one is permitted.