谨以此文献给那些致力于微服务开发的软件工程师们。
本文主要记录我开发上遇到的一些问题,以及是怎么解决的。
环境版本
-
spring cloud:2021.0.3
-
springboot:2.7.2
-
jdk:1.8
-
maven:3.5.4
-
mysql 5.7.22
-
redis 5.0.5
-
nacos 2.1.2
所有的问题都是基于以上版本,版本是一个很容易忽视的问题,他们之间的一些细微变化,会导致一些莫名其妙的问题。
返回信息的统一封装
作为接口,我们可能需要返回下面信息
{
"code":110,
"message":"ok",
"data":null
}
可以创建一个返回实体类,用来封装需要返回的信息
public class ApiRes<T> {
private String retCode;
private String retMsg;
private T data;
// ...省略构造方法、getter、setter方法
}
全局异常处理
业务内部抛出的异常,通常需要自己处理一下,返回我们需要的信息
springboot
利用切面处理controller的异常
@ControllerAdvice // 切面
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler // 处理controller异常
@ResponseBody
public ApiRes exception(Exception e){
log.info("GlobalExceptionHandler...", e);
// ...省略自定义处理异常
return ApiRes.exception(e.getMessage());
}
}
springcloud gateway
网关采用webflux模型,所以和mvc不太一样
@Order(-1)
@Configuration
public class GlobalExceptionConfiguration implements ErrorWebExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionConfiguration.class);
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
log.info("GlobalExceptionConfiguration...", ex);
ServerHttpResponse response = exchange.getResponse();
// 是否结束了
if (response.isCommitted()) {
return Mono.error(ex);
}
// 设置响应头类型
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
// 设置网关的响应状态码
if(ex instanceof ResponseStatusException){
response.setStatusCode(((ResponseStatusException) ex).getStatus());
}else{
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
}
return response
.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
try {
String message = ex.getMessage();
if(StringUtils.isNullOrEmpt1(message)){
return bufferFactory.wrap(objectMapper.writeValueAsBytes(ApiRes.exception(StatusEnum.SYS_ERR)));
}
return bufferFactory.wrap(objectMapper.writeValueAsBytes(ApiRes.exception(message)));
} catch (JsonProcessingException e) {
log.warn("Error writing response", ex);
return bufferFactory.wrap(new byte[0]);
}
}));
}
}
报文加解密
有了gateway,报文加解密全部放在网关处理
加密方式
报文数据按下面json格式传输,
key: 随机AES秘钥,使用RSA加密后的值
data:接口实际请求参数(详见各接口),使用AES加密后的值
sign: 签名值,使用md5对实际请求参数摘要,使用RSA对摘要进行签名后的值
{
"key":"122112121",
"data":"dfsfsdfs",
"sign":"ssfsdfsf"
}
返回报文也是一样的格式
gateway处理
创建全局过滤器实现 GlobalFilter, Ordered
请求解密
@Configuration
public class RequestDecrypFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
HttpMethod method = request.getMethod();
MediaType contentType = request.getHeaders().getContentType();
URI uri = request.getURI();
byte[] cachedBody;
if(HttpMethod.POST.equals(method)){
cachedBody = exchange.getAttribute(ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR);
if(cachedBody != null){
// ...省略自定义的加解密
cachedBody = 你的解密之后的数据;
}else{
cachedBody = new byte[0];
}
// 构造一个新请求
ServerHttpRequest newRequest = request.mutate().uri(uri).build();
// 组装请求body
DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();
Flux<DataBuffer> dataBufferFlux = Flux.just(dataBufferFactory.wrap(cachedBody));
// 组装请求头
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(request.getHeaders());
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
httpHeaders.setContentLength(cachedBody.length);
// 新请求组装头,body
newRequest = new ServerHttpRequestDecorator(newRequest){
@Override
public Flux<DataBuffer> getBody() {
return dataBufferFlux;
}
@Override
public HttpHeaders getHeaders() {
return httpHeaders;
}
};
return chain.filter(exchange.mutate().request(newRequest).build());
}else if("GET".equals(method)){
log.info("当前是get请求");
return chain.filter(exchange);
}
return chain.filter(exchange);
}
}
响应加密
@Configuration
public class ResponseEncryptFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("ResponseEncryptFilter...");
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory dataBufferFactory = response.bufferFactory();
ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response){
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if(body instanceof Flux){
Flux<? extends DataBuffer> bodyFlux = (Flux<? extends DataBuffer>) body;
return super.writeWith(bodyFlux.buffer().map(dataBuffer -> {
//DataBufferFactory defaultDataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffer);
byte[] resContent = new byte[join.readableByteCount()];
join.read(resContent);
DataBufferUtils.release(join);
String retData = new String(resContent);
log.info("ret data:{}", retData);
// ...省略你的加密方法
String encryptRetData = 你的加密方法;
log.info("ret encrypt data:{}", encryptRetData);
return dataBufferFactory.wrap(encryptRetData.getBytes());
}));
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(responseDecorator).build());
}
}
每个过滤器有一个这个方法,你需要用这个来控制过滤器的执行顺序
越小优先级越高
@Override
public int getOrder() {
return -10;
}
token验证
对于接口的权限验证,直接交给网关来统一处理,下游的服务只要完成自己的业务就行
网关使用自定义 GatewayFilterFactory
GatewayFilterFactory 只需继承AbstractGatewayFilterFactory
@Component
public class TokenGatewayFilterFactory extends AbstractGatewayFilterFactory<TokenGatewayFilterFactory.Config> {
private static final Logger log = LoggerFactory.getLogger(TokenGatewayFilterFactory.class);
private static final String EXCLUSION_URLS = "exclusionUrls";
@Autowired
private RedisTemplate redisTemplate;
public TokenGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(EXCLUSION_URLS);
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String exclusionUrls = config.getExclusionUrls();
if(log.isDebugEnabled()){
log.debug("exclusionUrls:{}", exclusionUrls);
}
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if(exclusionUrls.contains(path)){
log.info("当前请求:{}, 不需要验证token", path);
return chain.filter(exchange);
}
log.info("当前请求:{}, 需要验证token", path);
// ...省略自定义验证token的逻辑
return chain.filter(exchange);
}
};
}
public static class Config {
private String exclusionUrls = "";
public String getExclusionUrls() {
return exclusionUrls;
}
public void setExclusionUrls(String exclusionUrls) {
this.exclusionUrls = exclusionUrls;
}
}
}
这个过滤器写完后,只需在网关 application.yml 配置文件中加上这段配置即可
spring:
cloud:
gateway:
routes: #列表
- id: user_router # 路由对象唯一标识
#uri: http://localhost:8081 #地址
# 负载均衡的写法
uri: lb://USER-SERVICE
predicates:
- Path=/user/** # 访问/user/的路径
filters:
- Token=/login;/register
重点看 - Token=/login;/register 这一行
请求参数检验
利用 spring-boot-starter-validation 提供的注解来实现
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
controller 接口入参实体类添加注解 @Validated
@PostMapping(value="/updatePass", produces = {MediaType.APPLICATION_JSON_VALUE})
ApiRes<?> updatePass(@RequestBody @Validated UserUpdatePassDTO userUpdatePassDTO);
实体类具体参数添加校验注解
public class UserLoginReqDTO extends BaseReqDTO{
private Integer userId;
@NotBlank(message = "密码不能为空")
private String password;
@Email(regexp = ".*@.*", message = "邮箱格式不正确")
@NotBlank(message = "邮箱不能为空")
private String email;
// ...省略构造方法、getter、setter方法
}
mysql自增序列
mysql没有seqence用于获取自增id
解决办法是新增一个表用来记录你需要的seqence信息,接着写一个获取下一值的函数,直接调用该函数即可。
-
建表
CREATE TABLE `sequence` ( `seq_name` varchar(50) NOT NULL, `current_val` int(11) NOT NULL, `increment_val` int(11) NOT NULL DEFAULT '1', PRIMARY KEY (`seq_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -
建函数
CREATE DEFINER=`root`@`localhost` FUNCTION `nextval`(v_seq_name VARCHAR(50)) RETURNS int(11) begin update sequence set current_val = current_val + increment_val where seq_name = v_seq_name; return currval(v_seq_name); end -
使用
<select id="queryNextValByName" resultType="int"> SELECT nextval(#{sequenceName}); </select>
自定义springboot starter使用
上面讲到 springboot 的全局异常处理需要在每个 springboot 中添加这块处理代码,非常繁琐。
我们可以把这块代码写到 springboot starter 中,告别重复代码。
步骤
-
新建一个 springboot ,将全局异常处理类复制到文件夹里(处理好依赖)
-
新建文件 src/main/resources/META-INF/spring.factories
在其中配置自动配置类
如下:org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.XXX.XXX.Class # 你的配置类 -
在需要的 springboot 项目中引入这个starter依赖,即可享用