关于springcloud项目的一些问题总结

394 阅读5分钟

谨以此文献给那些致力于微服务开发的软件工程师们。

本文主要记录我开发上遇到的一些问题,以及是怎么解决的。

环境版本

  • 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信息,接着写一个获取下一值的函数,直接调用该函数即可。

  1. 建表

    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;
    
  2. 建函数

    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
    
  3. 使用

    <select id="queryNextValByName" resultType="int">
        SELECT nextval(#{sequenceName});
    </select>
    

自定义springboot starter使用

上面讲到 springboot 的全局异常处理需要在每个 springboot 中添加这块处理代码,非常繁琐。
我们可以把这块代码写到 springboot starter 中,告别重复代码。

步骤

  1. 新建一个 springboot ,将全局异常处理类复制到文件夹里(处理好依赖)

  2. 新建文件 src/main/resources/META-INF/spring.factories
    在其中配置自动配置类
    如下:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.XXX.XXX.Class  # 你的配置类
    
  3. 在需要的 springboot 项目中引入这个starter依赖,即可享用