前言:最近公司需要对系统进行重构,基于以前微服务系统架构上的不足,我在此总结了一些问题以及优化思路、方案等,适用于Springboot单体项目,希望能对眼前的你提供一些帮助。

问题与思路:

1. Controller层的【try catch】处理

问题描述:在系统中充斥着大量的try catch的代码,当接口出现问题时,由于你的粗心少写一个方法的try catch,给前端返回了一串Exception的异常错误信息。

@GetMapping("/get/user")
 public Re<User> auth(){
     try {
         return servie.getUser();
     } catch (Exception e) {
         e.printStackTrace();
     }
     return Re.failed();
 }

解决思路 :全局异常处理

解决方案:核心为 @RestControllerAdvice、@ExceptionHandler 注解。

@RestControllerAdvice
public class UnityExceptionAdvice {
    @ExceptionHandler(value =Exception.class)
    public final Re<?> exceptionHandler(Exception e){
        log.error("系统异常错误:",e);
        return Re.failed();
    }
}

2. Service业务逻辑的中断处理

问题描述:在开发过程中,难免需要业务逻辑判断,直接返回Result结果集确实是一种方案,不过在service层之间调用时还需要处理Result是否成功,代码量剧增。在基于全局异常处理的架构下,抛出异常也是一种常用方案,不过却是在代码中写满了new RuntimeException() 且存在不能返回特定code码的Result结果集的问题。

示例1public Re<User> getUser(String name){
    User user = userMapper.selectUser(name);
    if (user == null) {
        return Re.failed(ReCode.USER_NOT_FOUND);
    }
    return Re.success(user);
}

示例2public User getUser(String name){
    User user = userMapper.selectUser(name);
    if (user == null) {
        throw new RuntimeException("用户不存在");
    }
    return user;
}

解决思路 :断言处理

解决方案:核心为Spring提供的工具类 org.springframework.util.Assert ,进行二次封装。此处需要避坑,默认的Assert工具类中的expression逻辑判断都是反着来的,且抛出的异常为new IllegalStateException(),以下代码的Asserts工具类中改为正向的处理,为自定义异常,具体可查看Assert源码

public class Asserts extends Assert {

    /**
     * 抛出指定类型状态码
     * @param reCode 状态码
     * @throws AnywayException -
     */
    public static void state(ReCode reCode) throws AnywayException{
        throw new AnywayException(reCode);
    }

    /**
     * 验证 - 抛出指定类型状态码
     * @param expression 描述业务正确的逻辑判断
     * @param reCode 自定义返回码与消息
     * @throws AnywayException -
     */
    public static void state(boolean expression, ReCode reCode) throws AnywayException{
        if (expression) {
            throw new AnywayException(reCode);
        }
    }

    /**
     * 抛出指定类型Code 与 错误消息
     * @param expression 描述业务正确的逻辑判断
     * @param reCode 自定义返回码与消息
     * @param errorMsgTemplate 错误消息模板
     * @param params 参数 替换模板中 {} 符号
     * @throws AnywayException -
     */
    public static void state(boolean expression, ReCode reCode, String errorMsgTemplate, Object... params) throws AnywayException{
        if (expression) {
            throw new AnywayException(reCode, StrUtil.format(errorMsgTemplate, params));
        }
    }
}

自定义异常类:

@Data
@EqualsAndHashCode(callSuper = true)
public class AnywayException extends RuntimeException {

    /** 结果集状态码 */
    private final ReCode reCode;

    /** 异常 */
    public AnywayException(ReCode reCode) {
        super(reCode.getMsg());
        this.reCode = reCode;
    }

    /** 异常 */
    public AnywayException(ReCode reCode, String msg) {
        super(msg);
        this.reCode = reCode;
    }
}

示例:

public User getUser(String name){
     User user = userMapper.selectUser(name);
     Asserts.state(user == null, ReCode.USER_NOT_FOUND);
     return user;
 }

3. Controller层数据响应处理

问题描述:在数据响应的时候,我们都会进行一层包装,一成不变的返回Result结果集数据,此响应可能会在Service层就会包装,也可能在Controller层进行包装处理。对于一些新手玩家规范不到位,如果在Re结果集上不加泛型的话,会增加代码阅读的复杂度。

// 示例1:
@GetMapping("/get/user")
public Re<User> auth(){        
    return servie.getUser();
}

// 示例2:
@GetMapping("/get/user")
public Re<User> auth(){        
    return Re.success(servie.getUser());
}

解决思路 :全局统一响应处理

解决方案:与全局异常处理一样,其核心为 @RestControllerAdvice 注解与 ResponseBodyAdvice 接口 。

@RestControllerAdvice
public class UnityResponseAdvice implements ResponseBodyAdvice<Object> {

