持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第29天,点击查看活动详情
一、理解独立的Spring应用
1. jar、war的执行模块spring-boot-loader
将jar包解压开,执行 java org.springframework.boot.loader.JarLauncher发现程序运行了起来。
我们同样的方式执行java thinkinginspringboot...(省略).FirstAppByGuiApplication会提示SpringApplication无法找到。那么这是为什么呢?
1.1 JarLauncher的实现原理?
引入pom依赖来查看源码
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<scope>provided</scope>
</dependency>
public class JarLauncher extends ExecutableArchiveLauncher {
private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx";
static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
if (entry.isDirectory()) {
return entry.getName().equals("BOOT-INF/classes/");
}
return entry.getName().startsWith("BOOT-INF/lib/");
};
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
// Only needed for exploded archives, regular ones already have a defined order
if (archive instanceof ExplodedArchive) {
String location = getClassPathIndexFileLocation(archive);
return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
}
return super.getClassPathIndex(archive);
}
private String getClassPathIndexFileLocation(Archive archive) throws IOException {
Manifest manifest = archive.getManifest();
Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
return (location != null) ? location : DEFAULT_CLASSPATH_INDEX_LOCATION;
}
@Override
protected boolean isPostProcessingClassPathArchives() {
return false;
}
@Override
protected boolean isSearchCandidate(Archive.Entry entry) {
return entry.getName().startsWith("BOOT-INF/");
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
执行java JarLauncher实际上是执行new JarLauncher().launch(args);
打包War文件是一种兼容措施,既能WarLauncher启动,又能兼容Servlet容器环境。
2. 理解固化的Maven依赖
2.1 spring-boot-starter-parent与spring-boot-dependencies
通过引入spring-boot-starter-parent依赖的方式很方便,但是也存在一定的限制,因为是单继承的方式,所以如果已有自定义parent依赖就没法加该依赖了。
那么不使用spring-boot-starter-parent该怎么构建一个springboot应用呢?
示例如下:
<?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>
<!-- <parent>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-parent</artifactId>-->
<!-- <version>2.3.4.RELEASE</version>-->
<!-- </parent>-->
<groupId>thinking-in-spring-boot</groupId>
<artifactId>first-app-by-gui</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>first-app-by-gui</name>
<description>Demo project for Spring Boot</description>
<packaging>war</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.1</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.4.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
需要注意:
- maven-war-plugin:2.2中,默认的打包方式是必须存在WEB-INF/web.xml的,而3.1.0调整了。
- 单独引入spring-boot-maven-plugin时,需要配置repackage元素,否则不会添加spring-boot引导依赖。
3. 理解嵌入式Web容器
问题:为什么引入了spring-boot-starter-tomcat就能引导tomcat容器?
3.1 嵌入式Servlet容器
SpringBoot支持3种嵌入式Servlet3.1+ 容器
| Name | Servlet Version |
|---|---|
| Tomcat8.5 | 3.1 |
| Jetty9.4 | 3.1 |
| Undertow1.4 | 3.1 |
3.2 切换容器为Jetty
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
==tomcat maven插件与嵌入式Tomcat的区别:==
- tomcat maven插件: 不需要编码,不需要外置容器,将当前应用直接打包为可运行的Jar或War文件,并且将完整的Tomcat运行时资源打包到Jar或War中,通过java -jar命令启动,当被java -jar引导后会将其归档内容解压到.extract目录
- 嵌入式Tomcat: spring boot2.0的实现,他利用了Tomcat Api构建TomcatWebServer Bean,由Spring应用上下文引导,其嵌入式Tomcat组件的运行(如Context,Connecter)等由Spring Boot框架代码实现.
4. 理解自动装配
2.3.4版本的@SpringBootApplication
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication
@ComponentScan并非使用了默认值,而是添加了排除项.TypeExcludeFilter用于排除已注册的TypeExcludeFilter Bean,AutoConfigurationExcludeFilter用于排除同时标注了@Configuration和@EnableAutoConfiguration的类.
@Configuration下声明的Bean属于“完全模式”,会执行CGLIB提升的操作.
4.1 自动配置机制
@ConditionalOnClass和@ConditionalOnMissingBean是最常见的注解,当@ConditionalOnClass标注在@Configuration类上时,当且仅当目标类存在Class Path下时才予以装配.
自动配置是spring boot的核心模块,他们统一存放在org.springframework.boot.autoconfigure包或子包下.同时,这些类均配置在META-INF/spring.factories中.
4.2 自定义自动配置类
(1). 在resources的目录下新建META-INF/spring.factories,配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=thinking.in.spring.boot.autoconfigure.WebAutoConfiguration (2). 创建WebConfiguration
@Configuration
public class WebConfiguration {
@Bean
public RouterFunction<ServerResponse> helloWorld(){
return RouterFunctions.route(GET("/hello"),
request -> ServerResponse.ok().body(
Mono.just("Now is " + new SimpleDateFormat("HH:mm:ss").format(new Date())), String.class)
);
}
@Bean
public ApplicationRunner runner(BeanFactory beanFactory){
return args -> {
System.out.println("当前helloWorld实现类为:"+beanFactory.getBean("helloWorld").getClass().getName());
System.out.println("当前WebConfiguration的实现类为:"+beanFactory.getBean(WebConfiguration.class).getClass().getName());
};
}
@EventListener(WebServerInitializedEvent.class)
public void onWebServerReady(WebServerInitializedEvent event){
System.out.println("当前WebServer实现类为:"+event.getWebServer().getClass().getName());
}
}
(3). 创建WebAutoConfiguration
@Configuration
@Import(WebConfiguration.class)
public class WebAutoConfiguration {
}
5. 理解Production Ready特性
5.1 Spring Boot Actuator
- 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 添加配置
仅有health 和info是默认暴露的Web Endpoints,如果需要其他的,可以在application.properties中增加配置
management.endpoints.web.exposure.include=beans
- 访问路径
6. 注解驱动编程
- 元注解
- Spring模式注解
自定义@Component的派生注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface StringRepository {
String value() default "";
}
==为什么我们什么都不需要做,在注解上加一个@Component就能达到@Component的效果呢?==
我们从component-scan元素扫描看起.
public class ContextNamespaceHandler extends NamespaceHandlerSupport {
public ContextNamespaceHandler() {
}
public void init() {
this.registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
this.registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
this.registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
this.registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
this.registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
this.registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
this.registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
this.registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
}
}
component-scan的处理器是ComponentScanBeanDefinitionParser,他的parse方法如下:
public BeanDefinition parse(Element element, ParserContext parserContext) {
String basePackage = element.getAttribute("base-package");
basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
String[] basePackages = StringUtils.tokenizeToStringArray(basePackage, ",; \t\n");
ClassPathBeanDefinitionScanner scanner = this.configureScanner(parserContext, element);
Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
this.registerComponents(parserContext.getReaderContext(), beanDefinitions, element);
return null;
}
在上面的代码里有一个doscan方法,doscan方法里又会调用ClassPathScanningCandidateComponentProvider#findCandidateComponents方法.然后会调用到ClassPathScanningCandidateComponentProvider#isCandidateComponent(org.springframework.core.type.classreading.MetadataReader)用于判断元信息数据,这个方法判断是根据下面两个字段决定到.
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return isConditionMatch(metadataReader);
}
}
return false;
}
==派生注解支持多层型== 即如果在注解@StringRepository上加了注解@Repository,那么@StringRepository也具备了@Component的特性. 3. Spring组合注解
所谓组合注解是指某个注解“元标注”了一个或多个其他注解.如@TransactionalService标注了@Transactional和@Service注解.
备注:获取注解的方式
@StringRepository
public class TransactionalServiceBootstrap {
public static void main(String[] args) throws IOException {
String className = TransactionalServiceBootstrap.class.getName();
MetadataReaderFactory metadataReaderFactory=new CachingMetadataReaderFactory();
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(className);
AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
annotationMetadata.getAnnotationTypes().forEach(anno->{
Set<String> metaAnnotationTypes = annotationMetadata.getMetaAnnotationTypes(anno);
metaAnnotationTypes.forEach(metaAnnotationType->{
System.out.println("元标注:"+anno+",具体的:"+metaAnnotationType);
});
});
}
}
-
Spring注解属性别名和覆盖
-
AnnotationMetadata存在两种实现:基于ASM的AnnotationMetadataReadingVisitor和基于Java反射API的StandardAnnotationMetadata.
-
public class TransactionalServiceBootstrap {
public static void main(String[] args) throws IOException {
//反射实现
StandardAnnotationMetadata standardAnnotationMetadata = new StandardAnnotationMetadata(SpringBootApplication.class);
System.out.println(standardAnnotationMetadata.getAnnotationTypes());
SimpleMetadataReaderFactory factory = new SimpleMetadataReaderFactory();
MetadataReader metadataReader = factory.getMetadataReader(SpringBootApplication.class.getName());
//ASM实现
AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
System.out.println(annotationMetadata.getAnnotationTypes());
}
}
什么是注解属性覆盖?
@Compoent
|- @Service
|- @TransactionalService
- 指我们在@Service上设置了value,那么@Compoent上的value会被覆盖.这叫“隐形覆盖”
- 当A @AliasFor B时,属性A会显性覆盖属性B的内容.
7. 注解驱动设计模式
7.1 @Enable模块驱动实现
- 基于注解驱动实现@Enable
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(HelloWorldConfiguration.class)
public @interface EnableHelloWorld {
}
@Configuration
public class HelloWorldConfiguration {
@Bean
public String helloWorld2(){
return "hello,World";
}
}
- 基于“接口编程”实现@Enable模块
方法1: ImportSelector接口实现
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ServerImportSelector.class)
public @interface EnableServer {
Server.Type type();
}
public class FtpServer implements Server{
@Override
public void start() {
System.out.println("Ftp 启动");
}
@Override
public void stop() {
System.out.println("Ftp 关闭");
}
}
public class HttpServer implements Server{
@Override
public void start() {
System.out.println("HTTP服务器启动中");
}
@Override
public void stop() {
System.out.println("HTTP服务器关闭");
}
}
public interface Server {
void start();
void stop();
enum Type{
HTTP,FTP;
}
}
public class ServerImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableServer.class.getName());
Server.Type type = (Server.Type)annotationAttributes.get("type");
String[] importClassNames = new String[0];
switch (type){
case HTTP:
importClassNames=new String[]{HttpServer.class.getName()};
break;
case FTP:
importClassNames=new String[]{FtpServer.class.getName()};
break;
}
return importClassNames;
}
}
方法2:基于ImportBeanDefinitionRegistrar
public class ServerImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableServer.class.getName());
Server.Type type = (Server.Type)annotationAttributes.get("type");
String[] importClassNames = new String[0];
switch (type){
case HTTP:
importClassNames=new String[]{HttpServer.class.getName()};
break;
case FTP:
importClassNames=new String[]{FtpServer.class.getName()};
break;
}
Stream.of(importClassNames)
.map(BeanDefinitionBuilder::genericBeanDefinition)
.map(BeanDefinitionBuilder::getBeanDefinition)
.forEach(beanDefinition-> BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition,registry));
}
}
7.2 @Enable模块驱动原理
前面讨论中@Enable模块无论是自建还是自定义,都是使用@Import实现.@Import的职责是装载导入类,将其定义为Spring Bean,当前导入类主要为@Configuration,ImportSelector实现及ImportBeanDefinitionRegistrar.
在以前Spring Mvc里我们一般是使用context:annotation-config/和context:component-scan/配合.
- context:annotation-config/的底层实现类AnnotationConfigBeanDefinitionParser
- context:component-scan/的底层实现类ComponentScanBeanDefinitionParser均调用了AnnotationConfigUtils#registerAnnotationConfigProcessors(org.springframework.beans.factory.support.BeanDefinitionRegistry, java.lang.Object)
@Configuration的处理实现类ConfigurationClassPostProcessor.他默认会被XML元素context:annotation-config/或context:component-scan/注册,如果是注解驱动中他也会被AnnotationConfigApplicationContext中的AnnotatedBeanDefinitionReader中的reader在构造时注册.
ConfigurationClassPostProcessor作为最高优先级的BeanFactoryPostProcessor实现,会在postProcessBeanFactory方法被调用时处理@Configration类和@Bean方法.
==7.3 @EnableAutoConfiguration的原理==
主要代码:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//读取候选装配组件,所有EnableAutoConfiguration类的集合
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
//用set去除重复
configurations = removeDuplicates(configurations);
//排除自动装配组件
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
//过滤自动装配组件
configurations = getConfigurationClassFilter().filter(configurations);
//自动装配事件
fireAutoConfigurationImportEvents(configurations, exclusions);
r
7.4 Spring Web自动装配
WebApplicationInitializer接口提供了以编程的方式替换传统的web.xml方式.
当然也可以采用继承AbstractAnnotationConfigDispatcherServletInitializer或AbstractDispatcherServletInitializer的方式.
示例: (1) 新增一个Controller
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "hello,World!!!";
}
}
(2) 新增Spring Mvc配置
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "thinking.in.spring.webmvc")
public class SpringWebMvcConfiguration {
}
(3) 自动装配实现
public class SpringWebMvcServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[0];
}
@Override
protected Class<?>[] getServletConfigClasses() {
return of(SpringWebMvcConfiguration.class);
}
@Override
protected String[] getServletMappings() {
return of("/*");
}
//便利API,减少new[]代码
private static <T> T[] of(T... values){
return values;
}
}
它支持自动装配,并不是spring的原生能力,而是Servlet3.0的技术,其中的两个特性“ServletContext”和“运行时插拔”是“Web自动化装配”的技术保障.
从全局的角度看,SpringServletContainerInitializer通过实现Servlet3.0的SPI接口ServletContainerInitializer,与@HandlesTypes配合过滤出WebApplicationInitializer具体实现类集合,随后顺序迭代该集合的元素,进而利用Servlet3.0配置API实现Web自动装配的目的.
7.5 spring 条件装配
在spring低版本,可以通过这样来实现条件装配
可以在启动的时候通过-Denv=prod来替换掉占位符.
在Spring Framework3.1之后,引入了XML属性和@Profile
代码示例:
@Service
@Profile("Java7")
public class IterationCalculatingService implements CalculatingService{
@Override
public Integer sum(Integer... values) {
System.out.println("Java7实现");
int sum=0;
for (Integer value:values){
sum+=value;
}
return sum;
}
}
@Service
@Profile("Java8")
public class LambdaCalculatingService implements CalculatingService{
@Override
public Integer sum(Integer... values) {
System.out.println("java8 进行实现");
Integer sum = Stream.of(values).reduce(0, Integer::sum);
return sum;
}
}
启动时命令:
java -jar spring-webmvc-sample/target/spring-webmvc-sample-0.0.1-SNAPSHOT-war-exec.jar -Dspring.profiles.active=Java7
原理:Spring Bean注册的判断逻辑里采用Environment#acceptsProfiles(),有一个抽象类ConditionEvaluator来实现对@Profile的处理