SpringBoot-原理篇

课程单元 前置知识 要求
原理篇 Spring 了解Spring加载bean的各种方式 知道Spring容器底层工作原理,能够阅读简单的Spring底层源码

1.自动配置工作流程

①bean的加载方式

❶配置文件+``标签

最初级的bean的加载方式其实可以直击spring管控bean的核心思想,就是提供类名,然后spring就可以管理了。所以第一种方式就是给出bean的类名,内部通过反射机制加载类名后创建对象,对象就是spring管控的bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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

1
2
@Component("tom")
public class Cat {}

由于我们无法在第三方提供的技术源代码中去添加上述4个注解,因此当你需要加载第三方开发的bean的时候可以使用下列方式定义注解式的bean。定义一个方法添加@Bean,当前方法的返回值就可以交给spring管控,记得这个方法所在的类添加@Component或者@Configuration

1
2
3
4
5
6
7
8
@Component
public class DbConfig {
@Bean
public DruidDataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
}

上面提供的仅仅是bean的声明,spring并没有感知到这些东西,像极了上课积极回答问题的你,手举的非常高,可惜老师都没有往你的方向看上一眼。可以通过下列xml配置设置spring去检查哪些包,发现定了对应注解,就将对应的类纳入spring管控范围,声明成bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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替代❷中的包扫描这个动作。

1
2
3
4
5
6
7
8
9
@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修饰

1
2
3
@Import({Dog.class,DbConfig.class})
public class SpringConfig {
}

❺编程形式

前面介绍的加载bean的方式都是在容器启动阶段完成bean的加载,下面这种方式就比较特殊了,可以在容器初始化完成后手动加载bean。通过这种方式可以实现编程式控制bean的加载。

1
2
3
4
5
6
7
public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
//上下文容器对象已经初始化完毕后,手工加载bean
ctx.register(Cat.class);
}
}

❻ImportSelector接口

是否可以在容器初始化过程中进行控制呢?答案是必须的。实现ImportSelector接口的类可以设置加载的bean的全路径类名,记得一点,只要能编程就能判定,能判定意味着可以控制程序的运行走向,进而控制一切。

1
2
3
4
5
6
7
8
9
10
11
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的初始化进行更加细粒度的控制

1
2
3
4
5
6
7
8
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注册都折腾完后,把最后一道关,它是最后一个运行的。

1
2
3
4
5
6
7
8
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的加载是一种常见操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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。
1
2
3
4
5
@Bean
@ConditionalOnClass(name = "com.jianjian.bean.Wolf")
public Cat tom(){
return new Cat();
}
  • @ConditionalOnMissingClass注解实现了虚拟机中没有加载指定的类才加载对应的bean。
1
2
3
4
5
@Bean
@ConditionalOnMissingClass("com.jianjian.bean.Dog")
public Cat tom(){
return new Cat();
}

这种条件还可以做并且的逻辑关系,写2个就是2个条件都成立,写多个就是多个条件都成立。

1
2
3
4
5
6
@Bean
@ConditionalOnClass(name = "com.jianjian.bean.Wolf")
@ConditionalOnMissingClass("com.jianjian.bean.Mouse")
public Cat tom(){
return new Cat();
}
  • @ConditionalOnWebApplication判定当前容器环境是否是web环境。
1
2
3
4
5
@Bean
@ConditionalOnWebApplication
public Cat tom(){
return new Cat();
}
  • @ConditionalOnNotWebApplication判定容器环境是否是非web环境。
1
2
3
4
5
@Bean
@ConditionalOnNotWebApplication
public Cat tom(){
return new Cat();
}
  • @ConditionalOnBean判定是否加载了指定名称的bean
1
2
3
4
5
@Bean
@ConditionalOnBean(name="jerry")
public Cat tom(){
return new Cat();
}

总结

springboot定义了若干种控制bean加载的条件设置注解,由spring固定加载bean变成了可以根据情况选择性的加载bean

③bean的依赖属性

bean的加载及加载控制已经搞完了,下面研究一下bean内部的事情。bean在运行的时候,实现对应的业务逻辑时有可能需要开发者提供一些设置值,有就是属性了。如果使用构造方法将参数固定,灵活性不足,这个时候就可以使用bean的属性配置进行灵活的配置了。

**步骤①:**先通过yml配置文件,设置bean运行需要使用的配置信息。

1
2
3
4
5
6
7
cartoon:
cat:
name: "图多盖洛"
age: 5
mouse:
name: "泰菲"
age: 1

