一、 Spring Cloud分布式开发五大组件
- 服务发现——Netflix Eureka、SpringCloud Alibaba Nacos Discovery
- 客户端负载均衡——Netflix Ribbon
- 断路器——Netflix Hystrix、SpringCloud Alibaba Sentinel
- 服务网关——Netflix Zuul、Spring Cloud Gateway
- 分布式配置——Spring Cloud Config、SpringCloud Alibaba Nacos Config
spring & Netflix | 其他 | 其他 | |
---|---|---|---|
服务发现与注册 | Netflix Eureka(client、server) | nacos | |
客户端负载均衡 | Netflix Ribbon | ||
断路器 | Netflix Hystrix | ||
服务网关 | Netflix Zuul | Gateway | |
分布式配置 | Spring Cloud Config | alibaba-nacos-config | apollo |
分布式注册中心 | Spring Cloud Server | alibaba-nacos-discovery |
二、服务调用
1.Feign调用
(1)传递非业务数据
当微服务之间互相调用的时候,需要传递非业务数据的时候,可以使用拦截器实现,具体实现方式是实现feign.RequestInterceptor接口,实现具体的apply()方法逻辑。
package com.irootech.useraccess.config;
import com.irootech.common.util.SessionBean;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
// 设置本地变量
requestTemplate.header("tenantId", SessionBean.getTenantId());
requestTemplate.header("userId", SessionBean.getUserId());
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
if (requestTemplate.headers().containsKey(name)) {
continue;
}
if (name.equals("content-length") || (name.equalsIgnoreCase("content-type"))) {//此处是个坑,会把现有的content-length去调用feigin接口,报错
continue;
}
String values = request.getHeader(name);
requestTemplate.header(name, values);
}
}
}
}
}
(2)错误和异常数据解析
实现 feign.codec.ErrorDecoder 接口
package com.irootech.useraccess.config;
import com.irootech.common.exception.SysException;
import com.irootech.common.util.JsonParsingUtil;
import com.irootech.useraccess.feign.vo.ACLBaseResponse;
import com.irootech.useraccess.feign.vo.ACLErrorMessage;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.util.List;
/**
* @Author: fei.yu
* @Date: 2021/2/1 10:20
*/
@Configuration
@Slf4j
public class FeignClientErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
log.info("feign client response:", response);
UserAccessResultCode userAccessResultCode = UserAccessResultCode.getByCode(response.status());
if (userAccessResultCode != UserAccessResultCode.ERROR) {
return new SysException(userAccessResultCode);
}
RemoteExceptionEnumCode remoteExceptionCode = null;
if (response.body() == null) {
log.error("feign调用异常,body为空");
// remoteExceptionCode = new RemoteExceptionCode(response.status(),"", "");
remoteExceptionCode = RemoteExceptionEnumCode.getByMessageEN("");
}else if(response.headers().containsKey("x-rootcloud-setpw-token")){
List<String> setPwTokenParam = (List<String>)response.headers().get("x-rootcloud-setpw-token");
SysException sysException = new SysException(UserAccessResultCode.getByCode(8610),setPwTokenParam.get(0));
throw sysException;
} else {
try {
String body = Util.toString(response.body().asReader());
log.error("feign调用异常,body为 ==>{}",body);
ACLBaseResponse baseResponse = JsonParsingUtil.fromJson(body, ACLBaseResponse.class);
try {
ACLErrorMessage errorMessage = JsonParsingUtil.fromJson(baseResponse.getMessage(), ACLErrorMessage.class);
// remoteExceptionCode = new RemoteExceptionCode(baseResponse.getStatus(),
// errorMessage.getMessage(), errorMessage.getMessage());
remoteExceptionCode = RemoteExceptionEnumCode.getByMessageEN(errorMessage.getMessage());
} catch (Exception e) {
// remoteExceptionCode = new RemoteExceptionCode(baseResponse.getStatus(),
// baseResponse.getMessage(), baseResponse.getMessage());
remoteExceptionCode = RemoteExceptionEnumCode.getByMessageEN(baseResponse.getMessage());
}
} catch (IOException e) {
log.error("feign.IOException", e);
throw new SysException(userAccessResultCode);
}
}
return new SysException(remoteExceptionCode);
}
}
三、服务启动与加载
1. Springboot读取配置文件原理和加载顺序优先级 :
(1)读取配置文件原理
Springboot读取配置文件是通过事件监听的方式读取的,在Springboot启动的时候,会发布一个ApplicationEnvironmentPreparedEvent事件,ConfigFileApplicationListener监听器监听了这个事件,在该监听器中读取配置文件。
通过事件监听的方式读取的配置文件,这个监听器是ConfigFileApplicationListener。
(2)配置文件加载顺序:
(开发环境下)
使用SpringCloudConfig这种统一配置时Spring Boot 配置文件的加载顺序,依次为bootstrap.properties-> bootstrap.yml ->application.properties-> application.yml,其中bootstrap.properties配置为最高优先级。
(部署环境下)
下面目录下的配置文件的优先级从高到低,高优先级的配置覆盖低优先级的配置,所有配置会形成互补配置。
# jar文件同级目录下的config文件夹下的配置文件file:./config/
# jar文件同级目录下的配置文件file:./
# classpath目录下的config文件夹下的配置文件classpath:config/
# classpath目录下的配置文件classpath
2.项目启动初始化操作实现方案
项目启动的时候,需要加载字典数据到redis里面,除了单例代码块初始化实现之外,还可以使用下面两种方式。
(1)使用@PostConstruct注解实现,在service实现类对应的方法上面加这个注解,Spring容器就会在依赖注入之后调用此方法。
// 可以看到,它是通过注解@PostConstruct实现的自动加载数据,带有该注解的方法会在依赖注入之后调用
/**
* 项目启动时,初始化字典到缓存
*/
@PostConstruct
public void init()
{
loadingDictCache();
}
import javax.annotation.PostConstruct;
public class MyBean {
private String property;
public void setProperty(String property) {
this.property = property;
}
@PostConstruct
public void init() {
// 自定义初始化逻辑
System.out.println("Bean 初始化完成,属性值为: " + property);
}
}
(2)InitializingBean 是 Spring 框架中的一个接口,用于在 Spring 容器中初始化 bean 时执行特定的初始化逻辑。这个接口定义了一个方法 afterPropertiesSet(),当 bean 的所有属性被设置后(即依赖注入完成后),Spring 容器会调用这个方法。通过实现这个接口,你可以在 bean 初始化完成后执行自定义的初始化操作。
import org.springframework.beans.factory.InitializingBean;
public class MyBean implements InitializingBean {
private String property;
public void setProperty(String property) {
this.property = property;
}
@Override
public void afterPropertiesSet() throws Exception {
// 自定义初始化逻辑
System.out.println("Bean 初始化完成,属性值为: " + property);
}
}
3.不需要连接数据库的微服务启动
例如Gateway 服务,需要在启动类加排除数据库自动注册的类
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class })
/**
* 网关启动程序
*
* @author ruoyi
*/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class })
public class RuoYiGatewayApplication
{
public static void main(String[] args)
{
SpringApplication.run(RuoYiGatewayApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ 若依网关启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ \\ \\ \\ / / \n" +
" | ( ' ) | \\ _. / ' \n" +
" |(_ o _) / _( )_ .' \n" +
" | (_,_).' __ ___(_ o _)' \n" +
" | |\\ \\ | || |(_,_)' \n" +
" | | \\ `' /| `-' / \n" +
" | | \\ / \\ / \n" +
" ''-' `'-' `-..-' ");
}
}
四、windows启动java服务
1. 方式一:java -jar xxx.jar
2. 方式二:直接进程运行,可关闭窗口
(1)定义startup.bat文件一
这种方式需要手动关闭窗口
@echo off
@javaw -jar ruoyi-admin.jar > ruoyi-admin.log
exit
(2)定义startup.bat文件二
@echo off
start javaw -jar ruoyi-admin.jar > ruoyi-admin.log
exit
这中这种方式自动关闭窗口,
(3)定义stop.bat文件关闭进程
@echo off
taskkill -f -t -im javaw.exe
exit
3.更多方式参考:Windows 下后台启动 jar 包,UTF-8 启动 jar 包_windows启动jar包-优快云博客
五、分布式事务之Seata
Seata 会有 4 种分布式事务解决方案,分别是 AT 模式、TCC 模式、Saga 模式。
5.1 AT 模式
AT 模式是一种无侵入的分布式事务解决方案。在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
5.2 TCC模式
TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法。
- Try:资源的检测和预留;
- Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;
- Cancel:预留资源释放;
相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多。
5.3 Saga 模式
Saga 模式是 Seata 即将开源的长事务解决方案,将由蚂蚁金服主要贡献。在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga 模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga 模式是一种长事务解决方案。
aga 模式适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁、长流程情况下可以保证性能。
事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,可以使用 Saga 模式。
Saga模式的优势是:
一阶段提交本地数据库事务,无锁,高性能;
参与者可以采用事务驱动异步执行,高吞吐;
补偿服务即正向服务的“反向”,易于理解,易于实现;
缺点:Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性。后续会讲到对于缺乏隔离性的应对措施。
目前 Saga 的实现一般有两种,一种是通过事件驱动架构实现,一种是基于注解加拦截器拦截业务的正向服务实现。Seata 目前是采用事件驱动的机制来实现的,Seata 实现了一个状态机,可以编排服务的调用流程及正向服务的补偿服务,生成一个 json 文件定义的状态图,状态机引擎驱动到这个图的运行,当发生异常的时候状态机触发回滚,逐个执行补偿服务。当然在什么情况下触发回滚用户是可以自定义决定的。该状态机可以实现服务编排的需求,它支持单项选择、并发、异步、子状态机调用、参数转换、参数映射、服务执行状态判断、异常捕获等功能。
Seata 的定位是分布式事全场景解决方案,未来还会有 XA 模式的分布式事务实现,每种模式都有它的适用场景,AT 模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本。TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,也可以使用 Saga 模式。
六、Spring boot解决循环依赖
在 application.properties
中允许循环依赖(仅适用于 Spring Boot 2.6 之前版本):
spring.main.allow-circular-references=true
注意:Spring Boot 2.6+ 默认禁止循环依赖,且不推荐使用此配置。
最佳实践
- 优先考虑重构:循环依赖通常是设计问题的信号
- 使用构造器注入:它更明确地表达了依赖关系
- 合理使用
@Lazy
:作为临时解决方案而非长期策略 - 保持层次清晰:服务层、仓库层等应有明确的分层
- 使用
@PostConstruct
来延迟设置依赖;
@Service
public class AService {
private final BService bService;
public AService(@Lazy BService bService) {
this.bService = bService;
}
}
@Service
public class AService {
private BService bService;
@Autowired
public void setBService(BService bService) {
this.bService = bService;
}
}
@Service
public class AService {
@Autowired
private BService bService;
@PostConstruct
public void init() {
bService.setAService(this);
}
}
七、SpringBoot–关于配置
7.1 配置文件加载位置
springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件
–file:./config/
–file:./
–classpath:/config/
–classpath:/
优先级由高到底,高优先级的配置会覆盖低优先级的配置;
SpringBoot会从这四个位置全部加载主配置文件;互补配置;
我们还可以通过spring.config.location来改变默认的配置文件位置
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;指定配置文件和默认加载的这些配置文件共同起作用形成互补配置;
java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --spring.config.location=G:/application.properties
7.2外部配置加载顺序
SpringBoot也可以从以下位置加载配置; 优先级从高到低;高优先级的配置覆盖低优先级的配置,所有的配置会形成互补配置
SpringBoot也可以从以下位置加载配置; 优先级从高到低;高优先级的配置覆盖低优先级的配置,所有的配置会形成互补配置
命令行参数
所有的配置都可以在命令行上进行指定
java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --server.port=8087 --server.context-path=/abc
多个配置用空格分开; --配置项=值
参考文章:spring boot 启动读取config目录的 springboot读取配置文件原理_mob6454cc6dac54的技术博客_51CTO博客