所有的组件都应该放进IOC容器中,组件之间的关系通过容器实现自动装配,也就是依赖注入。对于如何将组件注册到容器中,本文从使用的角度出发详细阐述配置文件和注解的实现方式。涉及的注解还是挺多的,不过还是需要记忆一下,尤其是设置bean作用域的注解,面试中被问到过如何设置为多例。

新建一个maven工程,引入spring-context依赖。

1. @Configuration & @Bean给容器注册组件

以往的方式注册一个bean

新建一个实体类Person:

1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Person {
private String name;
private Integer age;
}

那么,我们可以在beans.xml中注册这个bean,给他赋值。

1
2
3
4
5
6
7
8
9
10
<?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="person" class="com.swg.bean.Person">
<property name="age" value="10"/>
<property name="name" value="张三"/>
</bean>
</beans>

那么,我们就可以拿到张三这个人了:

1
2
3
4
5
6
7
public class MainTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
Person person = (Person) applicationContext.getBean("person");
System.out.println(person);
}
}//输出:Person(name=张三, age=10)
注解的方式注册bean
  • 配置类 = 配置文件
  • @Configuration 告诉spring这是一个配置类
  • @Bean 给容器注册一个Bean,类型为返回值类型,id默认是方法名
1
2
3
4
5
6
7
@Configuration
public class MainConfig {
@Bean
public Person person(){
return new Person("李四",20);
}
}

如何获取这个bean呢?

1
2
3
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
Person person = applicationContext.getBean(Person.class);
System.out.println(person);//Person(name=李四, age=20)

我们还可以根据类型来获取这个bean在容器中名字是什么:

1
2
3
4
String[] names = applicationContext.getBeanNamesForType(Person.class);
for(String name:names){
System.out.println(name);//person
}

上面提到,id默认是方法名。如果我们修改MainConfig中的person这个方法名,果然打印结果也随着这个方法名改变而改变;也可以自己另外指定这个bean在容器中的名字:@Bean(“hello”),那么这个bean的名字就变成了hello.

2. @ComponentScan自动扫描组件以及扫描规则

配置文件中配置包扫描时这样配置的:

1
2
<!--包扫描,只要标注了@Controller,@Service,@Repository,@Component,就会被自动扫描到加入到容器中-->
<context:component-scan base-package="com.swg"/>

现在用注解来实现这个功能:

只需要加上注解即可:

1
2
@ComponentScan(value = "com.swg")//java8可以写多个@ComponentScan
//java8以前虽然不能写多个,但是也可以实现这个功能,用@ComponentScans配置即可

我们增加BookController.java,BookService.java以及BookDao.java三个类,并且分别加上注解:@Controller,@Service,@Repository;那么包扫描就可以把这些类全部注册到IOC容器中了。

我们来打印一下目前所有注册到IOC容器的类的名称:

1
2
3
4
5
6
7
8
9
@Test
public void shouldAnswerWithTrue()
{
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
String[] names = applicationContext.getBeanDefinitionNames();
for(String name:names){
System.out.println(name);
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalRequiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
//以上是spring IOC容器自身需要的组件,下面是我们自定义的组件
mainConfig//主配置类,因为有注解@Configuration,而这个注解本身是有@Component的,所以也是一个bean
bookController//@Controller
bookDao//@Repository
bookService//@Service
person//这是由自己@Bean注册进去的

上面的扫描路径是扫描所有的,有的时候我们需要排除掉一些扫描路径或者只扫描某个路径,如何做到呢?

excludeFilters来排除,里面可以指定排除规则,这里是按照ANNOTATION来排除,排除掉所有@Controller注解的类。classes也是个数组,可以排除很多。

1
2
3
@ComponentScan(value = "com.swg",excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class)
})

那么效果就是controller没有了,但是servicedao都在。

那如果我想只包含controller呢?

1
2
3
@ComponentScan(value = "com.swg", includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class)
},useDefaultFilters = false)

注意要useDefaultFilters = false,因为默认为true,就是扫描所有,不设置为false无效。

3. 自定义TypeFilter制定过滤规则

上面包扫描是按照FilterType.ANNOTATION规则来实现的,他还有其他几种规则:

1
2
3
4
5
6
7
8
9
10
public enum FilterType {
ANNOTATION,//注解,最常用
ASSIGNABLE_TYPE,//按照给定的类型,比如指定是BookService.class,那么只要是BookService这个类型就会被规则配置进来,子类或者实现类都可以
ASPECTJ,//ASPECTJ表达式,不常用
REGEX,//正则
CUSTOM;//自定义规则

private FilterType() {
}
}

对于最后的CUSTOM,这里着重说一说怎么用。

首先是要求实现FilterType接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyTypeFilter implements TypeFilter{
/**
*
* @param metadataReader:读取到的当前正在扫描的类的信息
* @param metadataReaderFactory:可以获取到其他任何类信息
* @return
* @throws IOException
*/
@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();
System.out.println("---->"+className);

return false;
}
}

