@Import注解的魅力

    本篇主要介绍Spring注解@Import的魅力所在:它能让你高度自由的定义配置类装载规则与Bean注册逻辑。@ImportSpring体系中的一个比较重要的注解,下面让我们一起看看它都有哪些神奇的魅力吧!

注册Bean

   @Import第一个功能就是注册BeanSpring容器中,用法非常简单,将需要注册到Spring的类通过@Import直接导入即可,这时候注册的Bean对应的名称为类完全限定名。如下所示:

package com.swj.mj.autoconfig.service;

@Slf4j
public class HelloService {

    public HelloService() {
        log.info("HelloService Initialization...");
    }

    public String sayHello(String name) {
        return "Hello " + name;
    }

}

    首先任意定义一个类,然后我们直接在某个Bean或者配置类上通过@Import(HelloService.class)方法注册Bean,如下:

package com.swj.mj.web.hello;

@RestController
@AllArgsConstructor
@RequestMapping("/hello")
@Import(HelloService.class)
public class HelloController implements ApplicationContextAware {

    private static ApplicationContext appCtx;
    private final HelloService helloService;

    @GetMapping
    public Map<String, Object> hello(String name) {
        String qualifiedBeanName = HelloService.class.getName();
        assert appCtx.containsBean(qualifiedBeanName) : "Spring容器中找不到名称为" + qualifiedBeanName + "的Bean";
        String[] beanNames = appCtx.getBeanNamesForType(HelloService.class);
        assert beanNames.length == 1 && qualifiedBeanName.equals(beanNames[0]);
        return ImmutableMap.of("name", name, "helloResult", helloService.sayHello(name));
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        appCtx = applicationContext;
    }

}

    创建一个启动类:

package com.swj.mj.web;

@SpringBootApplication
public class WebApplication {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(WebApplication.class, args);
    }

}

    添加JVM选项-ea(激活断言功能)并启动容器后(启动类的所在包路径为com.swj.mj.web,并且没有额外配置扫描包,所以默认扫描的包是com.swj.mj.web),可以看到如下日志打印,说明@Import可以用来注册Bean

@Import注解的魅力
@Import注册SpringBean

    同时,请求后可以看到两个断言都成立,说明通过@Import注册的SpringBeanbeanName为类对应的类完全限定名。

@Import注解的魅力
@Import注册的SpringBean对应名称

    如果我们去掉了@Import(HelloService.class),那么应用启动后将直接失败并终止:

@Import注解的魅力
去掉@Import后将直接找不到类以致于启动失败

导入配置类

    第二个功能是直接导入配置类。我们在原先的基础上新增一个配置类HelloServiceConfiguration

@Configuration
public class HelloServiceConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public HelloService helloService() {
        return new HelloService();
    }

}

    该配置类会在容器中没有helloService这个Bean时自动注册HelloService。然后修改HelloController

@RestController
@AllArgsConstructor
@RequestMapping("/hello")
@Import(HelloServiceConfiguration.class)
public class HelloController implements ApplicationContextAware {

    private static ApplicationContext appCtx;
    private final HelloService helloService;

    @GetMapping
    public Map<String, Object> hello(String name) {
        String beanName = "helloService";
        assert appCtx.containsBean(beanName) : "Spring容器中找不到名称为" + beanName + "的Bean";
        String[] beanNames = appCtx.getBeanNamesForType(HelloService.class);
        assert beanNames.length == 1 && beanName.equals(beanNames[0]);
        return ImmutableMap.of("name", name, "helloResult", helloService.sayHello(name));
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        appCtx = applicationContext;
    }

}

    这里通过@Import(HelloServiceConfiguration.class)直接导入配置类,由配置类注册HelloService,这时候的beanName就不是类完全限定名了,而是方法名helloService

    实际上HelloServiceConfiguration同样会被注册到Spring容器中,通过appCtx.getBean(HelloServiceConfiguration.class)可以得到配置类。所以可以说第一个功能与第二个功能是重合的,这点在源码上也可以得到解释,具体可以参考org.springframework.context.annotation.ConfigurationClassParser#processImports。本篇不讲源码哈,只介绍如何使用。

选择性装载配置类

    第三个功能是通过实现org.springframework.context.annotation.ImportSelector接口完成动态开启配置类。

    首先看一下ImportSelector接口的定义:

