SpringBoot编程思想(上)

123 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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+ 容器

NameServlet Version
Tomcat8.53.1
Jetty9.43.1
Undertow1.43.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

  1. 添加依赖
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>

  1. 添加配置

仅有health 和info是默认暴露的Web Endpoints,如果需要其他的,可以在application.properties中增加配置

management.endpoints.web.exposure.include=beans

  1. 访问路径

http://localhost:8080/actuator

6. 注解驱动编程

  1. 元注解
  2. 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);
            });
        });
    }
}
  1. Spring注解属性别名和覆盖

    1. 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模块驱动实现

  1. 基于注解驱动实现@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";
    }
}
  1. 基于“接口编程”实现@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的处理