返回false,表示不匹配,返回true的就匹配。这里默认是false;

mainConfig类中配置这个自定义的过滤规则:

1
2
3
@ComponentScan(value = "com.swg", includeFilters = {
@ComponentScan.Filter(type = FilterType.CUSTOM,classes = {MyTypeFilter.class})
},useDefaultFilters = false)

那么此时输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
---->com.swg.AppTest
---->com.swg.bean.Person
---->com.swg.controller.BookController
---->com.swg.dao.BookDao
---->com.swg.FilterType.MyTypeFilter
---->com.swg.MainTest
---->com.swg.service.BookService

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalRequiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
mainConfig
person

就是显示了所有他处理的类,最后由于都返回fasle,那么那些controller,service都将被过滤掉。

下面指定通过一个:

1
2
3
if(className.contains("er")){
return true;
}

输出:

1
2
3
4
5
mainConfig
person
bookController//new
myTypeFilter//new
bookService//new

4. @Scope-设置组件作用域

spring的bean默认是单实例,下面佐证一下:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test02()
{
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);
String[] names = applicationContext.getBeanDefinitionNames();
for(String name:names){
System.out.println(name);
}
Object bean = applicationContext.getBean("person");
Object bean1 = applicationContext.getBean("person");
System.out.println(bean == bean1);//true
}

那么我们可以配置bean为多例吗?显然是可以的:

1
2
3
4
5
@Bean
@Scope("prototype")
public Person person(){
return new Person("李四",20);
}

@Scope注解中有四个选项:

  • prototype:多例
  • singleton:单例,默认
  • request:同一次请求创建一个实例
  • session:同一个session创建一个实例

着重看一下singletonprototype,他们的加载时机?

⭐⭐⭐singletonIOC容器启动时调用方法创建对象放到IOC容器中,以后每次获取都直接从容器中拿,类似于map.get();

⭐⭐⭐prototype:IOC容器启动时不会创建对象,而是在每次获取时才会调用方法创建对象;并且是新new出来的对象,都是不一样的。

5. @lazy-bean-懒加载

单实例bean,默认在容器启动时创建对象。

即只要执行了:

1
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);

Person这个对象就会加载在容器中。测试一下:

1
2
3
4
5
6
7
8
@Configuration
public class MainConfig2 {
@Bean
public Person person(){
System.out.println("创建对象Person");//容器启动的时候就会执行这个方法,创建Perosn对象
return new Person("李四",20);
}
}

懒加载:容器启动时不创建对象,第一次使用(获取)Bean创建对象。

1
2
3
4
5
6
7
8
9
@Configuration
public class MainConfig2 {
@Bean
@Lazy
public Person person(){
System.out.println("创建对象Person");
return new Person("李四",20);
}
}

这个时候,就不会在容器一启动的时候就加载了。那什么时候加载呢?

我获取一下这个对象:

1
2
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);
Object bean = applicationContext.getBean("person");

这个时候,@Bean就被创建了。这就是懒加载。

6. @Conditional-按照条件注册bean

按照一定的条件进行判断,满足条件给容器注册bean。

先创建三个bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class MainConfig2 {
@Bean
public Person person(){
return new Person("李四",20);
}

@Bean("bill")
public Person person01(){
return new Person("Bill",60);
}

@Bean("linus")
public Person person02(){
return new Person("linus",50);
}
}

打印一下创建的bean:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test03()
{
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);
String[] names = applicationContext.getBeanNamesForType(Person.class);
for(String name:names){
System.out.println("--->"+name);
}

Map<String,Person> types = applicationContext.getBeansOfType(Person.class);
System.out.println(types);
}

打印结果:

1
2
3
4
--->person
--->bill
--->linus
{person=Person(name=李四, age=20), bill=Person(name=Bill, age=60), linus=Person(name=linus, age=50)}

那假设一个场景:如果系统是windows,给容器注册“bill”;如果系统是linux,给容器注册“linus”;

至于获取操作系统是什么,我们可以:

1
2
3
ConfigurableEnvironment environment = (ConfigurableEnvironment) applicationContext.getEnvironment();
String osName = environment.getProperty("os.name");
System.out.println(osName);//wondows 7

那么我们如何根据条件来注册bean呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//判断是否是linux系统
public class LinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
//1.获取IOC使用的bean factory,这个factory就是创建对象并进行装配的工厂
ConfigurableListableBeanFactory factory = conditionContext.getBeanFactory();
//2.获取类加载器
ClassLoader classLoader = conditionContext.getClassLoader();
//3.获取当前环境信息
Environment environment = conditionContext.getEnvironment();
//4.获取到bean定义的注册类
BeanDefinitionRegistry registry = conditionContext.getRegistry();

//如果是liunx系统,就让其注册进容器,windows也是如此
String osName = environment.getProperty("os.name");
if(osName.contains("Linux")){
return true;
}
return false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class MainConfig2 {
@Bean
public Person person(){
return new Person("李四",20);
}

@Conditional({WindowsCondition.class})//传condition数组
@Bean("bill")
public Person person01(){
return new Person("Bill",60);
}

@Conditional(LinuxCondition.class)
@Bean("linus")
public Person person02(){
return new Person("linus",50);
}
}

