问题与思路: 1. Controller层的【try catch】处理前言:最近公司需要对系统进行重构,基于以前微服务系统架构上的不足,我在此总结了一些问题以及优化思路、方案等,适用于Springboot单体项目,希望能对眼前的你提供一些帮助。
问题描述:在系统中充斥着大量的try catch的代码,当接口出现问题时,由于你的粗心少写一个方法的try catch,给前端返回了一串Exception的异常错误信息。
@GetMapping("/get/user")
public Re 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结果集的问题。
示例1: public RegetUser(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 {
public static void state(ReCode reCode) throws AnywayException{
throw new AnywayException(reCode);
}
public static void state(boolean expression, ReCode reCode) throws AnywayException{
if (expression) {
throw new AnywayException(reCode);
}
}
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 auth(){
return servie.getUser();
}
// 示例2:
@GetMapping("/get/user")
public Re auth(){
return Re.success(servie.getUser());
}
解决思路 :全局统一响应处理
解决方案:与全局异常处理一样,其核心为 @RestControllerAdvice 注解与 ResponseBodyAdvice 接口 。
@RestControllerAdvice public class UnityResponseAdvice implements ResponseBodyAdvice
示例:
@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 test1(@RequestParam("t") String t);
@PostMapping("t2")
Re test2(@RequestBody DemoDTO demoDTO);
}
// 服务提供方
@RestController
public class DemoController {
@GetMapping("t1")
public Re test1(@RequestParam("t") String t) {
return Re.success("success");
}
@PostMapping("t2")
public Re 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模块)// 需要将引入包的Fiegn进行导入,此处提供feign包路径 @EnableFeignClients("cn.code72.base") @SpringBootApplication public class AuthApplication { public static void main(String[] args) { SpringApplication.run(AuthApplication.class,args); } } cn.code72 base-user-feign 1.0.0
// 服务提供方
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 messageConverters;
@Bean
public Decoder feignDecoder() {
return new OptionalDecoder(new ResponseEntityDecoder(new FeignResponseDecoder(this.messageConverters)));
}
}
@AllArgsConstructor
public class FeignResponseDecoder implements Decoder {
ObjectFactory messageConverters;
@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);
}
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> headers) {
HttpHeaders httpHeaders = new HttpHeaders();
for (Map.Entry> entry : headers.entrySet()) {
httpHeaders.put(entry.getKey(), new ArrayList<>(entry.getValue()));
}
return httpHeaders;
}
}
public class Asserts extends Assert {
public static void state(ReCode reCode) throws AnywayException{
throw new AnywayException(reCode);
}
public static void state(boolean expression, ReCode reCode) throws AnywayException{
if (expression) {
throw new AnywayException(reCode);
}
}
public static void state(boolean expression, Re> re) throws FeignDecoderException {
if (expression) {
throw new FeignDecoderException(re);
}
}
public static void state(boolean expression, Re> re, String url) throws FeignDecoderException {
if (expression) {
throw new FeignDecoderException(re, url);
}
}
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;
private String url;
public FeignDecoderException(Re> re) {
super(re.getMessage());
this.re = re;
}
public FeignDecoderException(Re> re, String url) {
super(re.getMessage());
this.re = re;
this.url = url;
}
}
public class FeignDefine {
public static List NON_INTERCEPT_CODE = new ArrayList(){{
add(DefaultReCode.SUCCESS.getCode());
}};
}
总结:此处代码的核心处理思路就是找到Feign接口调用的切入点,获取返回的数据结果集,校验结果集返回的是否成功,不成功则直接抛出异常(对于Feign接口调用的时候,你是否为每个Feign接口的结果集判断是否是成功而苦恼过?此处即可解决这个问题,省略业务上调用Feign接口之后的判断),成功则从结果集中获取到data数据,将data数据的类型需要转为返回类型,否则会抛出类型转换的异常。将数据转为返回类型之后return。
以上就是我在重构系统中的一些优化思路,欢迎各位小伙伴们一起来探讨!



