0%

Spring @Import和@Enable*注解和Spring的SPI完成二方包的注入

简介

最近在写一些基于SpringBoot的jar供其它工程调用,学习了一些开源和了解了Spring的自动配置后做了以下总结,在最开始接触Spring时当时网上说Spring基于xml的控制反转和依赖注入可以很好的将代码解耦,当时并不理解,明明依赖还是存在为什么能很好的解耦?直到接触了多模块后才渐渐理解它的含义,在多模块中代码供应商提供统一的接口,犹如一些插件,软件商提供主要接口,接入商负责实现,接入商的又有很多,那么如何做到可插拔开箱即用而各个接入商之间又不互相影响呢?这是依赖注入就很好的解决了这个问题,开发商和接入商不再进行强耦合,而是依赖类似与中间件一样的组件,将实现都由中间去管理,如果我想替换掉某个接入商的实现就在中间中更换接口的实现即可,对供应商的代码无任何影响,这就是Spring做的事。

将二方包注入到调用者的Spring容器中

我们都知道SpringBoot工程的入口需要有一个@SpringBootApplication注解,然后SpringBoot会自动扫描这个类同级或者子包的Spring注解然后注入到Spring容器中,但是我们编写的二方包往往和调用者有者不同的目录结构,如何才能将我们提供的二方包注入到调用者的容器中呢?这里有3中方法。

保持相同的目录结构

简单粗暴的方法就是调用者和被调用者有着相同的目录结构,使二方包中的配置类在调用者启动类如果的同级包或者子包下,那么这样SpringBoot在扫描时就会扫描到二方包中的Spring配置自动注入到容器中。

  • 我们创建一个二方包工程,POM文件如下,只引入Spring的容器模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.liu</groupId>
<artifactId>enable_core2</artifactId>
<version>1.0-SNAPSHOT</version>


<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>

</dependencies>


<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<!--<archive>
<manifest>
<mainClass>配置程序入口如果有的话</mainClass>

</manifest>
</archive>-->
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<!-- <goal>assembly</goal> -->
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<!--use commend: mvn assembly:assembly -DskipTests -->
</plugins>
</build>


</project>
  • 编写二方包接口
1
2
3
4
5
6
7
8
9
10
11
12
13

package com.liu.app1;

/**
* @author Liush
* @description
* @date 2019/12/30 17:35
**/
public interface ServiceI {

String getServiceName();

}
  • 接口实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.liu.app1;

/**
* @author Liush
* @description
* @date 2019/12/30 17:35
**/
public class ServiceImpl implements ServiceI {
@Override
public String getServiceName() {
return "core2...........";
}
}
  • Spring配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.liu.app1;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author Liush
* @description
* @date 2019/12/30 17:36
**/
@Configuration
public class Config {

@Bean
public ServiceI createService(){

return new ServiceImpl();
}

}

执行mvn install将二方包打入本地仓库


创建调用工程

  • 我们创建调用者POM文件,创建一个web工程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.liu</groupId>
<artifactId>enable_test</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--引入自定义二方包 -->
<dependency>
<groupId>com.liu</groupId>
<artifactId>enable_core2</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>


</dependencies>


</project>
  • 接着我们编写入口和一个Rest接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.liu.app1;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author Liush
* @description
* @date 2019/12/30 17:26
**/
@RestController
public class Rest {
@Autowired
private ServiceI serviceI;

@RequestMapping("/getName")
public String getName(){

return serviceI.getServiceName();

}



}
  • 编写调用包的启动类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.liu.app1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

/**
* @author Liush
* @description
* @date 2019/12/30 16:51
**/
@SpringBootApplication

public class APP {

public static void main(String[] args) {

SpringApplication.run(APP.class,args);

}


}
总结

这种方法需要调用者和二方包有相同的目录接口才能使调用方扫描到二方包的Spring配置,这种在实际中可行性较低

使用@Import注解

  • 第一种方法在项目中可行性很低于是我们可以采用这种方法来编写二方包

  • 创建工程POM文件,注意这里和第一种方法的POM文件没有什么不同除了我们将 改成了enable_core

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.liu</groupId>
<artifactId>enable_core</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/>
</parent>


<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>

</dependencies>




<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<!--<archive>
<manifest>
<mainClass>配置程序入口如果有的话</mainClass>

</manifest>
</archive>-->
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<!-- <goal>assembly</goal> -->
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<!--use commend: mvn assembly:assembly -DskipTests -->
</plugins>
</build>
</project>
  • 这次我们在和调用方不同的路径下编写代码,总体和第一步都相同,主要就是包的路径不同,首先编写接口
1
2
3
4
5
6
7
8
9
10
11
12
package com.sunnada;

/**
* @author Liush
* @description
* @date 2019/12/30 17:05
**/
public interface ServiceI {


String getServiceName();
}
  • 编写实现类
1
2
3
4
5
6
7
8
package com.sunnada;

public class ServiceImpl implements ServiceI {
@Override
public String getServiceName() {
return "core包中的服务实现";
}
}
  • 编写配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.sunnada;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author Liush