那么运行结果可以预测到:由于我们是windows系统,所以linux的就不能注册进容器了。

1
2
3
4
Windows 7
--->person
--->bill
{person=Person(name=李四, age=20), bill=Person(name=Bill, age=60)}

7. @Import-给容器中快速导入一个组件

上面所说得给容器注册组件的方式是:

包扫描+组件标注注解:@Controller@Service@Repository
@Component

比较方便,但是有局限性:如果是注册第三方包怎么办呢?

有一种是:@Bean[导入第三方包里面的组件],对于简单的可用这样用

还有一种是:@Import,快速给容器导入一个组件

比如我随便新建一个类叫Dog,里面啥注解和内容都不写。默认他是不会导入进去的。但是我在webconfig类上增加注解:

@Import(Dog.class)

那么再次打印出所有注册进容器的组件时,会出现

com.swg.bean.Dog

可见,@import注解可以方便快速地导入一个组件,并且id默认是组件的全类名

那如何导入多个呢?

@Import({Dog.class, Cat.class})

8. @Import-使用ImportSelector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 自定义逻辑返回需要导入的组件
*/
public class MyImportSelector implements ImportSelector{
/**
*
* @param annotationMetadata 当前标注Import注解的类的所有注解信息
* @return 返回值就是导入到容器中的组件的全类名
*/
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {

return new String[]{"com.swg.bean.Dog","com.swg.bean.Cat","com.swg.bean.pig"};
}
}

然后打上注解导入进来即可:

@Import(MyImportSelector.class)

这里导入的实际上不是MyImportSelector.class这个类,而是他返回的组件全类名

9. @Import-使用ImportBeanDefinitionRegistrar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
*
* @param annotationMetadata 当前类的注解信息
* @param registry beanDefinition注册类;
* 把所有需要添加进容器的bean:调用BeanDefinitionRegistry.registerBeanDefinition
* 来手工注册进来
*
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
//判断这两个bean是否都已经存在于容器中
boolean definition = registry.containsBeanDefinition("com.swg.bean.Pig");
boolean definition2 = registry.containsBeanDefinition("com.swg.bean.Cat");
//如果两个bean都有,则注册一头牛
if(definition && definition2){
//指定bean定义信息
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Bull.class);
//注册一个bean,指定bean的名字
registry.registerBeanDefinition("bull",rootBeanDefinition);
}
}
}

然后打上注解导入进来即可:

@Import(MyImportBeanDefinitionRegistrar.class)

10. 使用FactoryBean注册组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class AnimalFactory implements FactoryBean{
/**
* @return 返回一个Pig对象,这个对象会添加到容器中
* @throws Exception
*/
@Override
public Object getObject() throws Exception {
System.out.println("AnimalFactory...getObject()...");
return new Pig();
}

@Override
public Class<?> getObjectType() {
return Pig.class;
}

/**
* 是单例吗?返回true表示是单例,在容器中保存一份
* false:表示是多例,每次获取都创建新的,每次调用getObject()这个方法
* @return
*/
@Override
public boolean isSingleton() {
return true;
}
}

我们先将它用@Bean添加进容器看看:

1
2
3
4
@Bean
public AnimalFactory animalFactory(){
return new AnimalFactory();
}

显示的id是animalFactory,我们根据这个id获取一下这个bean的类型:

1
2
Object bean = applicationContext.getBean("animalFactory");
System.out.println("bean的类型:"+bean.getClass());

结果显示:

1
2
AnimalFactory...getObject()...
bean的类型:class com.swg.bean.Pig

就是说,这个bean的类型就是getObject方法中返回的Pig对象。

那如果我们想获取这个工厂对象呢?也是可以的,id前面加上&即可。

1
2
Object bean = applicationContext.getBean("&animalFactory");
System.out.println("bean的类型:"+bean.getClass());//bean的类型:class com.swg.bean.AnimalFactory

原因是在BeanFactory中定义了一个前缀&,只要是以&为前缀,表示拿FactoryBean本身。

1
2
public interface BeanFactory {
String FACTORY_BEAN_PREFIX = "&";

11. 总结

给容器中注册组件:

  1. 包扫描+组件标注注解:@Controller,@Service,@Repository,@Component

  2. @Bean[导入第三方包里面的组件]

  3. @Import,快速给容器导入一个组件—重要

    1).@Import(要导入到容器中的组件);容器会自动注册这个组件,id默认是全类名

    2).@ImportSelector:返回要导入的组件的全类名数组

    3).@ImportBeanDefinitionRegistrar:手动注册bean到容器中

  4. 使用spring提供的@FactoryBean(工厂bean)来注册bean

  5. @Conditional按照条件注册bean—重要

  6. @Scope作用域

  7. 懒加载,单例和多例是不一样的