**步骤②:**然后定义一个封装属性的专用类,加载配置属性,读取对应前缀相关的属性值。

1
2
3
4
5
6
@ConfigurationProperties(prefix = "cartoon")
@Data
public class CartoonProperties {
private Cat cat;
private Mouse mouse;
}

**步骤③:**最后在使用的位置注入对应的配置即可。

1
2
3
4
5
@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse{
@Autowired
private CartoonProperties cartoonProperties;
}

建议在业务类上使用@EnableConfigurationProperties声明bean,这样在不使用这个类的时候,也不会无故加载专用的属性配置类CartoonProperties,减少spring管控的资源数量。

④自动装配原理

啥叫自动配置呢?简单说就是springboot根据我们开发者的行为猜测你要做什么事情,然后把你要用的bean都给你准备好。springboot咋做到的呢?就是看你导入了什么类,就知道你想干什么了。然后把你有可能要用的bean都给你加载好,你直接使用就行了,springboot把所需要的一切工作都做完了。整体过程分为2个阶段:

阶段一:准备阶段

  1. springboot的开发人员先大量收集Spring开发者的编程习惯,整理开发过程每一个程序经常使用的技术列表,形成一个技术集A
  2. 收集技术集A的最常用的设置,把这些设置作为默认值直接设置好,得到开发过程中每一个技术的常用设置,形成每一个技术对应的设置集B

阶段二:加载阶段

  1. springboot初始化Spring容器基础环境,读取用户的配置信息,加载用户自定义的bean和导入的坐标,形成初始化环境
  2. springboot将技术集A包含的所有技术在SpringBoot启动时默认全部加载,这时肯定加载的东西有一些是无效的,没有用的
  3. springboot会对技术集A中每一个技术约定启动这个技术对应的条件,并设置成按条件加载,由于开发者导入了一些bean和坐标,也就是与初始化环境,这个时候就可以根据这个初始化环境与springboot的技术集A进行比对了,哪个匹配上加载哪个
  4. 因为有些技术不做配置就无法工作,所以springboot先加载设置集B中的默认值,这样可以减少开发者配置参数的工作量
  5. 但是默认配置不一定能解决问题,于是springboot开放修改设置集B的接口,可以由开发者根据需要决定是否覆盖默认配置

假定我们想自己实现自动配置的功能,都要做哪些工作呢?

  • 首先指定一个技术X,我们打算让技术X具备自动配置的功能,这个技术X可以是任意功能,这个技术隶属于上面描述的技术集A
1
2
public class CartoonCatAndMouse{
}
  • 然后找出技术X使用过程中的常用配置Y,这个配置隶属于上面表述的设置集B
1
2
3
4
5
6
7
cartoon:
cat:
name: "图多盖洛"
age: 5
mouse:
name: "泰菲"
age: 1
  • 然后定义一个属性类封装对应的配置属性,这个过程其实就是上一节咱们做的bean的依赖属性管理,一模一样
1
2
3
4
5
6
@ConfigurationProperties(prefix = "cartoon")
@Data
public class CartoonProperties {
private Cat cat;
private Mouse mouse;
}
  • 最后做一个配置类,当这个类加载的时候就可以初始化对应的功能bean,并且可以加载到对应的配置
1
2
3
4
@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse implements ApplicationContextAware {
private CartoonProperties cartoonProperties;
}
  • 当然,你也可以为当前自动配置类设置上激活条件,例如使用@CondtionOn为其设置加载条件
1
2
3
4
5
@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文件,在其中添加设置,说明哪些类要启动自动配置就可以了。

1
2
3
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.jianjian.bean.CartoonCatAndMouse

其实这个文件就做了一件事,通过这种配置的方式加载了指定的类。那自动配置的核心究竟是什么呢?自动配置其实是一个小的生态,可以按照如下思想理解:

  1. 自动配置从根本上来说就是一个bean的加载
  2. 通过bean加载条件的控制给开发者一种感觉,自动配置是自适应的,可以根据情况自己判定,但实际上就是最普通的分支语句的应用,这是蒙蔽我们双眼的第一层面纱
  3. 使用bean的时候,如果不设置属性,就有默认值,如果不想用默认值,就可以自己设置,也就是可以修改部分或者全部参数,也是一种自适应的形式,其实还是需要使用分支语句来做判断的,这是蒙蔽我们双眼的第二层面纱
  4. springboot技术提前将大量开发者有可能使用的技术提前做好了,条件也写好了,用的时候你导入了一个坐标,对应技术就可以使用了,其实就是提前帮我们把spring.factories文件写好了,这是蒙蔽我们双眼的第三层面纱