    /**
     * 统一响应处理注解
     */
    public static final Class<? extends Annotation> UNITY_RESPONSE = RestController.class;
    
    /**
     * Whether this component supports the given controller method return type
     * and the selected {@code HttpMessageConverter} type.
     *
     * @param returnType    the return type
     * @param converterType the selected converter type
     * @return {@code true} if {@link #beforeBodyWrite} should be invoked;
     * {@code false} otherwise
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 当前类或方法是否有统一响应注解
        return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), UNITY_RESPONSE) || returnType.hasMethodAnnotation(UNITY_RESPONSE);
    }

    /**
     * Invoked after an {@code HttpMessageConverter} is selected and just before
     * its write method is invoked.
     *
     * @param body                  the body to be written
     * @param returnType            the return type of the controller method
     * @param selectedContentType   the content type selected through content negotiation
     * @param selectedConverterType the converter type selected to write to the response
     * @param request               the current request
     * @param response              the current response
     * @return the body that was passed in or a modified (possibly new) instance
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 防止重复包裹 Re
        if (body instanceof Re) {
            return body;
        }

        return Re.success(body);
    }
}

示例:

@GetMapping("/get/user")
public User auth(){        
    return servie.getUser();
}

4. 微服务Feign接口冗余重复代码与类

问题描述:服务之间的调用都是通过Feign实现的,在使用Feign的过程中,我们会在调用方定义一套Feign接口,与提供方的Controller接口一模一样,就好比service与serviceImpl的关系,且返回的DTO或者VO在两方服务都分别创建,在修改Collection接口的时候需要同步将调用方的Feign接口进行修改,如果调用方是n,那需要修改n个服务。

// 服务调用方
@FeignClient("base-user")
public interface BaseUserFeign {
    @GetMapping("t1")
    Re<String> test1(@RequestParam("t") String t);

    @PostMapping("t2")
    Re<DemoVO> test2(@RequestBody DemoDTO demoDTO);
}
// 服务提供方
@RestController
public class DemoController {

    @GetMapping("t1")
    public Re<String> test1(@RequestParam("t") String t) {
        return Re.success("success");
    }

    @PostMapping("t2")
    public Re<DemoVO> test2(@RequestBody DemoDTO demoDTO) {
        System.out.println(demoDTO);
        return Re.success(new DemoVO("success"));
    }
}

解决思路 :服务调用方与提供方共用Fiegn模块

解决方案:服务在划分模块的时候将Feign接口单独创建子模块,将Feign接口从服务调用方的代码转变为服务提供方的代码,除了包含Feign接口之后,还包含对应DTO与VO,服务调用方继承了此模块,即可开箱即用所有的Feign接口,而服务提供方在Controller中实现对应的Feign接口。如果需要对外服务提供接口的,都写在Feign接口中,不需要对外提供接口服务的,按照正常实现即可。

// 服务调用方(无需手写Feign接口,直接引入服务提供方的Feign模块)
<dependency>
    <groupId>cn.code72</groupId>
    <artifactId>base-user-feign</artifactId>
    <version>1.0.0</version>
</dependency>

// 需要将引入包的Fiegn进行导入,此处提供feign包路径
@EnableFeignClients("cn.code72.base")
@SpringBootApplication
public class AuthApplication {
	public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class,args);
    }
}
// 服务提供方
base-user (父模块)
	base-user-feign (子模块 - feign接口)
	base-user-ms (子模块 - 业务)

@FeignClient("base-user")
public interface BaseUserFeign {
    @GetMapping("t1")
    String test1(@RequestParam("t") String t);

    @PostMapping("t2")
    DemoVO test2(@RequestBody DemoDTO demoDTO);
}

	
@RestController
public class DemoController implements BaseUserFeign {

	// 对外提供服务的接口
    @Override
    public String test1(@RequestParam String t) {
        return "success";
    }

    @Override
    public DemoVO test2(@RequestBody DemoDTO demoDTO) {
        System.out.println(demoDTO);
        return new DemoVO("success");
    }

	// 不对外提供服务的接口
    @GetMapping("t3")
    public Integer test3(String t) {
        System.out.println(t);
        return 333333333;
    }
}

5. 基于全局统一响应处理与共用Feign模块后的服务调用

问题描述:对于刚才的架构优化提供了全局统一响应处理(第3条)与Feign模块共用(第4条)的思路,单独任意一处优化都减少了一些重复造轮子的过程。不过当一起使用的时候,不知道聪明的你有没有发现问题呢?是的,当一起使用时,Feign调用的返回数据类型居然跟Controller响应的类型匹配不上?你敢信?其实是因为统一响应处理对返回类型进行了一次包装,看到的接口返回类型是个String,Feign的类型也是String,实际却是个Result结果集类型,String只是结果集中的data数据而已。

解决思路 :改造Feign接口调用的实现

解决方案:我能想到的有两种解决方案,第一种是将所有Feign接口的返回类型都处理为Re结果集类型,不对外提供的接口还是正常的数据类型作为返回类型即可,这样保证了Feign接口调用时返回的类型匹配的上。第二种就是我将要提供的解决方案, 改造Feign接口的调用实现,在结果集取出对应的数据映射到返回类型上

@Configuration
public class FeignConfiguration {

    @Autowired
    ObjectFactory<HttpMessageConverters> messageConverters;

    /**
     * Feign 响应解码
     *
     * @see FeignClientsConfiguration#feignDecoder()
     * @return Decoder
     */
    @Bean
    public Decoder feignDecoder() {
        return new OptionalDecoder(new ResponseEntityDecoder(new FeignResponseDecoder(this.messageConverters)));
    }
}
@AllArgsConstructor
public class FeignResponseDecoder implements Decoder {

