运行原理探究(2.6.2)
基于狂神的springboot运行原理探究,自己操作一下 2.6.2版本
org.springframework.boot
spring-boot-starter-parent
2.6.2
前置知识
java元注解
@Target 表示该注解用于什么地方。
@Retention 表示在什么级别保存该注解信息。
@Documented 将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。
@Inherited 允许子类继承父类中的注解。
@SpringBootApplication
主启动类,主要目的是开启自动配置
@Target({ElementType.TYPE})// 元注解,该注解可修饰类,接口(包括注解类型)或enum声明
@Retention(RetentionPolicy.RUNTIME)// 元注解,注解保留级别 VM将在运行期保留注释,可通过反射读取注解的信息
@Documented// 元注解 注解会被javadoc工具提取成文档
@Inherited// 元注解 允许子类继承父类中的注解
@SpringBootConfiguration// springboot配置注解
@EnableAutoConfiguration// 自动配置注解 核心
@ComponentScan(// 组件扫描注解
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
...
}
@SpringBootConfiguration
SpringBoot的配置类,继承了@Configuration,功能类似
@Target({ElementType.TYPE})// 元注解,该注解可修饰类,接口(包括注解类型)或enum声明
@Retention(RetentionPolicy.RUNTIME)// 元注解,注解保留级别 VM将在运行期保留注释,可通过反射读取注解的信息
@Documented// 元注解 注解会被javadoc工具提取成文档
@Configuration// 配置注解 将当前类标注为配置类,配合@bean使用
@Indexed// 索引注解,为Spring的模式注解添加索引,以提升应用启动性能。
public @interface SpringBootConfiguration {
...
}
@EnableAutoConfiguration
开启自动配置
@Target({ElementType.TYPE})// 元注解,该注解可修饰类,接口(包括注解类型)或enum声明
@Retention(RetentionPolicy.RUNTIME)// 元注解,注解保留级别 VM将在运行期保留注释,可通过反射读取注解的信息
@Documented// 元注解 注解会被javadoc工具提取成文档
@Inherited// 元注解 允许子类继承父类中的注解
@AutoConfigurationPackage// 自动配置包注解,启动类注解所在包的自包
@Import({AutoConfigurationImportSelector.class})// 导入自动配置导入选择器组件
public @interface EnableAutoConfiguration {
...
}
AutoConfigurationImportSelector
自动配置导入选择器
protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List 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;
}
调用SpringFactoriesLoader.loadFactoryNames()方法
public static List loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
调用SpringFactoriesLoader.loadSpringFactories()方法
private static Map> loadSpringFactories(ClassLoader classLoader) {
Map> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
源头
@AutoConfigurationPackage
@Target({ElementType.TYPE})// 元注解,该注解可修饰类,接口(包括注解类型)或enum声明
@Retention(RetentionPolicy.RUNTIME)// 元注解,注解保留级别 VM将在运行期保留注释,可通过反射读取注解的信息
@Documented// 元注解 注解会被javadoc工具提取成文档
@Inherited// 元注解 允许子类继承父类中的注解
@Import({Registrar.class})// 导入注册组件,将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器
public @interface AutoConfigurationPackage {
...
}
@ComponentScan
自动扫描并加载符合条件的组件或bean
总结
@SpringBootApplication 主配置类
@EnableAutoConfiguration springboot自动配置
@ComponentScan 自动扫描并加载符合条件的组件或bean
结论:
SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值
将这些值作为自动配置类导入容器 , 自动配置类生效 ,进行自动配置工作
整个J2EE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中
它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件
自动装配原理
//表明为配置类
@Configuration(
proxyBeanMethods = false
)
//判断 有DataSource.class, EmbeddedDatabaseType.class,实例化
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
//判断 无io.r2dbc.spi.ConnectionFactory,实例化
@ConditionalOnMissingBean(
type = {"io.r2dbc.spi.ConnectionFactory"}
)
//自动配置之前加载SqlInitializationAutoConfiguration.class
@AutoConfigureBefore({SqlInitializationAutoConfiguration.class})
//将配置文件中对应的值和DataSourceProperties绑定起来;并把DataSourceProperties加入到ioc容器中
@EnableConfigurationProperties({DataSourceProperties.class})
//导入DataSourcePoolMetadataProvidersConfiguration.class, InitializationSpecificCredentialsDataSourceInitializationConfiguration.class, SharedCredentialsDataSourceInitializationConfiguration.class
@Import({DataSourcePoolMetadataProvidersConfiguration.class, InitializationSpecificCredentialsDataSourceInitializationConfiguration.class, SharedCredentialsDataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
public DataSourceAutoConfiguration() {
}
...
}
总结
一但这个配置类生效;这个配置类就会给容器中添加各种组件;
这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;
配置文件能配置什么就可以参照某个功能对应的这个属性类
Springboot启动流程
SpringBoot启动加载大量的自动配置类(spring.factories)
SpringBoot默认写好的自动配置类
自动配置类中配置组件
给容器中自动配置类添加组件的时候,会从properties类中获取某些属性(用配置文件可修改)。
Yaml注入配置文件
用@ConfigurationProperties(prefix = "person")代替@Value
此注解需要导入配置文件处理器
org.springframework.boot
spring-boot-configuration-processor
true
配置javabean Person
@PropertySource(value = "classpath:person.yaml")//指定配置文件
@Component //注册bean
@ConfigurationProperties(prefix = "person")//与配置文件下person属性对应
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map maps;
private List lists;
private Dog dog;
//有参无参构造、get、set方法、toString()方法
}
yaml文件 person.yaml
person:
name: qinjiang${random.uuid} # 随机uuid 占位符
age: ${random.int} # 随机int 占位符
happy: false
birth: 2000/01/01
maps: {k1: v1,k2: v2}
lists:
- code
- girl
- music
dog:
name: 旺财
age: 1
数据校验
类注解@Validated
参数注解如下
空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
多配置文件切换
2.4版本之后spring.profiles.active弃用升级到spring.config.activate.on-profile
properties
yaml
配置文件优先级
优先级自上而下
项目路径下的config文件夹配置文件
项目路径下配置文件
资源路径下的config文件夹配置文件
资源路径下配置文件
properties
yaml
自定义Starter
数据源
Hikari(默认)
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource #Spring Boot 2.0 默认,无需配置
hikari:
maximum-pool-size: 15 #最大连接数
minimum-idle: 5 #最小空闲连接
connection-timeout: 60000 #连接超时时间(毫秒)
idle-timeout: 600000 #连接在连接池中空闲的最长时间
max-lifetime: 3000000 #连接最大存活时间
connection-test-query: select 1 #连接测试查询
auto-commit: true #自动提交行为
pool-name: HikariCPDataSource
Druid
传统配置
pom
com.alibaba
druid
1.2.6
application.yaml
spring:
datasource:
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#druid 数据源专有配置,Spring Boot无法解析,自己绑定
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计 log4j:日志记录 wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
DruidConfig.java
Druid配置文件:绑定druid专有配置,设置监控及其过滤器
@Configuration
public class DruidConfig {
//绑定配置文件
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() {
return new DruidDataSource();
}
//配置 Druid 监控管理后台的Servlet;
//内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式
@Bean
public ServletRegistrationBean statViewServlet() {
ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
// 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet
// 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
Map initParams = new HashMap<>();
initParams.put("loginUsername", "admin"); //后台管理界面的登录账号
initParams.put("loginPassword", "123456"); //后台管理界面的登录密码
//后台允许谁可以访问
//initParams.put("allow", "localhost"):表示只有本机可以访问
//initParams.put("allow", ""):为空或者为null时,表示允许所有访问
initParams.put("allow", "");
//deny:Druid 后台拒绝谁访问
//initParams.put("kuangshen", "192.168.1.20");表示禁止此ip访问
//设置初始化参数
bean.setInitParameters(initParams);
return bean;
}
//配置 Druid 监控 之 web 监控的 filter
//WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
@Bean
public FilterRegistrationBean webStatFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
//exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
Map initParams = new HashMap<>();
initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
bean.setInitParameters(initParams);
//"/*" 表示过滤所有请求
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
}
starter
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/test01
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
# Druid数据源配置
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1
#申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle: true
#配置从连接池获取连接时,是否检查连接有效性,true每次都检查;false不检查。做了这个配置会降低性能。
testOnBorrow: false
#配置向连接池归还连接时,是否检查连接有效性,true每次都检查;false不检查。做了这个配置会降低性能。
testOnReturn: false
#打开PsCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
#配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,log4j
#合并多个DruidDatasource的监控数据
useGlobalDataSourceStat: true
#通过connectProperties属性来打开mergesql功能罗慢sQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500;
#开启监控页,设置账号密码
stat-view-servlet:
enabled: true
login-password: 123456
login-username: admin
#设置web过滤,过滤一些不必要的网页
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: *.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*
整合mybatis
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.0
mybatis:
config-locations: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
type-aliases-package: com.nuc.pojo
Web开发
静态资源处理
默认配置(resources文件夹之下)
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
手动指定
spring:
resources:
static-locations: classpath:/coding/,classpath:/kuang/
首页处理
静态资源文件夹下的所有 index.html 页面;被 /** 映射
图标
Thymeleaf
无需配置
org.springframework.boot
spring-boot-starter-thymeleaf
语法
命名空间
类比jsp
国际化
resources文件夹新建i18n文件夹
其中新建properties文件
命名格式 xxx.properties, xxx_en_US.properties, xxx_zh_CN.properties
右击资源包,可以添加新的属性文件
双击资源包,进入国际化视图,可以快速添加新属性
配置路径
spring:
messages:
basename: i18n.login
Thymeleaf获取国际化值 #{...}
前端链接修改
中文
English
后端组件处理
public class MyLocaleResolver implements LocaleResolver {
//解析请求
@Override
public Locale resolveLocale(HttpServletRequest request) {
String language = request.getParameter("l");
Locale locale = Locale.getDefault(); // 如果没有获取到就使用系统默认的
//如果请求链接不为空
if (!StringUtils.isEmpty(language)){
//分割请求参数
String[] split = language.split("_");
//国家,地区
locale = new Locale(split[0],split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
}
}
web配置中注册bean
@Bean
public LocaleResolver localeResolver() {
return new MyLocaleResolver();
}
Swagger
swagger-ui
pom
io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
swaggerConfig
@Configuration //配置类
@EnableSwagger2// 开启Swagger2的自动配置
public class SwaggerConfig {
@Bean
public Docket docket(Environment environment) {
Profiles of = Profiles.of("dev", "test");
// 判断当前是否处于该环境
// 通过 enable() 接收此参数判断是否要显示
boolean swaggerEnable = environment.acceptsProfiles(of);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(swaggerEnable) // 配置是否启用Swagger,如果是false,在浏览器将无法访问
.select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
.apis(RequestHandlerSelectors.basePackage("com.nuc.controller.controller"))// RequestHandlerSelectors配置如何扫描接口
.paths(PathSelectors.ant("/nuc/**")) // 配置如何通过path过滤,即这里只扫描请求以/nuc开头的接口
.build();
}
private ApiInfo apiInfo() {
Contact contact = new Contact("石一歌", "http://xxx.xxx.com/shiyge", "联系人邮箱");
return new ApiInfoBuilder()
.title("XX服务后端API")
.description("XX服务专用API,其他系统请勿调用!")
.version("1.0")
.termsOfServiceUrl("http://www.springboot.vue.com")
.contact(contact)
.license("Apach 2.0 许可")
.licenseUrl("许可链接").build();
}
}
搭配使用的注解
@Api:作用于类上,标识此类是Swagger的资源。
@ApiOperation:主要作用于方法上,用于对一个接口进行说明。
@ApiParam:用于方法,参数,字段上,用于对参数进行说明。
@ApiModel:作用于类上,主要用于实体类当接口参数时使用,对实体类进行说明。
@ApiModelProperty:作用于方法和字段上,一般用于实体类的属性说明。
举例
实体类
@ApiModel(value = "管理员实体")
@Data
public class AdminDTO {
@ApiModelProperty(value = "用户ID", required = true, example = "1548w4dwf7as1a21cv4")
private String personId;
@ApiModelProperty(value = "用户名", example = "陈皮")
private String name;
@ApiModelProperty(value = "用户年龄", required = true, example = "18")
private Integer age;
}
接口类
@Api(tags = {"管理员相关"})
@RestController
@RequestMapping("admin")
public class AdminController {
@ApiOperation(value = "添加管理员", notes = "注意:只有管理员权限才能添加管理员", produces = "application/json",
consumes = "application/json")
@PostMapping("add")
public AdminDTO add(@ApiParam(value = "参数体", required = true) @RequestBody AdminDTO adminDTO) {
return adminDTO;
}
@ApiOperation(value = "管理员列表", notes = "注意:只有管理员权限才能获取管理员列表", produces = "application/json")
@GetMapping("list")
public List list(
@ApiParam(value = "页码,从1开始,1,2,3...", required = true,
example = "1") @RequestParam Integer pageIndex,
@ApiParam(value = "页量", required = true,
example = "10") @RequestParam Integer pageSize) {
return new ArrayList<>();
}
}
Knife4J(推荐)
目前已经发行的Knife4j版本,其本身已经引入了springfox,所以我们不需要再单独引入Springfox的具体版本,否则会导致版本冲突。注意,使用Knife4j2.0.6及以上的版本,SpringBoot的版本必须大于等于2.2.x
pom
com.github.xiaoymin
knife4j-spring-boot-starter
3.0.3
Layui-ui
pom
com.github.caspar-chen
swagger-ui-layer
1.1.3
mg-ui
pom
com.zyplayer
swagger-mg-ui
2.0.1
异步任务
@Async
该注解加在异步方法上,告诉springboot,该方法为异步方法
@EnableAsync
该注解加在主启动类上,开启异步注解功能
定时任务
@Scheduled
该注解加在定时方法上,告诉springboot,该方法为定时方法
配合cron表达式使用,插一句,云服务器上跑脚本也常常用cron表达式
@Scheduled(cron = "0 * * * * 0-7")
@EnableScheduling
该注解加在主启动类上,开启定时任务功能
邮件任务
pom
org.springframework.boot
spring-boot-starter-mail
yaml
# qq需要配置ssl
spring:
mail:
username: 24736743@qq.com
password: qq授权码
host: smtp.qq.com
properties:
mail:
smtp:
ssl:
enable: true
简单使用
@Autowired
JavaMailSenderImpl mailSender;
@Test
public void contextLoads() {
//简单邮件
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject("标题");
message.setText("正文");
message.setTo("收信地址");
message.setFrom("发信地址");
mailSender.send(message);
}
@Test
public void contextLoads2() throws MessagingException {
//复杂邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("标题");
helper.setText("正文(可使用富文本)",true);
helper.addAttachment("1.jpg",new File(""));//发送附件
helper.setTo("收信地址");
helper.setFrom("发信地址");
mailSender.send(mimeMessage);
}
富文本编辑器
Dubbo + Zookeeper
SpringSecurity
Shiro