你在不知道自动配置这个知识的情况下,经过上面这一二三,你当然觉得自动配置是一种特别牛的技术,但是一窥究竟后发现,也就那么回事。而且现在springboot程序启动时,在后台偷偷的做了这么多次检测,这么多种情况判定,不用问了,效率一定是非常低的,毕竟它要检测100余种技术是否在你程序中使用。

总结

  1. springboot启动时先加载spring.factories文件中的org.springframework.boot.autoconfigure.EnableAutoConfiguration配置项,将其中配置的所有的类都加载成bean
  2. 在加载bean的时候,bean对应的类定义上都设置有加载条件,因此有可能加载成功,也可能条件检测失败不加载bean
  3. 对于可以正常加载成bean的类,通常会通过@EnableConfigurationProperties注解初始化对应的配置属性类并加载对应的配置
  4. 配置属性类上通常会通过@ConfigurationProperties加载指定前缀的配置,当然这些配置通常都有默认值。如果没有默认值,就强制你必须配置后使用了

⑤变更自动配置

springboot的自动配置并不是必然运行的,可以通过配置的形式干预是否启用对应的自动配置功能。系统默认会加载100多种自动配置的技术,我们可以先手工干预此工程,禁用一些自动配置

方式一:通过yaml配置设置排除指定的自动配置类

1
2
3
4
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration

方式二:通过注解参数排除自动配置类

1
@EnableAutoConfiguration(excludeName = "",exclude = {})

方式三:排除坐标(应用面较窄)

例如web程序启动时会自动启动tomcat服务器,可以通过排除坐标的方式,让加载tomcat服务器的条件失效。不过需要提醒一点,你把tomcat排除掉,记得再加一种可以运行的服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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秒刷新一次。

1
2
3
4
5
   IP访问监控
+-----ip-address-----+--num--+
| 192.168.0.135 | 15 |
| 61.129.65.248 | 20 |
+--------------------+-------+

本功能最终要实现的效果是在现有的项目中导入一个starter,对应的功能就添加上了,删除掉对应的starter,功能就消失了,要求功能要与原始项目完全解耦。因此需要开发一个独立的模块,制作对应功能。

在进行具体制作之前,先对功能做具体的分析

  1. 数据记录在什么位置

最终记录的数据是一个字符串(IP地址)对应一个数字(访问次数),此处可以选择的数据存储模型可以使用java提供的map模型,也就是key-value的键值对模型,或者具有key-value键值对模型的存储技术,例如redis技术。本案例使用map作为实现方案。

  1. 统计功能运行位置

因为每次web请求都需要进行统计,因此使用拦截器会是比较好的方案,本案例使用拦截器来实现。不过在制作初期,先使用调用的形式进行测试,等功能完成了,再改成拦截器的实现方案。

  1. 为了提升统计数据展示的灵活度,为统计功能添加配置项。
  • 输出频度,默认10秒
  • 数据特征:累计数据 / 阶段数据,默认累计数据
  • 输出格式:详细模式 / 极简模式

在开发中我们分成若干个步骤实现。先完成最基本的统计功能的制作,然后开发出统计报表,接下来把所有的配置都设置好,最后将拦截器功能实现,整体功能就做完了。

最终实现代码:https://jwt1399.lanzouv.com/iG0LW0buql7g

项目文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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是访问次数

1
2
3
public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
}

有些小伙伴可能会有疑问,不设置成静态的,如何在每次请求时进行数据共享呢?记得,当前类加载成bean以后是一个单例对象,对象都是单例的,哪里存在多个对象共享变量的问题。

步骤二:制作统计功能

制作统计操作对应的方法,每次访问后对应ip的记录次数+1。需要分情况处理,如果当前没有对应ip的数据,新增一条数据,否则就修改对应key的值+1即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);
}
}

步骤三:定义自动配置类

我们需要做到的效果是导入当前模块即开启此功能,因此使用自动配置实现功能的自动装载,需要开发自动配置类在启动项目时加载当前功能。

1
2
3
4
5
6
7
@Configuration
public class IpAutoConfiguration {
@Bean
public IpCountService ipCountService(){
return new IpCountService();
}
}

自动配置类需要在spring.factories文件中做配置方可自动运行。

1
2
3
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.jianjian.autoconfigure.IpAutoConfiguration

步骤四:在其它项目中模拟调用,测试功能

在其它项目调用本项目,导入当前开发的starter,切记使用之前先clean后install安装到maven仓库,确保资源更新

