Springboot自定义starter注入到第三方项目IOC容器里

一 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)注解

这种方式,一般常用的是以下两种场景:

  1. 配置类
  2. 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类,怎么实现自动注入呢?自定义的包,在打包前,按照以下四部走即可(第四章节有具体实战)

  1. 自己打的jar包里,需要有CommonConfig.java类
  2. 新建自动配置类,并且该类上需要添加俩注解@AutoConfiguration、@Import,其中@AutoConfiguration表明该类是配置类,@Import用来把配置类导入进来
  3. 提供.imports文件
  4. 把自动配置文件的全类名写入上边的imports文件里

按照上边的步骤,打包后如下,并通过pom.xml引入到我们的新Springboot项目里:

在新项目里引入jar包后,在启动类里获取Country或者Province的bean对象,看是否能获取到

可以看到,获取到了Province的bean对象,说明已经自动注入成功了。此时,启动类上的@Import也就不需要了,因为可以自动配置了。

说一说SpringBoot自动配置原理

  1. 在主启动类上添加了SpringBootApplication注解, 这个注解组合了EnableAutoConfiguration注解
  2. EnableAutoConfiguration注解又组合了Import注解,导入了AutoConfigurationImportSelector类
  3. 实现selectImports方法, 这个方法经过层层调用,最终会读取META-INF 目录下的 后缀名 为imorts的文件 ,当然了,boot2.7以前的版本,读取的是spring.factories文件(boot2.7~3.0,这俩文件都支持,boot3.0后只支持import文件)
  4. 读取到全类名了之后,会解析注册条件,也就是@Conditional及其衍生注解,把满足注册条件的Bean对象自动注入到IOC容器中

四 自定义Starter

在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用。而在SpringBoot的项目中,一般会将这些公共组件封装为SpringBoot 的 starter。

我们创建一个maven工程,该工程下新建俩子模块,分别是autoconfigure、starter模块,这俩模块作用为:

  1. autoconfigure 模块,提供自动配置功能,并自定义配置文件 META-INF/spring/xxx.imports(Springboot2.7之前是META-INF/spring.factories,本文使用的Springboot版本是2.5.15)
  2. 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为前缀的值,如下

此时,再访问接口,获取到的结果值如下

相关推荐
Kali_076 分钟前
使用 Mathematical_Expression 从零开始实现数学题目的作答小游戏【可复制代码】
java·人工智能·免费
rzl0218 分钟前
java web5(黑马)
java·开发语言·前端
君爱学习24 分钟前
RocketMQ延迟消息是如何实现的?
后端
guojl37 分钟前
深度解读jdk8 HashMap设计与源码
java
Falling4241 分钟前
使用 CNB 构建并部署maven项目
后端
guojl43 分钟前
深度解读jdk8 ConcurrentHashMap设计与源码
java
程序员小假1 小时前
我们来讲一讲 ConcurrentHashMap
后端
爱上语文1 小时前
Redis基础(5):Redis的Java客户端
java·开发语言·数据库·redis·后端
A~taoker1 小时前
taoker的项目维护(ng服务器)
java·开发语言