spring-ioc-基础知识
基础篇不涉及后置处理器,BeanDefinition等和 Spring 加载原理相关的知识,重点在充分应用 Spring 提供的 ioc 的特性 -> 怎么创建一个好用的bean
引入
ioc 是怎么来的
接口->实现类需求变更 -> 使用静态工厂来创建
当缺少实现类编译无法通过 -> 使用反射读取类的全限定名,延迟加载时间
全限定名写死了 -> 外部化配置,把beanName和全限定名存储在配置文件中,静态工厂根据配置文件去读取
每次获取类都是创建一个新的 -> 使用缓存把创建出出来的对象存储起来
入门
使用 Spring
根据xml配置文件来创建一个对象,并且完成属性的注入
依赖查找
我们用 ClassPathXmlApplicationContext
这个BeanFactory的实现来创建Spring的容器,然后指定加载的bean的xml配置文件,然后根据配置文件中bean的id或者类型来拿到我们需要装配出来的bean对象
<bean id="person" class="com.linkedbear.spring.basic_dl.a_quickstart_byname.bean.Person"></bean>
public static void main(String[] args) throws Exception {
BeanFactory factory = new ClassPathXmlApplicationContext("basic_dl/quickstart-byname.xml");
Person person = (Person) factory.getBean("person");
System.out.println(person);
}
- 通过bean的id查找
getBean(String name)
,需要强转 - 通过类型查找
getBean(Class<T> requiredType)
,不需要强转(已给定类型)
依赖注入
我们上一个例子中创建出来的 bean 是没有属性的空白bean,我们想创建出来有给定初始值的bean
我们先来看看一个基础类型的赋值(注入)
public class Person {
private String name;
private Integer age;
// getter and setter ......
}
<bean id="person" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Person">
<property name="name" value="test-person-byset"/>
<property name="age" value="18"/>
</bean>
我们再来一个复杂类型,比如要注入我们刚才装配好的Person
public class Cat {
private String name;
private Person master;
// getter and setter ......
}
<bean id="cat" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Cat">
<property name="name" value="test-cat"/>
<property name="master" ref="person"/>
</bean>
:::info
基础类型 <property name="name" value="xxx"/>
引用类型 <property name="name" ref="xxx"/>
:::
ioc基础
依赖查找高级 & ApplicationContext 与 BeanFactory
根据接口类型获取所有实现bean
我们一个接口有多个实现类
想拿到一个接口的所有实现类或者bean???
我们这里要改用 ApplicationContext
接口了,BeanFactory
下并没有这种方法
如何拿到一个接口的全部实现类 -> ApplicationContext.getBeansOfType()
拿到全部bean -> ApplicationContext.getBeanDefinitionNames()
根据被注解标注的类
@Color
public class Black {
}
Map<String, Object> beans = ctx.getBeansWithAnnotation(Color.class);
ApplicationContext
.Map<String, Object> getBeansWithAnnotation(Class<? extends Annotation> var1)
获取容器中所有 bean
String[] beanNames = ctx.getBeanDefinitionNames();
延迟查找(缺省加载)
需要依赖容器中某些bean,但是这些bean不存在的时候也要使用默认/缺省的逻辑来处理
// 判断是否存在 bean
ctx.containsBean("dog")
// 下面的代码会报Bean没有定义 NoSuchBeanDefinitionException
// Dog dog = ctx.getBean(Dog.class);
// 这一行代码不会报错
// 只有真正调用 dogProvider.getObject 真正获取包装里面的bean才会报错
ObjectProvider<Dog> dogProvider = ctx.getBeanProvider(Dog.class);
// 存在则返回,不存在返回null
Dog dog = dogProvider.getIfAvailable();
if (dog == null) {
dog = new Dog();
}
// java8 后函数式编程
// 给了一个找不到则返回默认的方法
Dog dog = dogProvider.getIfAvailable(Dog::new);
// 更多的也可以在存在之后执行一个 Consumer 方法(单个输入参数->默认传入当前对象,无输出参数)
dogProvider.ifAvailable(dog -> System.out.println(dog));
ApplicationContext
与 BeanFactory
ApplicationContext
是 BeanFactory
的子接口,扩展了一些其他功能
- AOP 的支持( AnnotationAwareAspectJAutoProxyCreator 作用于 Bean 的初始化之后 )
- 配置元信息( BeanDefinition 、Environment 、注解等 )
- 资源管理( Resource 抽象 )
- 事件驱动机制( ApplicationEvent 、ApplicationListener )
- 消息与国际化( LocaleResolver )
- Environment 抽象( SpringFramework 3.1 以后)
注解驱动Ioc和组件扫描
xml驱动的ioc使用的是ClassPathXmlApplicationContext
,注解驱动的ioc需要使用AnnotationConfigApplicationContext
配置类(对应之前的xml)如何编写
@Configuration
public class QuickstartConfiguration {
@Bean
public Person person() {
Person person = new Person();
person.setName("person");
person.setAge(123);
return person;
}
@Bean
public Cat cat() {
Cat cat = new Cat();
cat.setName("test-cat-anno");
// 直接拿上面的person()方法作为返回值即可,相当于ref
cat.setMaster(person());
return cat;
}
}
- 配置类使用
@Configuration
标注 - 每一个需要创建出来的bean,使用一个 @Bean 注解标注的方法,方法返回值对应需要创建出来Bean的类型,默认方法名是bean的id(也可以在 @Bean 注解中再次指定)
- 方法体中可以具体去实现bean的创建逻辑包括依赖注入
- 复杂类型注入使用方法的返回值
组件扫描
除了使用 @Bean 来手动指定 Bean 的注册以外,还可以使用组件扫描来根据类路径批量加载 Bean
@Configuration
@ComponentScan("com.linkedbear.spring.annotation.c_scan")
public class ComponentScanConfiguration {
}
// package com.linkedbear.spring.annotation.c_scan.bean;
@Component
public class Cat {
private final String name;
private final Person master;
@Autowired // spring 4.3 后可省略
public Cat(Person master, @Value("test-cat") String name) {
this.master = master;
this.name = name;
}
}
组件扫描 @ComponentScan
-> 扫描所有 @Componnet
及其派生注解(比如@Service …)的类
组件扫描的方式的依赖注入有多种方式,我们目前使用构造器注入
简单类型使用 @Value,复杂类型,默认按照类型注入,如果存在多个@Qualifier
指定bean名称
依赖注入->属性注入
之前的例子中,我们简单的详细的介绍了bean的创建,但是对于依赖注入讲的还是比较简单的,接下来我们详细的介绍属性注入的方式(影响注入时机)
setter 属性注入
无参构造器 + set 方法
xml -> 之前用的最多的方式
<bean id="person" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Person">
<property name="name" value="test-person-byset"/>
<property name="age" value="18"/>
</bean>
注解
@Bean
public Person person() {
Person person = new Person();
person.setName("test-person-anno-byset");
person.setAge(18);
return person;
}
构造器注入
需要添加全参构造器
xml
<bean id="person" class="com.linkedbear.spring.basic_di.b_constructor.bean.Person">
<constructor-arg index="0" value="test-person-byconstructor"/>
<constructor-arg index="1" value="18"/>
</bean>
注解
@Bean
public Person person() {
return new Person("test-person-anno-byconstructor", 18);
}
组件扫描简单类型注入
@Value 简单属性注入
@Value("0")
private Integer order;
我们在 @Value 中引用的值不仅可以来自与常量,还可以使用到配置文件中的值,或者使用 SpEL 表达式来引用更加复杂的情况(其他bean的值或者计算属性)
PropertySource配置文件注入
@PropertySource({properties配置文件的位置}) 可引入配置文件中的kv
// properties 文件
// red.name=red-value-byproperties
@PropertySource("classpath:basic_di/value/red.properties")
public class InjectValueConfiguration {
@Value("${red.name}")
private String name;
}
properties 文件(先转换成一个一个map)等配置项信息会读取到 Environment
对象中
补充-> SpEL表达式
在注入时可以使用 #{}
来引用其他bean的属性值,或者使用复杂的计算属性
自动注入
机制&复杂类型注入
在使用组件扫描的时候,如果要注入复杂类型怎么办呢,这里就涉及到自动注入
的机制了,本小节来单独介绍,当然,自动注入机制在注解驱动开发中也有其他的应用场景
@Autowired
注入的三个位置
@Component
public class Dog {
// 属性上标注
@Autowired
private Person person;
// 构造器上标注
@Autowired
public Dog(Person person) {
this.person = person;
}
// setter 方法上标注
@Autowired
public void setPerson(Person person) {
this.person = person;
}
}
注入的bean不存在
注入的bean不存在->报错 NoSuchBeanDefinitionException
->标注 @Autowired(required = false)
可以避免->只是一种异常处理策略,与延迟注入做区分,因为无法延迟使用注入对象的时机,后面延迟注入会详细说明
配置类中使用自动注入机制
对应前面 注解驱动,直接调用 @Bean 方法标注的方法
@Configuration
public class InjectConfiguration {
@Bean
@Autowired // 可不标注
public Dog dog(Person person) {
Dog dog = new Dog();
dog.setPerson(person);
return dog;
}
}
解决多个相同类型的bean注入冲突
多个相同类型的bean注入->报错 NoUniqueBeanDefinitionException
->标注 @Qualifier("beanName")
可以指定
@Autowired
@Qualifier("cat")
private Animal animal;
->标注 @Primary
可以指定优先级
@Primary // 指定同类型被默认注入的bean
@Bean
public Animal cat() {
return new Cat();
}
->使用不同的变量名
@Autowired
private Animal cat;
@Autowired
private Animal dog;
@Autowired
注入逻辑分析
- 优先按照类型查找
- 如果存在多个相同类型的bean,再按照名称查找
- 如果存在多个相同类型的bean,且没有指定名称,报错
注入多个相同类型的bean
使用 List
或者 Set
注入
@Autowired
private List<Animal> animals; // 会注入所有类型为Animal的bean
JSR250-@Resouce
注入
@Resouce
注入 -> 根据名称注入 -> @Autowired
+ @Qualifier
-> JSR250规范
JSR330-@Inject
注入
需要额外导入依赖
<!-- jsr330 -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
@Injecct
注入 -> 根据类型注入 -> @Autowired
-> JSR330规范 ( 需要导jar包 ) 不依赖spring
补充介绍-复杂类型注入
数组集合等类型,如何注入呢,这里不涉及到新的核心机制,仅仅是语法介绍
private String[] names;
private List<String> tels;
private Set<Cat> cats;
private Map<String, Object> events;
private Properties props;
xml 复杂类型注入
<property name="names">
<array>
<value>张三</value>
<value>三三来迟</value>
</array>
</property>
<property name="tels">
<list>
<value>13888</value>
<value>15999</value>
</list>
</property>
<property name="cats">
<set>
<bean class="com.linkedbear.spring.basic_di.g_complexfield.bean.Cat"/>
<ref bean="mimi"/>
</set>
</property>
<property name="events">
<map>
<entry key="8:00" value="起床"/>
<!-- 撸猫 -->
<entry key="9:00" value-ref="mimi"/>
<!-- 买猫 -->
<entry key="14:00">
<bean class="com.linkedbear.spring.basic_di.g_complexfield.bean.Cat"/>
</entry>
<entry key="18:00" value="睡觉"/>
</map>
</property>
<property name="props">
<props>
<prop key="sex">男</prop>
<prop key="age">18</prop>
</props>
</property>
注解
SpEL 表达式处理
@Component
public class Person2 {
@Value("#{new String[] {'张三', '张仨'}}")
private String[] names;
@Value("#{{'333333', '3333', '33'}}")
private List<String> tels;
// 引用现有的Bean,以及创建新的Bean
@Value("#{{@miaomiao, new com.linkedbear.spring.basic_di.g_complexfield.bean.Cat()}}")
private Set<Cat> cats;
@Value("#{{'喵喵': @miaomiao.name, '猫猫': new com.linkedbear.spring.basic_di.g_complexfield.bean.Cat().name}}")
private Map<String, Object> events;
@Value("#{{'123': '牵着手', '456': '抬起头', '789': '我们私奔到月球'}}")
private Properties props;
}
如果是 @Bean 的方式,手动 创建出需要的集合类型,set 进去就可以了
回调注入
机制&延迟注入
核心 -> Aware 接口
接口名 | 用途 |
---|---|
BeanFactoryAware | 回调注入 BeanFactory |
ApplicationContextAware | 回调注入 ApplicationContext(与上面不同,后续 IOC 高级讲解) |
EnvironmentAware | 回调注入 Environment(后续IOC高级讲解) |
ApplicationEventPublisherAware | 回调注入事件发布器 |
ResourceLoaderAware | 回调注入资源加载器(xml驱动可用) |
BeanClassLoaderAware | 回调注入加载当前 Bean 的 ClassLoader |
BeanNameAware | 回调注入当前 Bean 的名称 |
@Autowired 也可以实现除去最后两个bean注入以外的全部,因为最后两个bean和当前bean有关
@Component
public class AwaredTestBean implements ApplicationContextAware, BeanNameAware {
private String beanName;
private ApplicationContext ctx;
public String getName() {
return beanName;
}
public void printBeanNames() {
Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
}
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
this.ctx = ctx;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
}
在bean初始化的时候会把对应的对象注入进去(注入到重写的setApplicationContext
,和setBeanName
形参中,然后我们就可以在这个函数中执行我们手动赋值的操作,这个函数会在bean生命周期中自动调用)
补充-延迟注入
改为注入 ObjectProvider
setter
@Component
public class Dog {
private Person person;
@Autowired
public void setPerson(ObjectProvider<Person> person) {
// 有Bean才取出,注入
this.person = person.getIfAvailable();
}
}
构造器
@Component
public class Dog {
private Person person;
@Autowired
public Dog(ObjectProvider<Person> person) {
// 如果没有Bean,则采用缺省策略创建
this.person = person.getIfAvailable(Person::new);
}
}
属性注入 -> 不能注入确定类型,只能注入 ObjectProvider<需要的bean类型>,这个成员变量用的时候就需要get一次,因为类型不是确定的,一般不使用
@Autowired
private ObjectProvider<Person> person;
@Override
public String toString() {
// 每用一次都要getIfAvailable一次
return "Dog{" + "person=" + person.getIfAvailable(Person::new) + '}';
}
补充-依赖注入QA
q1 setter 注入如何体现可变性? -> 相比如构造器注入,在bean创建的时候就指定好了,setter注入可以在bean创建后再注入,就算是存在了bean,也可以再次注入
q2 参数注入 发生时机?-> 构造器注入 > 参数注入 > setter 注入(setter 注入中也可能会用到参数注入的一些值)
q3 字段注入如果发生在setter注入之前,那么如果一个beanA依赖另一个beanB,beanA中字段注入beanB,但是 beanB使用setter注入的,beanA中还能拿到beanB中setter注入的值么?
spring 先会完成所有无依赖的bean的实例化,然后再去实例化有依赖的bean,所以 beanA中字段注入beanB,beanB使用setter注入的,beanA中拿到的是beanB的实例化对象,而不是setter注入的对象
进一步的,我们所说的这三种方式的顺序是针对某一个bean的实例化来说的,对于不同bean的实例化,会按照bean之间的依赖关系进行
q3-plus 引申->循环依赖问题,beanA 初始化依赖 beanB,但beanB初始化又依赖beanA
构造器注入无法解决
但是 setter 注入和字段注入,使用单例模式的时候,spring 通过提前曝光来处理, beanB setter 执行之前,会先拿到 beanA 的一个引用
beanA字段注入拿到beanB的引用,最后 beanA 拿到 beanB 的 setter 注入完成后的新值。spring会保证bean在被使用之前是被完全初始化的。
bean 的类型
分为普通bean和FactoryBean
工厂bean FactoryBean
-> 用来创建一些初始化流程比较复杂的bean
public interface FactoryBean<T> {
// 返回创建的对象
@Nullable
T getObject() throws Exception;
// 返回创建的对象的类型(即泛型类型)
@Nullable
Class<?> getObjectType();
// 创建的对象是单实例Bean还是原型Bean,默认单实例
default boolean isSingleton() {
return true;
}
}
我们创建一个工厂bean,用于指定某一个类型的bean的生产逻辑,后续就可以直接从spring容器中按照我们顶底好的生产逻辑,直接拿到我们想要的类型的bean了
public class ToyFactoryBean implements FactoryBean<Toy> {
public ToyFactoryBean() {
System.out.println("ToyFactoryBean 初始化了。。。");
}
private Child child;
@Override
public Toy getObject() throws Exception {
switch (child.getWantToy()) {
case "ball":
return new Ball("ball");
case "car":
return new Car("car");
default:
return null;
}
}
@Override
public Class<Toy> getObjectType() {
return Toy.class;
}
public void setChild(Child child) {
this.child = child;
}
}
@Configuration
public class BeanTypeConfiguration {
@Bean
public Child child() {
return new Child();
}
@Bean
public ToyFactoryBean toyFactory() {
ToyFactoryBean toyFactory = new ToyFactoryBean();
/// 给工厂bean注入属性
toyFactory.setChild(child());
return toyFactory;
}
}
public class BeanTypeAnnoApplication {
public static void main(String[] args) throws Exception {
ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanTypeConfiguration.class);
// 可以直接拿目标生产的bean,工厂bean本身对用户是不可见的
Map<String, Toy> toys = ctx.getBeansOfType(Toy.class);
toys.forEach((name, toy) -> {
System.out.println("toy name : " + name + ", " + toy.toString());
});
// 测试工厂bean创建出来bean的作用域
Toy toy1 = ctx.getBean(Toy.class);
Toy toy2 = ctx.getBean(Toy.class);
System.out.println(toy1 == toy2);
ToyFactoryBean factoryBean = ctx.getBean(ToyFactoryBean.class);
System.out.println(factoryBean);
System.out.println(ctx.getBean("toyFactory"));
// 拿工厂bean本身需要加 &
System.out.println(ctx.getBean("&toyFactory"));
}
}
- FactoryBean 加载时机 -> 伴随 ApplicationContext 被加载
- FactoryBean 创建bean的时机 -> 延迟生产,等到getBean的时候再去加载
- 生产出来的bean是否单例 -> 默认单例,可重写 isSingleton() 来改变生产的bean的作用域
- 可通过 & 取出 BeanFactory 本体
bean 的作用域
单例->默认->每次获取的或者注入的都是同一个对象->ApplicationContext 被初始化就会被创建
原型bean->指定 @Scope(“prototype”)->每次获取就会创建一个新的对象->延迟了bean创建的时机
bean 实例化方式
实例化即指 new 出来bean 对象,之前介绍了前两种bean实例化出来的方式,这一节补充介绍静态工厂和实例工厂的方式,作为了解
普通bean
普通的 bean 标签 ,@Bean 注解标注的方式
FactoryBean方式
上一小节介绍过,注意一点 FactoryBean 本身也会作为一个特殊的bean注册到spring容器
静态工厂
这里初始化对象的方法是静态的
public class CarStaticFactory {
public static Car getCar() {
return new Car();
}
}
xml 中指定工厂类,以及静态初始化方法即可,自动根据静态初始化方法判断生产出来bean的类型
<bean id="car2" class="com.linkedbear.spring.bean.c_instantiate.bean.CarStaticFactory" factory-method="getCar"/>
注解驱动创建的写法
@Bean
public Car car2() {
return CarStaticFactory.getCar();
}
静态工厂不会注册到spring容器,只是需要用到这一点静态方法的逻辑
实例工厂
创建bean的方式不是静态的
public class CarInstanceFactory {
public Car getCar() {
return new Car();
}
}
<bean id="carInstanceFactory" class="com.linkedbear.spring.bean.c_instantiate.bean.CarInstanceFactory"/>
<bean id="car3" factory-bean="carInstanceFactory" factory-method="getCar"/>
需要先把实例工厂类注册到spring容器,再去指定这个bean是实例工厂bean,并且这个实例工厂bean的实例化方法
注解驱动写法
@Bean
public Car car3(CarInstanceFactory carInstanceFactory) {
return carInstanceFactory.getCar();
}
先注册工厂,再用工厂指定创建bean逻辑
bean 的生命周期
spring 可以在bean对象初始化完成以后,以及销毁之前执行一些自定义逻辑 ->spring 在初始化和销毁阶段会使用回调机制干预bean的生命周期
创建/实例化 new Person() ->
初始化 对 person 对象做一些初始化操作 ->
运行使用期 ->
销毁阶段 bean 的销毁 对象销毁前做一些特殊操作 ->
回收阶段
三种实现机制
- init-method&destroy-method
xml
<bean class="com.linkedbear.spring.lifecycle.a_initmethod.bean.Cat"
init-method="init" destroy-method="destroy">
<property name="name" value="mimi"/>
</bean>
注解驱动
@Bean(initMethod = "init", destroyMethod = "destroy")
public Dog dog() {
Dog dog = new Dog();
dog.setName("wangwang");
return dog;
}
- @PostConstruct&@PreDestroy JSR250规范 -> 组件扫描
@PostConstruct
public void addInk() {
System.out.println("钢笔中已加满墨水。。。");
this.ink = 100;
}
@PreDestroy
public void outwellInk() {
System.out.println("钢笔中的墨水都放干净了。。。");
this.ink = 0;
}
- InitializingBean&DisposableBean 接口
@Component
public class Pen implements InitializingBean, DisposableBean {
private Integer ink;
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("钢笔中已加满墨水。。。");
this.ink = 100;
}
@Override
public void destroy() throws Exception {
System.out.println("钢笔中的墨水都放干净了。。。");
this.ink = 0;
}
@Override
public String toString() {
return "Pen{" + "ink=" + ink + '}';
}
}
原型bean对象的生命周期
原型bean初始化不在spring容器初始化完成之前
原型bean的初始化行为与单例bean一致(init-method)
原型Bean的销毁不包括destroy-method
单例与原型bean生命周期对比
单例bean
bean初始化方法 -> set -> init-method -> @PostConstruct -> InitializingBean -> spring 容器初始化完成 -> bean使用
-> destroy-method -> @PreDestroy -> DisposableBean -> spring 容器销毁
原型 bean
spring 容器初始化完成 -> bean初始化方法 -> set -> init-method -> @PostConstruct -> InitializingBean -> bean使用
-> spring 容器销毁
ioc进阶
事件机制-了解
spring 当中事件监听模式的实现,我们可以创建一些 Listener 监听一些事件,自定义一些处理逻辑
// 基于xml
// 实现 ApplicationListener 接口 -> 扩展 自定义事件需要继承 ApplicationEvent
// @FunctionalInterface
// public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
// void onApplicationEvent(E event);
// }
@Component
public class ContextRefreshedApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("ContextRefreshedApplicationListener监听到ContextRefreshedEvent事件!");
}
}
// 基于注解
@Component
public class ContextClosedApplicationListener {
@EventListener
public void onContextClosedEvent(ContextClosedEvent event) {
System.out.println("ContextClosedApplicationListener监听到ContextClosedEvent事件!");
}
}
public static void main(String[] args) throws Exception {
System.out.println("准备初始化IOC容器。。。");
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
"com.linkedbear.spring.event.a_quickstart");
System.out.println("IOC容器初始化完成。。。");
ctx.close();
System.out.println("IOC容器关闭。。。");
}
准备初始化IOC容器。。。
ContextRefreshedApplicationListener监听到ContextRefreshedEvent事件!
IOC容器初始化完成。。。
ContextClosedApplicationListener监听到ContextClosedEvent事件!
IOC容器关闭。。。
- 监听器也是一个bean
- 监听器在spring容器初始化的时候创建好,在容器销毁时一起销毁
- 可以监听一下系统内置的时间
Spring 系统事件
<font style="color:#DF2A3F;">ContextRefreshedEvent</font>
容器刷新事件, 所有单例的bean初始化完毕
ContextStartedEvent
需要 start 之后
ContextClosedEvent
ioc 容器已经关闭,但是bean没有被完全销毁
ContextStoppedEvent
…
模块装配
之前我们学习了基于注解的编程式和声明式手动装配,但我们如果想快速引入一个功能模块的一连串bean(mvc场景,dao操作场景,有点像boot里的starter了是吧,但比起Boot复杂的实现,我们先基于做核心的需求做一个demo),手动导入的方式还是太过于复杂,,有没有什么办法可以快速导入一大个功能模块的bean呢,或者说涉及一个功能模块给外部调用的时候,如何设计方便模块调用者快速导入需要的bean呢
我们本小节总体想要实现的效果是,设计一个模块,里面有很多个需要引入的bean,调用者想要使用一个自定义注解,就可以引入所有模块设计者想要调用者导入的bean
把每个独立模块所需要的bean进行装配 -> 结合一个自定义注解 @Enabelxxx -> 通过 @Import 注解导入配置类
-> 快速实现某一个模块所需功能的引入
模块装配的实现方式
- 基于 @Import 注解 + bean (简单bean)/ 配置类(某些存在bean依赖注入的场景)
@Import({Boss.class, BartenderConfiguration.class})
public @interface EnableTavern {
}
- @Import + ImportSelector实现类
public class BarImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 返回类的全限定名
return new String[] {Bar.class.getName(), BarConfiguration.class.getName()};
}
}
@Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class})
public @interface EnableTavern {
}
- @Import + ImportBeanDefinitionRegistrar实现类 注入的是 BeanDefinition (重点注意,后续关联 BeanDefinitonRegistry 的知识)
public class BarImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 注册bean
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Bar.class);
registry.registerBeanDefinition("bar", beanDefinitionBuilder.getBeanDefinition());
}
}
条件装配
我们上一小节学习了如何方便的批量导入某一个模块的bean,但是,如果我们想要更细粒度的控制bean创建的规则呢,目前只能全量导入,比如,我们常见的根据环境(dev,prod)来配置生效的bean,再比如,我们的bean之间存在依赖关系,同样是导入一个酒馆,白酒就要配置白酒的杯子,红酒则需要高脚杯来喝
@Profile 根据当前环境装配
默认值 default
@Profile-> 指定环境下才会装配 ->bean environment.getProfile()
@Configuration
@Profile("city")
public class BartenderConfiguration {
@Bean
public Bartender zhangxiaosan() {
return new Bartender("张小三");
}
}
public static void main(String[] args) throws Exception {
// AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
// 会直接完成 refresh,无法控制bean初始化行为
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("city");
ctx.register(TavernConfiguration.class);
ctx.refresh();
Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
}
通过跟 env 手动设置激活的环境,注意,不能直接用configuration那个构造函数
也可以在vm参数中传入,来指定当前激活环境 -Dspring.profiles.active=city
:::info
实际案例
如何根据环境使用不同数据源,我们同一个bean类型根据环境不同声明多份,然后结合 PropertySource 定义不同的数据源配置文件的读取策略,就可以了
:::
@Bean
@Profile("dev")
public DataSource devDataSource() {
return null;
}
@Bean
@Profile("test")
public DataSource testDataSource() {
return null;
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
return null;
}
@Conditional 根据自定义条件装配
@Conditional(t extends Conditional) 条件注解
我们需要一个 Conditional 接口的实现类,定义matches 的逻辑
public class ExistBossCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getBeanFactory().containsBeanDefinition(Boss.class.getName());
}
}
metadata 中可以拿到注解中的信息,centext能拿到spring容器的信息(获取其他bean的信息)
public class OnBeanCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 先获取目标自定义注解ConditionalOnBean上的beanNames属性
String[] beanNames = (String[]) metadata.getAnnotationAttributes(ConditionalOnBean.class.getName()).get("beanNames");
// 逐个校验IOC容器中是否包含传入的bean名称
for (String beanName : beanNames) {
if (!context.getBeanFactory().containsBeanDefinition(beanName)) {
return false;
}
}
return true;
}
}
包扫描高级
指定扫描某一个类所在包下所有的类
我们可以指定扫描某一个类所在包下所有的类(是不是有点像,SpringBoot启动类的扫描逻辑)
包扫描的路径 -> @ComponentScan 的 value / basePackages 属性 指定扫描的包名 / basePackageClasses
指定扫描的类
@Configuration
@ComponentScan(basePackageClasses = {DemoService.class, DemoComponent.class})
public class BasePackageClassConfiguration {
}
指定包含过滤规则
包扫描的注册规则,默认规则 -> include / exclude 指定包含与不包含策略
可以使用 注解,类型,正则,自定义来定义规则
// 不包含
@Configuration
@ComponentScan(basePackages = "com.linkedbear.spring.annotation.f_typefilter",
includeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = Color.class),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.linkedbear.spring.annotation.f_typefilter.+Demo.+"),
@ComponentScan.Filter(type = FilterType.CUSTOM, value = GreenTypeFilter.class)},
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Animal.class))
public class TypeFilterConfiguration {
}
自定义规则 实现 TypeFilter
接口 ->metadataReader
读取到当前类的信息,metadataReaderFactory
读取到指定类的信息
public class GreenTypeFilter implements TypeFilter {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
throws IOException {
ClassMetadata classMetadata = metadataReader.getClassMetadata();
return classMetadata.getClassName().equals(Green.class.getName());
}
}
资源管理-了解
根接口 InputStreamSource
, 能获取到资源的输入流
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
Resource
接口中包含了资源的抽象
资源又可能包含不同的来源 -> 类路径,File, uri 这种来自于网络io等
PropertyResource 解析-了解
- 解析 properties 文件
- 解析 xml -> 引用的地方加 xml 前缀
xml 文件需要遵循 一个很复杂的约束,底层和解析 properties 是一样的 - 解析 yml -> 引用的地方 加 yml 前缀
@Value("${yml.jdbc.url}")
private String url;
snake-yaml 工具解析