1
2
3
4
5
<dependency>
<groupId>com.jianjian</groupId>
<artifactId>ipcount-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

推荐选择调用方便的功能做测试,推荐使用分页操作,当然也可以换其他功能位置进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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作为实现方案。

步骤一:开启定时任务功能

定时任务功能开启需要在当前功能的总配置中设置,结合现有业务设定,比较合理的位置是设置在自动配置类上。加载自动配置类即启用定时任务功能。

1
2
3
4
5
6
7
@EnableScheduling
public class IpAutoConfiguration {
@Bean
public IpCountService ipCountService(){
return new IpCountService();
}
}

步骤二:制作显示统计数据功能

定义显示统计功能的操作print(),并设置定时任务,当前设置每5秒运行一次统计数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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("|%18s |%5d |",key,value));
}
System.out.println("+--------------------+-------+");
}
}

显示样式

1
2
3
4
         IP访问监控
+-----ip-address-----+--num--+
| 127.0.0.1 | 1 |
+--------------------+-------+

当前效果:每次调用分页操作后,可以在控制台看到统计数据,到此基础功能已经开发完毕。

③使用属性设置功能参数

由于当前报表显示的信息格式固定,为提高报表信息显示的灵活性,需要通过yml文件设置参数,控制报表的显示格式。

步骤一:定义参数格式

设置3个属性,分别用来控制显示周期(cycle),阶段数据是否清空(cycleReset),数据显示格式(model)

1
2
3
4
5
tools:
ip:
cycle: 10
cycleReset: false
model: "detail"

步骤二:定义封装参数的属性类,读取配置参数

为防止项目组定义的参数种类过多,产生冲突,通常设置属性前缀会至少使用两级属性作为前缀进行区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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;
}
}
}

日志输出模式是在若干个类别选项中选择某一项,对于此种分类性数据建议制作枚举定义分类数据,当然使用字符串也可以。

步骤三:加载属性类

1
2
3
4
5
6
7
8
@EnableScheduling
@EnableConfigurationProperties(IpProperties.class)
public class IpAutoConfiguration {
@Bean
public IpCountService ipCountService(){
return new IpCountService();
}
}

步骤四:应用配置属性

在应用配置属性的功能类中,使用自动装配加载对应的配置bean,然后使用配置信息做分支处理。

注意:清除数据的功能一定要在输出后运行,否则每次查阅的数据均为空白数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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("|%18s |%5d |",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("|%18s |",key));
}
System.out.println("+--------------------+");
}
//阶段内统计数据归零
if(ipProperties.getCycleReset()){
ipCountMap.clear();
}
}
}

当前效果:在web程序端可以通过控制yml文件中的配置参数对统计信息进行格式控制。但是数据显示周期还未进行控制。

④使用属性设置定时器参数

在使用属性配置中的显示周期数据时,遇到了一些问题。由于无法在@Scheduled注解上直接使用配置数据,改用曲线救国的方针,放弃使用@EnableConfigurationProperties注解对应的功能,改成最原始的bean定义格式。

步骤一:属性类定义bean并指定bean的访问名称

如果此处不设置bean的访问名称,spring会使用自己的命名生成器生成bean的长名称,无法实现属性的读取

1
2
3
4
@Component("ipProperties")
@ConfigurationProperties(prefix = "tools.ip")
public class IpProperties {
}

步骤二:@Scheduled注解使用#{}读取bean属性值

此处读取bean名称为ipProperties的bean的cycle属性值

1
2
3
@Scheduled(cron = "0/#{ipProperties.cycle} * * * * ?")
public void print(){
}

步骤三:弃用@EnableConfigurationProperties注解对应的功能,改为导入bean的形式加载配置属性类

1
2
3
4
5
6
7
8
9
@EnableScheduling
//@EnableConfigurationProperties(IpProperties.class)
@Import(IpProperties.class)
public class IpAutoConfiguration {
@Bean
public IpCountService ipCountService(){
return new IpCountService();
}
}

当前效果:在web程序端可以通过控制yml文件中的配置参数对统计信息的显示周期进行控制

⑤使用拦截器开发

目前基础功能基本上已经完成了制作,下面进行拦截器的开发。使用拦截器后使用时不需要再引入对象调用方法了,导入 starter 即可实现功能。

步骤一:开发拦截器

使用自动装配加载统计功能的业务类,并在拦截器中调用对应功能