    /**
     * Http消息转换器(从 ObjectFactory Bean中获取)
     */
    ObjectFactory<HttpMessageConverters> messageConverters;

    /**
     * Decodes an http response into an object corresponding to its
     * {@link Method#getGenericReturnType() generic return type}. If you need to
     * wrap exceptions, please do so via {@link DecodeException}.
     *
     * @param response the response to decode
     * @param type     {@link Method#getGenericReturnType() generic return type} of the
     *                 method corresponding to this {@code response}.
     * @return instance of {@code type}
     * @throws IOException     will be propagated safely to the caller.
     * @throws DecodeException when decoding failed due to a checked exception besides IOException.
     * @throws FeignException  when decoding succeeds, but conveys the operation failed.
     */
    @SneakyThrows
    @Override
    public Object decode(Response response, Type type) throws DecodeException, FeignException {
        // 校验类型是否能转换为正常类
        Asserts.state(!(type instanceof Class || type instanceof ParameterizedType || type instanceof WildcardType), Re.failed());

        // 响应类型
        Class<?> responseClass;
        // 校验类型是否带泛型处理
        if (type instanceof ParameterizedTypeImpl) {
            // 泛型类型
            responseClass = ((ParameterizedTypeImpl) type).getRawType();
        }else {
            // 默认类型
            responseClass = Class.forName(type.getTypeName());
        }

        // 设置消息转换器的响应类型
        if (!responseClass.isAssignableFrom(String.class)) {
            type = Re.class;
        }

        // Http消息转换器提取器
        @SuppressWarnings({"unchecked", "rawtypes"})
        HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(type, this.messageConverters.getObject().getConverters());

        // 获取 Feign 中响应的数据
        Object extractData = extractor.extractData(new FeignResponseAdapter(response));
        log.info("接口:{},请求体:{},结果集:{}", response.request().url(), response.request().body() == null ? "" : new String(response.request().body()), extractData);

        // 响应数据为空校验
        Asserts.state(extractData == null, Re.failed(), response.request().url());

        // 结果集
        Re<?> result = JSONObject.parseObject(extractData.toString(), Re.class);
        // 数据转换校验
        Asserts.state(JSONObject.parseObject(extractData.toString(), Re.class) == null, Re.failed(), response.request().url());

        // 校验返回结果集是否成功 不成功则直接返回 省略代码中逻辑校验
        Asserts.state(!FeignDefine.NON_INTERCEPT_CODE.contains(result.getCode()), result, response.request().url());

        // 解析出 result data 数据,判断如果是 基本类型 or 引用类型 则直接返回
        if (ClassUtils.isPrimitiveOrWrapper(responseClass) || responseClass.isAssignableFrom(String.class)) {
            return typeAdapter(result.getData(), responseClass);
        }

        // 处理 data 数据为空的情况
        if (ObjectUtils.isEmpty(result.getData())) {
            return result.getData();
        }

        // 解析出 result data 数据,转换成对象
        return JSONObject.parseObject(result.getData().toString(), responseClass);
    }

    /**
     * 类型适配器
     *
     * @param o 适配数据
     * @param tClass 返回类型
     * @return 适配类型的数据
     */
    public Object typeAdapter(Object o, Class<?> tClass) {
        // 类型适配
        if (Long.class.equals(tClass)) {
            return Long.valueOf(o.toString());

        } else if (Short.class.equals(tClass)) {
            return Short.valueOf(o.toString());

        } else if (Byte.class.equals(tClass)) {
            return Byte.valueOf(o.toString());

        } else if (Float.class.equals(tClass)) {
            return Float.valueOf(o.toString());

        } else if (Double.class.equals(tClass)) {
            return Double.valueOf(o.toString());
        }
        return o;
    }
}

