原理篇包含自动配置工作流程、自定义starter开发、springboot程序启动流程、掌握SpringBoot内部工作流程、理解SpringBoot整合第三方技术的原理
小简从 0 开始学 Java 知识之 Java-学习路线 中的《SpringBoot-原理篇》,不定期更新所学笔记,期待一年后的蜕变吧!
- 学习目标
课程单元 | 学习目标 |
---|---|
原理篇 | 掌握SpringBoot内部工作流程理解SpringBoot整合第三方技术的原理实现自定义开发整合第三方技术的组件 |
- 前置知识
课程单元 | 前置知识 | 要求 |
---|---|---|
原理篇 | Spring | 了解Spring加载bean的各种方式知道Spring容器底层工作原理,能够阅读简单的Spring底层源码 |
1.自动配置工作流程
①bean的加载方式
❶配置文件 <bean/>
标签
最初级的bean的加载方式其实可以直击spring管控bean的核心思想,就是提供类名,然后spring就可以管理了。所以第一种方式就是给出bean的类名,内部通过反射机制加载类名后创建对象,对象就是spring管控的bean。
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--xml方式声明自己开发的bean-->
<bean id="cat" class="Cat"/>
<bean class="Dog"/>
<!--xml方式声明第三方开发的bean-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"/>
<bean class="com.alibaba.druid.pool.DruidDataSource"/>
<bean class="com.alibaba.druid.pool.DruidDataSource"/>
</beans>
❷配置文件扫描 注解
由于❶中需要将spring管控的bean全部写在xml文件中,非常不友好。现在哪一个类要受到spring管控加载成bean,就在这个类的上面加一个注解,还可以顺带起一个bean的名字(id)。这里可以使用的注解有@Component
以及三个衍生注解@Service
、@Controller
、@Repository
@Component("tom")
public class Cat {}
由于我们无法在第三方提供的技术源代码中去添加上述4个注解,因此当你需要加载第三方开发的bean的时候可以使用下列方式定义注解式的bean。定义一个方法添加@Bean
,当前方法的返回值就可以交给spring管控,记得这个方法所在的类添加@Component
或者@Configuration
@Component
public class DbConfig {
@Bean
public DruidDataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
}
上面提供的仅仅是bean的声明,spring并没有感知到这些东西,像极了上课积极回答问题的你,手举的非常高,可惜老师都没有往你的方向看上一眼。可以通过下列xml配置设置spring去检查哪些包,发现定了对应注解,就将对应的类纳入spring管控范围,声明成bean。
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<!--指定扫描加载bean的位置-->
<context:component-scan base-package="com.jianjian.bean,com.jianjian.config"/>
</beans>
❸纯注解
定义一个配置类并使用@ComponentScan
替代❷中的包扫描这个动作。
@Configuration
@ComponentScan({"com.itheima.bean","com.itheima.config"})
public class SpringConfig {
@Bean
public DruidDataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
}
❹@Import注解
由于使用@ComponentScan
扫描的时候范围太大了,使用@Import注解它可以只加载你需要的bean即可。只需要在注解的参数中写上加载的类对应的.class,而且加载的bean可以不用@Component修饰
@Import({Dog.class,DbConfig.class})
public class SpringConfig {
}
❺编程形式
前面介绍的加载bean的方式都是在容器启动阶段完成bean的加载,下面这种方式就比较特殊了,可以在容器初始化完成后手动加载bean。通过这种方式可以实现编程式控制bean的加载。
代码语言:javascript复制public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
//上下文容器对象已经初始化完毕后,手工加载bean
ctx.register(Cat.class);
}
}
❻ImportSelector接口
是否可以在容器初始化过程中进行控制呢?答案是必须的。实现ImportSelector接口的类可以设置加载的bean的全路径类名,记得一点,只要能编程就能判定,能判定意味着可以控制程序的运行走向,进而控制一切。
代码语言:javascript复制public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
//各种条件的判定,判定完毕后,决定是否装载指定的bean
boolean flag = metadata.hasAnnotation("org.springframework.context.annotation.Configuration");
if(flag){
return new String[]{"com.itheima.bean.Dog"};
}
return new String[]{"com.itheima.bean.Cat"};
}
}
❼ImportBeanDefinitionRegistrar接口
❻中提供了给定类全路径类名控制bean加载的形式,其实bean的加载不是一个简简单单的对象,spring中定义了一个叫做BeanDefinition的东西,它才是控制bean初始化加载的核心。BeanDefinition接口中给出了若干种方法,可以控制bean的相关属性。说个最简单的,创建的对象是单例还是非单例,在BeanDefinition中定义了scope属性就可以控制这个。如果你感觉❻中没有给你开放出足够的对bean的控制操作,那么可以通过定义一个类实现ImportBeanDefinitionRegistrar接口的方式定义bean,并且还可以让你对bean的初始化进行更加细粒度的控制
代码语言:javascript复制public class MyRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
BeanDefinition beanDefinition =
BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl2.class).getBeanDefinition();
registry.registerBeanDefinition("bookService",beanDefinition);
}
}
❽BeanDefinitionRegistryPostProcessor接口
当某种类型的bean被接二连三的使用各种方式加载后,在你对所有加载方式的加载顺序没有完全理解清晰之前,你不知道最后谁说了算。BeanDefinitionRegistryPostProcessor,全称bean定义后处理器,它在所有bean注册都折腾完后,把最后一道关,它是最后一个运行的。
代码语言:javascript复制public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
BeanDefinition beanDefinition =
BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl4.class).getBeanDefinition();
registry.registerBeanDefinition("bookService",beanDefinition);
}
}
②bean的加载控制
企业级开发中不可能在spring容器中进行bean的饱和式加载的。合理的加载方式是用什么加载什么。所以在spring容器中,通过判断一个类的全路径名是否能够成功加载来控制bean的加载是一种常见操作。
代码语言:javascript复制public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
try {
Class<?> clazz = Class.forName("com.jianjian.bean.Mouse");
if(clazz != null) {
return new String[]{"com.jianjian.bean.Cat"};
}
} catch (ClassNotFoundException e) {
//e.printStackTrace();
return new String[0];
}
return null;
}
}
通过上述的分析,可以看到此类操作将成为企业级开发中的常见操作,于是springboot将把这些常用操作给我们做了一次封装。通过注解实现上面的操作。
@ConditionalOnClass
注解实现了当虚拟机中加载了指定类时才加载对应的bean。
@Bean
@ConditionalOnClass(name = "com.jianjian.bean.Wolf")
public Cat tom(){
return new Cat();
}
@ConditionalOnMissingClass
注解实现了虚拟机中没有加载指定的类才加载对应的bean。
@Bean
@ConditionalOnMissingClass("com.jianjian.bean.Dog")
public Cat tom(){
return new Cat();
}
这种条件还可以做并且的逻辑关系,写2个就是2个条件都成立,写多个就是多个条件都成立。
代码语言:javascript复制@Bean
@ConditionalOnClass(name = "com.jianjian.bean.Wolf")
@ConditionalOnMissingClass("com.jianjian.bean.Mouse")
public Cat tom(){
return new Cat();
}
@ConditionalOnWebApplication
判定当前容器环境是否是web环境。
@Bean
@ConditionalOnWebApplication
public Cat tom(){
return new Cat();
}
@ConditionalOnNotWebApplication
判定容器环境是否是非web环境。
@Bean
@ConditionalOnNotWebApplication
public Cat tom(){
return new Cat();
}
@ConditionalOnBean
判定是否加载了指定名称的bean
@Bean
@ConditionalOnBean(name="jerry")
public Cat tom(){
return new Cat();
}
总结
springboot定义了若干种控制bean加载的条件设置注解,由spring固定加载bean变成了可以根据情况选择性的加载bean
③bean的依赖属性
bean的加载及加载控制已经搞完了,下面研究一下bean内部的事情。bean在运行的时候,实现对应的业务逻辑时有可能需要开发者提供一些设置值,有就是属性了。如果使用构造方法将参数固定,灵活性不足,这个时候就可以使用bean的属性配置进行灵活的配置了。
步骤①:先通过yml配置文件,设置bean运行需要使用的配置信息。
代码语言:javascript复制cartoon:
cat:
name: "图多盖洛"
age: 5
mouse:
name: "泰菲"
age: 1
步骤②:然后定义一个封装属性的专用类,加载配置属性,读取对应前缀相关的属性值。
代码语言:javascript复制@ConfigurationProperties(prefix = "cartoon")
@Data
public class CartoonProperties {
private Cat cat;
private Mouse mouse;
}
步骤③:最后在使用的位置注入对应的配置即可。
代码语言:javascript复制@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse{
@Autowired
private CartoonProperties cartoonProperties;
}
建议在业务类上使用@EnableConfigurationProperties声明bean,这样在不使用这个类的时候,也不会无故加载专用的属性配置类CartoonProperties,减少spring管控的资源数量。
④自动装配原理
啥叫自动配置呢?简单说就是springboot根据我们开发者的行为猜测你要做什么事情,然后把你要用的bean都给你准备好。springboot咋做到的呢?就是看你导入了什么类,就知道你想干什么了。然后把你有可能要用的bean都给你加载好,你直接使用就行了,springboot把所需要的一切工作都做完了。整体过程分为2个阶段:
阶段一:准备阶段
- springboot的开发人员先大量收集Spring开发者的编程习惯,整理开发过程每一个程序经常使用的技术列表,形成一个技术集A
- 收集技术集A的最常用的设置,把这些设置作为默认值直接设置好,得到开发过程中每一个技术的常用设置,形成每一个技术对应的设置集B
阶段二:加载阶段
- springboot初始化Spring容器基础环境,读取用户的配置信息,加载用户自定义的bean和导入的坐标,形成初始化环境
- springboot将技术集A包含的所有技术在SpringBoot启动时默认全部加载,这时肯定加载的东西有一些是无效的,没有用的
- springboot会对技术集A中每一个技术约定启动这个技术对应的条件,并设置成按条件加载,由于开发者导入了一些bean和坐标,也就是与初始化环境,这个时候就可以根据这个初始化环境与springboot的技术集A进行比对了,哪个匹配上加载哪个
- 因为有些技术不做配置就无法工作,所以springboot先加载设置集B中的默认值,这样可以减少开发者配置参数的工作量
- 但是默认配置不一定能解决问题,于是springboot开放修改设置集B的接口,可以由开发者根据需要决定是否覆盖默认配置
假定我们想自己实现自动配置的功能,都要做哪些工作呢?
- 首先指定一个技术X,我们打算让技术X具备自动配置的功能,这个技术X可以是任意功能,这个技术隶属于上面描述的技术集A
public class CartoonCatAndMouse{
}
- 然后找出技术X使用过程中的常用配置Y,这个配置隶属于上面表述的设置集B
cartoon:
cat:
name: "图多盖洛"
age: 5
mouse:
name: "泰菲"
age: 1
- 然后定义一个属性类封装对应的配置属性,这个过程其实就是上一节咱们做的bean的依赖属性管理,一模一样
@ConfigurationProperties(prefix = "cartoon")
@Data
public class CartoonProperties {
private Cat cat;
private Mouse mouse;
}
- 最后做一个配置类,当这个类加载的时候就可以初始化对应的功能bean,并且可以加载到对应的配置
@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse implements ApplicationContextAware {
private CartoonProperties cartoonProperties;
}
- 当然,你也可以为当前自动配置类设置上激活条件,例如使用@CondtionOn为其设置加载条件
@ConditionalOnClass(name="org.springframework.data.redis.core.RedisOperations")
@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse implements ApplicationContextAware {
private CartoonProperties cartoonProperties;
}
做到这里都已经做完了,但是遇到了一个全新的问题,如何让springboot启动的时候去加载这个类呢?如果不加载的话,我们做的条件判定,做的属性加载这些全部都失效了。springboot为我们开放了一个配置入口,在类路径(resources/)下创建META-INF
目录,并创建spring.factories
文件,在其中添加设置,说明哪些类要启动自动配置就可以了。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.jianjian.bean.CartoonCatAndMouse
其实这个文件就做了一件事,通过这种配置的方式加载了指定的类。那自动配置的核心究竟是什么呢?自动配置其实是一个小的生态,可以按照如下思想理解:
- 自动配置从根本上来说就是一个bean的加载
- 通过bean加载条件的控制给开发者一种感觉,自动配置是自适应的,可以根据情况自己判定,但实际上就是最普通的分支语句的应用,这是蒙蔽我们双眼的第一层面纱
- 使用bean的时候,如果不设置属性,就有默认值,如果不想用默认值,就可以自己设置,也就是可以修改部分或者全部参数,也是一种自适应的形式,其实还是需要使用分支语句来做判断的,这是蒙蔽我们双眼的第二层面纱
- springboot技术提前将大量开发者有可能使用的技术提前做好了,条件也写好了,用的时候你导入了一个坐标,对应技术就可以使用了,其实就是提前帮我们把spring.factories文件写好了,这是蒙蔽我们双眼的第三层面纱
你在不知道自动配置这个知识的情况下,经过上面这一二三,你当然觉得自动配置是一种特别牛的技术,但是一窥究竟后发现,也就那么回事。而且现在springboot程序启动时,在后台偷偷的做了这么多次检测,这么多种情况判定,不用问了,效率一定是非常低的,毕竟它要检测100余种技术是否在你程序中使用。
总结
- springboot启动时先加载spring.factories文件中的org.springframework.boot.autoconfigure.EnableAutoConfiguration配置项,将其中配置的所有的类都加载成bean
- 在加载bean的时候,bean对应的类定义上都设置有加载条件,因此有可能加载成功,也可能条件检测失败不加载bean
- 对于可以正常加载成bean的类,通常会通过@EnableConfigurationProperties注解初始化对应的配置属性类并加载对应的配置
- 配置属性类上通常会通过@ConfigurationProperties加载指定前缀的配置,当然这些配置通常都有默认值。如果没有默认值,就强制你必须配置后使用了
⑤变更自动配置
springboot的自动配置并不是必然运行的,可以通过配置的形式干预是否启用对应的自动配置功能。系统默认会加载100多种自动配置的技术,我们可以先手工干预此工程,禁用一些自动配置
方式一:通过yaml配置设置排除指定的自动配置类
代码语言:javascript复制spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
方式二:通过注解参数排除自动配置类
代码语言:javascript复制@EnableAutoConfiguration(excludeName = "",exclude = {})
方式三:排除坐标(应用面较窄)
例如web程序启动时会自动启动tomcat服务器,可以通过排除坐标的方式,让加载tomcat服务器的条件失效。不过需要提醒一点,你把tomcat排除掉,记得再加一种可以运行的服务器。
代码语言:javascript复制 org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-tomcat
org.springframework.boot
spring-boot-starter-jetty
2.自定义starter开发
自动配置学习完后,我们就可以基于自动配置的特性,开发springboot技术中最引以为傲的功能了,starter。其实通过前期学习,我们发现用什么技术直接导入对应的starter,然后就实现了springboot整合对应技术,再加上一些简单的配置,就可以直接使用了。这种设计方式对开发者非常友好,本章就通过一个案例的制作,开发自定义starter来实现自定义功能的快捷添加。
⓪案例:网站访问次数
本案例的功能是统计网站独立IP访问次数的功能,并将访问信息在后台持续输出。整体功能是在后台每10秒输出一次监控信息(格式:IP 访问次数) ,当用户访问网站时,对用户的访问行为进行统计。 例如:张三访问网站功能15次,IP地址:192.168.0.135,李四访问网站功能20次,IP地址:61.129.65.248。那么在网站后台就输出如下监控信息,此信息每10秒刷新一次。 IP访问监控 -----ip-address----- --num-- | 192.168.0.135 | 15 | | 61.129.65.248 | 20 | -------------------- ------- 本功能最终要实现的效果是在现有的项目中导入一个starter,对应的功能就添加上了,删除掉对应的starter,功能就消失了,要求功能要与原始项目完全解耦。因此需要开发一个独立的模块,制作对应功能。
在进行具体制作之前,先对功能做具体的分析
- 数据记录在什么位置
最终记录的数据是一个字符串(IP地址)对应一个数字(访问次数),此处可以选择的数据存储模型可以使用java提供的map模型,也就是key-value的键值对模型,或者具有key-value键值对模型的存储技术,例如redis技术。本案例使用map作为实现方案。
- 统计功能运行位置
因为每次web请求都需要进行统计,因此使用拦截器会是比较好的方案,本案例使用拦截器来实现。不过在制作初期,先使用调用的形式进行测试,等功能完成了,再改成拦截器的实现方案。
- 为了提升统计数据展示的灵活度,为统计功能添加配置项。
- 输出频度,默认10秒
- 数据特征:累计数据 / 阶段数据,默认累计数据
- 输出格式:详细模式 / 极简模式
在开发中我们分成若干个步骤实现。先完成最基本的统计功能的制作,然后开发出统计报表,接下来把所有的配置都设置好,最后将拦截器功能实现,整体功能就做完了。
最终实现代码:https://jwt1399.lanzouv.com/iG0LW0buql7g
项目文件结构
代码语言:javascript复制ipcount-spring-boot-starter
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── jianjian
│ ├── IpcountApplication.java
│ ├── autoconfigure
│ │ └── IpAutoConfiguration.java
│ ├── interceptor
│ │ ├── IpCountInterceptor.java
│ │ └── SpringMvcConfig.java
│ ├── properties
│ │ └── IpProperties.java
│ └── service
│ └── IpCountService.java
└── resources
├── META-INF
│ ├── spring-configuration-metadata.json
│ └── spring.factories
└── application.yml
①IP计数业务功能开发
步骤一:创建全新的模块,定义业务功能类
创建web模块ipcount-spring-boot-starter
,定义一个业务类,声明一个Map对象,用于记录ip访问次数,key是ip地址,value是访问次数
public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
}
有些小伙伴可能会有疑问,不设置成静态的,如何在每次请求时进行数据共享呢?记得,当前类加载成bean以后是一个单例对象,对象都是单例的,哪里存在多个对象共享变量的问题。
步骤二:制作统计功能
制作统计操作对应的方法,每次访问后对应ip的记录次数 1。需要分情况处理,如果当前没有对应ip的数据,新增一条数据,否则就修改对应key的值 1即可
代码语言:javascript复制public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
public void count(){
//每次调用当前操作,就记录当前访问的IP,然后累加访问次数
//1.获取当前操作的IP地址
String ip = null;
//2.根据IP地址从Map取值,并递增
Integer count = ipCountMap.get(ip);
if(count == null){
ipCountMap.put(ip,1);
}else{
ipCountMap.put(ip,count 1);
}
}
}
因为当前功能最终导入到其他项目中进行,而导入当前功能的项目是一个web项目,因此可以从容器中直接获取请求对象,获取IP地址的操作可以通过自动装配得到请求对象,然后获取对应的访问IP
代码语言:javascript复制public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
@Autowired
//当前的request对象的注入工作由使用当前web-starter的工程提供自动装配
private HttpServletRequest httpServletRequest;
public void count(){
//每次调用当前操作,就记录当前访问的IP,然后累加访问次数
//1.获取当前操作的IP地址
String ip = httpServletRequest.getRemoteAddr();
//2.根据IP地址从Map取值,并递增
Integer count = ipCountMap.get(ip);
if(count == null){
ipCountMap.put(ip,1);
}else{
ipCountMap.put(ip,count 1);
}
//打印出信息
System.out.println(ipCountMap);
}
}
//优化后代码
public class IpCountService {
private Map<String, Integer> ipCountMap = new HashMap<>();
@Autowired
//当前的request对象的注入工作由使用当前starter的工程提供自动装配
private HttpServletRequest httpServletRequest;
public void count(){
String ip = httpServletRequest.getRemoteAddr();
ipCountMap.put(ip,ipCountMap.getOrDefault(ip,0) 1);
System.out.println(ipCountMap);
}
}
步骤三:定义自动配置类
我们需要做到的效果是导入当前模块即开启此功能,因此使用自动配置实现功能的自动装载,需要开发自动配置类在启动项目时加载当前功能。
代码语言:javascript复制@Configuration
public class IpAutoConfiguration {
@Bean
public IpCountService ipCountService(){
return new IpCountService();
}
}
自动配置类需要在spring.factories文件中做配置方可自动运行。
代码语言:javascript复制# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.jianjian.autoconfigure.IpAutoConfiguration
步骤四:在其它项目中模拟调用,测试功能
在其它项目调用本项目,导入当前开发的starter,切记使用之前先clean后install安装到maven仓库,确保资源更新
代码语言:javascript复制<dependency>
<groupId>com.jianjian</groupId>
<artifactId>ipcount-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
推荐选择调用方便的功能做测试,推荐使用分页操作,当然也可以换其他功能位置进行测试。
代码语言:javascript复制@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private IpCountService ipCountService; //引入对象
@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage,@PathVariable int pageSize,Book book){
ipCountService.count(); //测试功能
IPage<Book> page = bookService.getPage(currentPage, pageSize,book);
if( currentPage > page.getPages()){
page = bookService.getPage((int)page.getPages(), pageSize,book);
}
return new R(true, page);
}
}
当前效果:每次调用分页操作后,可以在控制台输出当前访问的IP地址
温馨提示
由于当前制作的功能需要在对应的调用位置进行坐标导入,因此必须保障仓库中具有当前开发的功能,所以每次原始代码修改后,需要重新编译并安装到仓库中。为防止问题出现,建议每次安装之前先clean
然后install
,保障资源进行了更新。切记切记!!
②定时任务报表开发
当前已经实现了在业务功能类中记录访问数据,但是还没有定时输出监控的信息到控制台。由于监控信息需要每10秒输出1次,因此需要使用定时器功能。此处选用Spring内置的task作为实现方案。
步骤一:开启定时任务功能
定时任务功能开启需要在当前功能的总配置中设置,结合现有业务设定,比较合理的位置是设置在自动配置类上。加载自动配置类即启用定时任务功能。
代码语言:javascript复制@EnableScheduling
public class IpAutoConfiguration {
@Bean
public IpCountService ipCountService(){
return new IpCountService();
}
}
步骤二:制作显示统计数据功能
定义显示统计功能的操作print(),并设置定时任务,当前设置每5秒运行一次统计数据。
代码语言:javascript复制public class IpCountService {
private Map ipCountMap = new HashMap();
@Scheduled(cron = "0/5 * * * * ?")
public void print(){
System.out.println(" IP访问监控");
System.out.println(" -----ip-address----- --num-- ");
for (Map.Entry entry : ipCountMap.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(String.format("|s |] |",key,value));
}
System.out.println(" -------------------- ------- ");
}
}
显示样式
代码语言:javascript复制 IP访问监控
-----ip-address----- --num--
| 127.0.0.1 | 1 |
-------------------- -------
当前效果:每次调用分页操作后,可以在控制台看到统计数据,到此基础功能已经开发完毕。
③使用属性设置功能参数
由于当前报表显示的信息格式固定,为提高报表信息显示的灵活性,需要通过yml文件设置参数,控制报表的显示格式。
步骤一:定义参数格式
设置3个属性,分别用来控制显示周期(cycle),阶段数据是否清空(cycleReset),数据显示格式(model)
代码语言:javascript复制tools:
ip:
cycle: 10
cycleReset: false
model: "detail"
步骤二:定义封装参数的属性类,读取配置参数
为防止项目组定义的参数种类过多,产生冲突,通常设置属性前缀会至少使用两级属性作为前缀进行区分。
代码语言:javascript复制@Data
@ConfigurationProperties(prefix = "tools.ip")
public class IpProperties {
/**
* 日志显示周期
*/
private Long cycle = 5L; //默认值
/**
* 是否周期内重置数据
*/
private Boolean cycleReset = false;
/**
* 日志输出模式 detail:详细模式 simple:极简模式
*/
private String model = LogModel.DETAIL.value;
public enum LogModel{
DETAIL("detail"),
SIMPLE("simple");
private String value;
LogModel(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}
日志输出模式是在若干个类别选项中选择某一项,对于此种分类性数据建议制作枚举定义分类数据,当然使用字符串也可以。
步骤三:加载属性类
代码语言:javascript复制@EnableScheduling
@EnableConfigurationProperties(IpProperties.class)
public class IpAutoConfiguration {
@Bean
public IpCountService ipCountService(){
return new IpCountService();
}
}
步骤四:应用配置属性
在应用配置属性的功能类中,使用自动装配加载对应的配置bean,然后使用配置信息做分支处理。
注意:清除数据的功能一定要在输出后运行,否则每次查阅的数据均为空白数据。
代码语言:javascript复制public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
@Autowired
private IpProperties ipProperties;
@Scheduled(cron = "0/5 * * * * ?")
public void print(){
if(ipProperties.getModel().equals(IpProperties.LogModel.DETAIL.getValue())){
System.out.println(" IP访问监控");
System.out.println(" -----ip-address----- --num-- ");
for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(String.format("|s |] |",key,value));
}
System.out.println(" -------------------- ------- ");
}else if(ipProperties.getModel().equals(IpProperties.LogModel.SIMPLE.getValue())){
System.out.println(" IP访问监控");
System.out.println(" -----ip-address----- ");
for (String key: ipCountMap.keySet()) {
System.out.println(String.format("|s |",key));
}
System.out.println(" -------------------- ");
}
//阶段内统计数据归零
if(ipProperties.getCycleReset()){
ipCountMap.clear();
}
}
}
当前效果:在web程序端可以通过控制yml文件中的配置参数对统计信息进行格式控制。但是数据显示周期还未进行控制。
④使用属性设置定时器参数
在使用属性配置中的显示周期数据时,遇到了一些问题。由于无法在@Scheduled注解上直接使用配置数据,改用曲线救国的方针,放弃使用@EnableConfigurationProperties注解对应的功能,改成最原始的bean定义格式。
步骤一:属性类定义bean并指定bean的访问名称
如果此处不设置bean的访问名称,spring会使用自己的命名生成器生成bean的长名称,无法实现属性的读取
代码语言:javascript复制@Component("ipProperties")
@ConfigurationProperties(prefix = "tools.ip")
public class IpProperties {
}
步骤二:@Scheduled注解使用#{}
读取bean属性值
此处读取bean名称为ipProperties的bean的cycle属性值
代码语言:javascript复制@Scheduled(cron = "0/#{ipProperties.cycle} * * * * ?")
public void print(){
}
步骤三:弃用@EnableConfigurationProperties注解对应的功能,改为导入bean的形式加载配置属性类
代码语言:javascript复制@EnableScheduling
//@EnableConfigurationProperties(IpProperties.class)
@Import(IpProperties.class)
public class IpAutoConfiguration {
@Bean
public IpCountService ipCountService(){
return new IpCountService();
}
}
当前效果:在web程序端可以通过控制yml文件中的配置参数对统计信息的显示周期进行控制
⑤使用拦截器开发
目前基础功能基本上已经完成了制作,下面进行拦截器的开发。使用拦截器后使用时不需要再引入对象调用方法了,导入 starter 即可实现功能。
步骤一:开发拦截器
使用自动装配加载统计功能的业务类,并在拦截器中调用对应功能
代码语言:javascript复制public class IpCountInterceptor implements HandlerInterceptor {
@Autowired
private IpCountService ipCountService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
ipCountService.count();
return true;
}
}
步骤二:配置拦截器
配置mvc拦截器,设置拦截对应的请求路径。此处拦截所有请求,用户可以根据使用需要设置要拦截的请求。甚至可以在此处加载IpCountProperties中的属性,通过配置设置拦截器拦截的请求。
代码语言:javascript复制@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(ipCountInterceptor()).addPathPatterns("/**");
}
@Bean
public IpCountInterceptor ipCountInterceptor(){
return new IpCountInterceptor();
}
}
步骤三:去除之前的方法
代码语言:javascript复制@RestController
@RequestMapping("/books")
public class BookController {
//@Autowired 去掉
//private IpCountService ipCountService; 去掉
@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage,@PathVariable int pageSize,Book book){
//ipCountService.count(); 去掉
IPage<Book> page = bookService.getPage(currentPage, pageSize,book);
if( currentPage > page.getPages()){
page = bookService.getPage((int)page.getPages(), pageSize,book);
}
return new R(true, page);
}
}
当前效果:在web程序端导入对应的starter后功能开启,去掉坐标后功能消失,实现自定义starter的效果。
⑥开启yml提示功能
我们在使用springboot的配置属性时,都可以看到提示,导入了对应的starter后,也会有对应的提示信息出现。但是现在我们的starter没有对应的提示功能,这种设定就非常的不友好,本节解决自定义starter功能如何开启配置提示的问题。springboot提供有专用的工具实现此功能,仅需导入下列坐标。
代码语言:javascript复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
程序编译后,在target/META-INF目录中会生成对应的json
提示文件,然后拷贝json
文件到自己开发的META-INF目录中,并对其进行自定义编辑。生成的信息来自于属性类的注释信息。打开生成的json
文件,可以看到如下信息。
{
"groups": [
{
"name": "tools.ip",
"type": "com.jianjian.properties.IpProperties",
"sourceType": "com.jianjian.properties.IpProperties"
}
],
"properties": [
{
"name": "tools.ip.cycle",
"type": "java.lang.Long",
"description": "日志显示周期",
"sourceType": "com.jianjian.properties.IpProperties",
"defaultValue": 5
},
{
"name": "tools.ip.cycle-reset",
"type": "java.lang.Boolean",
"description": "是否周期内重置数据",
"sourceType": "com.jianjian.properties.IpProperties",
"defaultValue": false
},
{
"name": "tools.ip.model",
"type": "java.lang.String",
"description": "日志输出模式 detail:详细模式 simple:极简模式",
"sourceType": "com.jianjian.properties.IpProperties"
}
],
"hints": [
{
"name": "tools.ip.model",
"values": [
{
"value": "detail",
"description": "详细模式."
},
{
"value": "simple",
"description": "极简模式."
}
]
}
]
}
groups属性
定义了当前配置的提示信息总体描述,当前配置属于哪一个属性封装类,properties属性
描述了当前配置中每一个属性的具体设置,包含名称、类型、描述、默认值等信息。hints属性
默认是空白的,没有进行设置。hints属性
可以参考springboot源码中的制作,设置当前属性封装类专用的提示信息,上面为日志输出模式属性model设置了两种可选提示信息。
总结
- 自定义starter其实就是做一个独立的功能模块,核心技术是利用自动配置的效果在加载模块后加载对应的功能
- 通常会为自定义starter的自动配置功能添加足够的条件控制,而不会做成100%加载对功能的效果
- 本例中使用map保存数据,如果换用redis方案,在starter开发模块中就要导入redis对应的starter
- 对于配置属性务必开启提示功能,否则使用者无法感知配置应该如何书写
到此当前案例全部完成,自定义stater的开发其实在第一轮开发中就已经完成了,就是创建独立模块导出独立功能,需要使用的位置导入对应的starter即可。如果是在企业中开发,记得不仅需要将开发完成的starter模块install到自己的本地仓库中,开发完毕后还要deploy到私服上,否则别人无法使用。
代码语言:javascript复制
3.SpringBoot程序启动
①启动过程分析
不管是springboot程序还是spring程序,启动过程本质上都是在做容器的初始化,并将对应的bean初始化出来放入容器。在spring环境中,每个bean的初始化都要开发者自己添加设置,但是切换成springboot程序后,自动配置功能的添加帮助开发者提前预设了很多bean的初始化过程,加上各种各样的参数设置,使得整体初始化过程显得更简单,但是核心本质还是在做一件事,初始化容器。
作为开发者只要搞清楚springboot提供了哪些参数设置的环节,同时初始化容器的过程中都做了哪些事情就行了。springboot初始化的参数根据参数的提供方,划分成如下3个大类,每个大类的参数又被封装了各种各样的对象,具体如下:
- 环境属性(Environment)
- 系统配置(spring.factories)
- 参数(Arguments、application.properties)
以下通过代码流向介绍了springboot程序启动时每一环节做的具体事情。
代码语言:javascript复制Springboot30StartupApplication【10】->SpringApplication.run(Springboot30StartupApplication.class, args);
SpringApplication【1332】->return run(new Class<?>[] { primarySource }, args);
SpringApplication【1343】->return new SpringApplication(primarySources).run(args);
SpringApplication【1343】->SpringApplication(primarySources)
# 加载各种配置信息,初始化各种配置对象
SpringApplication【266】->this(null, primarySources);
SpringApplication【280】->public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources)
SpringApplication【281】->this.resourceLoader = resourceLoader;
# 初始化资源加载器
SpringApplication【283】->this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
# 初始化配置类的类名信息(格式转换)
SpringApplication【284】->this.webApplicationType = WebApplicationType.deduceFromClasspath();
# 确认当前容器加载的类型
SpringApplication【285】->this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
# 获取系统配置引导信息
SpringApplication【286】->setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
# 获取ApplicationContextInitializer.class对应的实例
SpringApplication【287】->setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
# 初始化监听器,对初始化过程及运行过程进行干预
SpringApplication【288】->this.mainApplicationClass = deduceMainApplicationClass();
# 初始化了引导类类名信息,备用
SpringApplication【1343】->new SpringApplication(primarySources).run(args)
# 初始化容器,得到ApplicationContext对象
SpringApplication【323】->StopWatch stopWatch = new StopWatch();
# 设置计时器
SpringApplication【324】->stopWatch.start();
# 计时开始
SpringApplication【325】->DefaultBootstrapContext bootstrapContext = createBootstrapContext();
# 系统引导信息对应的上下文对象
SpringApplication【327】->configureHeadlessProperty();
# 模拟输入输出信号,避免出现因缺少外设导致的信号传输失败,进而引发错误(模拟显示器,键盘,鼠标...)
java.awt.headless=true
SpringApplication【328】->SpringApplicationRunListeners listeners = getRunListeners(args);
# 获取当前注册的所有监听器
SpringApplication【329】->listeners.starting(bootstrapContext, this.mainApplicationClass);
# 监听器执行了对应的操作步骤
SpringApplication【331】->ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
# 获取参数
SpringApplication【333】->ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
# 将前期读取的数据加载成了一个环境对象,用来描述信息
SpringApplication【333】->configureIgnoreBeanInfo(environment);
# 做了一个配置,备用
SpringApplication【334】->Banner printedBanner = printBanner(environment);
# 初始化logo
SpringApplication【335】->context = createApplicationContext();
# 创建容器对象,根据前期配置的容器类型进行判定并创建
SpringApplication【363】->context.setApplicationStartup(this.applicationStartup);
# 设置启动模式
SpringApplication【337】->prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
# 对容器进行设置,参数来源于前期的设定
SpringApplication【338】->refreshContext(context);
# 刷新容器环境
SpringApplication【339】->afterRefresh(context, applicationArguments);
# 刷新完毕后做后处理
SpringApplication【340】->stopWatch.stop();
# 计时结束
SpringApplication【341】->if (this.logStartupInfo) {
# 判定是否记录启动时间的日志
SpringApplication【342】-> new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
# 创建日志对应的对象,输出日志信息,包含启动时间
SpringApplication【344】->listeners.started(context);
# 监听器执行了对应的操作步骤
SpringApplication【345】->callRunners(context, applicationArguments);
# 调用运行器
SpringApplication【353】->listeners.running(context);
# 监听器执行了对应的操作步骤
上述过程描述了springboot程序启动过程中做的所有的事情。
②干预启动过程
如果想干预springboot的启动过程,比如自定义一个数据库环境检测的程序,该如何将这个过程加入springboot的启动流程呢?遇到这样的问题,大部分技术是这样设计的,设计若干个标准接口,对应程序中的所有标准过程。当你想干预某个过程时,实现接口就行了。例如spring技术中bean的生命周期管理就是采用标准接口进行的。
代码语言:javascript复制public class Abc implements InitializingBean, DisposableBean {
public void destroy() throws Exception {
//销毁操作
}
public void afterPropertiesSet() throws Exception {
//初始化操作
}
}
springboot启动过程由于存在着大量的过程阶段,如果设计接口就要设计十余个标准接口,这样对开发者不友好,同时整体过程管理分散,十余个过程各自为政,管理难度大,过程过于松散。那springboot如何解决这个问题呢?它采用了一种最原始的设计模式来解决这个问题,这就是监听器模式
,使用监听器来解决这个问题。
springboot将自身的启动过程比喻成一个大的事件,该事件是由若干个小的事件组成的。例如:
- org.springframework.boot.context.event.ApplicationStartingEvent
- 应用启动事件,在应用运行但未进行任何处理时,将发送 ApplicationStartingEvent
- org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
- 环境准备事件,当Environment被使用,且上下文创建之前,将发送 ApplicationEnvironmentPreparedEvent
- org.springframework.boot.context.event.ApplicationContextInitializedEvent
- 上下文初始化事件
- org.springframework.boot.context.event.ApplicationPreparedEvent
- 应用准备事件,在开始刷新之前,bean定义被加载之后发送 ApplicationPreparedEvent
- org.springframework.context.event.ContextRefreshedEvent
- 上下文刷新事件
- org.springframework.boot.context.event.ApplicationStartedEvent
- 应用启动完成事件,在上下文刷新之后且所有的应用和命令行运行器被调用之前发送 ApplicationStartedEvent
- org.springframework.boot.context.event.ApplicationReadyEvent
- 应用准备就绪事件,在应用程序和命令行运行器被调用之后,将发出 ApplicationReadyEvent,用于通知应用已经准备处理请求
- org.springframework.context.event.ContextClosedEvent(上下文关闭事件,对应容器关闭)
上述列出的仅仅是部分事件,当应用启动后走到某一个过程点时,监听器监听到某个事件触发,就会执行对应的事件。除了系统内置的事件处理,用户就可以根据需要自定义开发当前事件触发时要做的动作。
代码语言:javascript复制//设定监听器,在应用启动开始事件时进行功能追加
public class MyListener implements ApplicationListener {
public void onApplicationEvent(ApplicationStartingEvent event) {
//自定义事件处理逻辑
}
}
按照上述方案处理,用户就可以干预springboot启动过程的所有工作节点,设置自己的业务系统中独有的功能点。
总结
- springboot启动流程是先初始化容器需要的各种配置,并加载成各种对象,初始化容器时读取这些对象,创建容器
- 整体流程采用事件监听的机制进行过程控制,开发者可以根据需要自行扩展,添加对应的监听器绑定具体事件,就可以在事件触发位置执行开发者的业务代码
4.Web开发
1.SpringMVC自动配置概览
- 内容协商视图解析器和BeanName视图解析器
- 静态资源(包括webjars)
- 自动注册
Converter,GenericConverter,Formatter
- 支持
HttpMessageConverters
- 自动注册
MessageCodesResolver
(国际化) - 静态index.html 页支持
- 自定义
Favicon
- 自动使用
ConfigurableWebBindingInitializer
(DataBinder负责将请求数据绑定到JavaBean上)
2.静态资源访问
a.静态资源目录
默认静态资源路径: /static
or /public
or /resources
or /META-INF/resources
访问 : 当前项目根路径/静态资源名
例如:127.0.0.1/jianjian.png
原理:当请求进来时,先去找 Controller 看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。
修改默认的静态资源路径,/static,/public,resources,/META-INF/resources 将失效
代码语言:javascript复制spring:
web:
resources:
static-locations: [classpath:/jianjian/]
静态资源访问前缀
代码语言:javascript复制spring:
mvc:
static-path-pattern: /res/**
访问 :当前项目根路径 static-path-pattern 静态资源名
例如:127.0.0.1/res/jianjian.png
webjar
可用jar方式添加css,js等资源文件,webjar 查找:https://www.webjars.org/,例如,添加jquery
代码语言:javascript复制<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
访问:localhost:8080/webjars/jquery/3.5.1/jquery.js
,后面地址要按照依赖里面的包路径。
b.欢迎页支持
- 静态资源路径下 index.html
- 可以配置静态资源路径
- 配置静态资源的访问前缀,会导致 index.html不能被默认访问
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致欢迎页功能失效
web:
resources:
static-locations: [classpath:/haha/]
- controller处理index
@RequestMapping("/")
public String home(){
return "index";
}
c.自定义Favicon
将 favicon.ico 放在静态资源目录下即可,但是配置静态资源访问前缀会导致 Favicon 功能失效
代码语言:javascript复制spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效
d.静态资源配置原理
。。。
3.请求参数处理
a.rest使用与原理
- @xxxMapping
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
- Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
- 以前:
/getUser
获取用户/deleteUser
删除用户/editUser
修改用户/saveUser
保存用户
- 现在:
/user
- GET-获取用户
- DELETE-删除用户
- PUT-修改用户
- POST-保存用户
- 以前:
- 核心Filter:HiddenHttpMethodFilter
用法
- 1.开启页面表单的Rest功能
spring:
mvc:
hiddenmethod:
filter:
enabled: true #开启页面表单的Rest功能
- 2.页面 form的属性method=post,隐藏域 _method=put、delete等(如果直接get或post,无需隐藏域)
<form action="/user" method="get">
<input value="REST-GET提交" type="submit" />
</form>
<form action="/user" method="post">
<input value="REST-POST提交" type="submit" />
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="DELETE"/>
<input value="REST-DELETE 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="PUT" />
<input value="REST-PUT提交"type="submit" />
<form>
- 3.编写请求映射
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
return "POST-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser(){
return "PUT-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-张三";
}
Rest原理(表单提交要使用REST的时候)
- 表单提交会带上
_method=PUT
- 请求过来被
HiddenHttpMethodFilter
拦截,检测请求是否正常,并且是POST- 获取到
_method
的值 - 兼容以下请求;PUT、DELETE、PATCH
- 原生request(post),包装模式 requesWrapper 重写了getMethod方法,返回的是传入的值(
_method
)。 - 过滤器链放行的时候用 wrapper。以后的方法调用getMethod是调用 requesWrapper
- 获取到