1
2
3
4
5
6
7
8
9
10
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中的属性,通过配置设置拦截器拦截的请求。

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(ipCountInterceptor()).addPathPatterns("/**");
}
@Bean
public IpCountInterceptor ipCountInterceptor(){
return new IpCountInterceptor();
}
}

步骤三:去除之前的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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提供有专用的工具实现此功能,仅需导入下列坐标。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

程序编译后,在target/META-INF目录中会生成对应的json提示文件,然后拷贝json文件到自己开发的META-INF目录中,并对其进行自定义编辑。生成的信息来自于属性类的注释信息。打开生成的json文件,可以看到如下信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
"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设置了两种可选提示信息。

总结

  1. 自定义starter其实就是做一个独立的功能模块,核心技术是利用自动配置的效果在加载模块后加载对应的功能
  2. 通常会为自定义starter的自动配置功能添加足够的条件控制,而不会做成100%加载对功能的效果
  3. 本例中使用map保存数据,如果换用redis方案,在starter开发模块中就要导入redis对应的starter
  4. 对于配置属性务必开启提示功能,否则使用者无法感知配置应该如何书写

到此当前案例全部完成,自定义stater的开发其实在第一轮开发中就已经完成了,就是创建独立模块导出独立功能,需要使用的位置导入对应的starter即可。如果是在企业中开发,记得不仅需要将开发完成的starter模块install到自己的本地仓库中,开发完毕后还要deploy到私服上,否则别人无法使用。

1

3.SpringBoot程序启动

①启动过程分析

不管是springboot程序还是spring程序,启动过程本质上都是在做容器的初始化,并将对应的bean初始化出来放入容器。在spring环境中,每个bean的初始化都要开发者自己添加设置,但是切换成springboot程序后,自动配置功能的添加帮助开发者提前预设了很多bean的初始化过程,加上各种各样的参数设置,使得整体初始化过程显得更简单,但是核心本质还是在做一件事,初始化容器。

作为开发者只要搞清楚springboot提供了哪些参数设置的环节,同时初始化容器的过程中都做了哪些事情就行了。springboot初始化的参数根据参数的提供方,划分成如下3个大类,每个大类的参数又被封装了各种各样的对象,具体如下:

  • 环境属性(Environment)
  • 系统配置(spring.factories)
  • 参数(Arguments、application.properties)

以下通过代码流向介绍了springboot程序启动时每一环节做的具体事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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的生命周期管理就是采用标准接口进行的。

1
2
3
4
5
6
7
8
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(上下文关闭事件,对应容器关闭)

上述列出的仅仅是部分事件,当应用启动后走到某一个过程点时,监听器监听到某个事件触发,就会执行对应的事件。除了系统内置的事件处理,用户就可以根据需要自定义开发当前事件触发时要做的动作。

1
2
3
4
5
6
//设定监听器,在应用启动开始事件时进行功能追加
public class MyListener implements ApplicationListener {
public void onApplicationEvent(ApplicationStartingEvent event) {
//自定义事件处理逻辑
}
}

按照上述方案处理,用户就可以干预springboot启动过程的所有工作节点,设置自己的业务系统中独有的功能点。

总结

  1. springboot启动流程是先初始化容器需要的各种配置,并加载成各种对象,初始化容器时读取这些对象,创建容器
  2. 整体流程采用事件监听的机制进行过程控制,开发者可以根据需要自行扩展,添加对应的监听器绑定具体事件,就可以在事件触发位置执行开发者的业务代码

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 将失效

1
2
3
4
spring:
web:
resources:
static-locations: [classpath:/jianjian/]

静态资源访问前缀

1
2
3
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

1
2
3
4
5
<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不能被默认访问
1
2
3
4
5
6
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致欢迎页功能失效
web:
resources:
static-locations: [classpath:/haha/]
  • controller处理index
1
2
3
4
@RequestMapping("/")
public String home(){
return "index";
}

c.自定义Favicon

将 favicon.ico 放在静态资源目录下即可,但是配置静态资源访问前缀会导致 Favicon 功能失效

1
2
3
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功能
1
2
3
4
5
spring:
mvc:
hiddenmethod:
filter:
enabled: true #开启页面表单的Rest功能
  • 2.页面 form的属性method=post,隐藏域 _method=put、delete等(如果直接get或post,无需隐藏域)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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.编写请求映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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 的值
    • 兼容以下请求;PUTDELETEPATCH
    • 原生request(post),包装模式 requesWrapper 重写了getMethod方法,返回的是传入的值( _method)。
    • 过滤器链放行的时候用 wrapper。以后的方法调用getMethod是调用 requesWrapper

b.请求映射原理