public interface ImportSelector {

 /**
  * Select and return the names of which class(es) should be imported based on
  * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
  * @return the class names, or an empty array if none
  */

 String[] selectImports(AnnotationMetadata importingClassMetadata);

 /**
  * Return a predicate for excluding classes from the import candidates, to be
  * transitively applied to all classes found through this selector's imports.
  * <p>If this predicate returns {@code true} for a given fully-qualified
  * class name, said class will not be considered as an imported configuration
  * class, bypassing class file loading as well as metadata introspection.
  * @return the filter predicate for fully-qualified candidate class names
  * of transitively imported configuration classes, or {@code null} if none
  * @since 5.2.4
  */

 @Nullable
 default Predicate<String> getExclusionFilter() {
  return null;
 }

}

    主要关注String[] selectImports(AnnotationMetadata importingClassMetadata)方法,参数importingClassMetadata表示使用了@Import(? extends ImportSelector)注解的类的元数据。通过该参数可以实现获取一些组合注解的自定义属性等内容,从而实现选择性装载配置类。

「注意这个方法的返回值不能是null,极端情况请返回空数组,否则运行将抛出NPE。」

    让我们先创建一个SpringUtil

package com.swj.mj.autoconfig.util;

public class SpringUtil implements BeanFactoryAwareApplicationContextAwareOrdered {

    private static ApplicationContext appCtx;
    private static BeanFactory beanFactory;

    public static ApplicationContext getAppCtx() {
        return appCtx;
    }

    public static BeanFactory getBeanFactory() {
        return beanFactory;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        SpringUtil.beanFactory = beanFactory;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtil.appCtx = applicationContext;
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 1;
    }

}

    这个类主要用于通过静态持有SpringApplicationContextBeanFactory,后续可以直接通过该类访问Spring容器。然后创建我们的ImportSelector配置类装载选择器:

package com.swj.mj.autoconfig.configuration;

public class CustomImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        // importingClassMetadata 指使用 @Import(HelloServiceImportSelector) 时 @Import 注解所在类的元数据
        String enableCustomConfig = EnableCustomConfig.class.getName();
        if (importingClassMetadata.hasAnnotation(enableCustomConfig)) {
            Map<String, Object> attrs = importingClassMetadata.getAnnotationAttributes(enableCustomConfig);
            if (MapUtils.isNotEmpty(attrs)) {
                String registerUtil = Optional.ofNullable(attrs.get("registerUtil")).map(Object::toString).orElse("false");
                if (Boolean.parseBoolean(registerUtil)) {
                    return new String[]{
                            HelloServiceConfiguration.class.getName(),
                            SpringUtil.class.getName()
                    };
                }
            }
        }
        return new String[]{
                HelloServiceConfiguration.class.getName()
        };
    }

}

    然后创建我们的EnableCustomConfig注解:

package com.swj.mj.autoconfig.annotation;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(CustomImportSelector.class)
public @interface EnableCustomConfig {

    /**
     * 是否注册 {@link com.swj.mj.autoconfig.util.SpringUtil} 工具类,默认为 {@literal true}
     */

    boolean registerUtil() default true;

}

    这里简单解释一下:首先@EnableCustomConfig注解组合了@Import(CustomImportSelector.class),这样在CustomImportSelector#selectImports方法中可以通过importingClassMetadata获取得到@EnableCustomConfig注解的属性配置。然后通过该配置实现添加装载配置。这里我们判断registerUtil是否为true(默认为true),为true时将装载SpringUtil以便后续可以通过SpringUtil访问Spring容器。

    接着调整HelloController

@RestController
@AllArgsConstructor
@RequestMapping("/hello")
@EnableCustomConfig
public class HelloController {

    private final HelloService helloService;

