一 Bean扫描
Springboot项目,我们不加@ComponentScan注解,但是也能扫描到@Controller、@Service标记的类,为什么呢?关键在于启动类的@SpringBootApplication注解,该注解由以下三个注解组成:
java
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
可以看到,已经默认有了@ComponentScan注解,但是我们没指定扫描路径,那它是怎么扫描到我们自定义的@Controller、@Service的呢?是因为它会默认扫描添加了该注解的类所在的包及其子包,也就是默认扫描启动类所在的包及启动类所在包的子包。
如果我们就想扫描不是启动类所在包以及子包下的目录,那么需要在启动类加@ComponentScan注解,并且指定要扫描的路径
@ComponentScan(basePackages = "com.example.controller")
总结:Springboot默认只能扫描到启动类所在的包以及该包下的子包,想要扫描其他路径的包,需要加@ComponentScan注解并指定包路径。
二 Bean注册
一般,我们在类上加下边这四个注解,就可以把该类对象注册到IOC容器里了
注解 | 说明 | 位置 |
---|---|---|
@Component | 声明bean的基础注解 | 不属于以下三类时,就用此注解 |
@Controller | @Component的衍生注解 | 标注在控制器类上 |
@Service | @Component的衍生注解 | 标注在业务类上 |
@Repository | @Component的衍生注解 | 标注在数据访问类上(DAO层,用于与mybatis等整合,用的少) |
如果要注册的bean对象来自于第三方(即,不是自定义的类),是无法用 @Component 及其衍生注解声明bean的,Springboot提供了两个注解解决这个问题:@Bean和@Import
如,自己通过maven制作了一个jar包,名字叫common-pojo-1.0-SNAPSHOT.jar,这个jar包里有两个类:Country.java、Province.java
首先通过maven install命令把该jar包安装到本地仓库,maven install命令如下
shell
# -Dfile后的参数换成自己的jar包所在的位置
mvn install:install-file -Dfile=C:\Users\Administrator\Desktop\common-pojo-2.0-SNAPSHOT.jar -DgroupId=cn.myt -DartifactId=common-pojo -Dversion=2.0 -Dpackaging=jar
或者在idea右侧,找到install,双击即可
在自己项目里通过maven坐标引入该jar包。接下来,我们注册这个jar包里的bean对象
2.1 方式一:在启动类注册第三方bean对象
在启动类添加方法,返回这个对象,且方法上加@Bean注解。一般不推荐使用这种方式,因为启动类只负责启动即可,推荐使用2.2章节里的方法注册第三方bean
验证是否注册成功
看到控制台有这个对象了,说明注册成功
2.2 方式二:在配置类里集中注册第三方bean
定义Config类,在类上加@Configuration注解,然后在类里声明和上边一样的方法即可。@Bean可以指定对象名字,如@Bean("myname"),不指定的话,对象默认的名字是方法名。
注意:Config类需要放到启动类所在包以及该包的子包下,能被扫描到才行。
如果我们注册的bean对象,需要依赖IOC容器里的已经存在的其他bean对象,那么就需要在注册时,参数上加上需要依赖的对象即可
2.3 方式三:在启动类加@Import(Xxx.class)注解
这种方式,一般常用的是以下两种场景:
- 配置类
- ImportSelector 接口实现类
导入配置类
如2.2章节里的Config配置类,如果不在启动类所在包以及子包下的话,就可以通过在启动类加@Import导入
如果多个配置类的话,Import写数组即可
导入ImportSelector 接口实现类
首先需要新建一个类实现ImportSelector 接口,然后重写selectImports方法,方法里返回要扫描的类的全类名即可,然后这些类的bean对象就可以注入到IOC容器里了
启动类里扫描上边这个类
2.4 注册条件
SpringBoot提供了设置注册生效条件的注解 @Conditional,这个注解用起来不方便,Springboot提供了该注解的衍生注解来使用,如下边这三注解
注解 | 说明 |
---|---|
@ConditionalOnProperty | 配置文件中存在对应的属性,才声明该bean |
@ConditionalOnMissingBean | 当IOC容器里不存在当前类型的bean1时,才声明该bean2 |
@ConditionalOnClass | 当前环境存在指定的这个类1时,才声明该bean |
@ConditionalOnProperty用法
配置文件添加配置
然后在注入类Config里的方法上,加上@ConditionalOnProperty注解,此时,如果配置文件里找不到@ConditionalOnProperty里的参数的话,就不会自动注入了
@ConditionalOnMissingBean用法
@ConditionalOnClass用法
三 自动配置原理
自动配置:遵循约定大约配置的原则,在SpringBoot程序启动后,起步依赖中的一些bean对象会自动注入到ioc容器。
很显然,上边注入对象的方式,并不是自动的,比如我们使用mybatis时,只要引入依赖后,可以不需要写一堆配置类就能使用相关的对象(即引入依赖后就自动注入了),而我们上边注入的对象,还需要手动写配置类。
源码分析
程序引入spring-boot-starter-web 起步依赖,启动后,会自动往ioc容器中注入DispatcherServlet。要验证IOC里是否有DispatcherServlet对象,在启动类里打印一下即可(不添加starter-web启动依赖):
上边的项目里加上web启动依赖,就不会报错了,因为此时就自动注入了DispatcherServlet对象,再次运行结果如下
那Springboot是如何做到,添加了web依赖后就自动注入DispatcherServlet对象的呢?此时我们来看源码,首先进入启动类的@SpringBootApplication注解里,就能看到@EnableAutoConfiguration注解(自动配置的核心注解)...依次按照下图进入
找到AutoConfigurationImportSelector类里的selectImports方法,之前说过,selectImports方法会被Springboot自动调用,从而得到全类名的字符串数组,然后把这些类的bean对象注入到IOC容器里(由于AutoConfigurationImportSelector是间接实现ImportSelector接口,Springboot实际没走这里的selectImports方法,但是注入原理是一样的)
此处的selectImports方法,和上边讲解的一样,需要读取配置文件去找自动配置的类,那读取的是哪个配置文件呢?跟踪源码,会找到,它读取的是:
注意,springboot2.7之后读取的是上边的文件,2.7版本之前,读取的是META-INF/spring.factories文件
我们开始在pom.xml里引入了web依赖,点进去看
进去后,可以看到引用了auto-config依赖
在左侧找到这个jar包,展开后,就能看到这个jar包里提供了Springboot所需的自动注入的配置文件了。打开这个配置文件,里边配置的就是一些类的全类名,如下
其中,有一个就是我们要找的DispatcherServletAutoConfiguration类全路径
进入这个类里
发现,和我们自己注入的bean逻辑一样。
自定义jar包里的对象实现自动注入
那么,我们之前自定义包里的Country.java类,怎么实现自动注入呢?自定义的包,在打包前,按照以下四部走即可(第四章节有具体实战)
- 自己打的jar包里,需要有CommonConfig.java类
- 新建自动配置类,并且该类上需要添加俩注解@AutoConfiguration、@Import,其中@AutoConfiguration表明该类是配置类,@Import用来把配置类导入进来
- 提供.imports文件
- 把自动配置文件的全类名写入上边的imports文件里
按照上边的步骤,打包后如下,并通过pom.xml引入到我们的新Springboot项目里:
在新项目里引入jar包后,在启动类里获取Country或者Province的bean对象,看是否能获取到
可以看到,获取到了Province的bean对象,说明已经自动注入成功了。此时,启动类上的@Import也就不需要了,因为可以自动配置了。
说一说SpringBoot自动配置原理
- 在主启动类上添加了SpringBootApplication注解, 这个注解组合了EnableAutoConfiguration注解
- EnableAutoConfiguration注解又组合了Import注解,导入了AutoConfigurationImportSelector类
- 实现selectImports方法, 这个方法经过层层调用,最终会读取META-INF 目录下的 后缀名 为imorts的文件 ,当然了,boot2.7以前的版本,读取的是spring.factories文件(boot2.7~3.0,这俩文件都支持,boot3.0后只支持import文件)
- 读取到全类名了之后,会解析注册条件,也就是@Conditional及其衍生注解,把满足注册条件的Bean对象自动注入到IOC容器中
四 自定义Starter
在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用。而在SpringBoot的项目中,一般会将这些公共组件封装为SpringBoot 的 starter。
我们创建一个maven工程,该工程下新建俩子模块,分别是autoconfigure、starter模块,这俩模块作用为:
- autoconfigure 模块,提供自动配置功能,并自定义配置文件 META-INF/spring/xxx.imports(Springboot2.7之前是META-INF/spring.factories,本文使用的Springboot版本是2.5.15)
- starter模块,需要在starter模块里引入上边的自动配置模块,以后在第三方项目引入starter模块即可
4.1 新建一个工程
新建后,把src目录删除,然后新建俩子模块
在此工程基础上需要新建两个子模块,一个是autoconfigure模块,另一个是starter模块
新建后的俩模块如下
其中,my-starter模块,只保留pom.xml,其他什么都不需要。
4.2 父工程
pom.xml文件如下
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mysdk</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>mysdk</name>
<description>mysdk</description>
<modules>
<module>my-autoconfigure</module>
<module>my-starter</module>
</modules>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.5.15</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.3 autoconfigure模块
pom.xml引入自动配置的依赖
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
定义MyProperties.java类,用于获取第三方项目配置文件里的动态值
java
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @Author:sgw
* @Date:2024/1/19
* @Description: 用于获取第三方项目,配置文件里的值
* 指定项目在属性文件中配置的前缀为sgw,如 sgw.str1=test1 ,就可以改变属性类字段 str1 的值了
*/
@SuppressWarnings("ConfigurationProperties")
@ConfigurationProperties(prefix = "sgw")
public class MyProperties {
public static final String DEFAULT_STR1 = "default value1";
public static final String DEFAULT_STR2 = "default value2";
private String str1 = DEFAULT_STR1;
private String str2 = DEFAULT_STR2;
public String getStr1() {
return str1;
}
public void setStr1(String str1) {
this.str1 = str1;
}
public String getStr2() {
return str2;
}
public void setStr2(String str2) {
this.str2 = str2;
}
}
定义MyTestService业务类,处理数据
java
import org.springframework.stereotype.Service;
/**
* @Author:sgw
* @Date:2024/1/18
* @Description: 业务类,处理数据,包括处理MyProperties里获取到的配置文件里的数据
*/
@Service
public class MyTestService {
private String str1;
private String str2;
public String getStr1() {
return str1;
}
public void setStr1(String str1) {
this.str1 = str1;
}
public String getStr2() {
return str2;
}
public void setStr2(String str2) {
this.str2 = str2;
}
public String myTest1() {
String name1 = "我的测试1数据是:" + str1;
String name2 = "我的测试2数据是:" + str2;
System.out.println(name1);
System.out.println(name2);
return name1 + name2;
}
}
定义MyAutoConfig自动配置类,将业务类自动装配注入到第三方的IOC容器里,这样第三方项目就可以自动注入MyTestService了
java
import com.sge.properties.MyProperties;
import com.sge.service.impl.MyTestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author:sgw
* @Date:2024/1/19
* @Description: 注入IOC容器配置类
*/
// 定义 java 配置类
@Configuration
//引入Service,这里是数组,可以注入多个Service
@ConditionalOnClass({MyTestService.class})
// 将 application.properties 的相关的属性字段与该类一一对应,并生成 Bean
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfig {
// 注入属性类
@Autowired
private MyProperties myProperties;
@Bean
// 当容器没有这个 Bean 的时候才创建这个 Bean
@ConditionalOnMissingBean(MyTestService.class)
public MyTestService testService() {
MyTestService myTestService = new MyTestService();
myTestService.setStr1(myProperties.getStr1());
myTestService.setStr2(myProperties.getStr2());
myTestService.myTest1();
return myTestService;
}
}
如果这个类需要注册过滤器或者需要注入多个service,参考下边
java
package com.zkts.fd.fd_register_server.config;
import com.zkts.fd.fd_register_server.mapper.UsersMapper;
import com.zkts.fd.fd_register_server.service.UsersService;
import com.zkts.fd.fd_register_server.service.impl.UsersServiceImpl;
import com.zkts.fd.fd_register_server.token.*;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* @Author:sgw
* @Date:2024/7/4
* @Description:
*/
@Configuration
@MapperScan("com.zkts.fd.fd_register_server.mapper")
public class UserdetailServiceAutoConfiguration {
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
@Bean
@ConditionalOnMissingBean(UsersService.class)
public UsersService usersService() {
return new UsersServiceImpl();
}
// 当容器没有这个 Bean 的时候才创建这个 Bean
// @ConditionalOnMissingBean(SysPasswordService.class)
@Bean
public SysPasswordService passwordService() {
return new SysPasswordService();
}
@ConditionalOnMissingBean(GlobalExceptionHandler.class)
public GlobalExceptionHandler globalExceptionHandler() {
return new GlobalExceptionHandler();
}
//@ConditionalOnMissingBean(RedisCache.class)
@Bean
public RedisCache redisCache() {
return new RedisCache();
}
@Bean
public AuthenticationEntryPointImpl unauthorizedHandler() {
return new AuthenticationEntryPointImpl();
}
//
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new LogoutSuccessHandlerImpl();
}
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
@Bean
@ConditionalOnMissingBean(CorsFilter.class)
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 设置允许访问的源(* 表示允许任何源)
config.addAllowedOriginPattern("*");
// 设置允许的请求方法
config.addAllowedMethod("*");
// 设置允许的头信息
config.addAllowedHeader("*");
// 是否允许携带Cookie
config.setAllowCredentials(true);
// 添加映射路径,可以是特定路径或所有路径
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config); // ** 表示对所有路径生效
return new CorsFilter(source);
}
//@ConditionalOnMissingBean(TokenService.class)
@Bean
public TokenService tokenService() {
return new TokenService();
}
@Bean
public PermitAllUrlProperties permitAllUrl() {
return new PermitAllUrlProperties();
}
}
在resource目录下定义META-INF/spring.factories文件,内容如下
shell
# Auto Configure,后边的值是自动配置类全路径
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.sge.config.MyAutoConfig
如果这个文件里需要写多个类的话,参考下边
shell
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zkts.fd.fd_register_server.token.SecurityConfig,\
com.zkts.fd.fd_register_server.config.UserdetailServiceAutoConfiguration,\
com.zkts.fd.fd_register_server.token.GlobalExceptionHandler
上边这个文件,是帮助第三方项目扫描自动配置类路径的,扫描后将其注入到IOC容器里
4.4 starter模块
starter模块只有一个功能,就是引入上边autoconfigure模块即可,所以starter模块的pom.xml如下
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>my-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
4.5 打包上传到maven仓库
注意,此处,install打包上传时,要打最外层父项目的maven
4.6 在第三方项目引入依赖使用上边的业务类
第三方项目引入上边的starter依赖即可
xml
<dependency>
<groupId>org.example</groupId>
<artifactId>my-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
然后注入MyTestService并调用相关方法
java
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private MyTestService myTestService;
@GetMapping("/list")
public R<List<UserEntity>> userList() {
String s = myTestService.myTest1();
return s;
}
}
注意,此时,第三方项目还没有在配置文件里配置sgw为前缀的值,我们调用接口,看结果如下
可以看到,此时获取到的结果值,是MyProperties里定义的常量值。
我们在第三方项目里配置上sgw为前缀的值,如下
此时,再访问接口,获取到的结果值如下