public class FeignResponseAdapter implements ClientHttpResponse {

    private final Response response;

    public FeignResponseAdapter(Response response) {
        this.response = response;
    }

    @Override
    public HttpStatus getStatusCode() throws IOException {
        return HttpStatus.valueOf(this.response.status());
    }

    @Override
    public int getRawStatusCode() throws IOException {
        return this.response.status();
    }

    @Override
    public String getStatusText() throws IOException {
        return this.response.reason();
    }

    @Override
    public void close() {
        try {
            this.response.body().close();
        }
        catch (IOException ex) {
            // Ignore exception on close...
        }
    }

    @Override
    public InputStream getBody() throws IOException {
        return this.response.body().asInputStream();
    }

    @Override
    public HttpHeaders getHeaders() {
        return getHttpHeaders(this.response.headers());
    }

    HttpHeaders getHttpHeaders(Map<String, Collection<String>> headers) {
        HttpHeaders httpHeaders = new HttpHeaders();
        for (Map.Entry<String, Collection<String>> entry : headers.entrySet()) {
            httpHeaders.put(entry.getKey(), new ArrayList<>(entry.getValue()));
        }
        return httpHeaders;
    }
}
public class Asserts extends Assert {

    /**
     * 抛出指定类型状态码
     * @param reCode 状态码
     * @throws AnywayException -
     */
    public static void state(ReCode reCode) throws AnywayException{
        throw new AnywayException(reCode);
    }

    /**
     * 验证 - 抛出指定类型状态码
     * @param expression 描述业务正确的逻辑判断
     * @param reCode 自定义返回码与消息
     * @throws AnywayException -
     */
    public static void state(boolean expression, ReCode reCode) throws AnywayException{
        if (expression) {
            throw new AnywayException(reCode);
        }
    }

    /**
     * 验证 - 抛出指定类型状态码
     * @param expression 描述业务正确的逻辑判断
     * @param re 结果集
     * @throws FeignDecoderException -
     */
    public static void state(boolean expression, Re<?> re) throws FeignDecoderException {
        if (expression) {
            throw new FeignDecoderException(re);
        }
    }

    /**
     * 验证 - 抛出指定类型状态码
     * @param expression 描述业务正确的逻辑判断
     * @param re 结果集
     * @param url Feign 异常的接口地址
     * @throws FeignDecoderException -
     */
    public static void state(boolean expression, Re<?> re, String url) throws FeignDecoderException {
        if (expression) {
            throw new FeignDecoderException(re, url);
        }
    }

    /**
     * 抛出指定类型Code 与 错误消息
     * @param expression 描述业务正确的逻辑判断
     * @param reCode 自定义返回码与消息
     * @param errorMsgTemplate 错误消息模板
     * @param params 参数 替换模板中 {} 符号
     * @throws AnywayException -
     */
    public static void state(boolean expression, ReCode reCode, String errorMsgTemplate, Object... params) throws AnywayException{
        if (expression) {
            throw new AnywayException(reCode, StrUtil.format(errorMsgTemplate, params));
        }
    }
}
@Getter
@EqualsAndHashCode(callSuper = true)
public class FeignDecoderException extends EncodeException {

    /**
     * 结果集
     */
    private final Re<?> re;

    /**
     * Feign 异常的接口地址
     */
    private String url;

    /**
     * 抛结果集异常
     *
     * @param re 结果集
     */
    public FeignDecoderException(Re<?> re) {
        super(re.getMessage());
        this.re = re;
    }

    /**
     * 抛结果集异常
     *
     * @param re 结果集
     * @param url Feign 异常的接口地址
     */
    public FeignDecoderException(Re<?> re, String url) {
        super(re.getMessage());
        this.re = re;
        this.url = url;
    }
}
public class FeignDefine {
    /**
     * Feign 返回无需拦截的 code 集
     */
    public static List<Integer> NON_INTERCEPT_CODE = new ArrayList<Integer>(){{
        add(DefaultReCode.SUCCESS.getCode());
    }};
}

总结:此处代码的核心处理思路就是找到Feign接口调用的切入点,获取返回的数据结果集,校验结果集返回的是否成功,不成功则直接抛出异常(对于Feign接口调用的时候,你是否为每个Feign接口的结果集判断是否是成功而苦恼过?此处即可解决这个问题,省略业务上调用Feign接口之后的判断),成功则从结果集中获取到data数据,将data数据的类型需要转为返回类型,否则会抛出类型转换的异常。将数据转为返回类型之后return。

以上就是我在重构系统中的一些优化思路,欢迎各位小伙伴们一起来探讨!

Logo

开源、云原生的融合云平台

更多推荐