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"> <bean id="cat" class="Cat"/> <bean class="Dog"/>
<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 "> <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); 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) { 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) { 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个阶段:
阶段一:准备阶段
- springboot的开发人员先大量收集Spring开发者的编程习惯,整理开发过程每一个程序经常使用的技术列表,形成一个技术集A
- 收集技术集A的最常用的设置,把这些设置作为默认值直接设置好,得到开发过程中每一个技术的常用设置,形成每一个技术对应的设置集B
阶段二:加载阶段
- springboot初始化Spring容器基础环境,读取用户的配置信息,加载用户自定义的bean和导入的坐标,形成初始化环境
- springboot将技术集A包含的所有技术在SpringBoot启动时默认全部加载,这时肯定加载的东西有一些是无效的,没有用的
- springboot会对技术集A中每一个技术约定启动这个技术对应的条件,并设置成按条件加载,由于开发者导入了一些bean和坐标,也就是与初始化环境,这个时候就可以根据这个初始化环境与springboot的技术集A进行比对了,哪个匹配上加载哪个
- 因为有些技术不做配置就无法工作,所以springboot先加载设置集B中的默认值,这样可以减少开发者配置参数的工作量
- 但是默认配置不一定能解决问题,于是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
| 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配置设置排除指定的自动配置类
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,功能就消失了,要求功能要与原始项目完全解耦。因此需要开发一个独立的模块,制作对应功能。
在进行具体制作之前,先对功能做具体的分析
- 数据记录在什么位置
最终记录的数据是一个字符串(IP地址)对应一个数字(访问次数),此处可以选择的数据存储模型可以使用java提供的map模型,也就是key-value的键值对模型,或者具有key-value键值对模型的存储技术,例如redis技术。本案例使用map作为实现方案。
- 统计功能运行位置
因为每次web请求都需要进行统计,因此使用拦截器会是比较好的方案,本案例使用拦截器来实现。不过在制作初期,先使用调用的形式进行测试,等功能完成了,再改成拦截器的实现方案。
- 为了提升统计数据展示的灵活度,为统计功能添加配置项。
- 输出频度,默认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(){ String ip = null; 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 private HttpServletRequest httpServletRequest; public void count(){ String ip = httpServletRequest.getRemoteAddr(); 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 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
| 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;
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
@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 { @GetMapping("{currentPage}/{pageSize}") public R getPage(@PathVariable int currentPage,@PathVariable int pageSize,Book book){ 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设置了两种可选提示信息。
总结
- 自定义starter其实就是做一个独立的功能模块,核心技术是利用自动配置的效果在加载模块后加载对应的功能
- 通常会为自定义starter的自动配置功能添加足够的条件控制,而不会做成100%加载对功能的效果
- 本例中使用map保存数据,如果换用redis方案,在starter开发模块中就要导入redis对应的starter
- 对于配置属性务必开启提示功能,否则使用者无法感知配置应该如何书写
到此当前案例全部完成,自定义stater的开发其实在第一轮开发中就已经完成了,就是创建独立模块导出独立功能,需要使用的位置导入对应的starter即可。如果是在企业中开发,记得不仅需要将开发完成的starter模块install到自己的本地仓库中,开发完毕后还要deploy到私服上,否则别人无法使用。
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启动过程的所有工作节点,设置自己的业务系统中独有的功能点。
总结
- 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 将失效
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:
web: resources: static-locations: [classpath:/haha/]
|
1 2 3 4
| @RequestMapping("/") public String home(){ return "index"; }
|
c.自定义Favicon
将 favicon.ico 放在静态资源目录下即可,但是配置静态资源访问前缀会导致 Favicon 功能失效
d.静态资源配置原理
。。。
3.请求参数处理
a.rest使用与原理
- @xxxMapping
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
- Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
- 以前:
/getUser
获取用户
/deleteUser
删除用户
/editUser
修改用户
/saveUser
保存用户
- 现在:
/user
- GET-获取用户
- DELETE-删除用户
- PUT-修改用户
- POST-保存用户
- 核心Filter:HiddenHttpMethodFilter
用法
1 2 3 4 5
| spring: mvc: hiddenmethod: filter: enabled: true
|
- 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>
|
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
的值
- 兼容以下请求;PUT、DELETE、PATCH
- 原生request(post),包装模式 requesWrapper 重写了getMethod方法,返回的是传入的值(
_method
)。
- 过滤器链放行的时候用 wrapper。以后的方法调用getMethod是调用 requesWrapper
b.请求映射原理