1.背景
IOC(Inversion of Control,控制反转)
控制反转是一种设计原则,它将对象的创建和管理责任从应用代码中移交给容器。
在Spring中,IOC容器负责管理应用中的所有对象,包括它们的生命周期和相互之间的依赖关系。
IOC的主要目的是为了减少代码之间的耦合,使代码更加模块化和可测试。
这里的控制反转,控制的是谁,反转又是反转到哪里了?
- 控制:对各种对象的控制
- 反转:将控制权反转给Spring容器
嗯,就是将对象的管理交给Spring容器,不用你自己来 new Xx();
。
好,我们也先别管它是怎么创建对象的了,我们思考一个问题。
我们类中,是不是经常会存在。
public class Xx{private MyClass myclass;
}
现在问题就来了,既然Spring容器要帮我们创建这个Xx对象,那么其中依赖的其他对象,例如此处的MyClass怎么办?
这就又谈到了我们的依赖注入(Dependency Injection, DI)。
依赖注入是指将对象的依赖(即它所需要的对象)在外部注入,而不是由对象自己创建或查找这些依赖。
你看,依赖注入只是帮助我们构建这个类,其根本目的还是为了实现咱们的控制反转。
好,在开始之前,我们先来大致了解几个点,留个印象。
- IOC创建对象:通过反射完成
- 依赖注入的几种方式
- 构造器注入:通过构造函数来注入依赖对象。
- Setter方法注入:通过Setter方法来注入依赖对象。
- 接口注入:通过接口来注入依赖对象(在Spring中不常用)。
后续的内容,基于SpringBoot程序演示。
2.@SpringBootApplication注解
上面已经提到了,IOC容器是用来管理咱们的对象的,Bean就是Spring IoC容器管理的一个对象,称呼而已。
好,在此之前,我们先来了解下,SpringBoot默认加载哪些bean。
回到梦开始的地方。
一切来自于咱们的这个@SpringBootApplication
注解。
前面4个,是咱们java自带的元注解。
注解 | 作用 |
---|---|
@Target({ElementType.TYPE}) | 表明可以应用于类、接口或枚举类型 |
@Retention(RetentionPolicy.RUNTIME) | 表明在运行时可用 |
@Documented | 表明使用的元素应被 javadoc 或类似工具记录 |
@Inherited | 表明可以被子类继承 |
后面3个,是咱们Spring的。
注解 | 作用 |
---|---|
@SpringBootConfiguration | 这是一个特殊的 @Configuration 注解,用于标记一个类作为 Spring 配置类。它继承自 @Configuration,表明该类可以作为 Spring IoC 容器的配置类。 |
@EnableAutoConfiguration | 这个注解启用 Spring Boot 的自动配置机制。 Spring Boot 会根据类路径中的依赖、定义的 Bean 和各种属性设置自动配置 Spring 应用程序的上下文。 它通过读取类路径中的 META-INF/spring.factories 文件,查找可以自动配置的类。 |
@ComponentScan | 这个注解启用组件扫描,默认扫描 @SpringBootApplication 所在包及其子包中的所有组件(如带有 @Component、@Service、@Repository 和 @Controller`的类)。 excludeFilters属性用于排除特定类型的组件。例如: - TypeExcludeFilter.class:一个自定义过滤器类型,用于排除特定类型的组件。 - AutoConfigurationExcludeFilter.class:用于排除自动配置类。 |
综上,使用了 @SpringBootApplication
注解:
-
ZaApplication
类被标记为一个配置类,可用于定义 Bean。 -
Spring Boot 的自动配置机制被启用:Spring Boot 会根据类路径中的依赖自动配置应用程序上下文。
-
Spring Boot 会自动扫描
cn.yang37.za
包及其子包中的组件,并将它们注册为 Spring Bean。
3.查看SpringBoot中的Bean
3.1 Actuator
注意啊,这个东西权限很大,不懂的话,你别在生产环境乱用。
这玩意就是个Spring自带的监控工具,用起来很简单。
3.1.1 导入pom
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
3.1.2 声明配置
这里就是声明暴露的端点,包含一个beans。
server:port: 9595
# actuator相关
management:endpoints:web:exposure:include: beans
3.1.3 启动后访问
启动后访问web,可以看到相关信息。
http://localhost:9595/actuator/beans
3.2 ApplicationContext
这个就比较简单了,直接注入了使用就行。
例如我注册一个bean,类型是MyConfig,名字是“6666”。
package cn.yang37.za.config;import org.springframework.stereotype.Component;@Component("6666")
public class MyConfig {}
然后,在测试类中获取它。
package cn.yang37.za.controller;import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;import javax.annotation.Resource;@Slf4j
@SpringBootTest
class BeanControllerTest {@Resourceprivate ApplicationContext applicationContext;@Testvoid name() {Object myConfig = applicationContext.getBean("6666");log.info("info : {}", myConfig);}
}
可以看到相关信息。
4.SpringBoot中创建bean的方式
4.1 @Componet + @ComponentScan
4.1.1 @Componet
Bean命名规则
-
类名的首字母小写:Spring会将类名的首字母小写,其他部分保持不变,作为默认的Bean名称。比如此处的beanName是myConfig。
-
保留原有大小写:如果类名的首字母和第二个字母都是大写(例如
URLConverter
),Spring会直接使用类名URLConverter
作为Bean名称,而不会改变首字母的大小写。
最简单的方式,就是直接用上咱们的@Componet
注解,标记这个类要放到容器中去。
@Component
// 也可以声明一个Bean名字@Component("MyConfig666")
public class MyConfig {}
使用的时候,直接使用@Autowired
或者@Resource
获取即可。
@RestController
@RequestMapping("/bean")
public class BeanController {@Resourceprivate MyConfig myConfig;@GetMapping("/info")public String info() {return myConfig.toString();}
}
我们平时用到的@Controller
等注解,都是它的子注解。
那我到底写@Component还是@Controller?要反过来说,@Controller这些注解都不合适的时候,再用@Component。
注解 | 作用 | 适用层次 |
---|---|---|
@Component | 标记为Spring管理的通用bean | 通用组件 |
@Controller | 标记为Spring MVC控制器,处理HTTP请求并返回视图 | 表现层(MVC控制器) |
@Service | 标记为服务层组件,表示业务逻辑 | 服务层 |
@Repository | 标记为数据访问层组件,提供数据访问功能并处理数据库异常转换 | 数据访问层 |
@RestController | 标记为RESTful Web服务控制器,默认返回JSON或XML格式的数据 | 表现层(REST控制器) |
4.1.2 @ComponentScan
该注解呢,用于扫描某个包,将所有声明了@Component
注解的类,注入到Spring容器中。
哎,你肯定问,我都@Componet
了?还scan个毛?@Componet不是已经可以加载进来了吗?
你忘啦,最开始说的,启动类上的@SpringBootApplication
注解包含了@ComponentScan
注解,但是它的默认扫描位置是:
- 启动类(即
@SpringBootApplication
的类)所在的包 - 子包
回到项目里,就是默认只扫描cn.yang37.za
文件夹和其中的文件夹。
我们在别的位置新建一个类,即便用上注解,它也是扫描不到的。
这个时候,咱们的@ComponentScan注解不就有用了吗?咱们直接在类中补上这个扫描的位置,它不就启动起来啦。
啊,你别懵,十分正常,你只是平时写习惯了,不妨看下咱们类加载器的知识:Java-JVM-类加载器
idea启动的时候,自动把classpath路径给我们传了,这下面所有的类都能在JVM里面找到。
所以咱们JVM中是有这个Demo类的,在配置了@ComponentScan到这个cn.yang的包下后,就一切正常了。
好,实际上不会这样搞对吧?咱们Java程序员向来喜欢搞各种xx包、xx类的概念,咱们根本就做不出来旁边新建个文件夹cn.yang的鬼操作对吧?
是滴,这里是在给你演示。
常见的方式是,某个组件没在你当前项目(比如此处的ZaApplication)代码里,比如某某jar包、比如跨模块,此时没加载到Spring容器中,不妨试试这个注解。
当然,正常情况下,这个jar包应该使用咱们2.5节spring.factories
的方式。
反正,你发现没自动加载上,你就试试手动声明下扫描位置,不用这么纠结到底要在何时使用。
注意,@ComponentScan
不要乱写在启动类上,用在启动类上会覆盖默认的扫描范围。
4.2 @Bean
Bean命名规则:默认情况下,其名字是方法名,也可以在注解中指明名字(注解的name属性)。
当然可以我们自己创建一个对象,把它放到我们的Spring容器中去。
哎,前面不是说,咱们把对象管理交给Spring去处理吗?什么玩意这么大的面子,还得我亲自创建?
先来段专业的话术。
@Bean
注解用于告诉 Spring 容器这个方法将返回一个需要注册为 Spring Bean 的对象,这样,我们可以自定义对象的创建逻辑,尤其是在对象创建需要复杂配置或依赖于其他组件时。
上例子,假设我们有个User对象,朴实无华。
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {private String name;private int age;
}
假设我创建它的时候,我要先到excel中读取下年龄数据,再调用xx接口获取下名字。
哈哈,你也知道我在乱扯。肯本原因就是,Spring哪能啥都知道啊,它也只是个框架,像上面的例子中,你还能指望Spring知道你要读取excel来加载年龄?
正如咱们自己写代码的时候,场景就是千变万化的,@Bean
的用处,是告诉你,当某些复杂场景搞不定的时候,你可以自己定义bean的注册逻辑。
所以,复杂点的场景,我要yang某活到1666岁,咱们可以自己放一个想要的Bean进去。
又比如你在用Redis时,经常看到的StringRedisTemplate,这不就是你不满意默认的StringRedisTemplate嘛,你要搞一个给它覆盖了。
@Beanpublic StringRedisTemplate stringRedisTemplate(@Qualifier("redissonConnectionFactory") RedisConnectionFactory connectionFactory) {StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();stringRedisTemplate.setConnectionFactory(connectionFactory);stringRedisTemplate.setKeySerializer(stringRedisSerializer);stringRedisTemplate.setValueSerializer(stringRedisSerializer);stringRedisTemplate.afterPropertiesSet();return stringRedisTemplate;}
4.3 @Configuration
Bean命名规则:跟
@Componen
一样的。
这个玩意呢,打开里面还是@Component
,适用于配置类的场景。
所以我们可以知道,它也能注册bean,那既然有自己的名字,肯定做了些定制化的操作。
Configuration这个单词你总该认识吧?什么,这你都不认识?
那看名字我们也能猜到了,它就是专门针对配置类的场景。
配置类的话,它的关键点在哪里,在于单例,即咱们的配置项key=123,它不会成key=456,所以@Configuration针对这点做了优化。
不过呢,这个优化点咱们一般感受不到。
Spring 会对 @Configuration
类进行 CGLIB 动态代理增强,确保@Bean
对应的方法只被调用一次,直接返回相同的实例(单例)。
有点抽象对不对,我们来看个例子。
@Component
public class MyConfig {@Beanpublic User user1() {return new User();}@Beanpublic User user2() {// 调用user1方法return user1();}}
这里我们用的@Component
注解,关键点在于下方的user2
,它是在调用user1()
方法。
此时,@Component
注解返回了两个不同的对象。
那我们改成@Configuration
。
@Configuration
public class MyConfig {@Beanpublic User user1() {return new User();}@Beanpublic User user2() {return user1();}}
此时,@Component
返回的对象是同一个。
哎,有点奇怪了对吧。
注意啊,注意,这不是说@Component
多例@Configuration
单例,是强调这两个类中的@Bean对应的方法执行时会不会被代理掉。
Bean的单例还是多例,取决于Bean它自己本身声明的是单例bean还是原型bean。
你别晕,我给你改造下,是@Component
对吧,注册了一个user1的bean(默认为单例bean)。
@Component
public class MyConfig {@Beanpublic User user1() {return new User();}}
因为它是默认的单例Bean,不管你怎么调用它都还是同一个。
你改成原型Bean。
@Component
public class MyConfig {@Bean@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)public User user1() {return new User();}
}
你看,生成出来的Bean是单例还是多例,取决于它自身是单例Bean还是原型Bean。
回顾一下之前的User2这个Bean。
@Beanpublic User user2() {return user1();}@Beanpublic User user2() {return new User();}
上方的根本逻辑是什么,是我们主动去触发user1()
这个方法了,user1方法干嘛的,给你new个User出来。
@Configuration 中所有带@Bean注解的方法都会被动态代理,调用该方法首次执行后,总该有返回值了吧?后续呢,它不会再执行,直接返回这个实例(除非你又自己主动去强调Bean的类型是原型Bean)。
@Component
,注意啊,这里的user2是个原型Bean,意味着每次都会返回新的对象,触发咱们的user1()方法。
改成@Configuration
呢,会发现,user1()方法不会多次触发。
哎,你肯定奇怪,那我平时也没有跑去调那个@Bean下面的方法呀。
声明就声明,我没事我乱主动调用干嘛,我自己注册两个User对象,我闲的没事我跑去用user1()方法创建啊?
这应该就是针对这个场景的防范式编码了。
- 首先呢,咱们不乱调用,Spring源码里面可能涉及到多次调用,是为了确保@Configuration它符合自己的特性。
- 其次呢,也是为了避免你乱来,在咱们@Configuration类中写的玩意,哪怕你自己乱用,咱们的配置对象也不会混乱。
4.4 @Import
Bean命名规则:跟
@Componen
一样的。
@Configuration
可以将某个类标记为配置类,当配置类比较多的时候呢,我们可以不用再在每个类上写@Configuration
,而是统一在一个类中借用@Import
引入。
比如这样:
MyConfig2、MyConfig3都上没有注解
public class MyConfig2 {@BeanUser user3(){return new User();}
}
public class MyConfig3 {@BeanUser user4() {return new User();}
}
咱们在AppConfig类中引入它们两个配置类。
@Configuration
@Import({MyConfig2.class, MyConfig3.class})
public class AppConfig {}
验证一下,两个类都能被成功导入。
嗯,这是@Import
的一个简单用法,这样我们只用在一个类中(例如AppConfig)去找都导入了哪些玩意,方便管理。
然后呢,还有个常用的用法就是动态导入配置,编写一个类来实现ImportSelector
接口即可。
配置选择类
public class MyImportSelector implements ImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata importingClassMetadata) {LinkedList<String> configList = new LinkedList<>();// 根据条件导入配置类if (true) {configList.add(MyConfig2.class.getName());configList.add(MyConfig3.class.getName());}return configList.toArray(new String[0]);}}
这里我list.toArray(new String[0])是个正确的写法,返回的就是对应的那个2个长度的数组。并且是建议这样写的,你可以去自己搜索下。
在@Import中使用配置选择类
@Configuration
@Import(MyImportSelector.class)
public class AppConfig {}
验证结果
4.5 FactoryBean接口
FactoryBean是一个工厂Bean,可以生成指定类型的Bean实例。
实现FactoryBean接口,可以对Bean创建提供更高的灵活性和扩展性,例如动态创建不同类型的 Bean、复杂初始化逻辑、延迟初始化等场景。
好,简单来说,就是我们想要创建某个类型的Bean的时候,可以找这个FactoryBean要。
public interface FactoryBean<T> {String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";@NullableT getObject() throws Exception;@NullableClass<?> getObjectType();default boolean isSingleton() {return true;}
}
比如上方的User,我们想要创建该类型的bean。
重写的getObject方法中,我们描述详细的创建逻辑。
- sex为1时,age是18。
- sex为2时,age是20。
@NoArgsConstructor
@AllArgsConstructor
public class UserFactoryBean implements FactoryBean<User> {private int sex;@Overridepublic User getObject() {User user = User.builder().name("yang").age(0).build();if (1 == sex) {user.setAge(18);}if (2 == sex) {user.setAge(20);}return user;}@Overridepublic Class<?> getObjectType() {return User.class;}}
注册这个UserFactoryBean。
- userFactoryBean1:sex为1,其中创建的user的age是18。
- userFactoryBean2:sex为2,其中创建的user的age是20。
@Slf4j
@Configuration
public class MyConfig {@Beanpublic UserFactoryBean userFactoryBean1() {return new UserFactoryBean(1);}@Beanpublic UserFactoryBean userFactoryBean2() {return new UserFactoryBean(2);}}
此时,咱们利用UserFactoryBean来获取User对象,并没有说去@Bean来一个user。
哈,这里我拿的Bean名字不是userFactoryBean1吗,咱直接返回里面的User对象了?往后看。
简单来说,就是为了方便,我都开始写FactoryBean了,当然是关注于拿到实际的Bean,咱默认设计成给你直接返回对应Bean实例。
然后呢,你还可以这样用。
额,这里你可能看到我用了个&userFactoryBean1
,这个&
是何用意?
&
前缀在 Spring 中是专门用来获取FactoryBean实例本身的。
即通过 getBean方法获取 Bean 时,FactoryBean这个玩意,默认情况下返回的是它创建的对象。
-
违背预期:大多数情况下,开发者希望获取的是由
FactoryBean
创建的对象,而不是FactoryBean
本身。例如,当你配置了一个
UserFactoryBean
,你希望得到的是User
对象,而不是UserFactoryBean
的 实例。 -
简化使用:通过
getBean
方法直接获取目标对象,简化了代码的使用,不需要开发者额外处理FactoryBean
实例。 -
避免混淆:明确区分
FactoryBean
和它创建的对象,有助于代码的可读性和可维护性。
那么在例子中,UserFactoryBean本身是不会直接注入的,默认注册的是 UserFactoryBean创建的User,为了注入 UserFactoryBean实例,得像我下面这样:
@Autowired@Qualifier("&userFactoryBean1")private UserFactoryBean userFactoryBean1;
不用太纠结,有个印象即可,毕竟,报错了你会搜索,而且,也不是不给你拿,你加&
指明下就好了。
有点懵是吧,上图。
4.6 BeanDefinitionRegistryPostProcessor接口
BeanDefinitionRegistryPostProcessor与上面FactoryBean接口类似,实现这个接口,也能创建出Bean。
@Slf4j
@Configuration
public class UserBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {@Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {log.info("=====> UserBeanDefinitionRegistryPostProcessor");// 创建User类的Bean定义BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(User.class);beanDefinitionBuilder.addPropertyValue("name", "yang");beanDefinitionBuilder.addPropertyValue("age", 166);// 注册User类的Bean定义beanDefinitionRegistry.registerBeanDefinition("user", beanDefinitionBuilder.getBeanDefinition());}@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {// 这里可以留空,或者用于进一步定制 BeanFactory}
}
4.7 spring.factories
这个呢,主要用于三方包的场景。
前面提到了,咱们默认扫描的是启动类和其子包,那我写给别人的stater,我还发个聊天消息给别人说,你来配置下扫描我的xx包?
spring.factories
呢无非就是一个固定的文件,咱们写在里面的类,会被自动注册到容器中。
原理呢是基于Java的SPI机制的。
它的位置固定为:
META-INF/spring.factories
好,不妨演示一下。
- 旧项目cn.yang37
- 新项目com.yang37
4.7.1 旧项目
我们直接把当前项目清空,你看,啥都没有,只有个简单的Controller和启动类。
记录下当前项目的坐标。
<groupId>cn.yang37</groupId><artifactId>yang-spring-za</artifactId><version>0.0.1-SNAPSHOT</version>
4.7.2 新项目
然后呢,直接新开一个项目,用不同的包名总可以了吧,新项目用com.yang37-2
,这差别够大了吧。
<groupId>com.yang37-2</groupId><artifactId>yang-spring-za2</artifactId><version>0.0.1-SNAPSHOT</version>
它里面啥也没有,只是导入了spring-boot-starter
,代码呢写了两个实体类和配置类,启动类都没有。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency>
两个实体类,朴实无华,也没用spring的注解。
package com.yang37.za.entity;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User2 {private String name;private int age;
}
package com.yang37.za.entity;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User3 {private String name;private int age;}
然后呢,配置类中,我们把它俩注册为Bean。
package com.yang37.za.config;import com.yang37.za.entity.User2;
import com.yang37.za.entity.User3;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class UserConfig {@Beanpublic User2 user2() {return User2.builder().name("user2").age(30).build();}@Beanpublic User3 user3() {return User3.builder().name("user3").age(30).build();}}
接着我们在META-INF\spring.factories
声明需要加载UserConfig。
# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.yang37.za.config.UserConfig
最后我们将这个项目install。
4.7.3 验证
现在,我们在旧工程中导入打包好的新项目。
<dependency><groupId>com.yang37-2</groupId><artifactId>yang-spring-za2</artifactId><version>0.0.1-SNAPSHOT</version></dependency>
你看,已经加载进来了。
停,先回顾下。
- 旧项目cn.yang37:啥也没有
- 新项目com.yang37:啊,是个外部的玩意,里面记录了要加载哪些bean。
你看,我们在当前项目中,引入了这个带有spring.factories的三方包,我们旧项目自身啥也没有对吧?
验证下,你看,一切都是如期而至。
哎,前面不是讲了,咱们用scan是不是也可以,我们把com.yang37里面的spring.factories删除了重新打包。
没了之后,我们不@ComponentScan直接报错。
加下@ComponentScan,是不是正常啦。
好咯,回顾下,条条大路通罗马,无非就是适用的场景不一样而已。
4.7.4 总结
上面的例子只是演示了一个类似于加载自定义配置类的玩意,实际上咱们有很多场景可以用到。
spring.factories
文件中常见配置项的总结。
配置项类型 | 说明 |
---|---|
org.springframework.boot.autoconfigure.EnableAutoConfiguration | 用于指定自动配置类。 |
org.springframework.context.ApplicationContextInitializer | 用于指定初始化 Spring 上下文的类。 |
org.springframework.context.ApplicationListener | 用于指定应用程序事件监听器。 |
org.springframework.boot.env.EnvironmentPostProcessor | 用于在 Spring Boot 应用程序环境准备好后进行处理的类。 |
org.springframework.boot.SpringApplicationRunListener | 用于在 SpringApplication 运行时的监听器。 |
demo:
# META-INF/spring.factories# Auto Configuration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.yang37.za.config.UserConfig,\com.yang37.za.config.OtherConfig# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\com.yang37.za.config.SomeInitializer,\com.yang37.za.config.AnotherInitializer# Application Listeners
org.springframework.context.ApplicationListener=\com.yang37.za.config.SomeListener,\com.yang37.za.config.AnotherListener# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\com.yang37.za.config.SomeEnvironmentPostProcessor,\com.yang37.za.config.AnotherEnvironmentPostProcessor# Spring Application Run Listeners
org.springframework.boot.SpringApplicationRunListener=\com.yang37.za.config.SomeRunListener,\com.yang37.za.config.AnotherRunListener