* @description
* @date 2019/12/30 17:01
**/
@Configuration
public class CoreConfig {

@Bean
public ServiceI createService(){

return new ServiceImpl();
}


}
  • 执行mvn clean install将代码打入仓库

调用方代码

  • 创建POM文件,引入二方包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.liu</groupId>
<artifactId>enable_test</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.liu</groupId>
<artifactId>enable_core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>


</dependencies>


</project>
  • 编写Rest接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.liu.app1;


import com.sunnada.ServiceI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author Liush
* @description
* @date 2019/12/30 17:26
**/
@RestController
public class Rest {
@Autowired
private ServiceI serviceI;

@RequestMapping("/getName")
public String getName(){

return serviceI.getServiceName();

}



}
  • 编写调用包的启动类,注意我们这里使用了@Import注解将我们二方包的配置文件导入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.liu.app1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

/**
* @author Liush
* @description
* @date 2019/12/30 16:51
**/
@SpringBootApplication
@Import({com.sunnada.CoreConfig.class})
public class APP {

public static void main(String[] args) {

SpringApplication.run(APP.class,args);

}


}
  • 访问接口
    输出 core包中的服务实现
总结

我们使用@Import在调用方中的启动类中手动指定需要注入的配置类,完成二方包的注入

更进一步

我们在调用开源工程的时候经常引入一些二方包,这些包中我们并不需要使用@Import注解,而是在启动类中使用@Enable* 注解即可完成二方包的注入,我们可以查看@EnableScheduling中的源码,发现其本质也是使用@Import注解完成二方包的注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.scheduling.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({SchedulingConfiguration.class})
@Documented
public @interface EnableScheduling {
}

基于Spring的SPI机制

新建配置文件

由于我们用maven构建工程,我们在resources下新建目录/META-INF/spring.factories文件,在配置文件中加入以下代码

1
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.liu.Config

我们注意配置文件等号左边的部分是Spring自动配置提供的类,右边是我们需要注入的类的全路径,到此我们会疑问SpringBoot是如何使用这个方法自动注入的呢?下面我们进入org.springframework.boot.autoconfigure.EnableAutoConfiguration查看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

Class<?>[] exclude() default {};

String[] excludeName() default {};
}

我们注意最重要的部分@Import({AutoConfigurationImportSelector.class}),我们进入类中查看,发现其实现DeferredImportSelector接口,而DeferredImportSelector又实现ImportSelector

1
2
3
4
5
6
7
8
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {


...



}

那ImportSelector接口的作用又是什么呢?其有只有一个方法

1
2
3
4
5
6
7
package org.springframework.context.annotation;

import org.springframework.core.type.AnnotationMetadata;

public interface ImportSelector {
String[] selectImports(AnnotationMetadata var1);
}

我们通过查找官方API,得知这个接口是决定哪些类可以导入Spring容器,也就是说我们可以通过实现这个接口来通过编码和配置文件的方式注入类到Spring的容器中

1
Interface to be implemented by types that determine which @Configuration class(es) should be imported based on a given selection criteria, usually one or more annotation attributes.

接着我们查看AutoConfigurationImportSelector的selectImports方法实现,查看到底在AutoConfigurationImportSelector中做了什么

1
2
3
4
5
6
7
8
9
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}

这里我们看的一头雾水,但是我们可以根据方法名来猜测loadMetadata方法大概是加载什么元数据之类的,getAutoConfigurationEntry方法是获取配置实体之类的我们进入该方法查看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.filter(configurations, autoConfigurationMetadata);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}

这里我们根据方法名大概可以知道这里做的就是获取配置文件中的信息,然后做了去重,过滤等一系列查找然后将属性封装出去

1
2
3
4
5
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}

这里我们进入getSpringFactoriesLoaderFactoryClass方法看一下发现其返回的是一个EnableAutoConfiguration对象,也就是我们配置文件中配置的org.springframework.boot.autoconfigure.EnableAutoConfiguration类,这里也就是解释了为什么我们需要在配置文件中在org.springframework.boot.autoconfigure.EnableAutoConfiguration类下配置类才能完成自动注入

1
2
3
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}

现在再根据方法名我们发现this.getCandidateConfigurations方法可能使获取配置类发生的地方我们进入该方法查看一下
这里我们发现了SpringFactoriesLoader类的身影,熟悉Spring spi的知道spring spi就是通过这个类来加载配置文件中的类来达成自动配置的目的的,我们在进入SpringFactoriesLoader查看一下,发现其中又一个属性为FACTORIES_RESOURCE_LOCATION ,这里就说明了我们建配置文件为什么必须目录为META-INF/spring.factories

1
2
3
4
5
6
public final class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

....

}

那么这里逻辑就已经清晰
我们在META-INF/spring.factories中配置SpringBoot需要自动注入的类,org.springframework.boot.autoconfigure.EnableAutoConfiguration是Spring帮我们封装好的一个注解我们在其后配置我们需要自动注入的类,那么SpringBoot就会使用AutoConfigurationImportSelector来帮助我们将我们配置的类一个个注入到Spring中