二次Spring
摘要
本篇博客参考线上教程对Spring、Springboot、Spring Cloud Alibaba的基本内容进行了总结,以便加深理解和记忆
Spring
1.概述
1)简介
Spring是分层的全栈full-stack轻量级开源框架,以IoC和AOP为内核,提供了展现层Spring MVC和业务层事务管理等众多企业级应用技术,还能整合众多第三方框架和类库。Spring Framework 5.x是其GA版本,对应着Jdk8
(IoC和AOP并非是Spring提出的,它们在Spring出现之前就已经存在,只不过偏向于理论化,Spring在技术层面将这两个思想进行了很好的实现)
2)优势
- 对象解耦,简化开发:通过Spring提供的IoC容器,可以将对象间的依赖关系交由Spring进⾏控制,避免硬编码所造成的过度程序耦合。⽤户也不必再为单例模式类、属性⽂件解析等这些很底层的需求编写代码,可以更专注于上层的应⽤】
- AOP:通过Spring的AOP功能,⽅便进⾏⾯向切⾯的编程,许多不容易⽤传统OOP实现的功能可以通过 AOP轻松应付
- 声明式事务:@Transactional 可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式⽅式灵活的进⾏事务的管理,提⾼开发效率和质量
- ⽅便程序测试:可以⽤⾮容器依赖的编程⽅式进⾏⼏乎所有的测试⼯作,测试不再是昂贵的操作,⽽是随⼿可做的事情
- ⽅便集成各种优秀框架
- 降低JavaEE API的使⽤难度:Spring对JavaEE API(如JDBC、JavaMail、远程调⽤等)进⾏了薄薄的封装层,使这些API的使⽤ 难度⼤为降低
- Spring源码是优秀的Java学习典范:Spring的源代码设计精妙、结构清晰、匠⼼独⽤,处处体现着⼤师对Java设计模式灵活运⽤以及对 Java技术的⾼深造诣。它的源代码⽆意是Java技术的最佳实践的范例
3)Spring架构:分层清晰;依赖关系、职责定位明确
- Spring核⼼容器(Core Container) :是Spring框架最核⼼的部分,它管理着Spring应⽤中 bean的创建、配置和管理。在该模块中,包括了Spring bean⼯⼚,它为Spring提供了DI的功能。 基于bean⼯⼚,我们还会发现有多种Spring应⽤上下⽂的实现。所有的Spring模块都构建于核⼼容器之上
- AOP:对⾯向切⾯编程提供了丰富的⽀持。这个模块是Spring应⽤系统中开发切⾯的基础,与DI⼀样,AOP可以帮助应⽤对象解耦
- 数据访问与集成(Data Access/Integration):Spring的JDBC和DAO模块封装了⼤量样板代码,这样可以使得数据库代码变得简洁,也可以更专注于我们的业务,还可以避免数据库资源释放失败⽽引起的问题。 另外,Spring AOP为数据访问 提供了事务管理服务,同时Spring还对ORM进⾏了集成,如Hibernate、MyBatis等。该模块由 JDBC、Transactions、ORM、OXM 和 JMS 等模块组成
- Web :该模块提供了SpringMVC框架给Web应⽤,还提供了多种构建和其它应⽤交互的远程调⽤⽅案。 SpringMVC框架在Web层提升了应⽤的松耦合⽔平。
- Test :通过该模块,Spring为使⽤Servlet、JNDI等编写单元测试提供了⼀系列的mock对象实现
2.IOC
1)思想
IOC(Inversion of Control):控制反转,是一种技术思想,指将对象创建、管理的权利(控制权)交给外部环境(IoC容器),解决了对象之间的耦合问题。
将编译阶段的强耦合问题改进为运行阶段的弱耦合问题
DI(Dependency Injection):依赖注入,有些教程认为DI是IOC的一种实现方式(通过依赖注入来实现控制反转),还有些教程认为DI和IOC是对同一种思想的另一种描述(IOC站在对象的角度、DI站在容器的角度)
2)朴素复现
- 静态
- 通过配置文件保存需要注入对象的全限定类名及其依赖关系
- 通过工厂模式读取配置文件,通过反射创建对象并在字典中保存,同时维护对象之间的依赖关系:根据配置文件中对象的依赖关系,通过set方法注入依赖关系(当一个对象A中定义/引用了对象B,其依赖关系应该是A依赖于B,因为A的创建/使用需要B),这样在某个对象使用引用时,则不需要显式的从工厂中根据名称获取对应对象
- 动态
- 通过配置信息/注解扫描对应包下的所有类文件,生成每个类的全限定类名。
- 在工厂根据刚才扫描的类名通过反射创建对象并在字典中保存,在反射的过程中通过特定的注解或直接读取该类所依赖的类名,从而获得对象间的依赖关系。
- 根据依赖关系和已经维护的对象字典通过set方法进行依赖注入
3)基础使用
①常用类与接口
-
applicationContext.xml
(beans.xml
前者是默认配置文件名)是Spring IOC的配置文件,它用于定义实例化对象的类的全限定类名以及各类之间依赖的描述。 -
BeanFactory
是Spring IOC的顶层接口,被称为基础容器,它只定义了一些基础的功能和规范。ApplicationContext
是其一个子接口(间接继承),它相比于BeanFactory
提供了更多的功能,是我们常用的接口。它具有三个实现类,通过反射技术实例化对象并维护对象之间的依赖关系。- 基于XML配置
ClassPathXmlApplicationContext
:从根的路径下加载配置文件(推荐使用)FileSystemXmlApplicationContext
:从磁盘路径上加载配置文件
- 基于注解配置
AnnotationConfigApplicationContext
:纯注解模式下启动Spring容器
- 基于XML配置
②启动Spring IOC
- JavaSE
// xml
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
// 注解
ApplicationContext applicationContext = new AnnotationConfigApplicationContext("SpringConfig.class");
- JavaEE:通过
ContextLoaderListener
加载xml或注解配置类
<!-- Web环境下启动IoC容器:Web.xml(Java EE的配置文件,在其中引入Spring相关的配置信息) -->
<!-- 1.从xml启动容器 -->
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!--配置Spring ioc容器的配置⽂件-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--使⽤监听器启动Spring的IOC容器-->
<listener>
<listenerclass>org.springframework.web.context.ContextLoaderListener</listenerclass>
</listener>
</web-app>
<!-- 2.从配置类启动容器 -->
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!--告诉ContextloaderListener知道我们使⽤注解的⽅式启动ioc容器-->
<context-param>
<param-name>contextClass</param-name>
<paramvalue>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<!--配置启动类的全限定类名-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.lagou.edu.SpringConfig</param-value>
</context-param>
<!--使⽤监听器启动Spring的IOC容器-->
<listener>
<listenerclass>org.springframework.web.context.ContextLoaderListener</listenerclass>
</listener>
</web-app>
③配置applicationContext.xml
:定义注入类及其依赖
-
纯XML方式
- XML文件头
<?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 https://www.springframework.org/schema/beans/spring-beans.xsd">
-
对象实例化
在基于xml的IoC配置中,bean标签是最基础的标签。它表示了IoC容器中的⼀个对象。
- 使用无参构造函数:默认情况下,IoC容器会通过反射调⽤⽆参构造函数来创建对象。如果类中没有⽆参构造函数,将创建失败
<!--配置service对象--> <bean id="userService" class="com.lagou.service.impl.TransferServiceImpl"></bean>
- 使用静态方法创建:例如,我们不会写
JDBC4Connection connection = new JDBC4Connection();
,因为我们要注册驱动,还要提供URL和凭证信息, ⽤DriverManager.getConnection
⽅法来获取连接(静态工厂)。
<!--使⽤静态⽅法创建对象的配置⽅式--> <bean id="transferService" class="com.lagou.factory.BeanFactory" factory-method="getTransferService"></bean>
- 使用实例化方法创建:在早期开发的项⽬中,⼯⼚类中的⽅法有可能是静态的,也有可能是⾮静态⽅法(动态工厂)
<!--使⽤实例⽅法创建对象的配置⽅式--> <bean id="beanFactory" class="com.lagou.factory.instancemethod.BeanFactory"></bean> <bean id="transferService" factory-bean="beanFactory" factorymethod="getTransferService"></bean>
-
实例化对象的属性标签
- id:⽤于给bean提供⼀个唯⼀标识。在⼀个标签内部,标识必须唯⼀
- class:⽤于指定创建Bean对象的全限定类名
- name:⽤于给bean提供⼀个或多个名称。多个名称⽤空格分隔
- factory-bean:⽤于指定创建当前bean对象的⼯⼚bean的唯⼀标识。当指定了此属性之后, class属性失效
- factory-method:⽤于指定创建当前bean对象的⼯⼚⽅法,如配合factory-bean属性使⽤, 则class属性失效。如配合class属性使⽤,则⽅法必须是static的
- scope:⽤于指定bean对象的作⽤范围
- singleton:单例模式(默认)
- prototype:多例模式
- 其他
- init-method:⽤于指定bean对象的初始化⽅法,此⽅法会在bean对象装配后调⽤。必须是 ⼀个⽆参⽅法
- destory-method:⽤于指定bean对象的销毁⽅法,此⽅法会在bean对象销毁前执⾏。它只 能为scope是singleton时起作⽤
-
依赖注入的配置(实例化对象时为其属性赋值)
- 按注入方式分类:
- 构造函数注入:
constructor-arg
标签- 利⽤带参构造函数实现对类成员的数据赋值(类中提供的构造函数参数个数必须和配置的参数个数⼀致,且数据类型匹 配。同时需要注意的是,当没有⽆参构造时,则必须提供构造函数参数的注⼊,否则Spring 框架会报错)
- 标签属性
- name:⽤于给构造函数中指定名称的参数赋值(标识参数名)
- index:⽤于给构造函数中指定索引位置的参数赋值(标识参数索引)
- value:⽤于指定基本类型或者String类型的数据
- ref:⽤于指定其他Bean类型的数据。写的是其他bean的唯⼀标识
- set方法注入:
property
标签- 通过类成员的set⽅法实现数据的注⼊(最多使用)
- 标签属性
- name:指定注⼊时调⽤的set⽅法名称
- index:指定注⼊的数据
- ref:指定注⼊的数据
- 构造函数注入:
- 按注入的数据类型分类
- 基本类型和String
- 其他bean类型:注⼊的数据类型是对象类型,称为其他Bean的原因是,这个对象是要求出现在IoC容器 中的。那么针对当前Bean来说,就是其他Bean了
- 集合类型:注⼊的数据类型是Aarry,List,Set,Map,Properties中的⼀种类型
- 在List结构的集合数据注⼊时, array , list , set 这三个标签通⽤
- 在Map结构的集合数据注⼊时, map 标签使⽤ entry ⼦标签实现数据注⼊, entry 标签可以使 ⽤key和value属性指定存⼊map中的数据。使⽤value-ref属性指定已经配置好的bean的引⽤, 同时 entry 标签中也可以使⽤ ref 标签
- property 标签中不能使 ⽤ ref 或者 bean 标签引⽤对象
- 按注入方式分类:
-
xml与注解相结合模式
实际企业开发中,纯xml模式使⽤已经很少了;而引⼊注解功能,不需要引⼊额外的jar;xml+注解结合模式,xml⽂件依然存在,所以,spring IOC容器的启动仍然从加载xml开始
建议:第三⽅jar中的bean定义在xml,⽐如druid数据库连接池‘;⾃⼰开发的bean定义使⽤注解
-
IoC容器的XML标签与注解的对应关系
xml标签 注解 bean标签 @Component(“accountDao”),注解加在类上
bean的id属性内容直接配置在注解后⾯如果不配置,默认定义个这个bean的id为类 的类名⾸字⺟⼩写
针对分层代码开发提供了@Componenet的三种别名@Controller、 @Service、@Repository分别⽤于控制层类、服务层类、dao层类的bean定义,这 四个注解的⽤法完全⼀样,只是为了更清晰的区分⽽已scope属性 @Scope(“prototype”),默认单例,注解加在类上 init-method属性 @PostConstruct,注解加在⽅法上,该⽅法就是初始化后调⽤的⽅法 destorymethod 属性 @PreDestory,注解加在⽅法上,该⽅法就是销毁前调⽤的⽅法 -
依赖注⼊的注解实现⽅式(填充对象的属性值)
注解 用法 @Autowired 推荐使⽤,默认按类型注入,若一个类型存在多个值,则需要配合@Qualifier使⽤。@Qualifier(name=“jdbcAccountDaoImpl”) @Resource 按照 ByName⾃动注⼊,@Resource(name=“studentDao”)
Ⅰ如果同时指定了 name 和 type,则从Spring上下⽂中找到唯⼀匹配的bean进⾏装配,找不 到则抛出异常
Ⅱ 如果指定了 name,则从上下⽂中查找名称(id)匹配的bean进⾏装配,找不到则抛出异 常
Ⅲ 如果指定了 type,则从上下⽂中找到类似匹配的唯⼀bean进⾏装配,找不到或是找到多个, 都会抛出异常
Ⅳ如果既没有指定name,⼜没有指定type,则⾃动按照byName⽅式进⾏装配
注意: @Resource 在 Jdk 11中已经移除,如果要使⽤,需要单独引⼊jar包
-
-
纯注解模式:将xml中遗留的内容全部以注解的形式迁移出去,最终删除xml,从Java配置类启动
注解 | |
---|---|
@Configuration | 表明当前类是⼀个配置类 |
@ComponentScan | 替代 context:component-scan |
@PropertySource | 引⼊外部属性配置⽂件 |
@Import | 引⼊其他配置类 |
@Value | 对变量赋值,可以直接赋值,也可以使⽤ ${} 读取资源配置⽂件中的信息 |
@Bean | 将⽅法返回对象加⼊ SpringIOC 容器 |
- 懒加载
ApplicationContext 容器的默认⾏为是在启动服务器时将所有 singleton bean 提前进⾏实例化。如果不想让⼀个singleton bean 在 ApplicationContext实现初始化时被提前实例化,那么可以将bean设置为延迟实例化。
应用场景:开启延迟加载⼀定程度提⾼容器启动和运转性能;对于不常使⽤的 Bean 设置延迟加载,这样偶尔使⽤的时候再加载,不必要从⼀开始该 Bean 就占 ⽤资源
<bean id="testBean" calss="seg.tef4.LazyBean" lazy-init="true" />
<beans default-lazy-init="true" />
- BeanFactory和FactoryBean
BeanFactory接⼝是容器的顶级接⼝,定义了容器的⼀些基础⾏为,负责⽣产和管理Bean的⼀个⼯⼚, 具体使⽤它下⾯的⼦接⼝类型,⽐如ApplicationContext
Spring中Bean有两种,⼀种是普通Bean,⼀种是⼯⼚Bean(FactoryBean),FactoryBean可以⽣成 某⼀个类型的Bean实例(返回给我们),也就是说我们可以借助于它⾃定义Bean的创建过程。
Bean创建的三种⽅式中的静态⽅法和实例化⽅法和FactoryBean作⽤类似,FactoryBean使⽤较多,尤 其在Spring框架⼀些组件中会使⽤,还有其他框架和Spring框架整合时使⽤
/**
*@description FactoryBean描述
**/
public interface FactoryBean<T> {
// 返回FactoryBean创建的Bean实例,如果isSingleton返回true,则该实例会放到Spring容器的单例对象缓存池中Map
@Nullable
T getObject() throws Exception;
// 返回FactoryBean创建的Bean类型
@Nullable
Class<?> getObjectType();
// 返回作⽤域是否单例
default boolean isSingleton() {
return true;
}
}
/** Company类:用于注入 **/
public class Company{}
/** Company泛型的FactoryBean 用于创建Company **/
public class CompanyFactoryBean implements FactoryBean<Company>{}
<!-- applicationContext.xml配置Bean -->
<bean id="companyBean" class="com.lagou.edu.factory.CompanyFactoryBean">
<!-- 注入属性的填充 -->
<property name="companyInfo" value="SEG,长春,500"/>
</bean>
// 获取FactoryBean产⽣的对象
Object companyBean = applicationContext.getBean("companyBean");
System.out.println("bean:" + companyBean);
// 获取FactoryBean,需要在id之前添加“&”
Object companyBean = applicationContext.getBean("&companyBean");
System.out.println("bean:" + companyBean);
- 后置处理器
- 接口
BeanFactoryPostProcessor
:BeanFactory级别的处理,针对整个Bean工厂进行处理 - 接口
BeanPostProcessor
:Bean级别的处理,可以针对某个具体的Bean
- 接口
4)源码剖析
- Spring IOC容器体系
- Bean⽣命周期关键时机点
Bean对象创建的⼏个关键时机点代码层级的调⽤都在AbstractApplicationContext 类的refresh⽅法中
关键点 | 触发代码 |
---|---|
构造器 | finishBeanFactoryInitialization(beanFactory)(beanFactory) |
BeanFactoryPostProcessor 初始化 | invokeBeanFactoryPostProcessors(beanFactory) |
BeanFactoryPostProcessor ⽅法调⽤ | invokeBeanFactoryPostProcessors(beanFactory) |
BeanPostProcessor 初始化 | registerBeanPostProcessors(beanFactory) |
BeanPostProcessor ⽅法调⽤ | finishBeanFactoryInitialization(beanFactory) |
- BeanFactory创建流程
- Bean创建流程
- Spring IoC循环依赖问题
循环依赖其实就是循环引⽤,也就是两个或者两个以上的 Bean 互相持有对⽅,最终形成闭环,Spring中循环依赖场景有: ①构造器的循环依赖(构造器注⼊) ②Field 属性的循环依赖(set注⼊)
无法解决报错的循环依赖情况:①单例 bean 构造器参数循环依赖②prototype 原型 bean循环依赖
单例bean通过set或@Autowired等注解的循环依赖解决方法:提前暴露:
ClassA调⽤setClassB⽅法,Spring⾸先尝试从容器中获取ClassB,此时ClassB不存在Spring 容器中 → Spring容器初始化ClassB,同时也会将ClassB提前暴露到Spring容器中 → ClassB调⽤setClassA⽅法,Spring从容器中获取ClassA ,因为第⼀步中已经提前暴露了 ClassA,因此可以获取到ClassA实例 ClassA通过spring容器获取到ClassB,完成了对象初始化操作。 这样ClassA和ClassB都完成了对象初始化操作,解决了循环依赖问题。
3.AOP
1)思想
⾯向切⾯编程AOP(Aspect oriented Programing),是OOP的延续。OOP是一种垂直继承体系,可以提供代码级别(属性、方法)的复用,但是无法提供横切逻辑代码的复用(在多个纵向流程中出现相同的子流程代码)。横切逻辑代码的使用场景有限,一般是事务控制、权限校验、日志等
横切逻辑代码的问题:横切代码重复、横切逻辑代码和业务代码混合,代码臃肿,维护不便。将横向和纵向代码拆分不难,问题是如何在不改变原有业务逻辑的情况下,将横切代码应用到原有的纵向业务逻辑中,做到无侵入,则是一个难题。
代理模式,可以解决这一点,静态代理硬式地在代理方法中使用纵向业务代码,而动态代理则可以根据纵向业务代码自动做增强。AOP此时出场,它就是基于了Java的动态代理机制,进行横向抽取。
为什么叫面向切面:①切:指的是横切逻辑,原有业务逻辑代码我们不能动,只能操作横切逻辑代码,所以⾯向横切逻辑 ②⾯:横切逻辑代码往往要影响的是很多个⽅法,每⼀个⽅法都如同⼀个点,多个点构成⾯,有⼀个⾯的概念在⾥⾯
2)朴素复现
编写横向切面代码、定义动态代理类,并实现动态代理方法(在其中调用横向切面代码和抽象的纵向业务代码)、定义纵向业务代码、在service层中实例化动态代理对象,传入纵向业务函数,执行。
3)基础使用
- 业务主线:指从浏览器发起请求,通过3层对象,访问数据库操作,最终响应浏览器的一个完整需求功能实现。下图红色和绿色标注的方法都是连接点,只有红色标注的方法是切入点,判断权限和事务相关方法是通知,包含判断权限和事务通知的类是切面,业务层和表现层都是目标对象,表现层和业务层代理对象是代理,在执行业务层方法时,执行增强代码的过程是织入,对业务层对象或表现层对象加入新的类成员或者方法是引介(引入)
- 相关术语
名称 | 解释 |
---|---|
Joinpoint(连接点) | 指的是那些可以⽤于把增强代码加⼊到业务主线中的点 |
Pointcut(切⼊点) | 指的是那些已经把增强代码加⼊到业务主线进来之后的连接点 |
Advice(通知/增强) | 指的是切⾯类中⽤于提供增强功能的⽅法,不同⽅法增强的时机是不⼀样的。分类有:前置通知 后置通知 异常通知 最终通知 环绕通 知 |
Target(⽬标对象) | 被代理对象 |
Proxy(代理) | 指的是⼀个类被AOP织⼊增强后,产⽣的代理类,即代理对象 |
Weaving(织⼊) | 指的是把增强应⽤到⽬标对象来创建新的代理对象的过程。spring采⽤动态代 理织⼊,⽽AspectJ采⽤编译期织⼊和类装载期织⼊ |
Aspect(切⾯) | 上述概念的⼀个综合,切⼊点(被增强的方法)+ ⽅位(被增强的位置,方法的前中后)+点增强(增强的方法) |
-
Spring中AOP的实现
①XML模式
- 引入依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.1.12.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency>
- AOP核⼼配置
<!-- applicationContext-aop.xml --> <!--Spring基于XML的AOP配置前期准备:在spring的配置⽂件中加⼊aop的约束 xmlns:aop="http://www.springframework.org/schema/aop" http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd--> <!-- Spring基于XML的AOP配置步骤: 第⼀步:把通知Bean交给Spring管理 第⼆步:使⽤aop:config开始aop的配置 第三步:使⽤aop:aspect配置切⾯ 第四步:使⽤对应的标签配置通知的类型 ⼊⻔案例采⽤前置通知,标签为aop:before--> <!--把通知(增强)bean交给spring来管理--> <bean id="logUtil" class="seg.tef4.utils.LogUtil"></bean> <!--开始aop的配置--> <aop:config> <!--配置切⾯--> <aop:aspect id="logAdvice" ref="logUtil"> <!--配置前置通知--> <aop:before method="printLog" pointcut="execution(public * seg.tef4.service.impl.TransferServiceImpl.addeUser(sef.tef4.pojo.User))" /> </aop:aspect> </aop:config>
上述配置信息可以直接写入
applicationContext.xml
中,也可以单独拿出来配置applicationContext-aop.xml
,再进行合并:<!-- applicationContext.xml --> <!-- 引入AOP配置文件 --> <import resource="classpath:applicationContext-aop.xml" />
/** 配置类引入 **/ @Configuration @ImportResource("classpath:applicationContext-aop.xml") public class AppConfig { // 其他配置类的定义 }
-
切入点表达式
切⼊点表达式,也称之为AspectJ切⼊点表达式,指的是遵循特定语法结构的字符串,其作⽤是⽤于对符合语法格式的连接点进⾏增强。它是AspectJ表达式的⼀部分。
AspectJ是⼀个基于Java语⾔的AOP框架,Spring框架从2.0版本之后集成了AspectJ框架中切⼊点表达式的部分,开始⽀持AspectJ切⼊点表达式。
// 使用示例 全限定⽅法名 访问修饰符(可以省略) 返回值(可以使⽤*,表示任意返回值) 包名.包名.包名.类名.⽅法名(参数列表) // 包名可以使⽤.表示任意包,但是有⼏级包,必须写⼏个 // 包名可以使⽤..表示当前包及其⼦包 // 类名和⽅法名,都可以使⽤.表示任意类,任意⽅法 // 参数列表,可以使⽤具体类型,基本类型直接写类型名称 : int ,引⽤类型必须写全限定类名:java.lang.String // 参数列表可以使⽤*,表示任意参数类型,但是必须有参数 // 参数列表可以使⽤..,表示有⽆参数均可。有参数可以是任意类型
-
改变代理⽅式
Spring 实现AOP思想使⽤的是动态代理技术。默认情况下,Spring会根据被代理对象是否实现接⼝来选择使⽤JDK还是CGLIB。当被代理对象没有实现任何接⼝时,Spring会选择CGLIB。当被代理对象实现了接⼝,Spring会选择JDK官⽅的代理技术,不过 我们可以通过配置的⽅式,让Spring强制使⽤CGLIB。
<!-- 1.使⽤aop:config标签配置--> <aop:config proxy-target-class="true"> <!-- 2.使⽤aop:aspectj-autoproxy标签配置 --> <!--此标签是基于XML和注解组合配置AOP时的必备标签,表示Spring开启注解配置AOP的⽀持--> <aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectjautoproxy>
-
五种通知类型
通知(增强) 解释 aop:before 前置通知在切⼊点⽅法(业务核⼼⽅法)执⾏之前执⾏,前置通知可以获取切⼊点⽅法的参数,并对其进⾏增强 aop:after-returning 正常执行的后置通知,pointcut:⽤于指定切⼊点表达式 pointcut-ref:⽤于指定切⼊点表达式的引⽤ aop:after-throwing 异常通知的执⾏时机是在切⼊点⽅法(业务核⼼⽅法)执⾏产⽣异常之后 aop:after 最终通知的执⾏时机是在切⼊点⽅法(业务核⼼⽅法)执⾏完成之后,切⼊点⽅法返回之前执⾏。 换句话说,⽆论切⼊点⽅法执⾏是否产⽣异常,它都会在返回之前执⾏ aop:around 环绕通知是Spring框架为我们提供的⼀种可以通过编码的⽅式,控制增强代码何时执⾏的通知类型。它⾥⾯借助的ProceedingJoinPoint接⼝及其实现类, 实现⼿动触发切⼊点⽅法的调⽤
② XML + 注解方式
<!-- applicationContext.xml --> <!--开启spring对注解aop的⽀持--> <aop:aspectj-autoproxy/>
/*** 模拟记录⽇志 * 第⼀步:编写⼀个⽅法 * 第⼆步:在⽅法使⽤@Pointcut注解 * 第三步:给注解的value属性提供切⼊点表达式 * 细节: * 1.在引⽤切⼊点表达式时,必须是⽅法名+(),例如"pointcut()"。 * 2.在当前切⾯中使⽤,可以直接写⽅法名。在其他切⾯中使⽤必须是全限定⽅法名。 */ @Component @Aspect public class LogUtil { @Pointcut("execution(* seg.tef4.service.impl.*.*(..))") public void pointcut(){} @Before("pointcut()") public void beforePrintLog(JoinPoint jp){ Object[] args = jp.getArgs(); System.out.println("前置通知:beforePrintLog,参数是:"+ Arrays.toString(args)); } @AfterReturning(value = "pointcut()",returning = "rtValue") public void afterReturningPrintLog(Object rtValue){ System.out.println("后置通知:afterReturningPrintLog,返回值是:"+rtValue); } @AfterThrowing(value = "pointcut()",throwing = "e") public void afterThrowingPrintLog(Throwable e){ System.out.println("异常通知:afterThrowingPrintLog,异常是:"+e); } @After("pointcut()") public void afterPrintLog(){ System.out.println("最终通知:afterPrintLog"); } @Around("pointcut()") public Object aroundPrintLog(ProceedingJoinPoint pjp){ //定义返回值 Object rtValue = null; try{ System.out.println("前置通知"); //1.获取参数 Object[] args = pjp.getArgs(); //2.执⾏切⼊点⽅法 rtValue = pjp.proceed(args); System.out.println("后置通知"); }catch (Throwable t){ System.out.println("异常通知"); t.printStackTrace(); }finally { System.out.println("最终通知"); } return rtValue; } }
③注解模式
在使⽤注解驱动开发aop时,其实就是注解替换掉配置⽂件中开启spring对aop支持的配置
<aop:aspectj-autoproxy/>
@Configuration @ComponentScan("com.lagou") @EnableAspectJAutoProxy //开启spring对注解AOP的⽀持 public class SpringConfiguration { }
-
AOP源码剖析
AopProxyFactory
创建AopProxy
→AopProxy
创建代理对象(AopProxyFactory
默认是DefaultAopProxyFactory
),在其CreateAopProxy
方法中,选择使用JDK Proxy还是Cglib代理
4.声明式事务
Spring对声明式事务的支持实际上是基于Spring的AOP和JDBC的原生事务的一种应用
1)基本概念
- 事务分类
编程式事务:在业务代码中添加事务控制代码,这样的事务控制机制就叫做编程式事务
声明式事务:通过xml或者注解配置的⽅式达到事务控制的⽬的,叫做声明式事务
- 事务特性
特性 | 解释 |
---|---|
原⼦性(Atomicity) | 事务中的操作要么都发⽣,要么都 不发⽣ |
⼀致性(Consistency) | 事务必须使数据库从⼀个⼀致性状态变换到另外⼀个⼀致性状态 |
隔离性(Isolation) | 多个⽤户并发访问数据库时,数据库为每⼀个⽤户开启的事务, 每个事务不能被其他事务的操作数据所⼲扰 |
持久性 | ⼀个事务⼀旦被提交,它对数据库中数据的改变就是永久性的 |
- 常见问题
问题 | 解释 |
---|---|
脏读 | ⼀个线程中的事务读到了另外⼀个线程中未提交的数据 |
不可重复读 | ⼀个线程中的事务读到了另外⼀个线程中已经提交的update的数据(前后内容不⼀样) |
虚读(幻读) | ⼀个线程中的事务读到了另外⼀个线程中已经提交的insert或者delete的数据(前后条数不⼀样) |
- 隔离级别
级别 | 解释 |
---|---|
Serializable(串⾏化) | 可避免脏读、不可重复读、虚读情况的发⽣ |
Repeatable read(可重复读) | 可避免脏读、不可重复读情况的发⽣ |
Read committed(读已提交) | 可避免脏读情况发⽣。不可重复读和幻读⼀定会发⽣ |
Read uncommitted(读未提交) | 以上情况均⽆法保证 |
MySQL的默认隔离级别是:REPEATABLE READ
查询当前使⽤的隔离级别: select @@tx_isolation
; 设置MySQL事务的隔离级别: set session transaction isolation level xxx;
(设置的是当前 mysql连接会话,不是永久改变)
- 事务的传播行为
事务往往在service层进⾏控制,如果出现service层⽅法A调⽤了另外⼀个service层⽅法B,A和B⽅法本身都已经被添加了事务控制,那么A调⽤B的时候,就需要进⾏事务的⼀些协商,这叫做事务的传播⾏为。
A调⽤B,我们站在B的⻆度来观察来定义事务的传播⾏为
传播行为 | 解释 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建⼀个事务,如果已经存在⼀个事务中, 加⼊到这个事务中(最常见) |
PROPAGATION_SUPPORTS | 如果当前没有事务,就以⾮事务⽅式执⾏ |
PROPAGATION_MANDATORY | 可避免脏读情况发⽣。不可重复读和幻读⼀定会发⽣ |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起 |
PROPAGATION_NOT_SUPPORTED | 以⾮事务⽅式执⾏操作,如果当前存在事务,就把当前事务挂起 |
PROPAGATION_NEVER | 以⾮事务⽅式执⾏,如果当前存在事务,则抛出异常 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执⾏。如果当前没有事务,则 执⾏与PROPAGATION_REQUIRED类似的操作 |
注:上述原本说A,B都增加了事务,可下面为什么会出现若当前没有事务的情况,这是说A方法被加上了事务控制,但是在调用B时,A的事务可能还没有执行或者已经提交了,因而会出现B执行时当前环境不存在事务的情况
- Spring中事务的API
public interface PlatformTransactionManager {
/**
* 获取事务状态信息
*/
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
/**
* 提交事务
*/
void commit(TransactionStatus status) throws TransactionException;
/**
* 回滚事务
*/
void rollback(TransactionStatus status) throws TransactionException;
}
此接⼝是Spring的事务管理器核⼼接⼝。Spring本身并不⽀持事务实现,只是负责提供标准,应⽤⽀持什么样的事务,需要提供具体实现类(此处也是策略模式的应⽤)
在Spring框架中,也为我们 内置了⼀些具体策略,例如:DataSourceTransactionManager
, HibernateTransactionManager
等等。( 和 HibernateTransactionManager
事务管理器在 spring-orm-5.1.12.RELEASE.jar
中)
Spring JdbcTemplate(数据库操作⼯具)、Mybatis(mybatis-spring.jar)、DataSourceTransactionManager
Hibernate框架、 HibernateTransactionManager
- Spring声明式事务配置
<!-- maven依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
①xml配置
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--定制事务细节,传播⾏为、隔离级别等-->
<tx:attributes>
<!--⼀般性配置-->
<tx:method name="*" read-only="false" propagation="REQUIRED" isolation="DEFAULT" timeout="-1"/>
<!--针对查询的覆盖性配置-->
<tx:method name="query*" read-only="true" propagation="SUPPORTS"/>
</tx:attributes>
</tx:advice>
<aop:config>
<!--advice-ref指向增强=横切逻辑+⽅位-->
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.lagou.edu.service.impl.TransferServiceImpl.*(..))"/>
</aop:config>
②xml + 注解方式
<!-- xml -->
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--开启spring对注解事务的⽀持-->
<tx:annotation-driven transaction-manager="transactionManager"/>
在接⼝、类或者⽅法上添加@Transactional注解@Transactional(readOnly = true,propagation = Propagation.SUPPORTS)
③纯注解方式
Spring基于注解驱动开发的事务控制配置,只需要把 xml 配置部分改为注解实现。只是需要⼀个注解替换掉xml配置⽂件中的<tx:annotation-driven transaction-manager="transactionManager"/>
配置。 在 Spring 的配置类上添加@EnableTransactionManagement
注解即可
//开启spring注解事务的⽀持
@EnableTransactionManagement
public class SpringConfiguration {
}
- 源码分析
- 通过
@import
引⼊了TransactionManagementConfigurationSelector
类 它的selectImports
⽅法导⼊了另外两个类:AutoProxyRegistrar
和ProxyTransactionManagementConfiguration
AutoProxyRegistrar
类分析:⽅法registerBeanDefinitions
引⼊了其他类,通过AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry
)引⼊InfrastructureAdvisorAutoProxyCreator
, 它继承了AbstractAutoProxyCreator
,是⼀个后置处理器类ProxyTransactionManagementConfiguration
是⼀个添加了@Configuration
注解的配置类- 属性解析器:
AnnotationTransactionAttributeSource
,内部持有了⼀个解析器集合Set<TransactionAnnotationParser> annotationParsers
SpringTransactionAnnotationParser
解析器,⽤来解析@Transactional
的事务属性- 事务拦截器:
TransactionInterceptor
实现了MethodInterceptor
接⼝,该通⽤拦截会在产⽣代理对象之前和aop增强合并,最终⼀起影响到代理对象 TransactionInterceptor
的invoke
⽅法中invokeWithinTransaction
会触发原有业务逻辑调⽤(增强事务)
- 属性解析器:
- 通过
Springboot
Springboot 在2016年在国内逐渐使用起来
1.概述
1)从Spring到Springboot
-
传统Spring项目的构建方式:创建maven-webapp项目 → pom.xml添加依赖 → 修改web.xml文件(配置监听器、过滤器、请求拦截器DispatcherServlet、Spring容器) → 编写dispatcher-servlet.xml配置文件 → 编写application-context.xml Spring IOC容器 → 设置TomCat容器 → 前后端业务编写、调试
-
Springboot项目的构建方式:创建maven项目 → pom.xml添加依赖 → 配置application.yml配置文件 → 全后端业务编写、调试
- 理解约定(软件设计范式,主要为了减少开发人员需要做决定的数量,在不失灵活性的基础上获得简单的好处)优于配置。Springboot理解约定的体现:项目结构、内置Web容器(TomCat、Jetty、Undetow、Reactor)、配置文件、starter依赖
- 注解驱动的发展
- Spring Framework 1.x:不支持注解
- Spring Framework 2.x:
@Autowired
依赖注入、@Qualifier
配合@Autowired
的依赖查找、@Component
@Service
@Controller
组件声明、@RequestMappering
等Spring MVC的参数解析 仍然没有脱离XML配置驱动 - Spring Framework 3.x:
@Configuration
配置类、@ImportResource("classpath:/META-INF/spring/other.xml")
导入遗留的xml配置文件、3.1版本增加了@ComponentScan("seg.tef4")
指定扫描路径下需要装配的类、@Import
导入分散的配置类、@Enable
启动一些默认未启动的特性 - Spring Framework 4.x:注解的完善时代
@Conditional
条件装配 - Spring Frameworl 5.x:Springboot 2.0的底层
@Index
为注解添加索引,避免由大量使用@ComponentScan
导致的注解解析时间过长
2.原理机制
1)SPI
- Java SPI
SPI(Service Provider Interface)是JDK内置的一种动态扩展,即JDK定义标准接口,由第三方库对该接口进行实现,程序在执行时会根据配置信息动态加载第三方实现的类,从而完成功能的动态扩展。如java.jdbc.Driver
许多开源框架都借鉴了Java SPI的思想,提供了自己的SPI框架,如Dubbo定义了ExtensionLoader
,实现功能的扩展、Spring提供了SpringFactoriesLoader
,实现外部功能的集成
简单用例:
/** 1.定义一个接口 **/
public interface ITest{
public void test();
}
/** 2.定义不同实现类 **/
public class TestImplement1 implements ITest{
@Override
public void test(){
System.out.println("测试1");
}
}
public class TestImplement2 implements ITest{
@Override
public void test(){
System.out.println("测试2");
}
}
/** 3.在resources目录下创建 META-INF/services 目录,然后在目录中创建一个文件,名称必须是定义的接口的全类路径名称。然后在文件中写上接口的实现类的全类路径名称。 **/
/** 4.加载使用 **/
public static void main(String[] args){
ServiceLoader<ITest> providers = ServiceLoader.load(ITest.class);
Iterator<ITest> iterator = providers.iterator();
while(iterator.hasNext()){
ITest next = iterator.next();
next.test();
}
}
- Springboot SPI
Springboot能够自动加载并使用Java SPI提供的扩展点,其过程为:
①Springboot程序启动,创建应用上下文ApplicationContext
②注册BeanFactory后置处理器用于配置类上的注解
③刷新应用上下文,执行注册的后置处理器
④执行配置类后置处理器(SPI核心逻辑):读取CLASSPATH下所有jar包中的META-INF/spring.factories
文件,加载需要自动配置的类、针对所有扫描的自动配置类再次执行配置处理逻辑、将所有通过配置类处理过符合条件的BeanDefinition注册到Spring容器中、Spring将所有单例BeanDefinition实例化初始化
2)Java 双亲委派机制
- Java类加载器
Java中的类加载器负责加载来自文件系统、网络或者其他来源的类文件、资源文件,JVM的类加载器默认采用的双亲委派机制。具体有3种默认的类加载器:
①Bootstrap ClassLoader:负责加载JDK自带的rt.jar包中的类文件,是所有类加载器的父类
②Extension ClassLoader:负责加载java的扩展库从jre/lib/etc目录或java.ext.dirs系统属性指定的目录下加载类
③System ClassLoader:负责从CLASSPATH环境变量中加载类文件
- 双亲委派机制
当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。
具体:根据双亲委派模式,在加载类文件的时候,子类加载器首先将加载请求委托给它的父加载器,父加载器会检测自己是否已经加载过类,如果已经加载则加载过程结束,如果没有加载的话则请求继续向上传递直Bootstrap ClassLoader。如果请求向上委托过程中,如果始终没有检测到该类已经加载,则Bootstrap ClassLoader开始尝试从其对应路劲中加载该类文件,如果失败则由子类加载器继续尝试加载,直至发起加载请求的子加载器为止。
采用双亲委派模式可以保证类型加载的安全性,不管是哪个加载器加载这个类,最终都是委托给顶层的BootstrapClassLoader来加载的,只有父类无法加载自己猜尝试加载,这样就可以保证任何的类加载器最终得到的都是同样一个Object对象。
- 双亲委派机制的缺陷与解决
问题:在双亲委派模型中,子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已经加载的。这就导致了双亲委派模型并不能解决所有的类加载器问题。
案例:SPI 的接口是 Java 核心库的一部分,是由BootstrapClassLoader加载的;SPI实现的Java类一般是由AppClassLoader来加载的。BootstrapClassLoader是无法找到 SPI 的实现类的,因为它只加载Java的核心库。它也不能代理给AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。
解决:使用线程上下文类加载器(ContextClassLoader)加载。 如果不做任何的设置,Java应用的线程的上下文类加载器默认就是AppClassLoader。在核心类库使用SPI接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到SPI实现的类。线程上下文类加载器在很多SPI的实现中都会用到。通常我们可以通过Thread.currentThread().getClassLoader()
和Thread.currentThread().getContextClassLoader()
获取线程上下文类加载器。
3)Springboot自动装配机制
Springboot的自动装配功能由@EnableAutoConfiguration注解提供,而@EnableAutoConfiguration注解的功能主要由以下两部分实现:
- @AutoConfigurationPackage:@AutoConfigurationPackage注解作用在Springboot启动类上,会向Spring容器注册一个类型为AutoConfigurationPackages.BasePackages的bean,这个bean中保存了Springboot启动类的包路径,后续Springboot就会扫描这个包路径下由@Component,@Controller,@Service,@Repository和@Configuration注解修饰的类。
- @Import(AutoConfigurationImportSelector.class)
- @Import(AutoConfigurationImportSelector.class) 会通过AutoConfigurationImportSelector延迟且分组的向Spring容器导入需要自动装配的组件的配置类,从而在解析这些配置类的时候能够将自动装配的组件的bean注册到容器中
- 所谓的延迟,是因为AutoConfigurationImportSelector实现了DeferredImportSelector接口,其逻辑会在Springboot启动类被解析完毕后才会执行
- 所谓的分组,是因为处理DeferredImportSelector是一组一组的进行的,只要DeferredImportSelector的实现类实现的getImportGroup() 方法返回的Class对象一样,那么这样的DeferredImportSelector的实现类就属于同一组
- AutoConfigurationImportSelector获取到需要自动装配的组件的配置类的全限定名,是通过SpringFactoriesLoader完成的,而SpringFactoriesLoader就是Spring中的SPI机制的实现
4)手写starter
- 创建Maven项目,添加依赖
<!-- springboot起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<!-- 需要扩展功能的依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
<!-- 可选 -->
<optional>true</optional>
</dependency>
<!-- 用于增加自定义配置信息 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.2.6.RELEASE</version>
<optional>true</optional>
</dependency>
- 定义接口(该接口是定义要实现的功能)
public interface FormatProcessor {
/**
* 定义一个格式化的方法
* @param obj
* @param <T>
* @return
*/
<T> String formate(T obj);
}
- 定义实现类(提供的功能实现)
public class JsonFormatProcessor implements FormatProcessor {
@Override
public <T> String formate(T obj) {
return "JsonFormatProcessor:" + JSON.toJSONString(obj);
}
}
public class StringFormatProcessor implements FormatProcessor {
@Override
public <T> String formate(T obj) {
return "StringFormatProcessor:" + obj.toString();
}
}
- 定义配置类
@Configuration
public class FormatAutoConfiguration {
@ConditionalOnMissingClass("com.alibaba.fastjson.JSON")
@Bean
@Primary // 优先加载
public FormatProcessor stringFormatProcessor(){
return new StringFormatProcessor();
}
@ConditionalOnClass(name="com.alibaba.fastjson.JSON")
@Bean
public FormatProcessor jsonFormatProcessor(){
return new JsonFormatProcessor();
}
}
- 创建自定义配置信息的属性类
有时我们需要动态的传递相关配置信息,比如Redis的Ip,端口等等,这些信息显然是不能直接写到代码中的,这时我们就可以通过SpringBoot的配置类来实现。
@ConfigurationProperties(prefix = HelloProperties.HELLO_FORMAT_PREFIX)
public class HelloProperties {
public static final String HELLO_FORMAT_PREFIX="mashibing.hello.format";
private String name;
private Integer age;
private Map<String,Object> info;
public Map<String, Object> getInfo() {
return info;
}
public void setInfo(Map<String, Object> info) {
this.info = info;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
- 定义模板工具
public class HelloFormatTemplate {
private FormatProcessor formatProcessor;
private HelloProperties helloProperties; // 此处增加自定义配置属性类
public HelloFormatTemplate(HelloProperties helloProperties,FormatProcessor processor){
this.helloProperties = helloProperties;
this.formatProcessor = processor;
}
public <T> String doFormat(T obj){
StringBuilder builder = new StringBuilder();
builder.append("Execute format : ").append("<br>");
builder.append("HelloProperties:").append(formatProcessor.formate(helloProperties.getInfo())).append("<br>");
builder.append("Object format result:").append(formatProcessor.formate(obj));
return builder.toString();
}
}
- 定义整合到springboot中的模板工具的配置类
@Configuration
@Import(FormatAutoConfiguration.class)
@EnableConfigurationProperties(HelloProperties.class) // 注意此处引入自定义配置属性类
public class HelloAutoConfiguration {
@Bean
public HelloFormatTemplate helloFormatTemplate(FormatProcessor formatProcessor){
return new HelloFormatTemplate(formatProcessor);
}
}
- 创建spring.factories文件
在resources下创建META-INF目录,再在其下创建spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=seg.tef4.autoconfiguration.HelloAutoConfiguration
- 设置自定义配置属性的提示信息
在的META-INF/下创建一个additional-spring-configuration-metadata.json
{
"properties": [
{
"name": "mashibing.hello.format.name",
"type": "java.lang.String",
"description": "账号信息",
"defaultValue": "root"
},
{
"name": "mashibing.hello.format.age",
"type": "java.lang.Integer",
"description": "年龄",
"defaultValue": 18
}
]
}
-
打包:Install打包项目
-
测试
-
引入依赖
<dependency> <groupId>org.example</groupId> <artifactId>format-spring-boot-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
使用
@RestController public class UserController { @Autowired private HelloFormatTemplate helloFormatTemplate; @GetMapping("/format") public String format(){ User user = new User(); user.setName("李一帆"); user.setAge(24); return helloFormatTemplate.doFormat(user); } }
-
3.常用基础
1)起步
项目构建、Maven插件、常用配置、Spring MVC分层
-
起步依赖的版本
-
继承父maven项目(parent)
-
在
<dependencyManagement>
中导入 -
继承时可以通过属性指定依赖项的版本
<properties><spring.version>4.1.6.RELEASE<spring.version></properties>
,但是导入时无法使用此特性:可以先自己通过继承构建一个jar包,再实际编写项目时,通过导入的方式导入该jar包
-
-
spring boot starter
starter | 解释 |
---|---|
spring-boot-starter | 是 Spring Boot 的核心启动器,包含了自动配置、日志和 YAML |
spring-boot-starter-actuator | 帮助监控和管理应用 |
spring-boot-starter-amqp | 通过 spring-rabbit 来支持 AMQP 协议(Advanced Message Queuing Protocol) |
spring-boot-starter-aop | 支持面向方面的编程即 AOP,包括 spring-aop 和 AspectJ |
spring-boot-starter-artemis | 通过 Apache Artemis 支持 JMS 的 API(Java Message Service API) |
spring-boot-starter-batch | 支持 Spring Batch,包括 HSQLDB 数据库 |
spring-boot-starter-cache | 支持 Spring 的 Cache 抽象 |
spring-boot-starter-cloud-connectors | 支持 Spring Cloud Connectors,简化了在像 Cloud Foundry 或 Heroku 这样的云平台上连接服务 |
spring-boot-starter-data-elasticsearch | 支持 ElasticSearch 搜索和分析引擎,包括 spring-data-elasticsearch |
spring-boot-starter-data-gemfire | 支持 GemFire 分布式数据存储,包括 spring-data-gemfire |
spring-boot-starter-data-jpa | 支持 JPA(Java Persistence API),包括 spring-data-jpa、spring-orm、Hibernate |
spring-boot-starter-data-mongodb | 支持 MongoDB 数据,包括 spring-data-mongodb |
spring-boot-starter-data-rest | 通过 spring-data-rest-webmvc,支持通过 REST 暴露 Spring Data 数据仓库 |
spring-boot-starter-data-solr | 支持 Apache Solr 搜索平台,包括 spring-data-solr |
spring-boot-starter-freemarker | 支持 FreeMarker 模板引擎 |
spring-boot-starter-groovy-templates | 支持 Groovy 模板引擎 |
spring-boot-starter-hateoas | 通过 spring-hateoas 支持基于 HATEOAS 的 RESTful Web 服务 |
spring-boot-starter-hornetq | 通过 HornetQ 支持 JMS |
spring-boot-starter-integration | 支持通用的 spring-integration 模块 |
spring-boot-starter-jdbc | 支持 JDBC 数据库 |
spring-boot-starter-jersey | 支持 Jersey RESTful Web 服务框架 |
spring-boot-starter-jta-atomikos | 通过 Atomikos 支持 JTA 分布式事务处理 |
spring-boot-starter-jta-bitronix | 通过 Bitronix 支持 JTA 分布式事务处理 |
spring-boot-starter-mail | 支持 javax.mail 模块 |
spring-boot-starter-mobile | 支持 spring-mobile |
spring-boot-starter-mustache | 支持 Mustache 模板引擎 |
spring-boot-starter-redis | 支持 Redis 键值存储数据库,包括 spring-redis |
spring-boot-starter-security | 支持 spring-security |
spring-boot-starter-social-facebook | 支持 spring-social-facebook |
spring-boot-starter-social-linkedin | 支持 spring-social-linkedin |
spring-boot-starter-social-twitter | 支持 pring-social-twitter |
spring-boot-starter-test | 支持常规的测试依赖,包括 JUnit、Hamcrest、Mockito 以及 spring-test 模块 |
spring-boot-starter-thymeleaf | 支持 Thymeleaf 模板引擎,包括与 Spring 的集成 |
spring-boot-starter-velocity | 支持 Velocity 模板引擎 |
spring-boot-starter-web | 支持全栈式 Web 开发,包括 Tomcat 和 spring-webmvc |
spring-boot-starter-websocket | 支持 WebSocket 开发 |
spring-boot-starter-ws | 支持 Spring Web Services |
spring-boot-starter-actuator | 面向产品上线相关的功能,比如测量和监控 |
spring-boot-starter-remote-shell | 增加了远程 ssh shell 的支持 |
spring-boot-starter-jetty | 引入了 Jetty HTTP 引擎 |
spring-boot-starter-log4j | 支持 Log4J 日志框架 |
spring-boot-starter-logging | Spring Boot 默认的日志框架 Logback |
spring-boot-starter-tomcat | Spring Boot 默认的 HTTP 引擎 Tomcat |
spring-boot-starter-undertow | Undertow HTTP 引擎 |
2)热部署
原理:Sprngtboot将应用程序和开发环境中的文件系统进行关联,并监听关联文件夹中的文件更改。当检测到文件更改时,Springboot会利用JVM的类加载机制,通过重新加载修改后的类实现热部署(注意:只在开发环境中使用,生产环境中拿掉)
方法
- spring-boot-devtools
pringBoot提供的热部署工具。实现资源修改后的自动重启等功能。启动应用程序时,DevTools会自动配置热部署,并在保存文件时重新启动应用程序。DevTools还提供了其他功能,如自动重新启动、自动刷新页面等,以提高开发效率。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
- Spring Loaded(不再维护)
- JRebel插件(付费)
- Spring Boot Maven插件
可以监控代码变动,自动重启应用
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
- IntelliJ IDEA配置热部署
- 安装
Spring Boot DevTools
插件 - 配置:Run → Edit Configurations → Spring Boot:将
On-frame deactivation
(当你切换到其他窗口时,配置的更新策略)和On-update action
(当检测到文件更改时,配置的更新策略)选项设置为Update classes and resources
- 安装
3)JSON数据类型
-
Springboot默认使用了Jackson进行前后端交互时,json数据类型的转换
4)Springboot全局异常捕获
@ControllerAdvice
public class GlobalDefaultExceptionHandler {
@ExceptionHandler(value = Exception.class)
public void defaultErrorHandler(HttpServletRequest req, Exception e) {
/**
* 1.If the exception is annotated with @ResponseStatus rethrow it and let
* the framework handle it - like the OrderNotFoundException example at the start of this post.
* Otherwise setup and send the user to a default error-view.
* tip:AnnotationUtils is a Spring Framework utility class.
*/
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
throw e;
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
/** 2.打印异常信息 **/
e.printStackTrace();
System.out.println("GlobalDefaultExceptionHandler.defaultErrorHandler()");
/*
* 3.返回 json 数据或者 String 数据:那么需要在方法上加上注解:@ResponseBody添加 return 即可。
*/
}
}
5)数据持久化
①JdbcTemplate
JdbcTemplate是Springboot基于JDK的JDBC自身提供的数据持久化方法,封装了对创建的SQL操作,如SQL语句执行、结果处理等功能,使得我们能更方便地使用JDBC
- 配置数据源与mysql数据驱动
# datasource
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.max-active=20
spring.datasource.max-idle=8
spring.datasource.min-idle=8
spring.datasource.initial-size=10
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
- 使用
@Service
public class UserService {
@Autowired
JdbcTemplate jdbcTemplate;
RowMapper<User> userMapper=new RowMapper<User>() {
public User mapRow(ResultSet resultSet, int rowNum) throws SQLException {
User user = new User();
user.setId(resultSet.getInt("id"));
user.setUsername(resultSet.getString("username"));
user.setPassword(resultSet.getString("password"));
return user;
}
};
// 插入一条用户数据
public void addUser(User user) {
String sql = "INSERT INTO user (username, password) VALUES (?, ?)";
jdbcTemplate.update(sql, user.getUsername(), user.getPassword());
}
// 删除一条用户数据
public void deleteUser(int id) {
String sql = "DELETE FROM user WHERE id = ?";
jdbcTemplate.update(sql, id);
}
// 更新一条用户数据
public void updateUser(User user) {
String sql = "UPDATE user SET username = ?, password = ? WHERE id = ?";
jdbcTemplate.update(sql, user.getUsername(), user.getPassword(), user.getId());
}
// 查询所有用户数据
public List<User> findAllUsers() {
String sql = "SELECT id, username, password FROM user";
return jdbcTemplate.query(sql, userMapper);
}
// 根据 ID 查询一条用户数据
public User findUserById(int id) {
String sql = "SELECT id, username, password FROM user WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{id}, userMapper);
}
}
②JPA
JPA(Java Persistence API):Java持久化API,是一种JSR规范,它基于hibernate定义了Java对象和关系型数据库之间如何映射ORM
-
配置数据源与引入mysql驱动(同上)
-
引入JPA
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
- JPA配置
# Specify the DBMS
spring.jpa.database=MYSQL
# Show or not log for each sql query
spring.jpa.show-sql=true
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto=update
# Naming strategy
spring.jpa.hibernate.naming-strategy=org.hibernate.cfg.ImprovedNamingStrategy
# stripped before adding them to the entity manager)
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
- 定义Mode
@Entity(name="user")
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(name = "username")
private String username;
@Column(name="password")
private String password;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
- 定义接口(这是JPA的关键,定义好实体关系映射)
import com.eryi.bean.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
@Service
public interface UserService extends JpaRepository<User,Integer> {
}
- 使用(JpaRepository自带了一套基础CRUD的API,直接调用即可)
@RequestMapping("getUser")
public User addUser(User user){
return userService.findById(user.getId()).get();
}
- 转换器
有时候数据库里存的字段值不是我们直接想要的,需要做一些转换,比如user如果有个性别字段,数据库里可能是用1表示男性,用2表示女性,而我们想要的是男女,Spring Boot Jpa提供了转换器来进行转换。
// 定义转换器
public class SexConverter implements AttributeConverter<SexEnum,Integer>{
// 将枚举转换为数据库列
@Override
public Integer convertToDatabaseColumn (SexEnum sex) { return sex.getId(); }
// 将数据库列转换为枚
@Override
public SexEnum convertToEntityAttrbute (Integer id) { return SexEnum.getEnumByid(id); }
}
// 使用转换器
public class User{
@Convert(converter=SexConverter.class)
private SexEnum sex;
}
③mybatis
将sql代码同业务逻辑分离
- 配置数据源与引入mysql驱动(同上)
- 引入依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
- mybatis起步配置
# 说明mapper的xml文件在哪里
mybatis.mapper-locations=classpath:mapper/*.xml
- Dao层接口定义
@Mapper
public interface UserDao {
User getUserById(Long id);
List<User> getAllUsers();
void addUser(User user);
void updateUser(User user);
void deleteUser(Long id);
}
- sql的实际编写
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.dao.UserDao">
<select id="getUserById" parameterType="long" resultType="com.example.demo.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
<select id="getAllUsers" resultType="com.example.demo.entity.User">
SELECT * FROM user
</select>
<insert id="addUser" parameterType="com.example.demo.entity.User">
INSERT INTO user (username, password) VALUES (#{username}, #{password})
</insert>
<update id="updateUser" parameterType="com.example.demo.entity.User">
UPDATE user SET username = #{username}, password = #{password} WHERE id = #{id}
</update>
<delete id="deleteUser" parameterType="long">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper>
6)静态资源处理
- 默认静态资源处理
Springboot默认通过WebMvcAutoConfiguration
提供了静态资源的处理,默认配置的/**
映射到:/static
、/public
、/resources
、/META-INF/resources
路径,这些路径都对应在classpath的目录下,如:src/main/resources/static
;默认的/webjars/**
映射到classpath:META-INF/resources/webjars/
同名静态资源的访问优先级:META/resources > resources > static > public
访问方式:http://localhost:8080/test.jpg
- 自定义静态资源处理
上述默认的静态资源都会在项目构建后,打到归档包中,而实际项目中很多资源是动态维护的,对于这种资源应该如何访问
①使用claspath
@Configuration
public class MyWebAppConfigurer extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/myres/**").addResourceLocations("classpath:/myres/");
super.addResourceHandlers(registry);
}
}
这种方式并不影响默认的Springboot映射,可以多次使用addResourceLocations
添加目录,优先级为:先添加的高于后添加的,其中addResourceLocations
的参数是动参 ,可以这样写addResourceLocations(“classpath:/img1/”, “classpath:/img2/”, “classpath:/img3/”);
访问方式:http://localhost:8080/myres/test.jpg
②使用外部目录
可以直接使用 addResourceLocations 指定磁盘绝对路径,同样可以配置多个位置,注意路径写法需要加上file:
registry.addResourceHandler("/api_files/**").addResourceLocations("file:D:/data/api_files");
7)任务调度
@Configuration // 声明类为系统配置类
@EnableScheduling // 开启调度任务
public class MyScheduleConfig {
private Logger logger = Logger.getLogger(getClass());
@Scheduled(cron = "0 0/1 * * * ?") // 定义调度器:指定 CronExpress 表达式
public void job1() {
logger.info("this is my first job execute");
}
}
8)非注入类调用Bean
如果想在一个类中SpringIOC容器中注入的对象,则需要将当前类也交给SpringIOC管理。此处给出非注入类调用Bean的方法
- 实例化应用上下文对象
ApplicationContext ac = new FileSystemXmlApplicationContext("applicationContext.xml");
ac.getBean("beanId");
这种方式在Java SE环境中是可行的,但是Spring项目中,由于项目启动时,应用上下文已经被实例化了,因此这种方式会导致重复实例化应用上下文
- 保存应用上下文实例
/**
* spring工具类,为了更方便的获取spring的applicationContext 直接实现接口ApplicationContextAware
*/
@Component
public class SpringUtil implements ApplicationContextAware {
private Logger logger = Logger.getLogger(getClass());
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
logger.info("ApplicationContext配置成功");
}
/**
* 获取spring上下文
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String beanName) {
return applicationContext.getBean(beanName);
}
public static <T> Object getBean(Class<T> class1) {
return applicationContext.getBean(class1);
}
public static <T> Object getBean(Class<T> class1, String beanName) {
return applicationContext.getBean(class1, beanName);
}
}
9)服务端渲染
①JSP
- 引入依赖
<!-- servlet 依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!--JSTL(JSP Standard Tag Library,JSP 标准标签库)是一个不断完善的开放源代码的 JSP 标签库,是由 apache 的 jakarta 小组来维护的。JSTL 只能运行在支持 JSP1.2 和 Servlet2.3 规范的容器上,如 tomcat4.x。在 JSP 2.0 中也是作为标准支持的。-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<!-- 在嵌入式 Tomcat 中支持 JSP 的库:可以在应用程序中使用 JSP 技术,而不需要将 Tomcat 服务器作为独立的服务运行。这样你的应用程序可以作为一个独立的 JAR 文件运行,并在需要时通过嵌入式 Tomcat 启动 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
- 配置
# 页面默认前缀目录
spring.mvc.view.prefix=/WEB-INF/views/
# 响应页面默认后缀
spring.mvc.view.suffix=.jsp
- 编写jsp页面
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>hello jsp</title>
</head>
<body>
${message}
</body>
- 编写控制器
@Controller
public class HelloJSPController {
@RequestMapping("/index")
public String hello(ModelMap map) {
map.put("message", "this data is from the backing server");
return "index";
}
}
- 静态资源(js、css):在
src/main/resources
下新建static
目录,再下建js、css、asset等目录,通过Spring默认的静态资源访问方式
②Thymeleaf
常用模板引擎:FreeMarker、Groovy、Thymeleaf、Velocity
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 配置
# spring.thymeleaf.prefix=classpath:/templates/
# spring.thymeleaf.suffix=.html
# spring.thymeleaf.mode=HTML5
# spring.thymeleaf.encoding=UTF-8;charset=<encoding> is added
# spring.thymeleaf.content-type=text/html
# set to false for hot refresh
spring.thymeleaf.cache=false
- 编写模板
<!-- src/main/resouces/templates/helloHtml.html -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<p th:text="${hello}"></p>
</body>
</html>
- 编写控制器
@Controller
public class ThymeleafController {
@RequestMapping("/helloHtml")
public String hello(Map<String, Object> map) {
map.put("hello", "this data is from backing server");
return "helloHtml";
}
}
③freemarker
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
- 配置文件
spring.freemarker.allow-request-override=false
spring.freemarker.cache=true
spring.freemarker.check-template-location=true
spring.freemarker.charset=UTF-8
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.expose-spring-macro-helpers=false
# spring.freemarker.prefix=
# spring.freemarker.request-context-attribute=
# spring.freemarker.settings.*=
# spring.freemarker.suffix=.ftl
# spring.freemarker.template-loader-path=classpath:/templates/#comma-separatedlist
# spring.freemarker.view-names= #whitelistofviewnamesthatcanberesolved
- 编写模板
<!-- helloHtml1.ftl -->
<html>
<head>
<meta charset="UTF-8">
<title>freemarker模板的使用</title>
</head>
<body>
<h1>${message}</h1>
</body>
</html>
- 编写控制器
@Controller
@RequestMapping("/freemarker")
public class FreemarkerController {
@RequestMapping("/hello")
public String hello(ModelMap map) {
map.put("message", "this data is from backing server , for freemarker");
return "helloHtml1";
}
}
10)Springboot集成Servlet、Filter、Listener
-
Servlet
Springboot默认使用DispatcherServlet,处理所有路由的请求
- 编写Servlet
@WebServlet(urlPatterns = "/myServlet/*", name = "servlet1", description = "my servlet in springboot") public class MyServlet1 extends HttpServlet { private static final long serialVersionUID = 6613439809483079873L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); resp.setCharacterEncoding("utf-8"); PrintWriter out = resp.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>Hello World</title>"); out.println("</head>"); out.println("<body>"); out.println("<h1>这是:MyServlet1</h1>"); out.println("</body>"); out.println("</html>"); } }
- 代码注册Servlet(已弃用)
@SpringBootApplication public class Application{ @Bean public ServletRegistrationBean regMyServlet1() { return new ServletRegistrationBean(new MyServlet1(), "/myServlet/*"); } public static void main(String[] args) throws Exception { SpringApplication.run(App.class, args); } }
- 注解注册
@SpringBootApplication @ServletComponentScan // 添加servlet注册扫描,将自动注册添加了@WebServlet的类为serlvet public class Application { public static void main(String[] args) throws Exception { SpringApplication.run(App.class, args); } }
-
Filter和Listener
在启动类上增加@ServletComponentScan
之后,注册的Filter和Listener都会被扫描到并注入
/** 过滤器 **/
@WebFilter(urlPatterns = "/*", filterName = "myFilter")
public class MyFilter implements Filter {
private Logger logger = Logger.getLogger(getClass());
@Override
public void destroy() {
logger.info("destroy()");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
logger.info("doFilter()");
logger.info("before filter");
chain.doFilter(request, response);
logger.info("after filter");
}
@Override
public void init(FilterConfig config) throws ServletException {
logger.info("init()");
}
}
/** 监听器 **/
@WebListener
public class Mylistener implements ServletContextListener {
private Logger logger = Logger.getLogger(getClass());
@Override
public void contextDestroyed(ServletContextEvent contextEvent) {
logger.info("contextDestroyed");
}
@Override
public void contextInitialized(ServletContextEvent contextEvent) {
logger.info("contextInitialized");
}
}
11)Spring拦截器HandlerInterceptor
Servlet、Filter、Listener是Java EE自带的控制层组件,而HandlerInterceptor则是Spring提供的控制层组件。它的功能与Filter相似,但是更为精细,它在请求的前后声明周期都提供了不同的回调,相当于简化了开发。我们不能通过拦截器修改请求的内容,但是可以通过抛出异常或返回false阻止请求的执行
①内置拦截器:实现UserRoleAuthorizationInterceptor
的拦截器有:ConversionServiceExposingInterceptor
、CorsInterceptor LocaleChangeInterceptor
、PathExposingHandlerInterceptor
、ResourceUrlProviderExposingInterceptor
、ThemeChangeInterceptor
、UriTemplateVariablesHandlerInterceptor
、UserRoleAuthorizationInterceptor
②自定义拦截器:
- 创建拦截器:根据切面(功能)定义不同的拦截器
/** 拦截器1 **/
public class MyInterceptor1 implements HandlerInterceptor {
private Logger logger = Logger.getLogger(getClass());
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object object, Exception exception) throws Exception {
logger.info("afterCompletion...");
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object object, ModelAndView modelAndView) throws Exception {
logger.info("postHandle...");
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object)throws Exception {
logger.info("preHandle...");
return true;
}
}
/** 拦截器2 **/
public class MyInterceptor2 implements HandlerInterceptor {
private Logger logger = Logger.getLogger(getClass());
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object object, Exception exception) throws Exception {
logger.info("afterCompletion...");
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object object, ModelAndView modelAndView) throws Exception {
logger.info("postHandle...");
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
logger.info("preHandle...");
return true;
}
}
- 通过重写
WebMvcConfigurerAdapter
的addInterceptors
方法,将自定义的拦截器
@Configuration
public class MyWebAppConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 重写addInterceptors方法并为拦截器配置拦截规则
registry.addInterceptor(new MyInterceptor1()).addPathPatterns("/**");
registry.addInterceptor(new MyInterceptor2()).addPathPatterns("/**");
// 排除路径
// registry.addInterceptor(new MyInterceptor2()).addPathPatterns("/**").excludePathPatterns("/Hello");
super.addInterceptors(registry);
}
}
12)系统启动任务
实际应用中,我们可能在项目启动时就去加载一些数据或者完成一些初始化等工作。为此Springboot为我们提供了一个方法,通过实现CommandLineRunner
来实现
@Component
@Order(value = 1) // 设置启动执行顺序
public class MyCommandRunner1 implements CommandLineRunner {
private Logger logger = Logger.getLogger(this.getClass());
@Override
public void run(String... arg0) throws Exception {
logger.info("执行启动任务1...");
}
}
@Component
@Order(value = 2) // 设置启动执行顺序
public class MyCommandRunner2 implements CommandLineRunner {
private Logger logger = Logger.getLogger(this.getClass());
@Override
public void run(String... arg0) throws Exception {
logger.info("执行启动任务2...");
}
}
Spring Boot 应用程序在启动后,会遍历CommandLineRunner
接口的实例并运行它们的 run 方法。
13)集成Junit单元测试
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
- 要测试的service
@Service("helloService")
public class HelloService {
public String sayHello() {
return "hello";
}
}
- JUnit单元测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { Application.class }) // 指定我们SpringBoot工程的Application启动类
public class TestHelloService {
@Autowired
private HelloService helloService; // 自动装配业务逻辑层
@Test
public void testSayHello() {
helloService.sayHello();
}
}
14)Springboot读系统环境变量
- 通过实现
EnvironmentAware
接口重写其方法setEnvironment
并注入SpringIOC容器中,可以在项目启动时获得到系统的环境变量和配置文件中的变量
@Configuration
public class MyEnvironment implements EnvironmentAware {
@Value("${spring.datasource.url}") // 使用el表达式读取spring主配置文件
private String jdbcUrl;
private Logger logger = Logger.getLogger(getClass());
@Override
public void setEnvironment(Environment environment) {
// springEL表达式获取的值
logger.info("springel表达式获取的值:" + jdbcUrl);
// 获取系统属性:
logger.info("JAVA_HOME" + environment.getProperty("JAVA_HOME"));
// 获取spring主配置文件中的属性
logger.info("spring.datasource.url:" + environment.getProperty("spring.datasource.url"));
// 获取前缀是“spring.datasource”的所有属性值
RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(environment,"spring.datasource.");
logger.info("通过前缀获取的url:" + propertyResolver.getProperty("url"));
logger.info("通过前缀获取的driverClassName:" + propertyResolver.getProperty("driverClassName"));
}
}
- 通过
@ConfigurationProperties
读取 application 属性配置文件中的属性
@ConfigurationProperties(prefix="spring.datasource.") // 指定配置文件中属性的前缀
public class MySqlConfig {
private String url;
private String username;
private String password;
private String driverClassName;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getDriverClassName() {
return driverClassName;
}
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
}
@Configuration
@EnableConfigurationProperties(MySqlConfig.class) // 将指定类的属性注入到 Spring 环境中
public class MyDataConfiguration {
@Autowired
private MySqlConfig config;
}
15)改变默认扫描包
Spring Boot 默认会扫描启动类同包以及子包下的注解
- 通过@ComponentScan 注解可以指定要扫描的包以及要扫描的类。
@ComponentScan(basePackages = { "cn.hpit", "org.hpit","com.hpit" }) // 自定义包扫描路径,该注解增加到启动类上
- 通过传统的XML配置文件配置不同包下的beans,在项目启动类上通过
@ImportResource(locations = { "applicationContext.xml" })
的方式,也可以让扫描不到的包中的Bean被注入
16)自定义启动Banner
- 在启动类方法中修改
SpringApplication application = new SpringApplication(App.class);
/*
* Banner.Mode.OFF:关闭;
* Banner.Mode.CONSOLE:控制台输出,默认方式;
* Banner.Mode.LOG:日志输出方式;
*/
application.setBannerMode(Banner.Mode.OFF);
application.run(args);
- 修改 banner.txt 文件
在 src/main/resouces 下新建 banner.txt,在文件中加入:
# MANIFEST.MF 文件中的版本号
${application.version}
# 上面的的版本号前面加 v 后上括号
${application.formatted-version}
# springboot 的版本号
${spring-boot.version}
# springboot 的版本号
${spring-boot.formatted-version}
- 重写接口 Banner 实现
SpringBoot 提供了一个接口org.springframework.boot.Banner
,他的实例可以被传给 SpringApplication 的setBanner(banner)
方法,重写Banner 接口的 printBanner 方法可以对输出内容进行修改
- 修改项目配置文件
# 是否显示 banner,可选值[true|false]
spring.main.show-banner=false
17)Actuator
- 介绍与引入依赖
actuator是Spring Boot提供的对应用程序进行监控、统计的模块,是系统运行状态、系统配置信息查看、系统排错、系统控制的有利工具,为如今微服务集群的自动化运维提供了基础。
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 版本区别
由于Spring Boot版本的不同会导致actuator模块的用法有一定差别,本文分别对Spring Boot 1.x大版本和Spring Boot 2.x大版本的区别进行介绍,具体是基于Spring Boot 1.3.7.RELEASE
和Spring Boot 2.1.10.RELEASE
版本。
①1.x版本
1.x版本的actuator在引入依赖后即自动进行了监控端点的开启、暴露,在项目启动时即可看到路由的映射,如下图所示。
在浏览器输入:http://localhost:{port}/{端点名}
,即可访问,如这里访问:http://localhost:9999/trace
即可查看trace端点的访问信息
②2.x版本
2.x版本的actuator出于安全等考虑,基于HTTP的访问默认只开启了health
端点和info
端点(基于JMX的访问基本都开启了,它通常用于实现客户端监控工具),如要开启其他端点需要进行额外的配置,且1.x版本与2.x版本的配置略有不同。
Spring Boot 2.0的端点基础路径由“/”调整到”/actuator”下,如: /info 调整为 /actuator/info 可 以通过以下配置改为和旧版本一致:
management.endpoints.web.base-path=/
actuator的一般配置主要包括:①端点的开启与禁用②端点的暴露控制③端点路由的自定义映射,下面给出常用的配置
# actuator端点管理
management:
endpoints:
# jmx方式的端点控制
jmx:
# 暴露端点控制
exposure:
# 暴露
include:
# 不暴露
exclude: "*"
# http方式的端点控制
web:
# 暴露端点控制
exposure:
# 暴露:注意*在yaml中有特殊含义,统配时需要增加双引号
include: "*"
# 不暴露
exclude: beans,env
# 自定义基本访问路由映射
# 修改http://{ip}:{port}/actuator为http://{ip}:{port}/manage
# 若基路径修改为:空或/则,为了避免路由重复,会禁用管理URL
base-path: /manage
# 自定义端点访问路由映射
path-mapping:
# 修改http://{ip}:{port}/actuator/health为http://{ip}:{port}/actuator/healthCheck
health: healthCheck
# 全部端点的开启与禁用管理,true为打开,false为禁用
# enabled-by-default: false
# 单个端点的开启与禁用管理,true为打开,false为禁用
endpoint:
# 开启shutdown端点
shutdown:
enabled: true
loggers:
enabled: false
health:
# 详细信息显示给所有用户
show-details: always
# 设置为本地ip,防止远程访问该连接进行关闭服务
server:
address: 127.0.0.1
- 部分原生端点介绍
注意以下内容均基于1.x版本
①应用配置类
应用配置类用于获取应用程序中加载的应用配置、环境变量、自动化配置报告等与Spring Boot应用密切相关的配置类信息
包扫描机制和自动化配置机制简化了配置过程,但配置信息离散到了各个配置类和注解上,这使得我们分析整个应用中资源和注入实例的各种关系变得困难,而应用配置类提供的端点可以帮助我们获得关于配置信息的详细报告。
这些信息报告在应用启动时就已经基本确定了其返回内容,属于静态报告。
- /autoconfig:该端点用于获取应用的自动化配置报告,包括所有自动化配置的属性,分为条件匹配成功的自动化配置
positiveMatches
和条件匹配不成功的自动化配置nagativeMatches
并列出了匹配条件 - /beans:获取应用上下文中创建的所有Bean
- /configprops:获取应用中配置的属性信息报告,prefix属性表示属性的前缀,propeties表示各个属性的键和值
- /env:获取应用所有可用的环境属性报告,包括环境变量、JVM属性、应用的配置属性、命令行中的参数,它可以方便地显示当前可加载的配置信息。当属性key包含password、secret、key等关键词时,返回value将以*形式返回
- /mappings:获取所有Spring MVC的控制器映射关系报告,bean标识了该路由映射的请求处理器、method标识了具体的类和处理函数
- /info:用来返回一些自定义的信息,默认只会返回{},属性名前缀为info,如info.app.name
②度量指标类
度量指标类的端点返回程序运行过程中的一些快照信息,属于动态报告。
- /metrics:返回当前应用的各项指标
属性 | 解释 |
---|---|
processors | 处理器数量 |
uptime、instance.uptime | 运行时间 |
systemload.average | 系统平均负载 |
mem.* | 内存概要信息,包括分配给应用的总内存数量和当前空闲的内存数量,来源于java.lang.Runtime |
heap.* | 堆内存使用情况,来源于java.lang.management.MemoryMXBean接口中的getHeapMemoryUsage方法获取的java.lang.management.MemoryUsage |
nonheap.* | 非堆内存使用情况,来源于java.lang.management.MemoryMXBean接口中的getNonHeapMemoryUsage方法获取的java.lang.management.MemoryUsage |
threads.* | 线程使用情况,包括线程数、守护线程数daemon、线程峰值peak等,来源于java.lang.management.ThreadMXBean |
classes.* | 应用加载和卸载类的统计,来源于java.lang.management.ClassLoadingMXBean |
gc.* | 垃圾收集器的详细信息,包括垃圾回收次数gc.ps_scavenge.count、垃圾回收消耗时间:gc.ps_scavenge.time、标记-清除算法的次数:gc.ps_marksweep.count、标记-清除算法的消耗时间:gc.marksweep.time。这些数据来源于java.lang.management.GarbageCollectionMXBean |
httpsessions.* | Tomcat容器的使用情况,包括最大会话数、活跃会话数等,这些指标只有在使用tomcat容器时才有 |
gauge.* | HTTP请求的性能指标之一,主要用于反应一个绝对值,如gauge.response.hello:5表示上一次hello请求的延迟时间为5毫秒 |
counter.* | HTTP请求的性能指标之一,主要作为计数器使用,记录了增减量,如counter.status.200.hello:11,表示返回状态为200的hello请求的次数 |
对于gauge.*和counter.*的统计,有一个特殊的内容请求star-start,它表示了对静态资源的访问。这两类指标非常重要,我们不仅可以使用它默认的统计指标,还可以在程序中轻松地增加自定义统计值。如下例所示:
@RestController
public calss HelloController{
@Autowired
private CounterService counterService;
@RequestMapping("/hello")
public String greet(){
counterService.increment("hello.count");
return "hello";
}
}
关于CounterService更详细的介绍可以参考这篇博客
/metrics端口的各项指标的监控内容和数据收集频率都有所不同,因而每次获取该端口的所有统计数据不太优雅,可以通过/metrics/{name}
获取更细粒度的统计信息
- /health:获取应用的各类健康指标信息
默认实现了一些常用资源的健康指标检测器,这些检测器都通过HealthIndicator接口实现,并会根据依赖关系的引入实现自动装配,如:DiskSpaceHealthIndicator低磁盘空间检测器、MongoHealthIndicator检查Mongo数据库是否可用等。
还可实现HealthIndicator接口自定义非默认组件的检测器
@Component
public class RocketMQHealthIndicator implements HealthIndicator{
// 重写health函数实现健康检查,其返回值Health对象中包含一个状态信息(UP、DOWN、UNKNOW、OUT_OF_SERVICE)和详细信息withDetail函数包含的内容
@Override
public Health health(){
int errorCode = check();
if(errorCode==0){
return Health.down().withDetail("Error Code",errorCode).build();
}
return Health.up.build();
}
private int check(){
// 检测RocketMQ是否可用
}
}
- /dump:获取程序中运行的线程信息,数据来源于java.lang.management.ThreadMXBean的dumpAllThreads方法返回的所有包含同步信息的活动线程详情
- /trace:获取HTTP跟踪信息,默认情况下,始终保存最近的100条请求记录
③操作控制类
相比于默认开启的应用配置类和度量指标类这种反应系统自身属性的端点,操作控制类端点提供了强大的远程控制功能,需要通过属性配置来开启。
原生端点中,默认只提供了一个用于关闭应用的端点:/shutdown,访问后可以实现关闭应用的远程操作。由于关闭应用是一件非常危险的事,真正上线需要加入一定的保护机制,如定制端点路径、整合安全组件进行校验。
- 自定义端点
如果我们需要扩展Endpoint,这时我们可以自定义实现,我们可以在类的头部定义如下的注解
@Endpoint
@WebEndpoint
@ControllerEndpoint
@RestControllerEndpoint
@ServletEndpoint
再给方法添加@ReadOperation
,@WritOperation
或@DeleteOperation
注解(分别对应get、post、delete请求)后,该方法将通过 JMX自动公开,并且在Web应用程序中也通过HTTP公开。
示例:
/**
* 自定义Endpoint
*/
@Component
@Endpoint(id = "sessions")
public class MyHealthEndPoint {
/**
* @Selector 获取传递的参数
*/
@ReadOperation
public Info get(@Selector String name){
return new Info(name,"18");
}
}
- JMX监控方式
jmx(Java Management Extensions)Java管理扩展
Actuator放开JMX:
# 放开 jmx 的 endpoint
management.endpoints.jmx.exposure.include=*
spring.jmx.enabled=true
使用jdk提供的jconsole工具:
- 开源监控系统
SpringBoot可以收集监控数据,但是查看不是很方便,这时我们可以选择开源的监控系统来解决:数据采集、数据存储、数据可视化
-
Prometheus
- 安装
# 下载 wget https://github.com/prometheus/prometheus/releases/download/v2.28.1/prometheus2.28.1.linux-amd64.tar.gz # 解压 tar -xvzf prometheus-2.28.1.linux-amd64.tar.gz
- 配置
cd prometheus-2.28.1.linux-amd64 vim prometheus.yml ### 修改的内容 ### - job_name: 'Prometheus' # 任务名称 metrics_path: '/actuator/prometheus' # 指标路径 scrape_interval: 5s # 多久采集一次 scrape_timeout: 30s # 采集超时时间 static_configs: - targets: ['{ip}:{port}'] # 实例地址/项目地址,可配置多个 labels: instance: Prometheus ### 修改的内容 ###
- 启动应用
./prometheus --config.file=prometheus.yml
- 访问应用
http://{ip}:9090
- 在Springboot项目中添加Prometheus的依赖
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>
-
Grafana
- 安装
wget https://dl.grafana.com/oss/release/grafana-8.0.6-1.x86_64.rpm sudo yum install grafana-8.0.6-1.x86_64.rpm
- 启动
sudo service grafana-server start sudo service grafana-server status
- 访问
http://{ip}:3000
默认账号密码均为admin- 使用
- 在Configuration中的Data source中添加数据源
- 添加SpringBoot的Dashboards(Spring Boot 2.1 Statistic)
- 找到其ID,导入
18)文件上传
- 引入依赖
<!-- spring boot 配置文件解析处理支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
- 文件上传配置
@Configure
public class MultipartConfigElement extends MultipartConfigFactory{
public MultipartConfigElement(){
super();
this.setMaxFileSize("128MB");// KB MB 设置单个上传文件大小
this.setMaxRequestSize("1024MB");
this.setLocation("/");// 设置文件上传路径
}
}
- 编写接口
@Controller
public class FileUploadController {
private Logger logger = Logger.getLogger(getClass());
/** 单文件上传 **/
@ResponseBody
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
BufferedOutputStream outputStream = new BufferedOutputStream(
new FileOutputStream(new File(file.getOriginalFilename())));
logger.info("文件名称:" + file.getOriginalFilename());
outputStream.write(file.getBytes());
outputStream.flush();
outputStream.close();
return "文件上传成功";
}
/** 多文件上传 **/
@RequestMapping(value = "multifileupload", method = RequestMethod.POST)
public @ResponseBody String upload(HttpServletRequest servletRequest) {
List<MultipartFile> files = ((MultipartHttpServletRequest) servletRequest).getFiles("file");
for (MultipartFile multipartFile : files) {
try {
BufferedOutputStream outputStream = new BufferedOutputStream(
new FileOutputStream(new File(multipartFile.getOriginalFilename())));
outputStream.write(multipartFile.getBytes());
outputStream.flush();
outputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
return "文件上传失败";
} catch (IOException e) {
e.printStackTrace();
return "文件上传失败";
}
}
return "文件上传成功";
}
}
19)缓存
- spring cache
spring cache提供了抽象的缓存方案,能够通过少量的注解达到对方法返回对象进行缓存的效果,它还支持SpEL语法
①@Cacheable
可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存 的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring 会在其被 调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需 要再次执行该方法。Spring 在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的 话,Spring 又支持两种策略,默认策略和自定义策略,需要注意的是当一个支持缓存的方法在对象内部被调用 时是不会触发缓存功能的。
@Cacheable 可以指定三个属性,value、key 和 condition:
value:缓存的名称,在 spring 配置文件中定义,必须指定至少一个。如@Cacheable(value=”mycache”)
或者@Cacheable(value={”cache1”,”cache2”}
key:缓存的 key,可以为空,如果指定要按照 SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
condition:缓存的条件,可以为空,使用 SpEL编写,返回 true 或者 false,只有为 true才进行缓存。如@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
除了上述使用方法参数作为 key 之外,Spring 还为我们提供了一个 root 对象可以用来生成 key。通过该 root 对象我们可以获取到以下信息:
属性名称 | 描述 | 示例 |
---|---|---|
methodName | 当前方法名 | #root.methodName |
method | 当前方法 | #root.method.name |
target | 当前被调用的对象 | #root.target |
targetClass | 当前被调用的对象的 class | #root.targetClass |
args | 当前方法参数组成的数组 | #root.args[0] |
caches | 当前被调用的方法使用的 Cache | #root.caches[0].name |
②@CachePut
在支持 Spring Cache 的环境下,对于使用@Cacheable 标注的方法,Spring 在每次执行前都会检查 Cache 中是 否存在相同 key 的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执 行并将返回结果存入指定的缓存中。@CachePut 也可以声明一个方法支持缓存功能。与@Cacheable 不同的是使 用@CachePut 标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法, 并将执行结果以键值对的形式存入指定的缓存中。 @CachePut 也可以标注在类上和方法上。使用@CachePut 时我们可以指定的属性跟@Cacheable 是一样的。
③@CacheEvict
@CacheEvict 是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执 行都会触发缓存的清除操作。@CacheEvict 可以指定的属性有 value、key、condition、allEntries 和 beforeInvocation。其中 value、key 和 condition 的语义与@Cacheable 对应的属性类似。即 value 表示清除 操作是发生在哪些 Cache 上的(对应 Cache 的名称);key 表示需要清除的是哪个 key,如未指定则会使用默认 策略生成的 key;condition 表示清除操作发生的条件。
allEntries:是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存。如:@CachEvict(value=”testcache”,allEntries=true)
beforeInvocation:是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候 就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存。如:@CachEvict(value=” testcache”,beforeInvocation=true)
④@Caching
@Caching 注解可以让我们在一个方法或者类上同时指定多个 Spring Cache 相关的注解。其拥有三个属性: cacheable、put 和 evict,分别用于指定@Cacheable、@CachePut 和@CacheEvict。如:@Caching(cacheable = @Cacheable("users"), evict = { @CacheEvict("cache2"),@CacheEvict(value = "cache3", allEntries = true) })
实例:
/** 缓存对象 **/
@Data
@AllConstructor
@NoConstructor
public class User implements Serializable{
private static final long serialVersionUID = 1L;
private int id;
private String name;
}
/** service **/
@Service
public class UserService { // 使用了一个缓存名叫 userCache
@Cacheable(value = "userCache",key="#userName")
public User getUserByName(String userName) {
return getFromDB(userName);
}
// 清空 accountCache 缓存
@CacheEvict(value = "userCache", key = "#user.name")
public void updateUser(User user) {
updateDB(user);
}
// 清空 accountCache 缓存
@CacheEvict(value = "userCache", allEntries = true,beforeInvocation=true)
public void reload() {
}
private User getFromDB(String userName) {
System.out.println("查询数据库..." + userName);
return new User(userName);
}
private void updateDB(User user) {
System.out.println("更新数据库数据..." + user.getName());
}
}
/** 缓存实现 **/
@Component
public class MyCache implements Cache {
private String name;
private Map<String, Object> store = new ConcurrentHashMap<String, Object>();;
public MyCache() {}
public MyCache(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public void clear() {
store.clear();
}
public void evict(Object obj) {
}
public ValueWrapper get(Object key) {
ValueWrapper result = null;
Object thevalue = store.get(key);
if (thevalue != null) {
result = new SimpleValueWrapper(thevalue);
}
return result;
}
public <T> T get(Object key, Class<T> clazz) {
return clazz.cast(store.get(key));
}
public String getName() {
return name;
}
public Object getNativeCache() {
return store;
}
public void put(Object key, Object value) {
store.put((String) key, value);
}
public ValueWrapper putIfAbsent(Object key, Object value) {
put(key, value);
return new SimpleValueWrapper(value);
}
}
注意:需在配置文件中开启缓存设置
- EHCache
第三实现的spring cache
引入依赖
<!-- 集成 ehcache 需要的依赖-->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<!-- 该依赖不是必要的,只是其集成了较多的功能,写此做提示
包含支持 UI 模版(Velocity,FreeMarker,JasperReports),
邮件服务,
脚本服务(JRuby),
缓存 Cache(EHCache),
任务计划 Scheduling(uartz)
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
配置EHCache
注册缓存管理对象 EhCacheCacheManager、缓存工厂对象 EhCacheManagerFactoryBean:
@Configuration
@EnableCaching //标注启动缓存
public class CacheConfiguration {
/**
* ehcache 主要的管理器
*/
@Bean
public EhCacheCacheManager ehCacheCacheManager(EhCacheManagerFactoryBean bean){
System.out.println("CacheConfiguration.ehCacheCacheManager()");
return new EhCacheCacheManager(bean.getObject());
}
/*
* 据 shared 与否的设置,Spring 分别通过 CacheManager.create()或 new CacheManager()方式来创建一个 ehcache 基地.
* 也说是说通过这个来设置 cache 的基地是这里的 Spring 独用,还是跟别的(如 hibernate 的Ehcache 共享)
*/
@Bean
public EhCacheManagerFactoryBean ehCacheManagerFactoryBean(){
System.out.println("CacheConfiguration.ehCacheManagerFactoryBean()");
EhCacheManagerFactoryBean cacheManagerFactoryBean = new EhCacheManagerFactoryBean();
cacheManagerFactoryBean.setConfigLocation(newClassPathResource("conf/ehcache.xml"));
cacheManagerFactoryBean.setShared(true);
return cacheManagerFactoryBean;
}
}
在src/main/resouces/
下编写ehcache.xml
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!--
diskStore:为缓存路径,ehcache 分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
user.home – 用户主目录
user.dir – 用户当前工作目录
java.io.tmpdir – 默认临时文件路径
-->
<diskStore path="java.io.tmpdir/Tmp_EhCache" />
<!--
defaultCache:默认缓存策略,当 ehcache 找不到定义的缓存时,则使用这个缓存策略。只能定义一个。参数解释如下:
name:缓存名称。
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:硬盘最大缓存个数。
eternal:对象是否永久有效,一但设置了,timeout 将不起作用。
overflowToDisk:是否保存到磁盘,当系统当机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,可选属性,默认值是 0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当 eternal=false 对象不是永久有效时使用,默认是 0.,也就是对象存活时间无穷大。
diskPersistent:是否缓存虚拟机重启期数据
diskSpoolBufferSizeMB:这个参数设置 DiskStore(磁盘缓存)的缓存区大小。默认是 30MB。每个 Cache 都应该有自己的一个缓冲区。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是 120 秒。
memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,Ehcache 将会根据指定的策略去清理内存。默认策略是 LRU(最近最少使用)。你可以设置为 FIFO(先进先出)或是 LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个 hit 属性,hit 值最小的将会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
-->
<defaultCache
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU" />
<cache
name="demo"
eternal="false"
maxElementsInMemory="100"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="300"
memoryStoreEvictionPolicy="LRU" />
</ehcache>
20)spring集成redis
Java原生整合redis的依赖Jredis参考此处
- 引入依赖
<!-- 添加 redis 支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
- 配置redis
# database name
spring.redis.database=0
# server host1
spring.redis.host=127.0.0.1
# server password
#spring.redis.password=
#connection port
spring.redis.port=6379
# pool settings ...
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
# name of Redis server
#spring.redis.sentinel.master=
# comma-separated list of host:port pairs
#spring.redis.sentinel.nodes=
- 编写redis模板和redis缓存的包装类
/**
* redis 缓存配置
* 注意:RedisCacheConfig 这里也可以不用继承:CachingConfigurerSupport
* 这里主要我们之后要重新实现 key 的生成策略,只要这里修改 KeyGenerator,其它位置不用修改就生效了。
* 使用普通类的方式的话,那么在使用@Cacheable 的时候还需要指定 KeyGenerator 的名称;这样编码的时候比较麻烦。
*/
@Configuration
@EnableCaching //启用缓存,这个注解很重要
public class RedisCacheConfig extends CachingConfigurerSupport {
/**
* 缓存管理器
*/
@Bean
public CacheManager cacheManager(RedisTemplate<?,?> redisTemplate) {
CacheManager cacheManager = new RedisCacheManager(redisTemplate);
returncacheManager;
}
/**
* redis 模板操作类,类似于 jdbcTemplate 的一个类;
虽然 CacheManager 也能获取到 Cache 对象,但是操作起来没有那么灵活;
这里在扩展下:RedisTemplate 这个类不见得很好操作,我们可以在进行扩展一个我们自己的缓存类,比如:RedisStorage 类;
*/
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>();
redisTemplate.setConnectionFactory(factory);
/**
key 序列化方式(不然会出现乱码),但是如果方法上有 Long 等非 String 类型的话,会报类型转换错误;
所以在没有自己定义 key 生成策略的时候,以下这个代码建议不要这么写,可以不配置或者自己实现 ObjectRedisSerializer,或者 JdkSerializationRedisSerializer 序列化方式;
**/
RedisSerializer<String> redisSerializer = new StringRedisSerializer(); //Long 类型不可以会出现异常信息;
redisTemplate.setKeySerializer(redisSerializer);
redisTemplate.setHashKeySerializer(redisSerializer);
return redisTemplate;
}
/*
自定义 key.此方法将会根据类名+方法名+所有参数的值生成唯一的一个 key,
即使@Cacheable 中的 value 属性一样,key 也会不一样。
*/
@Override
public KeyGenerator keyGenerator() {
System.out.println("RedisCacheConfig.keyGenerator()");
return new KeyGenerator() {
@Override
public Object generate(Object o, Method method, Object... objects) {
StringBuilder sb = new StringBuilder();
sb.append(o.getClass().getName());
sb.append(method.getName());
for (Object obj : objects) {
sb.append(obj.toString());
}
System.out.println("keyGenerator=" + sb.toString());
return sb.toString();
}
};
}
}
-
编写业务代码
-
利用外部的redis实现session共享
21)集成shiro权限控制
22)springboot多数据源
Spring Cloud Alibaba
1.微服务介绍
1)架构演进:单体→垂直应用架构(按功能横向切分应用,不同应用中可能存在冗余,且相互独立无法通信)→分布式架构(将后端分为表现层和服务层,系统耦合度增加,调用关系复杂)→SOA架构(在表现层和服务层之间增加服务注册中心进行调节)→微服务架构 服务网格化(Service Mesh)
2)微服务架构:这么多小服务,如何管理(服务治理)、如何通讯(resful、rpc)、如何访问(网关)、出错如何处理(容错,不被上游服务压垮、不被下游服务拖垮)、如何排错(链路追踪)
3)微服务架构常见解决方案:Apache ServiceComb、Spring Cloud、Spring Cloud Alibaba
4)SpringCloud Alibaba介绍
- 主要功能
功能 | 解释 |
---|---|
服务限流降级 | 默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修 改限流降级规则,还支持查看限流降级 Metrics 监控 |
服务注册与发现 | 适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持 |
分布式配置管理 | 支持分布式系统中的外部化配置,配置更改时自动刷新 |
消息驱动能力 | 基于 Spring Cloud Stream 为微服务应用构建消息驱动能力 |
分布式事务 | 使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题 |
阿里云对象存储 | 阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任 何时间、任何地点存储和访问任意类型的数据 |
分布式任务调度 | 提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。 同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行 |
阿里云短信服务 | 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建 客户触达通道 |
- 组件
组件 | 解释 |
---|---|
Sentinel | 把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳 定性 |
Nacos | 一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台 |
RocketMQ | 一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠 的消息发布与订阅服务 |
Dubbo | Apache Dubbo™ 是一款高性能 Java RPC 框架 |
Seata | 阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案 |
Alibaba Cloud ACM | 一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心 产品 |
Alibaba Cloud OSS | |
Alibaba Cloud SchedulerX | 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精 准、高可靠、高可用的定时(基于 Cron表达式)任务调度服务 |
Alibaba Cloud SMS | 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速 搭建客户触达通道 |
2.环境搭建
模块设计:springcloud-alibaba:父工程、shop-common:公共模块(实体类)、shop-user:用户微服务、shop-product:商品微服务、shop-order:订单微服务
- 引入父工程依赖
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<groupId>com.itheima</groupId>
<artifactId>springcloud-alibaba</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF8</project.reporting.outputEncoding>
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
<spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloudalibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- spring-cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- srping-cloud-alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
版本对应
Spring Cloud Version | Spring Cloud Alibaba Version | Spring Boot Version |
---|---|---|
Spring Cloud Greenwich | 2.1.x.RELEASE | 2.1.x.RELEASE |
Spring Cloud Finchley | 2.0.x.RELEASE | 2.0.x.RELEASE |
Spring Cloud Edgware | 1.5.x.RELEASE | 1.5.x.RELEASE |
- shop-common模块
依赖:springboot jpa、lombok、fastjson、mysql驱动
实体类:用户、商品、订单
- shop-user模块
引入shop-common模块,常规配置
- shop-product、shop-order模块
引入shop-common、springboot-web模块,常规配置。订单模块通过RestTemplate调用商品模块的服务
/** shop-product 模块 **/
@RestController
@Slf4j
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/product/{pid}")
public Product product(@PathVariable("pid") Integer pid) {
Product product = productService.findByPid(pid);
log.info("查询到商品:" + JSON.toJSONString(product));
return product;
}
}
/** shop-order 模块 **/
@RestController
@Slf4j
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private OrderService orderService;
// 准备买1件商品
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
// 通过restTemplate调用商品微服务
Product product = restTemplate.getForObject("http://localhost:8081/product/" + pid, Product.class);
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.save(order);
return order;
}
}
3.Nacos服务治理
常见的注册中心:Zookeeper、Eureka(SpringCloud Netflix 闭源)、Consul(二进制可执行文件,安装部署简单)、Nacos(eureka+config)
Nacos致力于发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,可以快速实现动态服务发现、服务配置、服务元数据及流量管理。
1)搭建nacos环境
- 安装nacos
下载zip包格式,然后解压:https://github.com/alibaba/nacos/releases
- 启动nacos
cd nacos/bin
start.cmd -m standalone
- 访问nacos
http://localhost:8848/nacos
,默认账号密码均为nacos
2)将服务注册到nacos上
将shop-product、shop-order模块注册到nacos服务上
- 引入nacos客户端依赖
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 配置nacos
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
- 启动类添加启动服务客户端的注解
@SpringBootApplication
@EnableDiscoveryClient
public class Application
- 在nacos的网页的服务列表中查看服务的注册情况
3)服务发现
在shop-order模块中通过nacos的服务发现机制动态获取shop-product服务并调用
@RestController
@Slf4j
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private OrderService orderService;
@Autowired
private DiscoveryClient discoveryClient;
// 准备买1件商品
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
// 从nacos中获取服务地址
ServiceInstance serviceInstance = discoveryClient.getInstances("service-product").get(0);
String url = serviceInstance.getHost() + ":" + serviceInstance.getPort();
log.info(">>从nacos中获取到的微服务地址为:" + url);
// 通过restTemplate调用商品微服务
Product product = restTemplate.getForObject("http://" + url + "/product/" + pid, Product.class);
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.save(order);
return order;
}
}
4)实现Ribbon的负载均衡
- 在RestTemplate 的生成方法上添加@LoadBalanced注解
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
- 直接使用微服务名字, 从nacos中获取服务地址
String url = "service-product";
Product product = restTemplate.getForObject("http://" + url + "/product/" + pid, Product.class);
- 负载均衡策略
Ribbon内置了多种负载均衡策略,内部负载均衡的顶级接口为com.netflix.loadbalancer.IRule
, 具体如下所示:
策略名 | 策略描述 | 实现说明 |
---|---|---|
BestAvailableRule | 选择一个最小的并发请求的server | 逐个考察Server,如果Server被 tripped了,则忽略,在选择其中 ActiveRequestsCount最小的server |
AvailabilityFilteringRule | 过滤掉那些因为一直 连接失败的被标记为 circuit tripped的后 端server,并过滤掉 那些高并发的的后端 server(active connections 超过配 置的阈值) | 使用一个AvailabilityPredicate来包含 过滤server的逻辑,其实就就是检查 status里记录的各个server的运行状态 |
WeightedResponseTimeRule | 根据响应时间分配一 个weight,响应时间越长,weight越小,被选中的可能性越低 | 一个后台线程定期的从status里面读 取评价响应时间,为每个server计算 一个weight。Weight的计算也比较简 单responsetime 减去每个server自己 平均的responsetime是server的权 重。当刚开始运行,没有形成statas 时,使用roubine策略选择server |
RetryRule | 对选定的负载均衡策 略机上重试机制 | 在一个配置时间段内当选择server不 成功,则一直尝试使用subRule的方 式选择一个可用的server |
RoundRobinRule | 轮询方式轮询选择 server | 轮询index,选择index对应位置的 server |
RandomRule | 随机选择一个server | 在index上随机,选择index对应位置 的server |
ZoneAvoidanceRule | 复合判断server所在 区域的性能和server 的可用性选择server | 使用ZoneAvoidancePredicate和 AvailabilityPredicate来判断是否选择 某个server,前一个判断判定一个 zone的运行性能是否可用,剔除不可 用的zone(的所有server), AvailabilityPredicate用于过滤掉连接 数过多的Server |
service-product: # 调用的提供者的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
5)基于Feign的服务调用
Feign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务一样简单, 只需要创建一个接口并添加一个注解即可。
Nacos很好的兼容了Feign,Feign默认集成了 Ribbon,所以在Nacos下使用Fegin默认就实现了负载均衡的效果。
- 引入依赖
<!--fegin组件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 启动类增加开启Feign的注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // 开启Fegin
public class Application {}
- 在消费者服务的Service接口上使用Feign:shop-product的服务接口
@FeignClient("service-product") // 声明调用的提供者的name
public interface ProductService {
// @FeignClient+@GetMapping 就是一个完整的请求路径 http://serviceproduct/product/{pid}
@GetMapping(value = "/product/{pid}") // 指定调用提供者的哪个方法
Product findByPid(@PathVariable("pid") Integer pid);
}
- 调用消费者服务Service中的服务调用接口
@RestController
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private ProductService productService;
// 准备买1件商品
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
// 通过fegin调用商品微服务
Product product = productService.findByPid(pid);
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.save(order);
return order;
}
}
4.Sentinel服务容错
1)压力测试工具:Apache JMeter
2)服务雪崩
在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会 出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。
由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩效应”。雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。
3)常见容错方法
方法 | 思路 |
---|---|
隔离 | 将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故 障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的 系统服务。常见的隔离方式有:线程池隔离和信号量隔离 |
超时 | 在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应, 就断开请求,释放掉线程 |
限流 | 是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到 的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的 |
熔断 | 上游服务为了保护系统整 体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断(熔断关闭、熔断开启、半熔断) |
降级 | 为服务提供一个托底方案,一旦服务无法正常调用,就使用托底方案 |
4)常见容错组件
Sentinel | Hystrix | resilience4j | |
---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基 于 RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
流量整形 | 支持预热模式、匀速器模式、预热排队模式 | 不支持 | 简单的 Rate Limiter 模式 |
系统自适应保护 | 支持 | 不支持 | 不支持 |
控制台 | 提供开箱即用的控制台,可配置规则、 查看秒级监控、机器发现等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |
Sentinel 和 Hystrix 在熔断降级上的原则是一致的, 都是当一个资源出现问题时, 让其快速失败, 不要波及到其它服务 但是在限制的手段上, 确采取了完全不一样的方法: Hystrix 采用的是线程池隔离的方式, 优点是做到了资源之间的隔离, 缺点是增加了线程切换的成本。Sentinel 采用的是通过并发线程的数量和响应时间来对资源做限制。
5)Sentinel
- 介绍
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案。它以流量 为切入点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性。
-
特点
- 丰富的应用场景(秒杀:控制突发流量在系统容量的可承受范围内、消息削峰填谷、集群流量控制、实施熔断下游不可用服务)
- 完备的实时监控(500台以下)
- 广泛的开源生态
- 完善的SPI扩展点
-
两个组成
- 核心库(Java客户端):不依赖任何框架和库,能够运行于所有Java运行时环境,同时对Dubbo/Spring Cloud等框架有较好的支持
- 控制台(Dashboard):基于Springboot开发,打包后可以直接运行
-
基本概念
- 资源:资源就是Sentinel要保护的东西。它可以是 Java 应用程序中的任何内容,可以是一个服务,也可以是 一个方法,甚至可以是一段代码。
- 规则:用来定义如何进行保护资源的。作用在资源之上定义以什么样的方式保护资源,主要包括流量控制规则、熔断降级规则以及系统保护规则。
-
主要功能
- 流量控制:流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。 Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。
- 熔断降级:当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则 对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。Sentinel采用了两种方法:
- 通过并发线程数进行限制:Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。当某个资源 出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆 积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的 线程完成任务后才开始继续接收请求。
- 通过响应时间对资源进行降级:Sentinel 还可以通过响应时间来快速降级不稳定的资源。 当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的 时间窗口之后才重新恢复。
- 系统负载保护:当系统负载较高的时候,如果还持续让 请求进入可能会导致系统崩溃,无法响应。在集群环境下,会把本应这台机器承载的流量转发到其 它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,Sentinel 提供了对应的保 护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
-
安装Sentinel控制台
Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。Sentinel的控制台其实就是一个SpringBoot编写的程序。我们需要将我们的微服务程序注册到控制台上, 即在微服务中指定控制台的地址, 并且还要开启一个跟控制台传递数据的端口, 控制台也可以通过此端口调用微服务中的监控程序获取微服务的各种信息。
下载:下载地址
启动控制台:
# 直接使用jar命令启动项目(控制台本身是一个SpringBoot项目)
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar
- 微服务集成Sentinel
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
spring:
cloud:
sentinel:
transport:
port: 9999 # 跟控制台交流的端口,随意指定一个未使用的端口即可
dashboard: localhost:8080 # 指定控制台服务的地址
- 访问Sentinel控制台
http://localhost:8080/
账号密码均为:sentinel
-
Sentinel规则
- 流控规则:流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时 对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
点击簇点链路 → 点击对应接口的流控按钮 → 进入流控规则配置页面
资源名:唯一名称,默认是请求路径,可自定义
针对来源:指定对哪个微服务进行限流,默认指default,意思是不区分来源,全部限制
阈值类型/单机阈值:QPS(每秒请求数量): 当调用该接口的QPS达到阈值的时候,进行限流、线程数:当调用该接口的线程数达到阈值的时候,进行限流
是否集群
- 流控模式
点击上面设置流控规则的编辑按钮 → 在编辑页面点击高级选项(流控模式)
直接(默认):接口达到限流条件时,开启限流
关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步] 指定上游消费者
链路:当从某个接口过来的资源达到限流条件时,开启限流 指定上级接口,相比于关联而言,粒度更细
- 流控效果(上图)
快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果
Warm Up:它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的 1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景
排队等待:让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设 置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃
- 降级规则
降级规则就是设置当满足什么条件的时候,对服务进行降级。Sentinel提供了三个衡量条件:
平均响应时间 :当资源的平均响应时间超过阈值(以 ms 为单位)之后,资源进入准降级状态。 如果接下来 1s 内持续进入 5 个请求,它们的 RT都持续超过这个阈值,那么在接下的时间窗口 (以 s 为单位)之内,就会对这个方法进行服务降级
异常比例:当资源的每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态,即在接下的 时间窗口(以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0]
异常数 :当资源近 1 分钟的异常数目超过阈值之后会进行服务降级。注意由于统计时间窗口是分 钟级别的,若时间窗口小于 60s,则结束熔断状态后仍可能再进入熔断状态
- 热点规则
热点参数流控规则是一种更细粒度的流控规则, 它允许将规则具体到参数上
引入注解:
@RequestMapping("/order/message")
@SentinelResource("message") //注意这里必须使用这个注解标识,热点规则不生效
public String message(String name, Integer age) {
return name + age;
}
配置热点规则:
- 授权规则
要根据调用来源来判断该次请求是否允许放行,使用 Sentinel 的来源访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过:
若配置白名单,则只有请求来源位于白名单内时才可通过; 若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过。
自定义来源处理规则:
Sentinel提供了 RequestOriginParser 接口来处理来源。 只要Sentinel保护的接口资源被访问,Sentinel就会调用 RequestOriginParser 的实现类去解析访问来源。
@Component
public class RequestOriginParserDefinition implements RequestOriginParser{
@Override
public String parseOrigin(HttpServletRequest request) {
String serviceName = request.getParameter("serviceName");
return serviceName;
}
}
授权规则配置
- 系统规则
- Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过 系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般 是 CPU cores * 2.5
- RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒
- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护
- CPU使用率:当单台机器上所有入口流量的 CPU使用率达到阈值即触发系统保护
从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 、CPU 使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。 系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效
- 自定义异常返回
@Data
@AllArgsConstructor
@NoArgsConstructor
class ResponseData {
private int code;
private String message;
}
// 异常处理页面
@Component
public class ExceptionHandlerPage implements UrlBlockHandler {
// BlockException 异常接口,包含Sentinel的五个异常
// FlowException 限流异常
// DegradeException 降级异常
// ParamFlowException 参数限流异常
// AuthorityException 授权异常
// SystemBlockException 系统负载异常
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException e) throws IOException {
response.setContentType("application/json;charset=utf-8");
ResponseData data = null;
if (e instanceof FlowException) {
data = new ResponseData(-1, "接口被限流了...");
} else if (e instanceof DegradeException) {
data = new ResponseData(-2, "接口被降级了...");
}
response.getWriter().write(JSON.toJSONString(data));
}
}
- @SentinelResource
在定义了资源点之后,我们可以通过Dashboard来设置限流和降级策略来对资源点进行保护。同时还能 通过@SentinelResource来指定出现异常时的处理策略。
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。其主要参数如下:
(1)value:资源名称
(2)entryType:entry类型,标记流量的方向,取值IN/OUT,默认是OUT
(3)blockHandler:处理BlockException的函数名称,函数要求:
1. 必须是 public
2.返回类型 参数与原方法一致
3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置blockHandlerClass ,并指定blockHandlerClass里面的方法。
(4)blockHandlerClass:存放blockHandler的类,对应的处理函数必须static修饰
(5)fallback:用于在抛出异常的时候提供fallback处理逻辑。fallback函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。函数要求:
1. 返回类型与原方法一致
2. 参数类型需要和原方法相匹配
3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定fallbackClass里面的方法。
(6)fallbackClass:存放fallback的类。对应的处理函数必须static修饰
(7)defaultFallback:用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的异常进行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函数要求:
1. 返回类型与原方法一致
2. 方法参数列表为空,或者有一个 Throwable 类型的参数。
3. 默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定 fallbackClass 里面的方法。
(8)exceptionsToIgnore:指定排除掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。
(9)exceptionsToTrace:需要trace的异常
定义限流和降级后的处理方法:
/** 方式一:直接将限流和降级方法定义在方法中 **/
@Service
@Slf4j
public class OrderServiceImpl {
int i = 0;
@SentinelResource(
value = "message",
blockHandler = "blockHandler", // 指定发生BlockException时进入的方法
fallback = "fallback" // 指定发生Throwable时进入的方法
)
public String message() {
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message";
}
// BlockException时进入的方法
public String blockHandler(BlockException ex) {
log.error("{}", ex);
return "接口被限流或者降级了...";
}
// Throwable时进入的方法
public String fallback(Throwable throwable) {
log.error("{}", throwable);
return "接口发生异常了...";
}
}
}
/** 方式二: 将限流和降级方法外置到单独的类中 **/
@Service
@Slf4j
public class OrderServiceImpl {
int i = 0;
@SentinelResource(
value = "message",
blockHandlerClass = OrderServiceImplBlockHandlerClass.class,
blockHandler = "blockHandler",
fallbackClass = OrderServiceImplFallbackClass.class,
fallback = "fallback"
)
public String message() {
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message4";
}
}
@Slf4j
public class OrderServiceImplBlockHandlerClass {
// 注意这里必须使用static修饰方法
public static String blockHandler(BlockException ex) {
log.error("{}", ex);
return "接口被限流或者降级了...";
}
}
@Slf4j
public class OrderServiceImplFallbackClass {
// 注意这里必须使用static修饰方法
public static String fallback(Throwable throwable) {
log.error("{}", throwable);
return "接口发生异常了...";
}
}
- Sentinel规则持久化
通过Dashboard来为每个Sentinel客户端设置各种各样的规 则,但是这里有一个问题,就是这些规则默认是存放在内存中,极不稳定,所以需要将其持久化。本地文件数据源会定时轮询文件的变更,读取规则。这样我们既可以在应用本地直接修改文件来更 新规则,也可以通过 Sentinel 控制台推送规则。
首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的 规则保存到本地的文件中
编写处理类
// 规则持久化
public class FilePersistence implements InitFunc {
@Value("spring.application:name")
private String appcationName;
@Override
public void init() throws Exception {
String ruleDir = System.getProperty("user.home") + "/sentinelrules/"+appcationName;
String flowRulePath = ruleDir + "/flow-rule.json";
String degradeRulePath = ruleDir + "/degrade-rule.json";
String systemRulePath = ruleDir + "/system-rule.json";
String authorityRulePath = ruleDir + "/authority-rule.json";
String paramFlowRulePath = ruleDir + "/param-flow-rule.json";
this.mkdirIfNotExits(ruleDir);
this.createFileIfNotExits(flowRulePath);
this.createFileIfNotExits(degradeRulePath);
this.createFileIfNotExits(systemRulePath);
this.createFileIfNotExits(authorityRulePath);
this.createFileIfNotExits(paramFlowRulePath);
// 流控规则
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>(
flowRulePath,flowRuleListParser);
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>(flowRulePath,this::encodeJson);
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
// 降级规则
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(
degradeRulePath,degradeRuleListParser);
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(degradeRulePath,this::encodeJson);
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
// 系统规则
ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>(systemRulePath,
systemRuleListParser);
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>(systemRulePath,this::encodeJson);
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
// 授权规则
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>(authorityRulePath,
authorityRuleListParser);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>(authorityRulePath,
this::encodeJson);
WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
// 热点参数规则
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>(
paramFlowRulePath,
paramFlowRuleListParser);
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>(
paramFlowRulePath,this::encodeJson);
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
}
private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<FlowRule>>() {
}
);
private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<DegradeRule>>() {
}
);
private Converter<String, List<SystemRule>> systemRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<SystemRule>>() {
}
);
private Converter<String, List<AuthorityRule>> authorityRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<AuthorityRule>>() {
}
);
private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<ParamFlowRule>>() {
}
);
private void mkdirIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
}
private void createFileIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
}
添加配置:
在resources下创建配置目录META-INF/services
,然后添加文件com.alibaba.csp.sentinel.init.InitFunc
,在文件中添加配置类的全路径:
com.itheima.config.FilePersistence
- Feign整合Sentinel
引入依赖:
<!--sentinel客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
在配置文件中开启Feign对Sentinel的支持:
feign:
sentinel:
enabled: true
创建容错类:
// 容错类要求必须实现被容错的接口,并为每个方法实现容错方案
@Component
@Slf4j
public class ProductServiceFallBack implements ProductService {
@Override
public Product findByPid(Integer pid) {
Product product = new Product();
product.setPid(-1);
return product;
}
}
为被容器的接口指定容错类:
//value用于指定调用nacos下哪个微服务,fallback用于指定容错类
@FeignClient(value = "service-product", fallback = ProductServiceFallBack.class)
public interface ProductService {
@RequestMapping("/product/{pid}") // 指定请求的URI部分
Product findByPid(@PathVariable Integer pid);
}
编写Controller
通过fallbackFactory拿过具体的错误类型(与fallback只能使用其中之一)
5.Gateway服务网关
API网关是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等。
常用服务网关:Ngnix+lua、Kong、Zuul(Netflix原生API网关)、Spring Cloud Gateway
1)Gateway简介
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术 开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流。
优点:性能强(是第一代Zuul的1.6倍)、功能强(转发、监控、限流等)、设计优雅易于扩展
缺点:其实现依赖于Netty和WebFlux,不是传统的Servlet编程,学习成本高、不能部署在Tomcat等Servlet容器里,只能打jar包运行,Springboot 2.0以上的版本才支持
2)Gateway基本概念
-
路由:gateway中的基本组件,表示一个具体的路由信息载体
- id
- uri:目的地uri,即客户端请求最终被转发到的微服务
- order:用于多个路由之间的排序,数值越小排序越靠前,匹配优先级越高
- predicate:断言,进行条件判断,断言都为真的才执行路由
- filter:过滤器,用于修改请求和相应信息
-
执行流程
- Gateway Client向Gateway Server发送请求
- 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
- 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping
- RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用
- 如果过断言成功,由FilteringWebHandler创建过滤器链并调用
- 请求会一次经过PreFilter–微服务–PostFilter的方法,最终返回响应
3)快速入门
实现通过客户端访问API网关,再通过网关将请求转发到微服务
- 引入依赖
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 创建主类
@SpringBootApplication
@EnableDiscoveryClient
public class Application {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
- 修改配置文件
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
- 访问:
{网关ip}:{网关port}/{微服务名}/{接口地址}
,如:localhost:7000/service-product/product/1
4)断言
- 内置断言工厂
SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配
断言工厂 | 条件 |
---|---|
AfterRoutePredicateFactory | 接收一个日期参数,判断请求日期是否晚于指定日期 -After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai] |
BeforeRoutePredicateFactory | 接收一个日期参数,判断请求日期是否早于指定日期 |
BetweenRoutePredicateFactory | 接收两个日期参数,判断请求日期是否在指定时间段内 |
RemoteAddrRoutePredicateFactory | 接收一个IP地址段,判断请求主 机地址是否在地址段中 -RemoteAddr=192.168.1.1/24 |
CookieRoutePredicateFactory | 接收两个参数,cookie 名字和一个正则表达式。 判断请求 cookie是否具有给定名称且值与正则表达式匹配 -Cookie=chocolate, ch. |
HeaderRoutePredicateFactory | 接收两个参数,标题名称和正则表达式。 判断请求Header是否 具有给定名称且值与正则表达式匹配 -Header=X-Request-Id, \d+ |
HostRoutePredicateFactory | 接收一个参数,主机名模式。判断请求的Host是否满足匹配规则 -Host=**.testhost.org |
MethodRoutePredicateFactory | 接收一个参数,判断请求类型是否跟指定的类型匹配 -Method=GET |
PathRoutePredicateFactory | 接收一个参数,判断请求的URI部分是否满足路径规则 -Path=/foo/{segment} |
QueryRoutePredicateFactory | 接收两个参数,请求param和正则表达式, 判断请求参数是否具 有给定名称且值与正则表达式匹配 -Query=baz, ba. |
WeightRoutePredicateFactory | 接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发 |
内置断言工厂的使用:
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 服务service的地址
gateway:
discovery:
locator:
enabled: true
routes: # 添加路由
- id: product_route
uri: lb://service-product # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
predicates: # 为路由配置断言
- Path=/product-serv/**
- Before=2019-11-28T00:00:00.000+08:00 # 1.限制请求时间在2019-11-28之前
- Method=POST # 2.限制请求方式为POST
filters:
- StripPrefix=1
- 自定义断言工厂
在配置文件中,给路由配置自定义断言
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: product-route
uri: lb://service-product
predicates:
- Path=/product-serv/**
- Age=18,60 # 自定义断言:限制年龄只有在18到60岁之间的人能访问
filters:
- StripPrefix=1
自定义一个断言工厂, 实现断言方法
// 泛型用于接收一个配置类,配置类用于接收中配置文件中的配置
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
// 用于从配置文件中获取参数值赋值到配置类中的属性上
@Override
public List<String> shortcutFieldOrder() {
// 这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("minAge", "maxAge");
}
// 断言
@Override
public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
// 从serverWebExchange获取传入的参数
String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age");
if (StringUtils.isNotEmpty(ageStr)) {
int age = Integer.parseInt(ageStr);
return age > config.getMinAge() && age < config.getMaxAge();
}
return true;
}
};
}
}
自定义配置类
// 自定义一个配置类, 用于接收配置文件中的参数
@Data
class Config {
private int minAge;
private int maxAge;
}
测试:
http://Tocalhost:7000/product-serv/product/1?age=30
http://Tocalhost:7000/product-serv/product/1?age=70
5)过滤器
-
过滤器的生命周期(时机)
- PRE: 在请求被路由之前调用。可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等
- POST:在微服务响应请求以后执行。可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
-
局部过滤器:针对单个路由的过滤器
- 内置局部过滤器
过滤器工厂 作用 参数 AddRequestHeader 为原始请求添加Header Header的名称及值 AddRequestParameter 为原始请求添加请求参数 参数名称及值 AddResponseHeader 为原始响应添加Header Header的名称及值 DedupeResponseHeader 剔除响应头中重复的值 需要去重的Header名称及去重策略 Hystrix 为路由引入Hystrix的断路器保护 HystrixCommand的名称 FallbackHeaders 为fallbackUri的请求头中添加具体的异常信息 Header的名称 PrefixPath 为原始请求路径添加前缀 前缀路径 PreserveHostHeader 为请求添加一个 preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host 无 RequestRateLimiter 用于对请求限流,限流算法为令牌桶 keyResolver、rateLimiter、statusCode、 denyEmptyKey、emptyKeyStatus RedirectTo 将原始请求重定向到指定的URL http状态码及重定向的 url RemoveHopByHopHeadersFilter 为原始请求删除IETF组织规定的一系列Header 默认就会启用,可以通过配置指定仅删除哪些 Header RemoveRequestHeader 为原始请求删除某个Header Header名称 RemoveResponseHeader 为原始响应删除某个Header Header名称 RewritePath 重写原始的请求路径 原始路径正则表达式以 及重写后路径的正则表 达式 RewriteResponseHeader 重写原始响应中的某个Header Header名称,值的正则表达式,重写后的值 SaveSession 在转发请求之前,强制执行WebSession::save操作 无 secureHeaders 为原始响应添加一系列起安全作用的响应头 无,支持修改这些安全响应头的值 SetPath 修改原始的请求路径 修改后的路径 SetResponseHeader 修改原始响应中某个Header的值 Header名称,修改后的值 SetStatus 修改原始响应的状态码 HTTP 状态码,可以是数字,也可以是字符串 StripPrefix 用于截断原始请求的路径 使用数字表示要截断的 路径的数量 Retry 针对不同的响应进行重试 retries、statuses、 methods、series RequestSize 设置允许接收最大请求包的大 小。如果请求包大小超过设置的 值,则返回 413 Payload Too Large 请求包大小,单位为字 节,默认值为5M ModifyRequestBody 在转发请求之前修改原始请求体内容 修改后的请求体内容 ModifyResponseBody 修改原始响应体的内容 修改后的响应体内容 内置过滤器使用:
server: port: 7000 spring: application: name: api-gateway cloud: nacos: discovery: server-addr: localhost:8848 gateway: discovery: locator: enabled: true routes: - id: product_route uri: lb://service-product order: 1 predicates: - Path=/product-serv/** filters: - StripPrefix=1 - SetStatus=2000 # 修改返回状态
- 自定义局部过滤器
配置文件中添加自定义过滤器的配置
spring: application: name: gateway cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: discovery: locator: enabled: true routes: - id: consumer order: -1 uri: lb://consumer predicates: - Path=/consumer-serv/** filters: - StripPrefix=1 - Log=true,false # 自定义过滤器:控制日志是否开启
自定义过滤器工厂的实现
@Component public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> { public LogGatewayFilterFactory() { super(LogGatewayFilterFactory.Config.class); } // 读取配置文件中的参数 赋值到 配置类中 @Override public List<String> shortcutFieldOrder() { return Arrays.asList("consoleLog", "cacheLog"); } // 过滤器逻辑 @Override public GatewayFilter apply(LogGatewayFilterFactory.Config config) { return new GatewayFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { if (config.isCacheLog()) { System.out.println("cacheLog已经开启了...."); } if (config.isConsoleLog()) { System.out.println("consoleLog已经开启了...."); } return chain.filter(exchange); } }; } }
自定义配置类,接收配置参数
@Data @NoArgsConstructor public static class Config { private boolean consoleLog; private boolean cacheLog; }
-
全局过滤器:全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。
- 内置全局过滤器
- 自定义全局过滤器
示例逻辑:当客户端第一次请求服务时,服务端对用户进行信息认证(登录)、认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证、以后每次请求,客户端都携带认证的token、服务端对token进行解密,判断是否有效
自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求 参数“token”则不转发路由,否则执行正常的逻辑。
// 自定义全局过滤器需要实现GlobalFilter和Ordered接口
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
// 完成判断逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (StringUtils.isBlank(token)) {
System.out.println("鉴权失败");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 调用chain.filter继续向下游执行
return chain.filter(exchange);
}
// 执行顺序,数值越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
6)API网关限流
Sentinel支持对SpringCloud Gateway、Zuul等主流网关进行限流。从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流:
route维度:即在Spring配置文件中配置的路由条目,资源名为对应的routeId
自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组
- 导入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
- 编写配置类
基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的 SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>>viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
// 配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(
new GatewayFlowRule("product_route") // 资源名称,对应路由id
.setCount(1) // 限流阈值
.setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
);
GatewayRuleManager.loadRules(rules);
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK);
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
- 自定义API分组
自定义API分组是一种更细粒度的限流规则定义
6.Sleuth链路追踪
1)分布式链路追踪
在大型系统的微服务化构建中,一个系统被拆分成了许多模块。这些模块负责不同的功能,组合成系统,最终可以提供丰富的功能。在这种架构中,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心,也就意味着这种架构形式也会存在一些问题:
如何快速发现问题? 如何判断故障影响范围? 如何梳理服务依赖以及依赖的合理性? 如何分析链路性能问题以及实时容量规划?
分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记 录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪 台机器上、每个服务节点的请求状态等等。
常见的链路追踪技术:cat(大众点评开源)、zipkin(Twitter公司开源)、pinpoint、skywalking、Sleuth
SpringCloud alibaba技术栈中并没有提供自己的链路追踪技术,我们可以采用Sleuth + Zinkin来做链路追踪解决方案
2)Sleuth
-
相关概念
- Trace:由一组Trace Id相同的Span串联形成一个树状结构。为了实现请求跟踪,当请求到达分布式系统的入口端点时,只需要服务跟踪框架为该请求创建一个唯一的标识(即TraceId),同时在分布式系统内部流转的时候,框架始终保持传递该唯一值,直到整个请求的返回。那么我们就可以使用该唯 一标识将所有的请求串联起来,形成一条完整的请求链路。
- Span :代表了一组基本的工作单元。为了统计各处理单元的延迟,当请求到达各个服务组件的时 候,也通过一个唯一标识(SpanId)来标记它的开始、具体过程和结束。通过SpanId的开始和结束时间戳,就能统计该span的调用时间,除此之外,我们还可以获取如事件的名称、请求信息等元数据。
- Annotation:用它记录一段时间内的事件,内部使用的重要注释
- cs(Client Send)客户端发出请求,开始一个请求的声明
- sr(Server Received)服务端接受到请求开始进行处理, sr-cs = 网络延迟(服务调用的时间)
- ss(Server Send)服务端处理完毕准备发送到客户端,ss - sr = 服务器上的请求处理时间
- cr(Client Reveived)客户端接受到服务端的响应,请求结束。 cr - sr = 请求的总时间
-
集成Sleuth
- 引入依赖:在父工程中引入Sleuth依赖
<!--链路追踪 Sleuth--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency>
- Sleuth的日志输出
输出格式:[服务名称,TraceId,SpanId,是否输出到第三方平台] e.g. [api-gateway,3977125f73391553,3977125f73391553,false]
查看日志文件并不是一个很好的方法,当微服务越来越多日志文件也会越来越多,通过Zipkin可以 将日志聚合,并进行可视化展示和全文检索。
3)Zipkin
- 介绍
Zipkin 是 Twitter 的一个开源项目,基于Google Dapper实现,致力于收集服务的定时数据, 以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。
我们使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的REST API接口来辅助我们查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系 统性能瓶颈的根源。
除了面向开发的 API 接口之外,它也提供了方便的UI组件来帮助我们直观的搜索跟踪信息和分析请求链路明细,比如:可以查询某段时间内各用户请求的处理时间等。
- 可拔插的数据存储方式:In-Memory、MySql、Cassandra 、 Elasticsearch
- 架构
- Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为 Zipkin内部处理的 Span 格式,以支持后续的存储、分析、展示等功能
- Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中, 我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中
- RESTful API:API 组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接 系统访问以实现监控等
- Web UI:UI 组件, 基于API组件实现的上层应用。通过UI组件用户可以方便而有直观地查询和分 析跟踪信息
Zipkin分为两端,一个是 Zipkin服务端,一个是 Zipkin客户端,客户端也就是微服务的应用。 客户端会 配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth 的监听器监 听,并生成相应的 Trace 和 Span 信息发送给服务端。
-
ZipKin服务端安装
- 下载ZipKin的jar包:
https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec
- 启动ZipKin Server:
java -jar zipkin-server-2.12.9-exec.jar
- 访问Web UI:
http://localhost:9411
- 下载ZipKin的jar包:
-
ZipKin客户端集成
- 在每个微服务上添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
- 在每个微服务上添加配置
spring: zipkin: base-url: http://127.0.0.1:9411/ # zipkin server的请求地址 discoveryClientEnabled: false # 让nacos把它当成一个URL,而不要当做服务名 sleuth: sampler: probability: 1.0 # 采样的百分比
- 访问服务测试
-
ZipKin持久化
-
mysql持久化
- 创建mysql数据环境
CREATE TABLE IF NOT EXISTS zipkin_spans ( `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit', `trace_id` BIGINT NOT NULL, `id` BIGINT NOT NULL, `name` VARCHAR(255) NOT NULL, `parent_id` BIGINT, `debug` BIT(1), `start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL', `duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query' ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci; ALTER TABLE zipkin_spans ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `id`) COMMENT 'ignore insert on duplicate'; ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`, `id`) COMMENT 'for joining with zipkin_annotations'; ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds'; ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames'; ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range'; CREATE TABLE IF NOT EXISTS zipkin_annotations ( `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit', `trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id', `span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id', `a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1', `a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB', `a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation', `a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp', `endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null', `endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address', `endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null', `endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null' ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci; ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate'; ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans'; ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds'; ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames'; ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces'; ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces'; ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job'; CREATE TABLE IF NOT EXISTS zipkin_dependencies ( `day` DATE NOT NULL, `parent` VARCHAR(255) NOT NULL, `child` VARCHAR(255) NOT NULL, `call_count` BIGINT ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci; ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(`day`, `parent`, `child`);
- 启动ZipKin Server时,指定数据保存的mysql信息
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=root
-
elasticsearch持久化
- 下载elasticsearch:
https://www.elastic.co/cn/downloads/past-releases/elasticsearch-6-8-4
- 启动elasticsearch
cd elasticsearch-6-8-4\bin elasticsearch.bat
- 启动ZipKin Server时,指定数据保存的elasticsearch信息
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=elasticsearch --ESHOST=localhost:9200
- 下载elasticsearch:
-
7.RocketMQ消息驱动
1)消息队列
消息队列MQ(Message Queue)是一种跨进程的通信机制,用于传递消息。
使用场景:异步解耦(生产者的生产速度和消费者的消费速度不匹配)、流量削峰(上游请求激增、下游服务无法承受,在两者之间加入消息队列)
常见MQ产品:ZeroMQ(号称最快、C实现、对socket库的封装、非持久性)、RabbitMQ(erlang实现、适合企业级开发、不利于二次开发和维护)、ActiveMQ(Apache 可与spring-jms融合 在队列数较多时情况不好)、RocketMQ(阿里巴巴 java开发 使用简单)、Kafka(Apache 高性能 分布式)
2)RocketMQ
- 架构与概念
- Broker(邮递员):Broker是RocketMQ的核心,负责消息的接收,存储,投递等功能
- NameServer(邮局):消息队列的协调者,Broker向它注册路由信息,同时Producer和Consumer向其获取路由信息
- Producer(寄件人):消息的生产者,需要从NameServer获取Broker信息,然后与Broker建立连接,向Broker发送消息
- Consumer(收件人):消息的消费者,需要从NameServer获取Broker信息,然后与Broker建立连接,从Broker获取消息
- Topic(地区):用来区分不同类型的消息,发送和接收消息前都需要先创建Topic,针对Topic来发送和接收消息
- Message Queue(邮件):为了提高性能和吞吐量,引入了Message Queue,一个Topic可以设置一个或多个Message Queue,这样消息就可以并行往各个Message Queue发送消息,消费者也可以并行的从多个 Message Queue读取消息
- Message:消息的载体
- Producer Group:生产者组,简单来说就是多个发送同一类消息的生产者称之为一个生产者组
- Consumer Group:消费者组,消费同一类消息的多个 consumer 实例组成一个消费者组
-
环境准备:在Linux系统中安装RocketMQ
-
下载:
http://rocketmq.apache.org/release_notes/release-notes-4.4.0/
-
安装:将文件上传到linux系统
ls /usr/local/src/ # rocketmq-all-4.4.0-bin-release.zip
-
解压
unzip rocketmq-all-4.4.0-bin-release.zip mv rocketmq-all-4.4.0-bin-release ../rocketmq
-
启动
# 进入安装目录 # 启动NameServer nohup ./bin/mqnamesrv & # 启动Broker nohup bin/mqbroker -n localhost:9876 &
-
测试
# 消息发送 export NAMESRV_ADDR=localhost:9876 bin/tools.sh # 消息接收 export NAMESRV_ADDR=localhost:9876 bin/tools.sh
-
关闭
bin/mqshutdown broker bin/mqshutdown namesrv
-
-
RocketMQ控制台安装
- 下载:
https://github.com/apache/rocketmq-externals/releases
- 修改配置文件
vim rocketmq-console\src\main\resources\application.properties server.port=7777 # 项目启动后的端口号 rocketmq.config.namesrvAddr=192.168.109.131:9876 # nameserv的地址,注意防火墙要开启9876端口
- 打包启动
# 进入控制台项目,将工程打成jar包 mvn clean package -Dmaven.test.skip=true # 启动控制台 java -jar target/rocketmq-console-ng-1.0.0.jar
- 访问控制台:
localhost:7777/#/
- 下载:
-
消息收发示例
- 引入依赖
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.0.2</version> </dependency>
- 发送消息
public class RocketMQSendTest { public static void main(String[] args) throws Exception { // 1. 创建消息生产者, 指定生产者所属的组名 DefaultMQProducer producer = new DefaultMQProducer("myproducer-group"); // 2. 指定Nameserver地址 producer.setNamesrvAddr("192.168.109.131:9876"); // 3. 启动生产者 producer.start(); // 4. 创建消息对象,指定主题、标签和消息体 Message msg = new Message("myTopic", "myTag", ("RocketMQ Message").getBytes()); // 5. 发送消息 SendResult sendResult = producer.send(msg,10000); System.out.println(sendResult); // 6. 关闭生产者 producer.shutdown(); } }
- 接收消息
public class RocketMQReceiveTest { public static void main(String[] args) throws MQClientException { // 1. 创建消息消费者, 指定消费者所属的组名 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("myconsumergroup"); // 2. 指定Nameserver地址 consumer.setNamesrvAddr("192.168.109.131:9876"); // 3. 指定消费者订阅的主题和标签 consumer.subscribe("myTopic", "*"); // 4. 设置回调函数,编写处理消息的方法 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { System.out.println("Receive New Messages: " + msgs); // 返回消费状态 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); // 5. 启动消息消费者 consumer.start(); System.out.println("Consumer Started."); } }
-
示例:模拟下单成功后,向用户发送短信
-
shop-order
模块:生产者- 引入依赖
<!--rocketmq--> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.0.2</version> </dependency> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.4.0</version> </dependency>
- 添加配置
rocketmq: name-server: 192.168.109.131:9876 # rocketMQ服务的地址 producer: group: shop-order # 生产者组
- 消息推送
@RestController @Slf4j public class OrderController2 { @Autowired private OrderService orderService; @Autowired private ProductService productService; @Autowired private RocketMQTemplate rocketMQTemplate; // 准备买1件商品 @GetMapping("/order/prod/{pid}") public Order order(@PathVariable("pid") Integer pid) { log.info(">>客户下单,这时候要调用商品微服务查询商品信息"); // 通过fegin调用商品微服务 Product product = productService.findByPid(pid); if (product == null){ Order order = new Order(); order.setPname("下单失败"); return order; } log.info(">>商品信息,查询结果:" + JSON.toJSONString(product)); Order order = new Order(); order.setUid(1); order.setUsername("测试用户"); order.setPid(product.getPid()); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1); orderService.save(order); // 下单成功之后,将消息放到mq中 rocketMQTemplate.convertAndSend("order-topic", order); return order; } }
-
shop-user
模块:消费者- 引入依赖(同上)、添加配置(同上)
- 接收消息
@Slf4j @Service @RocketMQMessageListener(consumerGroup = "shop-user", topic = "order-topic") public class SmsService implements RocketMQListener<Order> { @Override public void onMessage(Order order) { log.info("收到一个订单信息{},接下来发送短信", JSON.toJSONString(order)); // 发送短信 } }
-
-
消息的类型
-
普通消息
- 可靠同步发送:消息发送方发出数据后,会在收到接收方发回响应之后才发下一个数据包的通讯方式。此种方式应用场景非常广泛,例如重要通知邮件、报名短信通知、营销短信系统等。
- 可靠异步发送:异步发送是指发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。发送 方通过回调接口接收服务器响应,并对响应结果进行处理。异步发送一般用于链路耗时较长,对 RT 响应时间较为敏感的业务场景,例如用户视频上传后通知 启动转码服务,转码完成后通知推送转码结果等。
- 单向发送:发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不 等待应答。适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。
发送方式 发送 TPS 发送结果反馈 可靠性 同步发送 快 有 不丢失 异步发送 快 有 不丢失 单向发送 最快 无 可能丢失 /** 注意这是在JUnit单元测试中进行的 **/ @RunWith(SpringRunner.class) @SpringBootTest(classes = OrderApplication.class) public class MessageTypeTest { @Autowired private RocketMQTemplate rocketMQTemplate; // 同步消息 @Test public void testSyncSend() { // 参数一: topic, 如果想添加tag 可以使用"topic:tag"的写法 // 参数二: 消息内容 SendResult sendResult = rocketMQTemplate.syncSend("test-topic-1", "这是一条同步消息"); System.out.println(sendResult); } // 异步消息 @Test public void testAsyncSend() throws InterruptedException { public void testSyncSendMsg() { // 参数一: topic, 如果想添加tag 可以使用"topic:tag"的写法 // 参数二: 消息内容 // 参数三: 回调函数, 处理返回结果 rocketMQTemplate.asyncSend("test-topic-1", "这是一条异步消息", new SendCallback() { @Override public void onSuccess(SendResult sendResult) { System.out.println(sendResult); } @Override public void onException(Throwable throwable) { System.out.println(throwable); } }); // 让线程不要终止 Thread.sleep(30000000); } // 单向消息 @Test public void testOneWay() { rocketMQTemplate.sendOneWay("test-topic-1", "这是一条单向消息"); } }
-
顺序消息:消息队列提供的一种严格按照顺序来发布和消费的消息类型
/** 给出发送同步 顺序消息的示例, 异步顺序、单向顺序消息的写法类似 **/ public void testSyncSendOrderly() { // 第三个参数用于队列的选择 rocketMQTemplate.syncSendOrderly("test-topic-1", "这是一条异步顺序消息", "xxxx"); }
-
事务消息:通过事务消息就能达到分布式事务的最终一致
- 事务消息交互流程
- 两个概念
- 半事务消息:暂不能投递的消息,发送方已经成功地将消息发送到了RocketMQ服务端,但是服务 端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的 消息即半事务消息
- 消息回查:由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失, RocketMQ服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该 消息的最终状态(Commit 或是 Rollback),该询问过程即消息回查
- 事务消息发送步骤
- 发送方将半事务消息发送至RocketMQ服务端
- RocketMQ服务端将消息持久化之后,向发送方返回Ack确认消息已经发送成功,此时消息为半事务消息
- 发送方开始执行本地事务逻辑
- 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息
- 事务消息回查步骤
- 在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达服务端,经过固定时 间后服务端将对该消息发起消息回查
- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行操作
- 示例
// 事物日志 @Entity(name = "shop_txlog") @Data public class TxLog { @Id private String txLogId; private String content; private Date date; }
// 消息发送 @Service public class OrderServiceImpl { @Autowired private OrderDao orderDao; @Autowired private TxLogDao txLogDao; @Autowired private RocketMQTemplate rocketMQTemplate; public void createOrderBefore(Order order) { String txId = UUID.randomUUID().toString(); // 发送半事务消息 rocketMQTemplate.sendMessageInTransaction( "tx_producer_group", "tx_topic", MessageBuilder.withPayload(order).setHeader("txId", txId).build(), order); } // 本地事物 @Transactional public void createOrder(String txId, Order order) { // 本地事物代码 orderDao.save(order); // 记录日志到数据库,回查使用 TxLog txLog = new TxLog(); txLog.setTxLogId(txId); txLog.setContent("事物测试"); txLog.setDate(new Date()); txLogDao.save(txLog); } }
/** 实际执行 **/ @RocketMQTransactionListener(txProducerGroup = "tx_producer_group") public class OrderServiceImplListener implements RocketMQLocalTransactionListener { @Autowired private TxLogDao txLogDao; @Autowired private OrderServiceImpl orderServiceImpl; // 执行本地事务 @Override public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) { try { // 本地事务 orderServiceImpl.createOrder((String) msg.getHeaders().get("txId"),(Order) arg); return RocketMQLocalTransactionState.COMMIT; } catch (Exception e) { return RocketMQLocalTransactionState.ROLLBACK; } } // 消息回查 @Override public RocketMQLocalTransactionState checkLocalTransaction(Message msg) { // 查询日志记录 TxLog txLog = txLogDao.findById((String) msg.getHeaders().get("txId")).get(); if (txLog == null) { return RocketMQLocalTransactionState.COMMIT; } else { return RocketMQLocalTransactionState.ROLLBACK; } } }
-
集群和广播消息
- 广播消费: 每个消费者实例都会收到消息,也就是一条消息可以被每个消费者实例处理
- 集群消费: 一条消息只能被一个消费者实例消费(默认)
@RocketMQMessageListener( consumerGroup = "shop", // 消费者分组 topic = "order-topic", // 要消费的主题 consumeMode = ConsumeMode.CONCURRENTLY, // 消费模式:无序和有序 messageModel = MessageModel.CLUSTERING, // 消息模式:广播和集群,默认是集群 ) public class SmsService implements RocketMQListener<Order> {}
-
8.SMS短信服务
阿里云短信服务(Short Message Service)具有短信通知、短信验证码、推广短信、异步通知、数据统计等功能,具有覆盖全面、高并发处理、消息堆积处理、开发管理简单、智能监控调度等特性,适用于:短信验证码、系统消息推送、推广短信等场景
Java程序 → 阿里云短信服务 → 三大运营商 → 用户手机
1)阿里云短信服务的开通和使用
注册阿里云 → 开通短信服务 → 获取AcessKey → 申请短信签名 → 申请短信模板 → 短信接口配置 → 发送短信
2)SMS API
-
发送短信:SendSms
- 请求参数
名称 类型 是否必选 示例值 描述 PhoneNumbers String 是 15900000000 接收短信的手机号码 SignName String 是 阿里云 短信签名名称 TemplateCode String 是 SMS_153055065 短信模板ID TemplateParam String 否 {“code”:“1111”} 短信模板变量的值,JSON格式 - 返回数据
名称 类型 示例值 描述 BizId String 900619746936498440^0 发送回执ID,可根据它查询具体的发送状态 Code String OK 请求状态码。返回OK代表请求成功 Message String OK 状态码的描述 RequestId String F655A8D5-B967-440B8683 请求ID -
短信查询:QuerySendDetails
- 请求参数
名称 类型 是否必选 示例值 描述 CurrentPage Long 是 1 分页查看,指定发送记录的的当前页码 PageSize Long 是 10 分页查看,指定每页显示的短信记录数量 PhoneNumber String 是 15900000000 接收短信的手机号码 SendDate String 是 20181228 短信发送日期,支持查询最近30 天的记录 BizId String 否 134523^4351232 发送回执ID,即发送流水号 - 返回数据
名称 类型 示例值 描述 Code String OK 请求状态码。返回OK代表请求成功 Message String OK 状态码的描述 RequestId String F655A8D5-B967-440B8683 请求ID TotalCount String 1 短信发送总条数 SmsSendDetailDTOs Array 短信发送明细
3)功能测试
- 引入依赖
<!--短信发送-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alicloud-sms</artifactId>
</dependency>
- Demo
public class SmsDemo {
// 产品名称:云通信短信API产品,开发者无需替换
static final String product = "Dysmsapi";
// 产品域名,开发者无需替换
static final String domain = "dysmsapi.aliyuncs.com";
// TODO 此处需要替换成开发者自己的AK(在阿里云访问控制台寻找)
static final String accessKeyId = "yourAccessKeyId";
static final String accessKeySecret = "yourAccessKeySecret";
// 短信发送
public static SendSmsResponse sendSms() throws ClientException {
// 可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
// 初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
// 组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();
// 必填:待发送手机号
request.setPhoneNumbers("15000000000");
// 必填:短信签名-可在短信控制台中找到
request.setSignName("云通信");
// 必填:短信模板-可在短信控制台中找到
request.setTemplateCode("SMS_1000000");
// 可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
request.setTemplateParam("{\"name\":\"Tom\", \"code\":\"123\"}");
// 选填-上行短信扩展码(无特殊需求用户请忽略此字段)
// request.setSmsUpExtendCode("90997");
// 可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
request.setOutId("yourOutId");
// hint 此处可能会抛出异常,注意catch
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
return sendSmsResponse;
}
//短信查询
public static QuerySendDetailsResponse querySendDetails(String bizId) throws ClientException {
// 可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
// 初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
// 组装请求对象
QuerySendDetailsRequest request = new QuerySendDetailsRequest();
// 必填-号码
request.setPhoneNumber("15000000000");
// 可选-流水号
request.setBizId(bizId);
// 必填-发送日期 支持30天内记录查询,格式yyyyMMdd
SimpleDateFormat ft = new SimpleDateFormat("yyyyMMdd");
request.setSendDate(ft.format(new Date()));
// 必填-页大小
request.setPageSize(10L);
// 必填-当前页码从1开始计数
request.setCurrentPage(1L);
// hint 此处可能会抛出异常,注意catch
QuerySendDetailsResponse querySendDetailsResponse = acsClient.getAcsResponse(request);
return querySendDetailsResponse;
}
public static void main(String[] args) throws ClientException, InterruptedException {
// 发短信
SendSmsResponse response = sendSms();
System.out.println("短信接口返回的数据----------------");
System.out.println("Code=" + response.getCode());
System.out.println("Message=" + response.getMessage());
System.out.println("RequestId=" + response.getRequestId());
System.out.println("BizId=" + response.getBizId());
Thread.sleep(3000L);
// 查明细
if(response.getCode() != null && response.getCode().equals("OK")) {
QuerySendDetailsResponse querySendDetailsResponse = querySendDetails(response.getBizId());
System.out.println("短信明细查询接口返回数据----------------");
System.out.println("Code=" + querySendDetailsResponse.getCode());
System.out.println("Message=" + querySendDetailsResponse.getMessage());
int i = 0;
for(QuerySendDetailsResponse.SmsSendDetailDTO smsSendDetailDTO :querySendDetailsResponse.getSmsSendDetailDTOs())
{
System.out.println("SmsSendDetailDTO["+i+"]:");
System.out.println("Content=" + smsSendDetailDTO.getContent());
System.out.println("ErrCode=" + smsSendDetailDTO.getErrCode());
System.out.println("OutId=" + smsSendDetailDTO.getOutId());
System.out.println("PhoneNum=" + smsSendDetailDTO.getPhoneNum());
System.out.println("ReceiveDate=" + smsSendDetailDTO.getReceiveDate());
System.out.println("SendDate=" + smsSendDetailDTO.getSendDate());
System.out.println("SendStatus=" + smsSendDetailDTO.getSendStatus());
System.out.println("Template=" + smsSendDetailDTO.getTemplateCode());
}
System.out.println("TotalCount=" + querySendDetailsResponse.getTotalCount());
System.out.println("RequestId=" + querySendDetailsResponse.getRequestId());
}
}
}
4)微服务整合短信服务:下单后发送短信
- 引入依赖:
shop-user
中引入依赖
<!--短信发送-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alicloud-sms</artifactId>
</dependency>
- 封装SMS工具类
public class SmsUtil {
// 替换成自己申请的accessKeyId
private static String accessKeyId = "LTAIMLlf8NKYXn1M";
// 替换成自己申请的accessKeySecret
private static String accessKeySecret = "hqyW0zTNzeSIFnZhMEkOaZXVVcr3Gj";
static final String product = "Dysmsapi";
static final String domain = "dysmsapi.aliyuncs.com";
/**
* 发送短信
*
* @param phoneNumbers 要发送短信到哪个手机号
* @param signName 短信签名[必须使用前面申请的]
* @param templateCode 短信短信模板ID[必须使用前面申请的]
* @param param 模板中${code}位置传递的内容
*/
public static void sendSms(String phoneNumbers, String signName, String templateCode, String param) {
try {
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
// 初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam(param);
request.setOutId("yourOutId");
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
if (!"OK".equals(sendSmsResponse.getCode())) {
throw new RuntimeException(sendSmsResponse.getMessage());
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("发送短信失败");
}
}
}
- 编写短信发送业务
@Slf4j
@Service("shopSmsService")
@RocketMQMessageListener(
consumerGroup = "shop-user", // 消费者组名
topic = "order-topic", // 消费主题
consumeMode = ConsumeMode.CONCURRENTLY, // 消费模式
messageModel = MessageModel.CLUSTERING // 消息模式
)
public class SmsService implements RocketMQListener<Order> {
@Autowired
private UserDao userDao;
// 消费逻辑
@Override
public void onMessage(Order message) {
log.info("接收到了一个订单信息{},接下来就可以发送短信通知了", message);
// 根据uid 获取手机号
User user = userDao.findById(message.getUid()).get();
// 生成验证码
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 6; i++) {
builder.append(new Random().nextInt(9) + 1);
}
String smsCode = builder.toString();
Param param = new Param(smsCode);
try {
// 发送短信 {"code":"123456"}
SmsUtil.sendSms(user.getTelephone(), "黑马旅游网", "SMS_170836451", JSON.toJSONString(param));
log.info("短信发送成功");
} catch (Exception e) {
e.printStackTrace();
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Param {
private String code;
}
}
9.Nacos config服务配置
1)介绍
微服务架构中配置文件的问题:
- 配置文件相对分散:在一个微服务架构下,配置文件会随着微服务的增多变的越来越多,而且分散在各个微服务中,不好统一配置和管理
- 配置文件无法区分环境:微服务项目可能会有多个环境,例如:测试环境、预发布环境、生产环境。每一个环境所使用的配置理论上都是不同的,一旦需要修改,就需要我们去各个微服务下手动维护,这比较困难
- 配置文件无法实时更新:我们修改了配置文件之后,必须重新启动微服务才能使配置生效,这对一 个正在运行的项目来说是非常不友好的
配置中心的思路:
- 首先把项目中各种配置全部都放到一个集中的地方进行统一管理,并提供一套标准的接口
- 当各个服务需要获取配置的时候,就来配置中心的接口拉取自己的配置
- 当配置中心中的各种参数有更新的时候,也能通知到各个服务实时的过来同步最新的信息,使之动态更新
常见的配置中心:Apollo(携程开源、支持灰度发布、资料齐全)、Disconf(百度开源、基于Zookeeper)、SpringCloud Config、Nacos
2)基本概念
- 命名空间(Namespace):用于进行不同环境的配置隔离,一般一个环境划分到一个命名空间
- 配置分组(Group):用于将不同的服务归类到同一分组,一般将一个项目的配置分到一组
- 配置集(Data ID):一个配置文件通常就是一个配置集,一般微服务的配置就是一个配置集
3)应用
使用nacos作为配置中心,其实就是将nacos当做一个服务端,将各个微服务看成是客户端,我们 将各个微服务的配置文件统一存放在nacos上,然后各个微服务从nacos上拉取配置即可
- 搭建nacos环境(同上)、引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 在微服务中添加nacos的配置
不能使用原来的application.yml作为配置文件,而是新建一个bootstrap.yml作为配置文件
配置文件优先级:bootstrap.properties -> bootstrap.yml -> application.properties -> application.yml
# service-product中的service-product.yaml
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 # nacos中心地址
file-extension: yaml # 配置文件格式
profiles:
active: dev # 环境标识
- 在nacos的WEB UI中配置各微服务的配置信息
配置列表 → 加号 → 新建配置
- 注释本地的application.yam中的内容, 启动程序进行测试(自动从nacos拉取配置)
- 配置动态刷新
在nacos中的service-product-dev.yaml配置中添加下面config
配置:
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 # nacos中心地址
file-extension: yaml # 配置文件格式
profiles:
active: dev # 环境标识
config:
appName: product
方式一:硬编码方式
@RestController
public class NacosConfigController {
@Autowired
private ConfigurableApplicationContext applicationContext;
@GetMapping("/nacos-config-test1")
public String nacosConfingTest1() {
return
applicationContext.getEnvironment().getProperty("config.appName");
}
}
方式二:注解方式
@RestController
@RefreshScope // 只需要在需要动态读取配置的类上添加此注解就可以
public class NacosConfigController {
@Value("${config.appName}")
private String appName;
// 2 注解方式
@GetMapping("/nacos-config-test2")
public String nacosConfingTest2() {
return appName;
}
}
- 配置共享
当配置越来越多的时候,有很多配置是重复的,这时候就考虑将公共配置文件提取出来,然后实现共享
①同一个微服务的不同环境之间共享配置
将同一微服务不同环境的公共配置,提取出一个以spring.application.name
命名的配置文件,将公共配置存入其中。
新建一个名为service-product-test.yaml配置存放测试环境的配置
新建一个名为consumer-dev.yaml配置存放开发环境的配置…
添加测试方法:
@RestController
@RefreshScope
public class NacosConfigController {
@Value("${config.env}")
private String env;
// 3 同一微服务的不同环境下共享配置
@GetMapping("/nacos-config-test3")
public String nacosConfingTest3() {
return env;
}
}
修改service-product微服务的bootstrap.yaml配置文件中的环境
spring:
profiles:
active: test # 环境标识
访问拉取更新配置的接口
②不同微服务中的共享配置
不同为服务之间实现配置共享的原理类似于文件引入,就是定义一个公共配置,然后在当前配置中引入:
在nacos WEB UI中定义一个DatalID为all-service.yaml
的配置,用于所有微服务共享:
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: root
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
在nacos WEB UI中修改service-product.yaml的配置文件(仅保留了端口号,和config)
server:
port: 8081
config:
appName: product
修改service-product本地的bootstrap.yaml
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 # nacos中心地址
file-extension: yaml # 配置文件格式
shared-dataids: all-service.yaml # 配置要引入的配置
refreshable-dataids: all-service.yaml # 配置要实现动态配置刷新的配置
profiles:
active: dev # 环境标识
10.Seata分布式事务
1)基本概念
-
事务
-
本地事务:本地事物其实可以认为是数据库提供的事务机制。数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单 元中的所有操作要么都成功,要么都失败。
-
分布式事务:指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
-
场景
- 单体系统访问多个数据库
- 多个微服务访问同一个数据库
- 多个微服务访问多个数据库
2)分布式事务解决方案
-
全局事务:基于DTP模型实现。DTP是由X/Open组织提出的一种分布式事务模型——X/Open Distributed Transaction Processing Reference Model
- 三种角色
- AP: Application 应用系统 (微服务)
- TM: Transaction Manager 事务管理器 (全局事务管理)
- RM: Resource Manager 资源管理器 (数据库)
- 两个阶段
- 阶段一: 表决阶段,所有参与者都将本事务执行预提交,并将能否成功的信息反馈发给协调者
- 阶段二: 执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地执行提交或者回滚
- 优点
- 提高了数据一致性的概率,实现成本较低
- 缺点
- 单点问题: 事务协调者宕机
- 同步阻塞: 延迟了提交时间,加长了资源阻塞时间
- 数据不一致: 提交第二阶段,依然存在commit结果未知的情况,有可能导致数据不一致
- 三种角色
-
可靠消息服务:通过消息中间件保证上、下游应用数据操作的一致性。假设有A和B两个系统,分别可以处理任务A和任务B。此时存在一个业务流程,需要将任务A和任务B在同一个事务中处理:
-
消息由系统A投递到中间件
- 在系统A处理任务A前,首先向消息中间件发送一条消息
- 消息中间件收到后将该条消息持久化,但并不投递。持久化成功后,向A回复一个确认应答
- 系统A收到确认应答后,则可以开始处理任务A
- 任务A处理完成后,向消息中间件发送Commit或者Rollback请求。该请求发送完成后,对系统A而言,该事务的处理过程就结束了
- 如果消息中间件收到Commit,则向B系统投递消息;如果收到Rollback,则直接丢弃消息。但是 如果消息中间件收不到Commit和Rollback指令,那么就要依靠"超时询问机制"
-
消息由中间件投递到系统B
- 消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务 处理完成后便向消息中间件返回应答
- 如果消息中间件收到确认应答后便认为该事务处理完毕
- 如果消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止。 一般消息中间件可以设置消息重试的次数和时间间隔,如果最终还是不能成功投递,则需要手工干 预。这里之所以使用人工干预,而不是使用让A系统回滚,主要是考虑到整个系统设计的复杂度问题
-
超时询问机制:系统A除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调 用。当消息中间件收到发布消息便开始计时,如果到了超时没收到确认指令,就会主动调用 系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果,中间件根据三 种结果做出不同反应:
- 提交:将该消息投递给系统B
- 回滚:直接将条消息丢弃
- 处理中:继续等待
-
-
最大努力通知:也被称为定期校对,其实是对第二种解决方案的进一步优化。它引入了本地消息表来记录错误消息,然后加入失败消息的定期校对功能,来进一步保证消息会被下游系统消费
- 消息由系统A投递到中间件
- 处理业务的同一事务中,向本地消息表中写入一条记录
- 准备专门的消息发送者不断地发送本地消息表中的消息到消息中间件,如果发送失败则重试
- 消息由中间件投递到系统B
- 消息中间件收到消息后负责将该消息同步投递给相应的下游系统,并触发下游系统的任务执行
- 当下游系统处理成功后,向消息中间件反馈确认应答,消息中间件便可以将该条消息删除,从而该事务完成
- 对于投递失败的消息,利用重试机制进行重试,对于重试失败的,写入错误消息表
- 消息中间件需要提供失败消息的查询接口,下游系统会定期查询失败消息,并将其消费
- 优点:一种非常经典的实现,实现了最终一致性
- 缺点:消息表会耦合到业务系统中,如果没有封装好的解决方案,会有业务无关的工作
- 消息由系统A投递到中间件
-
TCC事务(Try Confirm Cancel):补偿型分布式事务
- Try:尝试待执行的业务。这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源
- Confirm:确认执行业务。执行业务操作,不做任何业务检查, 只使用Try阶段预留的业务资源。通常情况下,采用TCC 则认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引入重试机制或人工处理。
- Cancel:取消待执行的业务。取消Try阶段预留的业务资源。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若 Cancel阶段真的出错了,需引入重试机制或人工处理。
- TCC两阶段提交与XA两阶段提交的区别
- XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁
- TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁
- 优点:把数据库层的二阶段提交上提到了应用层来实现,规避了数据库层的2PC性能低下问题
- 缺点:TCC的Try、Confirm和Cancel操作功能需业务提供,开发成本高
3)Seata介绍
- 介绍
由阿里巴巴中间件团队发起Fescar(Fast & EaSy Commit And Rollback)更名而来,愿景是让分布式事务的使用像本地事务的使用一样,简单和高效,并逐步解决开发者们 遇到的分布式事务方面的所有难题。
Seata的设计目标是对业务无侵入,因此从业务无侵入的2PC方案着手,在传统2PC的基础上演进。 它把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分 支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据 库的本地事务。
-
重要组件
- TC:Transaction Coordinator 事务协调器,管理全局的分支事务的状态,用于全局性事务的提交 和回滚
- TM:Transaction Manager 事务管理器,用于开启、提交或者回滚全局事务
- RM:Resource Manager 资源管理器,用于分支事务上的资源管理,向TC注册分支事务,上报分 支事务的状态,接受TC的命令来提交或者回滚分支事务
-
执行流程
- A服务的TM向TC申请开启一个全局事务,TC就会创建一个全局事务并返回一个唯一的XID
- A服务的RM向TC注册分支事务,并及其纳入XID对应全局事务的管辖
- A服务执行分支事务,向数据库做操作
- A服务开始远程调用B服务,此时XID会在微服务的调用链上传播
- B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖
- B服务执行分支事务,向数据库做操作
- 全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚
- TC协调其管辖之下的所有分支事务, 决定是否回滚
-
Seata实现2PC与传统2PC的区别
- 架构层次方面:传统2PC方案的 RM 实际上是在数据库层,RM本质上就是数据库自身,通过XA协议实现,而 Seata的RM是以jar包的形式作为中间件层部署在应用程序这一侧的
- 两阶段提交方面:传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成才释放。而Seata的做法是在Phase1 就将本地事务提交,这样就可以省去Phase2 持锁的时间,整体提高效率
4)应用
示例通过Seata中间件实现分布式事务,模拟电商中的下单和扣库存的过程。通过订单微服务执行下单操作,然后由订单微服务调用商品微服务扣除库存
-
未引入Seata分布式事务前
- service-order
/** controller **/ @RestController @Slf4j public class OrderController5 { @Autowired private OrderServiceImpl5 orderService; // 下单 @RequestMapping("/order/prod/{pid}") public Order order(@PathVariable("pid") Integer pid) { log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid); return orderService.createOrder(pid); } }
/** service **/ @Service @Slf4j public class OrderServiceImpl{ @Autowired private OrderDao orderDao; @Autowired private ProductService productService; @Autowired private RocketMQTemplate rocketMQTemplate; @GlobalTransactional public Order createOrder(Integer pid) { // 1 调用商品微服务,查询商品信息 Product product = productService.findByPid(pid); log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product)); // 2 下单(创建订单) Order order = new Order(); order.setUid(1); order.setUsername("测试用户"); order.setPid(pid); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1); orderDao.save(order); log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order)); // 3 扣库存 productService.reduceInventory(pid, order.getNumber()); // 4 向mq中投递一个下单成功的消息 rocketMQTemplate.convertAndSend("order-topic", order); return order; } }
/** ProductService:Feign声明的远程调用 **/ @FeignClient(value = "service-product") public interface ProductService { // 减库存 @RequestMapping("/product/reduceInventory") void reduceInventory(@RequestParam("pid") Integer pid, @RequestParam("num") int num); }
- service-product
/** controller **/ @RequestMapping("/product/reduceInventory") public void reduceInventory(Integer pid, int num) { productService.reduceInventory(pid, num); }
/** service **/ @Override public void reduceInventory(Integer pid, Integer number) { Product product = productDao.findById(pid).get(); if (product.getStock() < number) { throw new RuntimeException("库存不足"); } // int i = 1 / 0; 在此处模拟异常:这样会造成,用户下单了,即订单创建了,但是库存没有减少 product.setStock(product.getStock() - number); productDao.save(product); }
-
引入seata
-
Seata环境配置
- 下载:
https://github.com/seata/seata/releases/v0.9.0/
- 修改配置文件:将下载得到的压缩包进行解压,进入conf目录,调整下面的配置文件:
# registry.conf registry { type = "nacos" nacos { serverAddr = "localhost" namespace = "public" cluster = "default" } } config { type = "nacos" nacos { serverAddr = "localhost" namespace = "public" cluster = "default" } }
# nacos-config.txt # 这里的语法为: service.vgroup_mapping.${your-service-gruop}=default ,中间的${your-service-gruop} 为自己定义的服务组名称, 这里需要我们在程序的配置文件中配置。 service.vgroup_mapping.service-product=default service.vgroup_mapping.service-order=default
- 初始化seata在nacos的配置(在nacos目录下执行)
# 初始化seata 的nacos配置 # 注意: 这里要保证nacos是已经正常运行的 cd conf nacos-config.sh 127.0.0.1 # 执行成功后可以打开Nacos的控制台,在配置列表中,可以看到初始化了很多Group为SEATA_GROUP的配置
- 启动seata服务
cd bin seata-server.bat -p 9000 -m file # 启动后在 Nacos 的服务列表下面可以看到一个名为 serverAddr 的服务
- 下载:
-
数据库配置:在项目的数据库中加入一张undo_log表,这是Seata记录事务日志要用到的表
CREATE TABLE `undo_log` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `branch_id` BIGINT(20) NOT NULL, `xid` VARCHAR(100) NOT NULL, `context` VARCHAR(128) NOT NULL, `rollback_info` LONGBLOB NOT NULL, `log_status` INT(11) NOT NULL, `log_created` DATETIME NOT NULL, `log_modified` DATETIME NOT NULL, `ext` VARCHAR(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
-
整合Seata
- 引入依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
- 配置代理数据源
Seata 是通过代理数据源实现事务分支的,所以需要配置 io.seata.rm.datasource.DataSourceProxy 的 Bean,且是 @Primary默认的数据源,否则事务不会回滚,无法实现分布式事务
@Configuration public class DataSourceProxyConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DruidDataSource druidDataSource() { return new DruidDataSource(); } @Primary @Bean public DataSourceProxy dataSource(DruidDataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } }
- Seata配置文件
在resources下添加Seata的配置文件 registry.conf
registry { type = "nacos" nacos { serverAddr = "localhost" namespace = "public" cluster = "default" } } config { type = "nacos" nacos { serverAddr = "localhost" namespace = "public" cluster = "default" } }
- 项目配置文件(本地)的bootstrap.yaml
spring: application: name: service-product cloud: nacos: config: server-addr: localhost:8848 # nacos的服务端地址 namespace: public group: SEATA_GROUP alibaba: seata: tx-service-group: ${spring.application.name}
- 在order微服务开启全局事务
@GlobalTransactional //全局事务控制 public Order createOrder(Integer pid) {}
- 再次测试
-
-
流程分析
- 每个RM使用DataSourceProxy连接数据库,其目的是使用ConnectionProxy,使用数据源和数据连 接代理的目的就是在第一阶段将undo_log和业务数据放在一个本地事务提交,这样就保证了只要有业务操作就一定有undo_log
- 在第一阶段undo_log中存放了数据修改前和修改后的值,为事务回滚作好准备,所以第一阶段完成就已经将分支事务提交,也就释放了锁资源
- TM开启全局事务开始,将XID全局事务id放在事务上下文中,通过feign调用也将XID传入下游分支 事务,每个分支事务将自己的Branch ID分支事务ID与XID关联
- 第二阶段全局事务提交,TC会通知各各分支参与者提交分支事务,在第一阶段就已经提交了分支事 务,这里各各参与者只需要删除undo_log即可,并且可以异步执行,第二阶段很快可以完成
- 第二阶段全局事务回滚,TC会通知各各分支参与者回滚分支事务,通过 XID 和 Branch ID 找到相应 的回滚日志,通过回滚日志生成反向的 SQL 并执行,以完成分支事务回滚到之前的状态,如果回滚失 败则会重试回滚操作
11.Duboo-RPC通信
Dubbo是阿里巴巴开源的基于 Java 的高性能 RPC分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。Spring-cloud-alibaba-dubbo 是基于SpringCloudAlibaba技术栈对dubbo技术的一种封装,目的在于实现基于RPC的服务调用。
-
服务提供者
service-product
- 引入依赖
<!--dubbo--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-dubbo</artifactId> </dependency>
- 添加Duboo配置
dubbo: scan: base-packages: seg.tef4.service.impl # 开启包扫描 protocols: dubbo: name: dubbo # 服务协议 port: -1 # 服务端口 registry: address: spring-cloud://localhost # 注册中心
- 提供统一业务api
public interface ProductService { Product findByPid(Integer pid); }
- 编写并暴露服务
// 暴露服务:注意这里使用的是dubbo提供的注解@Service,而不是Spring的 @Service public class ProductServiceImpl implements ProductService { @Autowired private ProductDao productDao; @Override public Product findByPid(Integer pid) { return productDao.findById(pid).get(); } }
-
服务消费者
service-order
- 引入依赖
<!--dubbo--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-dubbo</artifactId> </dependency>
- 添加Dubbo配置
dubbo: registry: address: spring-cloud://localhost # 注册中心 cloud: subscribed-services: service-product # 订阅的提供者名称
- RPC调用服务
@RestController @Slf4j public class OrderController { @Autowired private OrderService orderService; // 引用服务 @Reference private ProductService productService; @RequestMapping("/order/prod/{pid}") public Order order(@PathVariable Integer pid) { log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid); // 调用商品微服务,查询商品信息 Product product = productService.findByPid(pid); log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product)); // 下单(创建订单) Order order = new Order(); order.setUid(1); order.setUsername("测试用户"); order.setPid(pid); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1); orderService.createOrder(order); log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order)); return order; } }