前言
上期写了一篇RestTemplate+Aop实现远程调用,其中关于Aop实现上传文件表单和参数解析有些问题,这期主要处理上期代码遗留的bug和结合上期代码实现springboot自动配置,即导入项目坐标即可使用,无需复制代码自行配置。
Aop重构
package org.neil.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.neil.aspect.annotation.RemoteCall;
import org.neil.modules.remote.entity.RemoteServer;
import org.neil.modules.remote.datasource.RemoteServerDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.io.AbstractResource;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 远程调用AOP
*
* @author zhu.zn 2024/4/30 18:12
*/
@Slf4j
@Aspect
public class RemoteCallAspect {
@Autowired
private RestTemplate restTemplate;
@Autowired
private RemoteServerDataSource remoteServerDataSource;
@Around("@annotation(org.neil.aspect.annotation.RemoteCall)")
public Object handlerAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
log.info("远程调用开始");
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 获取参数名称
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(method);
Object[] args = joinPoint.getArgs();
// 获取注解的参数
RemoteCall remoteCall = method.getAnnotation(RemoteCall.class);
String service = remoteCall.service();
String path = remoteCall.path();
HttpMethod httpMethod = remoteCall.method();
String[] headers = remoteCall.headers();
String[] params = remoteCall.params();
String body = remoteCall.body();
String[] variable = remoteCall.variable();
String file = remoteCall.file();
// 首先构建请求头
RemoteServer remoteServer = remoteServerDataSource.loadRemoteServerByServiceName(service);
HttpHeaders httpHeaders = handlerHeaders(remoteServer.getHeaders(), headers);
// 构建body和entity
HttpEntity<?> entity = new HttpEntity<>(handlerBody(args, parameterNames, body, file, httpHeaders), httpHeaders);
// 构建请求url,BaseUrl按需从配置中心获取,注解添加service方法,根据选择的服务拿到对应的Url
String url = handlerUrl(args, parameterNames, params, variable, remoteServer.getUrl(), path);
try {
log.info("远程调用的url为:{}", url);
ResponseEntity<?> responseEntity =
restTemplate.exchange(url, httpMethod, entity, method.getReturnType());
Object data = responseEntity.getBody();
log.info("远程调用响应结果为:{}", data);
return data;
} catch (RestClientException e) {
log.info("远程调用失败:{}", e.getMessage());
e.printStackTrace();
} finally {
log.info("远程调用结束,耗时:{}ms", (System.currentTimeMillis() - start));
}
return joinPoint.proceed(args);
}
/**
* 处理请求头
*
* @param headersConfig 服务请求头配置
* @param headers 当前请求接口的额外配置
* @return 返回HttpHeader配置
*/
private HttpHeaders handlerHeaders(Map<String, String> headersConfig, String[] headers) {
HttpHeaders httpHeaders = new HttpHeaders();
headersConfig.forEach(httpHeaders::add);
if (headers.length > 0 && headers.length % 2 == 0) {
for (int i = 0; i < headers.length; i += 2) {
httpHeaders.add(headers[i], headers[i + 1]);
}
}
return httpHeaders;
}
/**
* 解析配置yrl
*
* @param args 方法参数
* @param parameterNames 参数名称
* @param params 注解输入的param
* @param variable 注解输入的variable
* @param url 服务配置的url
* @param path 请求路径
* @return 拼接出的url
*/
private String handlerUrl(Object[] args, String[] parameterNames,
String[] params, String[] variable,
String url, String path) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url).path(path);
MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
for (String param : params) {
Object paramValue = getTargetArg(args, parameterNames, param);
paramMap.addAll(resolveArg(paramValue, param));
}
paramMap.forEach(builder::queryParam);
MultiValueMap<String, Object> variableMap = new LinkedMultiValueMap<>();
for (String var : variable) {
Object varValue = getTargetArg(args, parameterNames, var);
variableMap.addAll(resolveArg(varValue, var));
}
builder.uriVariables(variableMap.toSingleValueMap());
return builder.build().toUriString();
}
/**
* 解析请求体
*
* @param args 方法参数
* @param parameterNames 参数名称
* @param bodyName 注解输入的body参数名
* @param fileName 注解输入的文件参数名
* @param httpHeaders HttpHeaders
* @return 返回post请求体
*/
private Object handlerBody(Object[] args, String[] parameterNames, String bodyName, String fileName, HttpHeaders httpHeaders) {
Object bodyValue = getTargetArg(args, parameterNames, bodyName);
// 如果fileName为空,则说明是普通POST请求,直接返回参数对象
if (fileName.length() == 0) {
return bodyValue;
}
// 有文件上传的POST表单,将参数值解析成MultiValueMap表单
Object fileValue = getTargetArg(args, parameterNames, fileName);
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> bodyMap = resolveArg(bodyValue, bodyName);
MultiValueMap<String, Object> fileMap = resolveArg(fileValue, fileName);
bodyMap.addAll(fileMap);
return bodyMap;
}
/**
* 筛选出目标参数值
*
* @param args 方法参数
* @param parameterNames 方法参数名
* @param target 目标参数名
* @return 目标参数值
*/
private Object getTargetArg(Object[] args, String[] parameterNames, String target) {
for (int i = 0; i < parameterNames.length; i++) {
if (parameterNames[i].equals(target)) {
return args[i];
}
}
return null;
}
/**
* 解析参数(spring只能解析基本类型、基本类型数组和基本类型的List)
*
* @param obj 参数值
* @param paramName 参数名
* @return 返回多值Map
*/
private MultiValueMap<String, Object> resolveArg(Object obj, String paramName) {
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
if (obj == null) {
return map;
}
// 如果是基本类型
if (obj instanceof Number
|| obj instanceof Character
|| obj instanceof Boolean
|| obj instanceof String
|| obj instanceof Date
|| obj instanceof AbstractResource) {
map.add(paramName, obj);
} else if (obj instanceof List) {
map.addAll(paramName, ((List<Object>) obj));
} else if (obj.getClass().isArray()) {
map.addAll(paramName, Arrays.asList((Object[]) obj));
} else if (obj instanceof Map) {
map.setAll(((Map<String, Object>) obj));
} else {
map.addAll(resolveObject(obj));
}
return map;
}
/**
* 仿照表单解析对象,不深入解析(spring只能解析基本类型、基本类型数组和基本类型的List)
*
* @param obj 需要解析的对象
* @return 返回多值Map
*/
private MultiValueMap<String, Object> resolveObject(Object obj) {
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
Class<?> objClass = obj.getClass();
Field[] fields = objClass.getDeclaredFields();
for (Field field : fields) {
String fieldName = field.getName();
try {
field.setAccessible(true);
Object value = field.get(obj);
if (value == null) {
continue;
}
if (value instanceof Number
|| value instanceof Character
|| value instanceof Boolean
|| value instanceof String) {
map.add(fieldName, value);
} else if (value instanceof Date) {
DateTimeFormat annotation = field.getAnnotation(DateTimeFormat.class);
if (annotation != null) {
SimpleDateFormat sdf = new SimpleDateFormat(annotation.pattern());
map.add(fieldName, sdf.format(value));
} else {
map.add(fieldName, value);
}
} else if (value instanceof List) {
map.addAll(fieldName, ((List<Object>) value));
} else if (value.getClass().isArray()) {
map.addAll(fieldName, Arrays.asList((Object[]) value));
}
field.setAccessible(false);
} catch (IllegalAccessException e) {
throw new RuntimeException("解析字段:" + fieldName + "异常", e);
}
}
return map;
}
}
代码说明
上期代码关于解析方法参数部分我想的过于复杂,导致代码看起来比较乱,其实主要关注GET请求和POST请求即可。
GET请求
主要分为查询参数和path参数
1.查询参数指的是拼接在请求url后的参数(例:http://localhost:8080/path?query=queryParam),queryParm只能是基本数据类型(Number,String, Date,基本类型的数组,其他对象类型建议与服务端沟通用json字符串传输),因为SpringMvc默认实现了基本类型的参数解析,在请求前会将输入的参数转换成字符串,服务端会将字符串解析成对应的参数类型对象,所以只需要关注基本类型和数组类型即可。在发起请求前,先进行方法参数解析,拿到注解中的params,找到对应的参数值,解析参数值,封装成MutilValueMap,如果是基本类型,直接根据参数名放入Map即可,如果是对象类型,解析一次,将其中非空的基本类型参数放入map,key为字段名,最后根据map中的数据封装请求url。
2.path参数类型其实就是path中的模板{},类似@PathVariable注解解析的参数。解析方式和GET一样,将{}中从参数替换成Map中对应的value。
POST请求
POST请求主要是普通POST请求和表单POST请求
1.普通POST请求将对应参数用HttpEntity封装即可
2.表单POST请求就是有文件上传的请求,我的做法是表单参数和附件参数分离成两个参数,如果注解内的file参数为空,就是普通POST请求,走第一步即可,否则需要将body参数对应的参数值解析成表单,和GET请求一样,解析成MultiValueMap,将整个MultiValueMap做为Body封装Entity即可,并设置请求头为表单请求httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
文件参数必须是以org.springframework.core.io.AbstractResource为基类的实现,常用的实现类有:
1.org.springframework.core.io.FileSystemResource 用于本地文件上传。
2.org.springframework.core.io.InputStreamResource 用于输入流上传,必须重写getFilename和contentLength方法,上期已经说过补充些会报错
3.org.springframework.core.io.ByteArrayResource 字节数组上传,也必须重写2的方法
4.org.springframework.web.multipart.MultipartFileResource 用于用户上传的文件(没有实验过,如果报错,按照2重写)
按照我的想法,我们基本上会用的到的就是FileSystemResource 和MultipartFileResource,一个本地文件上传一个用户上传的文件上传这两类。如果是其他类型的会导致请求失败!!!
构建成自动配置项目
既然功能完善了,那么就需要拿来实际应用了。但是每换一个项目就得重新复制一次代码,想想就很复杂吧。所以我就想能不能写成自动配置,其他项目引入依赖后只要按照约定的配置格式在application.yml配置文件写好配置,即可使用。
项目结构介绍
---|neil-boot-system 测试模块
---|neil-modules 管理基本模块
---|neil-remote 远程调用模块
---|neil-starter starter模块
---|neil-remote-starter 远程调用自动配置模块
核心的模块就是远程调用模块和自动配置模块了,自动配置模块依赖远程调用模块
在自动配置模块中的org.neil.configuration.NeilRemoteStarterAutoConfiguration声明出需要被springboot自动配置的bean即可
在resource模块下创建META-INF/spring.fatories文件因为springboot启动时会扫描所有jar包内的这个文件配置所有bean
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.neil.configuration.NeilRemoteStarterAutoConfiguration
我还写了了一个RemoteServerDataSource抽象类,需要实现loadRemoteServerByServiceName方法,默认实现是基于yml配置读取service配置,如果你想基于数据库,只需要实现这个类即可,也可以基于yml和数据库配置,怎么用取决你的想法。
我的项目是基于springboot2.7.10构建的,如果你想使用我的源码打包成starter打包成jar包使用请自行创建一个springboot2.7.10的项目引入模块调整后打包。经过我自己引入我们项目使用测试后springboot2.3.10也是可以使用这个远程调用的的。
相关源码下载
源码我创建了一个gitee仓库,有需要的可以自行下载
链接: 源码下载
最后吐槽一下吧
这是我们组另一个后端发流程创建的一个请求,可以看出,整个实现就是多行代码,全是参数封装,我跟他沟通想用我这个办法实现远程调用,很可惜完全不听,虽然程序员为了偷懒最后把自己都给优化掉了,但是看着自己曾经写出多垃圾的代码总会忍不住骂一下。我也曾写出过很多垃圾代码,后期优化修改总是很麻烦,总想找机会偷懒,但是又总没偷懒的动力。这远程调用是我空闲时间写的,总会难免有bug是我没发现的,如果你使用了我的代码出了bug请在评论区指出,能改我就改,改不了就没办法了,一个人的能力有限,不喜勿喷。