    @GetMapping
    public Map<String, Object> hello(String name) {
        ApplicationContext appCtx = SpringUtil.getAppCtx();
        assert Objects.nonNull(appCtx) : "appCtx 为 null";
        assert appCtx.containsBean(SpringUtil.class.getName());
        assert appCtx.containsBean(HelloServiceConfiguration.class.getName());
        assert appCtx.getBean(HelloService.class) == helloService;
        return ImmutableMap.of("name", name, "helloResult", helloService.sayHello(name));
    }

}

    这里通过添加@EnableCustomConfig激活联动激活@Import(CustomImportSelector.class)以注册SpringUtilHelloServiceConfiguration,由于默认registerUtiltrue,所以会注册SpringUtil。启动应用后能访问http://localhost:8080/hello?name=Reka表示断言均成立,容器中确实注册了对应的Bean

    接着我们调整@EnableCustomConfig@EnableCustomConfig(registerUtil = false),再次启动容器访问http://localhost:8080/hello?name=Reka,这时候会发现断言失败:

@Import注解的魅力
通过注解属性配置选择性装载配置类

动态注册Bean

    这是@Import的第四个功能,通过org.springframework.context.annotation.ImportBeanDefinitionRegistrar自由注册SpringBean。该接口的定义如下:

public interface ImportBeanDefinitionRegistrar {

 default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
   BeanNameGenerator importBeanNameGenerator)
 
{

  registerBeanDefinitions(importingClassMetadata, registry);
 }

 default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
 }

}

    主要看void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry),其中参数importingClassMetadataImportSelector里的参数效用一致,registry参数简单理解是Bean注册器,通过它可以往Spring容器中注册Bean。现在让我们通过动态注册Bean的方式实现CustomImportSelector的功能。

package com.swj.mj.autoconfig.configuration;

public class CustomImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        AnnotationAttributes attrs = AnnotatedElementUtils.getMergedAnnotationAttributes(
                ClassUtils.resolveClassName(importingClassMetadata.getClassName(), null),
                EnableCustomBean.class);
        if (MapUtils.isNotEmpty(attrs) && BooleanUtils.isTrue(attrs.getBoolean("registerUtil"))) {
            registry.registerBeanDefinition("springUtil"new RootBeanDefinition(SpringUtil.class));
        }
        registry.registerBeanDefinition("helloService"new RootBeanDefinition(HelloService.class));
    }

}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(CustomImportBeanDefinitionRegistrar.class)
public @interface EnableCustomBean {

    /**
     * 是否注册 {@link com.swj.mj.autoconfig.util.SpringUtil} 工具类,默认为 {@literal true}
     */

    boolean registerUtil() default true;

}

    CustomImportBeanDefinitionRegistrar的逻辑与CustomImportSelector基本一致,只不过最后不是返回配置类,而是直接通过org.springframework.beans.factory.support.BeanDefinitionRegistry#registerBeanDefinition注册Bean。最后调整一下HelloController

@RestController
@AllArgsConstructor
@RequestMapping("/hello")
@EnableCustomBean
public class HelloController {

    private final HelloService helloService;

    @GetMapping
    public Map<String, Object> hello(String name) {
        ApplicationContext appCtx = SpringUtil.getAppCtx();
        assert Objects.nonNull(appCtx) : "appCtx 为 null";
        assert appCtx.containsBean("springUtil");
        assert appCtx.getBean(HelloService.class) == helloService;
        return ImmutableMap.of("name", name, "helloResult", helloService.sayHello(name));
    }

}

    实际运行效果与CustomImportSelect@EnableCustomConfig一致,请读者自行验证。

    如果仅仅是这样,你是不是认为就没必要有ImportBeanDefinitionRegistrar了。实际上通过该接口我们可以定义自己的容器Bean注解。实现很多特殊的功能:比如在三维家美家技术中使用到了SpringCloud Stream,但SCS@org.springframework.cloud.stream.annotation.EnableBinding注解很麻烦,每次引入新的@Input@Output都要将对应接口添加到@EnableBinding中,三维家美家通过ImportBeanDefinitionRegistrar和自定义注解实现新的Input/Ouput Bean扫描注册流程。后续会计划将该功能提PRSpringCloud项目中。

    通过自定义注册和ImportBeanDefinitionRegistrar可以更灵活地自定义Bean注册逻辑,限于篇幅原因,我们往后有机会再讲。各位,今天关于@Import的基本用法是否掌握了呢,建议可以自己实践一次以加深理解,如果对源码有兴趣,可以阅读org.springframework.context.annotation.ConfigurationClassParser类。


原文始发于微信公众号(三维家技术实践):@Import注解的魅力

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/30576.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!