2023-01-14
实战设计
0

目录

问题与思路:
1. Controller层的【try catch】处理
2. Service业务逻辑的中断处理
3. Controller层数据响应处理
4. 微服务Feign接口冗余重复代码与类
5. 基于全局统一响应处理与共用Feign模块后的服务调用
总结

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

问题与思路:

1. Controller层的【try catch】处理

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

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

解决思路 :全局异常处理

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

java
@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结果集的问题。

java
示例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源码

java
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)); } } }

自定义异常类:

java
@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; } }

示例:

java
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结果集上不加泛型的话,会增加代码阅读的复杂度。

java
// 示例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 接口 。

java
@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); } }

示例:

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

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

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

java
// 服务调用方 @FeignClient("base-user") public interface BaseUserFeign { @GetMapping("t1") Re<String> test1(@RequestParam("t") String t); @PostMapping("t2") Re<DemoVO> test2(@RequestBody DemoDTO demoDTO); }
java
// 服务提供方 @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接口中,不需要对外提供接口服务的,按照正常实现即可。

java
// 服务调用方(无需手写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); } }
java
// 服务提供方 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接口的调用实现,在结果集取出对应的数据映射到返回类型上。

java
@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))); } }
java
@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; } }
java
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; } }
java
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)); } } }
java
@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; } }
java
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。

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!