Spring组件注册(转载)
xml 方式
User
类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private Integer age;
}
在resources
目录下创建一个user.xml
配置文件
这里介绍三种注入方式:
-
构造方法注入
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="user" class="cc.bnblogs.springbasis.demo.pojo.User"> <!--构造方法注入--> <constructor-arg name="name" value="tom"/> <constructor-arg name="age" value="18"/> </bean> </beans>
-
setter方法注入
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="user" class="cc.bnblogs.springbasis.demo.pojo.User"> <!--setter方法注入--> <property name="name" value="admin"/> <property name="age" value="19"/> </bean> </beans>
-
p
名称空间注入<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" xmlns:p="http://www.springframework.org/schema/p"> <bean id="user" class="cc.bnblogs.springbasis.demo.pojo.User" p:name="alice" p:age="25"> </bean> </beans>
在SpringBoot
中,ClassPathXmlApplicationContext
是通过读取resources
目录下的的user.xml
,而FileSystemXmlApplicationContext
是读取该项目的相对路径中的user.xml
// 方式1
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("user.xml");
User user = (User) classPathXmlApplicationContext.getBean("user");
System.out.println(user.getName());
System.out.println(user.getAge());
// 方式2
FileSystemXmlApplicationContext fileSystemXmlApplicationContext = new FileSystemXmlApplicationContext("src/main/resources/user.xml");
User user = (User) fileSystemXmlApplicationContext.getBean("user");
System.out.println(user.getName());
System.out.println(user.getAge());
// 方式3
ApplicationContext context = new FileSystemXmlApplicationContext("src/main/resources/user.xml");
User user = context.getBean("user", User.class);
System.out.println(user.getName());
System.out.println(user.getAge());
@Bean 方式
User
类:
@AllArgsConstructor
@Data
public class User {
private String name;
private Integer age;
}
注册user
组件
通过@Bean
注解,我们向 IOC 容器注册了一个名称为user
(Bean 名称默认为方法名,我们也可以通过@Bean("myUser")
方式来将组件名称指定为myUser
)
@Configuration
public class WebConfig {
@Bean
public User user() {
return new User("tom", 18);
}
}
组件注册完成后,我们写一个测试方法从 IOC 容器中获取这个组件
@Test
void test() {
// 使用AnnotationConfigApplicationContext来获取相应的IOC容器,入参为配置类
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
User user = context.getBean(User.class);
System.out.println(user);
// 查看容器中已经存在的组件
String[] beanNames = context.getBeanNamesForType(User.class);
Arrays.stream(beanNames).forEach(System.out::println);
}
ClassPathXmlApplicationContext
、FileSystemXmlApplicationContext
都是继承于抽象类AbstractRefreshableConfigApplicationContext
,所以他们来是支持刷新的,而AnnotationConfigApplicationContext
是不支持的。
支持刷新的意思是ApplicationContext
会将容器里面的bean全部销毁,然后重新生成bean
使用@ComponentScan 扫描
@Component 注解及其衍生注解@RestController、@Controller、@Configration、@Service 和@Repository 都是组件注册注解。
@ComponentScan 注解主要是从约定的扫描路径中,识别标注了组件注册注解的类,并且把这些类自动注册到 spring IoC 容器中,这些类就是我们通常所言的bean。IoC 容器是 Spring 的特色之一,可以使用它管理 bean。
指定扫描策略
@ComponentScan(value = {"cc.bnblogs.springbasis.demo"},
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION,
classes = {Controller.class, Repository.class}),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = User.class)
})
上面我们指定了两种排除扫描的规则:
- 根据注解来排除(
type = FilterType.ANNOTATION
),这些注解的类型为classes = {Controller.class, Repository.class}
。即Controller
和Repository
注解标注的类不再被纳入到 IOC 容器中。 - 根据指定类型类排除(
type = FilterType.ASSIGNABLE_TYPE
),排除类型为User.class
,其子类,实现类都会被排除。
includeFilters
的作用和excludeFilters
相反,其指定的是哪些组件需要被扫描:
@ComponentScan(value = "cc.bnblogs.springbasis.demo",
includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Service.class)
}, useDefaultFilters = false)
上面配置了只将Service
纳入 IOC 容器,并且需要用useDefaultFilters = false
来关闭 Spring 默认的扫描策略才能让我们的配置生效
多扫描策略
java8
新增了@Repeatable
注解,使用该注解修饰的注解可以重复使用。ComponentScan
也是一个可重复使用的注解,源码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
...
}
组件作用域@Scope
有下面 4 种参数类型
singleton
:单实例(默认),在 Spring IOC 容器启动的时候会调用方法创建对象然后纳入到 IOC 容器中,以后每次获取都是直接从 IOC 容器中获取(map.get()
);prototype
:多实例,IOC 容器启动的时候并不会去创建对象,而是在每次获取的时候才会去调用方法创建对象;request
:一个请求对应一个实例;session
:同一个 session 对应一个实例。
这里介绍一下前两种类型
singleton
默认就是单例模式,在 IOC 容器中只会有一个实例存在,每次getBean()
获取的实例对象都是同一个
@Configuration
public class WebConfig {
@Bean
public User user() {
return new User("tom",18);
}
}
@Test
void test() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class); // 启动IOC容器
User user1 = context.getBean(User.class);
User user2 = context.getBean(User.class);
System.out.println(user1 == user2); // 输出true
}
prototype
多实例模式,每次获取 Bean 的时候会有一个新的实例
@Configuration
public class WebConfig {
@Bean
@Scope(value = "prototype")
public User user() {
return new User("tom",18);
}
}
@Test
void test() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
User user1 = context.getBean(User.class);
User user2 = context.getBean(User.class);
System.out.println(user1 == user2); // 输出true
}
- 使用 singleton 单例,采用饿汉加载(容器启动,Bean 实例就创建好了)
- 使用 prototype 多例,采用懒汉加载(IOC 容器启动的时候,并不会创建对象实例,而是在第一次使用的时候才会创建)
懒加载@Lazy
单例模式下使用懒汉加载只需要加上@Lazy
注解
@Configuration
public class WebConfig {
@Bean
@Scope
@Lazy
public User user() {
return new User("tom",18);
}
}
这样 IOC 容器启动的时候,就不会创建对象实例
可以修改 User 类来测试一下
@Data
public class User {
private String name;
private Integer age;
public User(String name,Integer age) {
this.name = name;
this.age = age;
System.out.println("User初始化成功");
}
}
没有加@Lazy
之前
@Test
void test() {
# IOC容器启动时直接创建一个User实例
# 控制台输出: User初始化成功
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
}
加上@Lazy
之后,单例模式采用懒汉加载
@Test
void test() {
# IOC容器启动时不会创建实例
# 控制台无输出
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
}
综上, 懒加载的功能是,在单例模式中,IOC 容器创建的时候不会马上去调用方法创建对象并注册,只有当组件第一次被使用的时候才会调用方法创建对象并加入到容器中。
条件注册组件
condition
使用@Conditional
注解我们可以指定组件注册的条件,即满足特定条件才将组件纳入到 IOC 容器中。
在使用该注解之前,我们需要创建一个类,实现Condition
接口:
public class MyCondition implements Condition {
/**
* @param context 上下文信息
* @param metadata 注解信息
* @return
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String osName = context.getEnvironment().getProperty("os.name");
return osName != null && osName.contains("Linux");
}
}
接着将这个条件添加到 User Bean 注册的地方
public class WebConfig {
@Bean(name = "myUser")
@Conditional(MyCondition.class)
public User user() {
return new User("tom",18);
}
}
测试一下
@Test
void test4() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
User user = context.getBean(User.class); // 报错
}
在 Linux 环境下,User 这个组件将被成功注册,由于我用的是 windows,所以这个组件将不会被注册到 IOC 容器中。
Profile
@profile
注解的作用是指定类或方法在特定的 Profile 环境生效,任何直接或者间接使用了@Component
注解的类都可以使用@Profile
注解。
新建一个接口CalculateService
:
public interface CalculateService {
Integer sum(Integer... value);
}
接着添加两个实现类
Java7CalculateServiceImpl
@Service
@Profile("java7")
public class Java7CalculateServiceImpl implements CalculateService {
@Override
public Integer sum(Integer... value) {
System.out.println("Java 7环境下执行");
int result = 0;
for (int i = 0; i <= value.length; i++) {
result += i;
}
return result;
}
}
Java8CalculateServiceImpl
@Service
@Profile("java8")
public class Java8CalculateServiceImpl implements CalculateService {
@Override
public Integer sum(Integer... value) {
System.out.println("Java 8环境下执行");
return Arrays.stream(value).reduce(0, Integer::sum);
}
}
通过@Profile
注解我们实现了:当环境变量包含java7
的时候,Java7CalculateServiceImpl
将会被注册到 IOC 容器中;当环境变量包含java8
的时候,Java8CalculateServiceImpl
将会被注册到 IOC 容器中。
修改启动方法测试一下:
public class SpringBasisApplication {
public static void main(String[] args) {
// SpringApplication.run(SpringBasisApplication.class, args);
ConfigurableApplicationContext context1 = new SpringApplicationBuilder(SpringBasisApplication.class)
.web(WebApplicationType.NONE)
.profiles("java8")
.run(args);
CalculateService service = context1.getBean(CalculateService.class);
System.out.println("求合结果: " + service.sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
}
}
导入组件
@Import
创建一个Test
类
public class Test {
public Test() {
System.out.println("Test类初始化成功");
}
}
然后在配置类中导入这个组件:
@Configuration
@Import({Test.class})
public class WebConfig {
...
}
查看容器中是否已经有上面这个组件
ApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
String[] beanNames = context.getBeanDefinitionNames();
Arrays.stream(beanNames).forEach(System.out::println);
可以发现,通过@Import
我们可以快速地往 IOC 容器中添加组件,Id 默认为全路径类名
@ImportSelector
通过@Import
我们已经实现了组件的导入,如果需要一次性导入较多组件,我们可以使用ImportSelector
来实现。
ImportSelector
是一个接口,包含一个selectImports
方法,方法返回类的全类名数组(即需要导入到 IOC 容器中组件的全类名数组),包含一个AnnotationMetadata
类型入参,通过这个参数我们可以获取到使用ImportSelector
的类的全部注解信息。
查看ImportSelector
源码:
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}
ImportSelector
是一个接口,包含一个selectImports
方法,方法返回类的全类名数组(即需要导入到 IOC 容器中组件的全类名数组),包含一个AnnotationMetadata
类型入参,通过这个参数我们可以获取到使用ImportSelector
的类的全部注解信息。
现在我们想要导入Apple
和Banana
两个组件
我们可以新建一个ImportSelector
实现类MyImportSelector
:
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{
"cc.bnblogs.springbasis.demo.test.Apple",
"cc.bnblogs.springbasis.demo.test.Banana",
};
}
}
上面方法返回了新增的两个类的全类名数组,接着我们在配置类的@Import
注解上使用MyImportSelector
来把这三个组件快速地导入到 IOC 容器中:
@Import({MyImportSelector.class})
public class WebConfig {
...
}
查看容器中是否已经有上面这两个组件:
ApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
String[] beanNames = context.getBeanDefinitionNames();
Arrays.stream(beanNames).forEach(System.out::println);
使用FactoryBean注册组件
Spring还提供了一个FactoryBean
接口,我们可以通过实现该接口来注册组件,该接口包含了两个抽象方法和一个默认方法:
public interface FactoryBean<T> {
String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
@Nullable
T getObject() throws Exception;
@Nullable
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}
我们新增一个Cherry
类,为了注册该类,创建FactoryBean
的实现类CherryFactoryBean
public class CherryFactoryBean implements FactoryBean<Cherry> {
/**
* 返回注册的组件对象
*/
@Override
public Cherry getObject() throws Exception {
return new Cherry();
}
/**
*返回需要注册的组件类型
*/
@Override
public Class<?> getObjectType() {
return Cherry.class;
}
/**
*该组件使用单例模式
*/
@Override
public boolean isSingleton() {
return true;
}
}
定义好CherryFactoryBean
后,我们在配置类中注册这个类:
@Bean
public CherryFactoryBean cherryFactoryBean() {
return new CherryFactoryBean();
}
测试从容器中获取:
ApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
Object object = context.getBean("cherryFactoryBean");
System.out.println(object.getClass());
输出结果:
class cc.bnblogs.springbasis.demo.test.Cherry
虽然我们获取的是Id为cherryFactoryBean
的组件,但其获取到的实际是getObject
方法里返回的对象。
如果我们要获取cherryFactoryBean
本身,则可以这样做,在id前加上&
ApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
Object object = context.getBean("&cherryFactoryBean");
System.out.println(object.getClass());
输出结果:
class cc.bnblogs.springbasis.demo.config.CherryFactoryBean
为什么加上&
就可以获取到呢,查看BeanFactory
源码
public interface BeanFactory {
String FACTORY_BEAN_PREFIX = "&";
...
}
在BeanFactory接口中定义了一个&前缀,只要我们使用bean的id来从Spring容器中获取bean时,Spring就会知道我们是在获取FactoryBean本身。