Spring import三种用法 前言 最近在看Spring Cloud相关的源码,每次引入一个新的starter,发现都会加一些enable的注解,比如:@EnableDiscoveryClient,用于将应用注册到Eureka Server并将Eureka Server有的服务拉取到微服务系统。点开EnableDiscoveryClient源码,便会发现里面用到了@import注解。源码如下:
1 2 3 4 5 6 7 8 9 10 11 @Target (ElementType.TYPE)@Retention (RetentionPolicy.RUNTIME)@Documented @Inherited @Import (EnableDiscoveryClientImportSelector.class ) public @interface EnableDiscoveryClient { boolean autoRegister () default true ; }
这个@import的作用是什么呢,它是如何工作的呢?我们在项目里如何应用@import导入我们自定义的类?
一、Import的用法 Spring 3.0之前,我们的Bean可以通过xml配置文件与扫描特定包下面的类来将类注入到Spring IOC容器内。
Spring 3.0之后提供了JavaConfig的方式,也就是将IOC容器里Bean的元信息以java代码的方式进行描述。我们可以通过@Configuration与@Bean这两个注解配合使用来将原来配置在xml文件里的bean通过java代码的方式进行描述。
@Import注解提供了@Bean注解的功能,同时还有xml配置文件里<import>
标签组织多个分散的xml文件的功能,当然在这里是组织多个分散的@Configuration。
先看看@Import注解的源码吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Target (ElementType.TYPE)@Retention (RetentionPolicy.RUNTIME)@Documented public @interface Import { Class<?>[] value(); }
从源码里可以看出Import可以配合 Configuration , ImportSelector, ImportBeanDefinitionRegistrar 来使用,下面的or表示也可以把Import当成普通的Bean来使用,只是使用方式上有点区别,@Import只允许放到类上面,不能放到方法上。下面我们来看具体的使用方式。在使用之前我们有几个类在下面几种方式里都会用到,下面把这些公共的类放到一起。
二、举例代码说明
首先这里有个User的实体类,做为Bean里的参数,在实际的测试中也可以没有这个类,为了例子的完整性,我还是把代码贴到下面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.ivan.entity;public class User { private Integer id; private String name; public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public String getName () { return name; } public void setName (String name) { this .name = name; } }
定义了一个UserService接口类,为了是体现Spring的面向接口编程。代码如下:
1 2 3 4 5 6 7 8 9 package com.ivan.service;import com.ivan.entity.User;public interface UserService { public int save (User user) ; }
UserService接口对应的实现类,也是我们想注入到Spring IOC容器里的类,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.ivan.service.impl;import com.ivan.entity.User;import com.ivan.service.UserService;public class UserServiceImpl implements UserService { public int save (User user) { System.out.println("调用了当前方法" ); return 1 ; } }
测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.ivan;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import com.ivan.config.Config;import com.ivan.service.UserService;import com.ivan.service.impl.UserServiceImpl;public class App { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class ) ; UserService userService = (UserService)context.getBean(UserServiceImpl.class ) ; userService.save(null ); context.close(); } }
在测试类App里,用到了Config.class,不同的Import使用方式,这里的代码是不一样的,下面来看具体Import在使用上的区别:
三、最简单的导入类的方式 这是最简单的一种将类加入到Spring IOC容器的方式,直接将类的class加到Import的value里,Config的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 package com.ivan.config;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Import;import com.ivan.service.impl.UserServiceImpl;@Configuration @Import (value={UserServiceImpl.class }) public class Config {}
启动测试类,发现调用到了我们的UserServiceImpl里的save方法,这样将类注入的方式有个问题就是没法注入参数。也就是说UserServiceImpl提供的应该是无参的构造方法。这种方式注入类在Spring内部用的并不多。最多的使用方式还是下面两种。
四、通过ImportBeanDefinitionRegistrar将类注入到Spring IOC容器 Import注解通过配合ImportBeanDefinitionRegistrar类将类注入Spring IOC容器里。ImportBeanDefinitionRegistrar类的源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface ImportBeanDefinitionRegistrar { public void registerBeanDefinitions ( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) ;}
从上面的代码可以看出在注入Spring IOC容器的时候,我们肯定是通过registry这个变量了,而importingClassMetadata可以得到类的元数据信息。我们自定义的UserServiceBeanDefinitionRegistrar 类定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.ivan.bean.registrar;import org.springframework.beans.factory.support.BeanDefinitionBuilder;import org.springframework.beans.factory.support.BeanDefinitionRegistry;import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;import org.springframework.core.type.AnnotationMetadata;import com.ivan.service.impl.UserServiceImpl;public class UserServiceBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { public void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { BeanDefinitionBuilder userService = BeanDefinitionBuilder.rootBeanDefinition(UserServiceImpl.class ) ; registry.registerBeanDefinition("userService" , userService.getBeanDefinition()); } }
Config类的Import class需要改成UserServiceBeanDefinitionRegistrar.classs。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 package com.ivan.config;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Import;import com.ivan.bean.registrar.UserServiceBeanDefinitionRegistrar;@Configuration (value="ivan_test" )@Import (value={UserServiceBeanDefinitionRegistrar.class }) public class Config {}
可以明显的看出通过ImportBeanDefinitionRegistrar的方式我们可以对类进行个性化的定制,比如对需要传入的参数进行修改,也可以通过ImportBeanDefinitionRegistrar注入一批相似的类。有BeanDefinitionRegistry对象也有可以控制Spring IOC容器里Bean的定义,想做些什么也就方便很多了。
通过ImportSelector方式注入Bean 上面通过ImportBeanDefinitionRegistrar的方式注入的实例需要我们操作BeanDefinitionRegistry 对象,而通过ImportSelector方式我们可以不操作BeanDefinitionRegistry 对象,只需要告诉容器我们需要注入类的完整类名就好。ImportSelector类的源码如下:
1 2 3 4 5 6 7 8 9 public interface ImportSelector { String[] selectImports(AnnotationMetadata importingClassMetadata); }
上面的代码中可以看出需要返回一个String数组给到容器,传入的参数可以得到注解的元数据信息,现在我们来模拟下我们在Cloud代码里看到的enable都是如何通过ImportSelector方式来实现的。首先我们需要自定义一个注解,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.ivan.enable;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;import org.springframework.context.annotation.Import;import com.ivan.select.UserServiceImportSelect;@Retention (RetentionPolicy.RUNTIME)@Documented @Target (ElementType.TYPE)@Import (UserServiceImportSelect.class ) public @interface EnableUserService { String name () ; }
EnableUserService 是个注解,里面通过@Import引入了UserServiceImportSelect,具体注入的逻辑在UserServiceImportSelect这个类里面,我们的注解同时定义了一个name属性,这里只是为了测试,在实际中你可以定义你需要的属性,然后在具体的ImportSelect里根据属性的值进行不同的配置。UserServiceImportSelect属性的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.ivan.select;import java.util.Map;import java.util.Map.Entry;import org.springframework.context.annotation.ImportSelector;import org.springframework.core.type.AnnotationMetadata;import com.ivan.enable.EnableUserService;import com.ivan.service.impl.UserServiceImpl;public class UserServiceImportSelect implements ImportSelector { public String[] selectImports(AnnotationMetadata importingClassMetadata) { Map<String , Object> map = importingClassMetadata.getAnnotationAttributes(EnableUserService.class .getName (), true ) ; for (Entry<String, Object> entry : map.entrySet()){ System.out.println("key is : " + entry.getKey() + " value is : " + entry.getValue()); } return new String[]{UserServiceImpl.class .getName ()} ; } }
可以通过importingClassMetadata这个属性得到用了EnableUserService注解的元信息。同时返回了一个我们需要注入的类的名称,这样就可以注入到容器里啦,Config类的代码如下:
1 2 3 4 5 6 7 8 9 10 11 package com.ivan.config;import org.springframework.context.annotation.Configuration;import com.ivan.enable.EnableUserService;@Configuration ()@EnableUserService (name="ivan_test" )public class Config {}
参考资料 Spring Import 三种用法与源码解读