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为前缀的值,如下

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

相关推荐
儿时可乖了26 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol28 分钟前
java基础概念37:正则表达式2-爬虫
java
Iced_Sheep36 分钟前
干掉 if else 之策略模式
后端·设计模式
xmh-sxh-131444 分钟前
jdk各个版本介绍
java
XINGTECODE1 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
天天扭码1 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺1 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端