前言:最近公司需要对系统进行重构,基于以前微服务系统架构上的不足,我在此总结了一些问题以及优化思路、方案等,适用于Springboot单体项目,希望能对眼前的你提供一些帮助。
问题描述:在系统中充斥着大量的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();
}
}
问题描述:在开发过程中,难免需要业务逻辑判断,直接返回Result结果集确实是一种方案,不过在service层之间调用时还需要处理Result是否成功,代码量剧增。在基于全局异常处理的架构下,抛出异常也是一种常用方案,不过却是在代码中写满了new RuntimeException() 且存在不能返回特定code码的Result结果集的问题。
示例1:
public Re<User> getUser(String name){
User user = userMapper.selectUser(name);
if (user == null) {
return Re.failed(ReCode.USER_NOT_FOUND);
}
return Re.success(user);
}
示例2:
public 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;
}
问题描述:在数据响应的时候,我们都会进行一层包装,一成不变的返回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();
}
问题描述:服务之间的调用都是通过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;
}
}
问题描述:对于刚才的架构优化提供了全局统一响应处理(第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。
以上就是我在重构系统中的一些优化思路,欢迎各位小伙伴们一起来探讨!