美文网首页工作生活
[深入学习]Spring 组件注册

[深入学习]Spring 组件注册

作者: lconcise | 来源:发表于2019-07-11 19:00 被阅读0次

    接触过Spring的同学肯定都听过IOC。在传统的Java编程中,当需要用到某个对象的时候,我们都是主动地显示创建一个对象实例(new)。使用Spring后就不需要这样做了,因为Spring会帮我们在需要用到某些对象的地方自动注入该对象,而无须我们自己去创建。这种模式俗称控制反转,即IOC(Inversion of Control)。那么Spring是从什么地方获取到我们所需要的对象呢?其实Spring给我们提供了一个IOC容器,里面管理着所有我们需要的对象,组件注册就是我们去告诉Spring哪些类需要交给IOC容器管理。

    这里主要记录组件注册的一些细节。

    1. 通过@Bean注册组件
    2. 通过@ComponentScan扫描,注册组件
      2.1 指定扫描策略
      2.2 多扫描策略策略
      2.3 自定义扫描策略
    3. 组件作用域@Scope
    4. 懒加载@Lazy
    5. 条件注册组件
      5.1 @Conditional
      5.2 @Profile
    6. 导入组件
      6.1 @Import
      6.2 ImportSelector
      6.3 ImportBeanDefinitionRegistrar
    7. 使用FactoryBean注册组件

    通过@Bean注册组件

    在较早版本的Spring中,我们都是通过XML的方式来往IOC容器中注册组件的,下面这段代码大家肯定不会陌生:

    // 返回 IOC 容器,基于 XML配置,传入配置文件的位置
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("xxx.xml");
    User user = (User) applicationContext.getBean("user");
    

    Spring 4后推荐我们使用Java Config的方式来注册组件。

    创建一个User

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public class User {
    
        private String name;
        private int age;
    }
    

    接着创建一个配置类,在里面通过@Bean注解注册User类:

    @Configuration
    public class WebConfig {
    
        @Bean
        public User user() {
            return new User("张小龙", 18);
        }
    }
    

    通过@Bean注解,我们向IOC容器注册了一个名称为user(Bean名称默认为方法名,我们也可以通过@Bean("myUser")方式来将组件名称指定为myUser)。

    组件注册完毕后,我们测试一下从IOC容器中获取这个组件。在Spring Boot入口类中编写如下代码:

        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
    
            // 返回 IOC 容器,使用注解配置,传入配置类
            ApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
            User user = context.getBean(User.class);
            System.out.println(user);
        }
    

    因为我们是通过注解方式来注册组件的,所以需要使用AnnotationConfigApplicationContext来获取相应的IOC容器,入参为配置类。

    启动项目,看下控制台输出:

    User(name=张小龙, age=18)
    

    说明组件注册成功。
    我们将组件的名称改为myUser,然后看看IOC容器中,User类型组件是否叫myUser:

        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
    
            // 返回 IOC 容器,使用注解配置,传入配置类
            ApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
            String[] namesForType = context.getBeanNamesForType(User.class);
            Arrays.stream(namesForType).forEach(System.out::println);
      }
    

    启动项目,观察控制台输出:

    myUser
    

    使用@ComponentScan扫描

    在使用XML配置组件扫描的时候,我们都是这样配置的:

    <context:component-scan base-package=""></context:component-scan>
    

    其中base-package指定了扫描的路径。路径下所有被@Controller、@Service、@Repository和@Component注解标注的类都会被纳入IOC容器中。

    现在我们脱离XML配置后,可以使用@ComponentScan注解来扫描组件并注册。

    在使用@ComponentScan扫描之前,我们先创建一个Controller,一个Service,一个Dao,并标注上相应的注解。

    然后修改配置类:

    @Configuration
    @ComponentScan("top.lconcise.demo")
    public class WebConfig {
    
    //    @Bean("myUser")
        public User user() {
            System.out.println("往ioc 容器中注册 user bean ");
            return new User("张小龙", 18);
        }
    }
    

    在配置类中,我们通过@ComponentScan("top.lconcise.demo")配置了扫描路径,并且将User组件注册注释掉了,取而代之的是在User类上加上@Component注解:

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    @Component
    public class User {
    
        private String name;
        private int age;
    }
    

    值得注意的是,我们不能将Spring Boot的入口类纳入扫描范围中,否则项目启动将出错。
    接下来我们看下在基于注解的IOC容器中是否包含了这些组件:

        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
    
            ApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
            String[] beanNamesForType = context.getBeanDefinitionNames();
            Arrays.stream(beanNamesForType).forEach(System.out::println);
       }
    

    启动项目,观察控制台:

    webConfig
    userService
    userController
    user
    userDao
    

    可见,组件已经成功被扫描进去了,并且名称默认为类名首字母小写。

    这里,配置类WebConfig也被扫描并注册了,查看@Configuration源码就会发现原因:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface Configuration {
        String value() default "";
    }
    

    指定扫描策略

    public class MyTypeFilter implements TypeFilter {
    
        @Override
        public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
            // 获取正在扫描的类的注解信息
            AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
            // 获取正在扫描的类的类信息
            ClassMetadata classMetadata = metadataReader.getClassMetadata();
            // 获取正在扫描的类的路径信息
            Resource resource = metadataReader.getResource();
    
            String className = classMetadata.getClassName();
            return StringUtils.hasText("er");
        }
    }
    
    /**
     * Created by liusj on 2019/7/11
     * 指定扫描策略.
     */
    @Configuration
    @ComponentScan(value = "top.lconcise.demo",
            excludeFilters = {
                    @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, Repository.class}),
                    @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = User.class)})
    public class WebConfig3 {
    }
    

    多扫描策略配置

    /**
     * Created by liusj on 2019/7/11
     * <p>
     * 多扫描策略配置.
     */
    @Configuration
    //@ComponentScan("top.lconcise.demo")
    @ComponentScan("top.lconcise.demo.Service")
    @ComponentScan("top.lconcise.demo.controller")
    public class WebConfig2 {
    
        //    @Bean("myUser")
        public User user() {
            return new User("张小龙", 18);
        }
    }
    

    自定义扫描策略

    public class MyTypeFilter implements TypeFilter {
    
        @Override
        public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
            // 获取正在扫描的类的注解信息
            AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
            // 获取正在扫描的类的类信息
            ClassMetadata classMetadata = metadataReader.getClassMetadata();
            // 获取正在扫描的类的路径信息
            Resource resource = metadataReader.getResource();
    
            String className = classMetadata.getClassName();
            return StringUtils.hasText("er");
        }
    }
    
    @Configuration
    @ComponentScan(value = "top.lconcise.demo",
            excludeFilters = {
                    @ComponentScan.Filter(type = FilterType.CUSTOM, classes = MyTypeFilter.class)})
    public class WebConfig4 {
    }
    

    组件作用域@Scope

    默认情况下,在Spring的IOC容器中每个组件都是单例的,即无论在任何地方注入多少次,这些对象都是同一个。

    1. singleton:单实例(默认),在Spring IOC容器启动的时候会调用方法创建对象然后纳入到IOC容器中,以后每次获取都是直接从IOC容器中获取(map.get());
    2. prototype:多实例,IOC容器启动的时候并不会去创建对象,而是在每次获取的时候才会去调用方法创建对象;
    3. request:一个请求对应一个实例;
    4. session:同一个session对应一个实例。
        @Bean("myUser")
        @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
        @Lazy
        public User user() {
            System.out.println("往ioc 容器中注册 user bean ");
            return new User("张小龙", 18);
        }
    

    懒加载@Lazy

    懒加载是针对单例模式而言的,正如前面所说,IOC容器中的组件默认是单例的,容器启动的时候会调用方法创建对象然后纳入到IOC容器中。

        @Bean("myUser")
        @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
        @Lazy
        public User user() {
            System.out.println("往ioc 容器中注册 user bean ");
            return new User("张小龙", 18);
        }
    

    条件注册组件

    @Conditional

    使用@Conditional注解我们可以指定组件注册的条件,即满足特定条件才将组件纳入到IOC容器中。

    public class MyCondition implements Condition {
    
        @Override
        public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
            String osName = conditionContext.getEnvironment().getProperty("os.name");
            return osName != null && osName.contains("Windows");
        }
    }
    
    @Configuration
    public class WebConfig5 {
    
        @Bean
        @Conditional(MyCondition.class)
        public User user() {
            return new User("张小龙", 19);
        }
    }
    

    @Profile

    @Profile可以根据不同的环境变量来注册不同的组件

    public interface SweetService {
    
        void eat();
    }
    
    @Service
    @Profile("java7")
    public class Java7SweetServiceImpl implements SweetService {
    
        @Override
        public void eat() {
            System.out.println("Java7 环境下执行");
        }
    }
    
    @Service
    @Profile("java8")
    public class Java8SweetServiceImpl implements SweetService {
    
        @Override
        public void eat() {
            System.out.println("Java8 环境下执行");
        }
    }
    
            ConfigurableApplicationContext context1 = new SpringApplicationBuilder(Application.class)
                    .web(WebApplicationType.NONE)
                    .profiles("java8")
                    .run(args);
            SweetService bean = context1.getBean(SweetService.class);
            bean.eat();
    

    导入组件

    @Import

    到目前为止,我们可以使用包扫描和@Bean来实现组件注册。除此之外,我们还可以使用@Import来快速地往IOC容器中添加组件。

    public class Hello {
    }
    
    @Configuration
    @Import({Hello.class})
    public class WebConfig6 {
    }
    

    ImportSelector

    通过@Import我们已经实现了组件的导入,如果需要一次性导入较多组件,我们可以使用ImportSelector来实现。

    public class MyImportSelector implements ImportSelector {
    
        @Override
        public String[] selectImports(AnnotationMetadata annotationMetadata) {
            return new String[]{
                    "top.lconcise.demo.domain.Demo1",
                    "top.lconcise.demo.domain.Demo2",
                    "top.lconcise.demo.domain.Demo3"
            };
        }
    }
    
    @Configuration
    @Import({MyImportSelector.class})
    public class WebConfig7 {
    }
    

    ImportBeanDefinitionRegistrar

    除了上面两种往IOC容器导入组件的方法外,我们还可以使用ImportBeanDefinitionRegistrar来手动往IOC容器导入组件。

    public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
            final String beanName = "demo4";
            boolean contain = beanDefinitionRegistry.containsBeanDefinition(beanName);
            if (!contain) {
                RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Demo4.class);
                beanDefinitionRegistry.registerBeanDefinition(beanName, rootBeanDefinition);
            }
        }
    }
    
    @Configuration
    @Import({MyImportBeanDefinitionRegistrar.class})
    public class WebConfig8 {
    }
    

    使用FactoryBean注册组件

    Spring还提供了一个FactoryBean接口,我们可以通过实现该接口来注册组件,该接口包含了两个抽象方法和一个默认方法:

    public class Demo5FactoryBean implements FactoryBean<Demo5> {
    
        @Override
        public Demo5 getObject() throws Exception {
            return new Demo5();
        }
    
        @Override
        public Class<?> getObjectType() {
            return Demo5.class;
        }
    
        @Override
        public boolean isSingleton() {
            return false;
        }
    }
    
    @Configuration
    public class WebConfig9 {
    
        @Bean
        public Demo5FactoryBean demo5FactoryBean() {
            return new Demo5FactoryBean();
        }
    }
    
            ApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
            Object demo5FactoryBean = context.getBean("demo5FactoryBean");
            System.out.println(demo5FactoryBean.getClass());
            Object demo5FactoryBean2 = context.getBean("&demo5FactoryBean");
            System.out.println(demo5FactoryBean2.getClass());
    

    源码连接:https://github.com/lbshold/springboot/tree/master/Spring-Register-Bean

    参考文章:https://mrbird.cc/Spring-Bean-Regist.html

    相关文章

      网友评论

        本文标题:[深入学习]Spring 组件注册

        本文链接:https://www.haomeiwen.com/subject/grhlhctx.html