Spring的了解与特性
简单介绍:快速开发Spring项目的脚手架。简化Spring应用的初始搭建以及开发过程。
特性
提供了很多内置的Starter结合自动配置,对主流框架的无配置集成、开箱即用。即不需要自己去引入很多依赖。
并且管理了常用的第三方依赖的版本,减少了版本冲突的问题。
简化开发,采用JavaConfig的方式可以使用0xml方式进行开发。
内置Web容器无需依赖外部Web服务器,省略了Web.xml,直接运行就使Web应用。
提供了监控功能,可以监控应用程序的运行状态、内存、线程池、HTTP请求统计等。
Spring与SpringBoot的关系
两者都是Spring生态的产品
前者是容器框架,后者是一个快速开发Spring的脚手架(真正意义上不算新的框架)
核心注解
-
@SpringBootApplication
-
@SpringBootConfiguration(标记为配置类的注解)
-
@Configuration
-
-
@EnableAutoConfiguration:向Spring容器中导入了一个Selector,用来加载ClassPath下SpringFactories中所定义的配置类,将这些自动加载为配置Bean。
-
@Conditional(待补充)
-
自动配置原理
通过@SpringBootApplication中的@EnableAutoConfiguration
@EnableAutoConfiguration引入了@Import
在Spring容器启动时会加载IoC容器解析@Import注解
@Import导入了@DeferredImportSeletor注解,会使SpringBoot的自动配置类的顺序在最后,方便扩展的覆盖
读取META-INF/spring.factories文件
过滤出所有的AutoConfigurationClass类型的类
最后通过@Condition排除无效的自动配置类
为什么SpringBoot的jar可以直接运行
-
部署SpringBoot的插件才能将package后的jar包使用:java -jar packageName运行
-
也就是在pom.xml中要有<build>中的<plugins>中<plugin>的spring-boot-maven-plugin。如果没有这个插件在运行命令的时候会报没有主清单属性。
-
-
使用插件打包后生成的其实是一个Fat jar(也就是jar包中还有jar包),包含了应用依赖的jar包和Spring Boot Loader相关的类。
-
java - jar会去找jar中的MANIFEST.MF文件,在那里面找到真正的启动类(Main-Class)。
-
Fat jar的启动Main函数是JarLauncher,它负责创建一个LaunchedURLClassLoader加载boot-lib下面的jar,并以一个新的线程启动应用Main函数(其实就是找到MANIFEST中的Start-Class)。
-
当然,为了去验证此说法,就需要引入依赖从而去看JarLauncher中的源码,引入此依赖后再package就可以查看源码了。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-loader</artifactId> </dependency>
启动原理
-
运行主启动类的main方法,会初始化SpringApplication,从spring.factories读取listener ApplicationContextInitializer。此primarySources就是当前SpringBoot项目的class文件。
-
在初始化SpringApplication中对一些属性的初始化,比如开启日志记录、关闭懒加载等等。不同版本的Boot在初始化的时候不一样,主要是最下面的七行。以下讲解最下面五行的作用。就不根据源码展示了。
-
将启动类primarySources放入到名为primartSrouces的LinkedHashSet集合中
-
推算当前Web应用类型webApplicationType
-
读取ApplicationContextInitializer初始化器
-
读取ApplicationListener监听器
-
将Main方法所在的类放入mainApplicationClass中
-
-
接下来就是运行run()方法了。代码太多就不展示了。主要是做了以下的操作:
-
从上面读取的ApplicationListener监听器去读取SpringApplicationRunListener监听器运行器
-
发布ApplicationStartingEvent事件
-
封装命令行参数ApplicationArguments
-
读取环境配置信息
-
根据webApplicationType创建环境变量对象
-
配置环境变量
-
将现有环境信息设置为@ConfigurationProperties的数据源 并且放在第一位
-
发布ApplicationEnvironmentPreparedEvent事件
-
将spring.main的配置绑定到SpringApplication属性上
-
将现有的环境信息设置为@ConfigurationProperties的数据源 更新放在第一位
-
-
设置忽略bean.spring.beaninfo.ignore
-
打印Banner
-
实例化Spring上下文 AnnotationConfigServletWebServerApplicationContext
-
初始化失败分析器
-
准备上下文:将启动类作为配置类进行读取 -> 将配置注册为BeanDefinition
-
将当前环境信息设置到context
-
调用ApplicationContextInitializer
-
发布ApplicationContextInitializerdEvent事件
-
打印启动信息和profile.active(测试/生产/开发)信息
-
将applicationArguments、printedBanner注册为Bean
-
设置不允许同名Bean
-
设置是否懒加载Bean
-
读取启动类为BeanDefinition
-
将SpringBoot的监听器添加到context发布 ApplicationPreparedEvent事件
-
-
加载IoC容器(refresh()方法)其中的十二个步骤。具体干了什么可以去看我另外几篇源码解读的博文。主要两个方法
-
invokeBeanFactoryPostProcessor() -> 解析@Import 加载所有的自动配置类
-
onRefresh() 创建内置的Servlet容器
-
-
待扩展(刷新后的处理)
- 发布ApplicationStartedEvent事件
- 发布ApplicationReadyEvent事件
-
如果启动异常会发送ApplicationFailedEvent事件
-
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
this.configureHeadlessProperty();
// 从上面读取的ApplicationListener监听器去读取SpringApplicationRunListener监听器运行器
SpringApplicationRunListeners listeners = this.getRunListeners(args);
// 发布ApplicationStartingEvent事件
listeners.starting();
Collection exceptionReporters;
try {
// 封装命令行参数ApplicationArguments
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 读取环境配置信息
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
// 设置忽略bean.spring.beaninfo.ignore
this.configureIgnoreBeanInfo(environment);
// 打印Banner
Banner printedBanner = this.printBanner(environment);
// 实例化Spring上下文 AnnotationConfigServletWebServerApplicationContext
context = this.createApplicationContext();
// 初始化失败分析器
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
// 准备上下文:将启动类作为配置类进行读取 -> 将配置注册为BeanDefinition
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 加载IoC容器
this.refreshContext(context);
// 待扩展(刷新后的处理)
this.afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}
// 发布ApplicationStartedEvent事件
listeners.started(context);
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
this.handleRunFailure(context, var10, exceptionReporters, listeners);
throw new IllegalStateException(var10);
}
try {
// 发布ApplicationReadyEvent事件
listeners.running(context);
return context;
} catch (Throwable var9) {
this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var9);
}
}
内置Tomcat启动原理
-
当依赖spring-boot-starter-web依赖时会在SpringBoot中添加:ServletWebServerFactoryAutoConfiguration(servlet容器自动配置类)
-
该自动配置类通过@Import导入了可用的(通过@ConditionalOnClass判断决定使用哪一个)的Web容器工厂(默认Tomcat)
-
在内嵌的Tomcat类中配置了一个TomcatServletWebServerFactory的Bean(Web容器工厂)
-
它会在SpringBoot启动时加载IoC容器、在onRefrush()方法中创建内嵌的Tomcat并启动
外部Tomcat启动原理
-
SpringBoot启动后会加载SpringServletContainerInitializer类,此类会去加载实现了WebApplicationInitializer接口的类,而其中SpringBootServletInitializer这个类就实现了这个接口,而需要继承这个类去传入当前应用的启动类。
WebApplicationContext rootAppContext = this.createRootApplicationContext(servletContext);
-
想要使用外部的Tomcat,就需要排除内部的Tomcat。
<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>
-
创建一个配置类去继承SpringBootServletInitializer去重写configure()方法,将当前模块的启动类传入进去 。
public class TomcatConfig extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(StudySpringApplication.class);
}
}
如何自定义Starter
-
首先知道什么是Starter:Starter又名启动器。众所周知SpringBoot是简化了企业级Spring框架的开发,采用了约定优于配置的理念,而这个理念的实现就是用Starter组件。其会把对应功能的jar包导入进来使得开发者无需在意版本冲突问题,而只需在乎业务逻辑即可,这是作为引入依赖方面的简化配置。而此处要介绍的Starter就是代替Spring的内部组件,使用自定义的组件去完成启动。步骤如下
-
新建resources/META-INF/spring.factories。这是因为Spring Boot在启动的时候会去读取ClassPath下的这个文件信息。
-
在spring.factories中去用伪SPI去扫描我们自定义的Starter启动器组件
-
读取配置文件的原理以及加载顺序
-
通过事件监听的方式读取配置文件,而读取配置文件的类则是ConfigFileApplicationListener
读取配置文件的方式
-
属性上使用注解@Value("${xxx.yyy}"),需要注意三点:
-
当前类是交给IoC容器管理的Bean,否则@Value注解不生效。
-
当前属性不能是static或者final修饰。
-
如果配置文件中没有对应的xxx: yyy的话会报错,但是可以在@Value中给默认值比如@Value("${xxx.yyy:}"),这样默认为""
-
缺点是一个属性对应一个,如果需要读取多个属性的话,效率低下。
-
-
类上使用注解@ConfigurationProperties(prefix = "xxx")去指定配置文件中的前缀去读取多个,然后属性采用同名方式去进行批量匹配。相比于上面那种而言,如果遇到大量的配置信息,则效率会更高。
@ConfigurationProperties(prefix = "xxx")
public class MyYamlConfig {
private String yyy;
}
-
通过Spring的应用上下文ApplicationContext去获取Environment对象。但相较于直接获取Environment对象,这样显得多此一举,除非除了要使用ApplicationContext获取运行时环境还有其它的作用。毕竟ApplicationContext是Spring应用程序中的中央接口,由于继承了多个组件所以有多个核心的功能。
-
Spring Bean类中去注入IoC管理的Environment对象。可以使用@Resource、@Autowired来注入。然后在代码块中通过此对象的getProperty("xxx.yyy")返回String对象进行使用即可。但是如果不用自动装配的方式(也就是非Spring管理的Bean),就需要此配置类去实现EnvironmentAware接口重写setEnvironment()方法去获取Environment对象。
-
配置类上获取外部properties后缀配置文件的方式,比如需要读取/resources/gok.properties文件。在配置类上使用注解@PropertySources。然后就可以在属性上使用@Value注解去读取。
@Configuraion
@PropertySources({@propertySource(value = "classpath:gok.properties", encoding = "UTF-8")})
public class MyYamlConfig {
@Value("${xxx.yyy}")
private String name;
}
-
获取/resources/gok.yml文件。在被@Configuration注解的配置类去配置
@Configuration
public class MyYamlConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer yamlConfigurer() {
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
YamlPropertiesFactoryBean yaml = new yamlPropertiesFactoryBean();
yaml.setResources(new ClassPathResource("gok.yml"));
configurer.setProperties(Object.requireNonNull(yaml.getObject()));
return configurer;
}
}
-
采用Java原生态的方式(InputStream流)去读取文件
public class MyYamlConfig {
public void yamlConfigurer() {
Properties props = new Properties();
try {
InputStreamReader reader = new InputStreamReader(
this.getClass().getClassLoader().getResourceAsStream("gok.properties"),
StandardCharsets.UTF_8);
props.load(reader);
} catch (IOException e) {
}
String name = props.getProperty("xxx.yyy");
}
}
解决跨域的方式
-
首先了解什么是跨域:两个URL的协议/域名/IP/端口不同就会产生跨域。跨域是在前端才会发生的问题,因为浏览器有一个同源策略,这是防止不安全的不同域访问,所以会抛出异常(也就是跨域问题)。
-
Jsoup
-
前端使用Ajax的jsoup方式,同时后端每个接口的参数都接收一个String callback,并且将其放到JSONObject的第一个参数返回。仅支持GET请求,而且存在安全问题。
-
-
CORS
-
前端无需多做处理,后端有三种方式可以进行实现
-
在接口上使用@CrossOrign注解,并且可以带上请求的协议以及域名/IP以及端口,比如"http://localhost:8080"
-
-
-
让配置类(@Configuration)去实现WebMvcConfigurer接口重写addCorsMappings()方法。这个的弊端主要是浏览器会自动添加一些附加的头信息,甚至还会多发送一次附加的请求,第一次就是"options",第二次才是接口真正调用的请求方法。
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 添加映射路径 .allowedOriginPatterns("*") // 设置放行哪些原始域 SpringBoot2.4.4低版本使用.allowedOrigins .allowedMethods("GET","HEAD","POST","PUT","DELETE","OPTIONS") // 放行哪些请求方式 .allowCredentials(true) // 是否发送Cookie .maxAge(3600) .allowedHeaders("*"); // 暴露哪些原始请求头部信息 }
-
Nginx
-
使用反向代理不需要在前后端多做处理,不需要添加代码量。只需要在Nginx的nginx.conf文件的server中进行配置,这里就略微详细说明一下:listen 端口; location /前端路径/ { proxy_pass http://localhost:8080/后端路径 }。
-
可以同时处理多少请求?
-
在yml中的Tomcat中配置(因为这是Spring Boot默认的Web容器)。配置与请求线程相关的配置进行测试。
server: tomcat: # 最大连接数 max-connections: 30 # 最大等待数 accept-count: 10 threads: # 最少线程数 min-spare: 10 # 最多线程数 max: 50
-
可以使用ApiPost/JMeter进行并发测试。如下所示就是测试结果。我们不难发现:首先执行了30个线程,而后先执行完的10个线程又再一次进来执行了。这说明了1s可以处理40次请求,并且这40次请求中可以一次性处理30次,而最多可以等待10次。即配置中的accept-count(最大等待数) + max-connections(最大连接数)。
-
在spring-configuration-metadata.json中可以发现最大连接数默认为10000,最大等待数为100。注意:我打开的是SpringBoot2.2.1版本,每个版本可能不一样。
默认的日志框架以及如何切换
-
默认情况下SpringBoot会采用slf4j(日志接口) + logback(日志框架)完成日志的实现。那么我们从哪里可以得出来呢?看过源码的基本都知道使用LoggerFactory中获取Logger对象从而记录日志的,所以我也创建了此对象来记录日志。
-
而这个LoggerFactory与Logger都是slf4j的日志接口,其中slf4j内有三种日志框架:logback(默认)、log4j、jul-to-slf4j
-
那么如何做到切换默认的日志实现框架呢?这里有两种情况:
-
将logback切换为lof4j2:这样就需要将logback的场景启动器排除,因为slf4j这个日志接口只能运行1个桥接器。并且添加log4j2的场景启动器
<!-- 如果不是Web应用就在spring-boot-starter中排除依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <!-- 排除logback的场景启动器 --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- Log4j2场景启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>
-
将logback切换为log4j。就需要将logback的桥接器排除。
<!-- 如果不是Web应用就在spring-boot-starter中排除依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <!-- 排除logback的场景启动器 --> <exclusion> <groupId>logback-classic</groupId> <artifactId>ch.qos.logback</artifactId> </exclusion> </exclusions> </dependency> <!-- Log4j桥接器 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </dependency>
-
如何优化启动速度
-
延迟初始化Bean
-
一般在SpringBoot中拥有很多的耗时任务,比如数据库连接、初始化线程池创建等等。我们可以延迟这些操作的初始化,来达到优化启动速度的目的。可以在yml中配置:spring:main:lazy-initialization: true。来将所有的Bean延迟初始化(懒加载机制)。
-
-
创建扫描索引(Spring 5.0+)
-
通过编译时创建一个静态候选列表来提高大型应用程序的启动性能。在项目中使用@Indexed之后,编译打包后的时候会在项目中自动生成META-INF/spring.components文件。当Spring应用上下文执行ComponentScan扫描时,就会用CandidateComponentsIndexLoader读取这个文件并加载,转换为CandidateComponentsIndex对象。
-
步骤如下:引入依赖、启动类加上注解@Indexed。首次编译运行后会在/target/classes/META-INF中生成文件
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-indexer</artifactId> <optional>true</optional> </dependency>
-
-
减少@ComponentScan即@SpringBootApplication扫描类的范围
-
关闭SpringBoot的JMX监控,yml中设置:spring:jmx:enabled: false
-
设置JVM参数 -noverify,不对类进行验证
-
排除项目中多余的依赖jar