整体思路
通过spring的Spell表达式解析变量的参数值,参数名定义为${XXX},在解析参数值后,将${XXX}替换成#XXX以匹配Spell表达式。
核心实现类
package com.example.spring_boot_study.spring.spell;
import cn.hutool.core.map.MapUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.Method;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.example.spring_boot_study.SpringEl.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.PropertyPlaceholderHelper;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@Component
public class DynamicUrlDemo {
private static final String VAR_PREFIX = "${";
private static final String VAR_SUFFIX = "}";
private static final String POINT = ".";
private static final String EQUAL = "=";
private static final String WELL = "#";
// 将表达式中含有多个${XXX}的进行分组,拿到所有参数名(如${XXX}+${YYY})
// 正则的含义是匹配${开始,非}字符的N个字符,其中以非}字符的N个字符作为分组,即分组为XXX和YYY
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\{([^}]*)");
private final PropertyPlaceholderHelper placeholderHelper =
new PropertyPlaceholderHelper(VAR_PREFIX, VAR_SUFFIX);
private final ExpressionParser parser = new SpelExpressionParser(SpringElUtil.CONFIG);
// 允许为空的参数
@Value("${valueMayBeEmptyParams:a,b,c}")
private String valueMayBeEmptyParams;
public void handle(RequestDemo requestDemo, JSONObject msgJson) {
String url = requestDemo.getUrl();
Map<String, Object> delayHeaders = new HashMap<>(2);
// 解析URL里包含的变量${XXX}
url = placeholderHelper.replacePlaceholders(url, variable -> {
VariableParser variableParser = VariableParseSelector.selectParser(variable);
if (variableParser == null) {
throw new RuntimeException(variable + "没有对应的参数解析器");
}
Object object = variableParser.parseVariable(variable, msgJson);
return String.valueOf(object);
});
try {
// 1、构建请求头
String requestHeader = requestDemo.getRequestHeader();
Map<String, String> headers = new HashMap<>(4);
if (StringUtils.isNotEmpty(requestHeader)) {
JSONObject requestHeaderJson = JSON.parseObject(requestHeader);
// 移除需要在解析完body后执行解析的头部字段 延后执行
// (即requestBody是所有入参的摘要算法结果,有些请求为了安全可能要对所有入参做一个摘要算法)
requestHeaderJson.entrySet().removeIf(header -> {
if (header.getValue().toString().contains("${requestBody}")) {
delayHeaders.put(header.getKey(), header.getValue());
return true;
}
return false;
});
headers = buildHeaders(requestHeaderJson, msgJson);
}
String responseStr;
String requestConfig = requestDemo.getRequestConfig();
Object object = JSON.parse(requestConfig);
HttpRequest httpRequest = HttpRequest.of(url).addHeaders(headers).timeout(10000);
if (object instanceof JSONObject) {
// 2、构建请求参数
JSONObject requestJson = (JSONObject) object;
Map<String, Object> form = parseParams(requestJson, msgJson);
// 写入${requestBody}
if (!delayHeaders.isEmpty()) {
msgJson.put("requestBody", new JSONArray(Collections.singletonList(form)).toJSONString());
httpRequest.addHeaders(buildHeaders(new JSONObject(delayHeaders), msgJson));
}
// 3、执行请求
if (RequestType.GET.getTypeCode() == requestDemo.getRequestType()) {
responseStr = httpRequest.method(Method.GET).form(form).execute().body();
} else {
String header = httpRequest.header("Content-Type");
if (header != null && header.contains("application/json")) {
responseStr = httpRequest.method(Method.POST).body(JSON.toJSONString(form)).execute().body();
} else {
responseStr = httpRequest.method(Method.POST).form(form).execute().body();
}
}
} else if (object instanceof JSONArray) {
JSONArray jsonArray = (JSONArray) object;
List<Map<String, Object>> form = new ArrayList<>(jsonArray.size());
jsonArray.forEach(obj -> form.add(parseParams((JSONObject) obj, msgJson)));
// 写入${requestBody}
if (!delayHeaders.isEmpty()) {
httpRequest.addHeaders(buildHeaders(new JSONObject(delayHeaders), msgJson));
}
// 3、执行请求
if (RequestType.GET.getTypeCode() == requestDemo.getRequestType()) {
responseStr = httpRequest.method(Method.GET).body(JSON.toJSONString(form)).execute().body();
} else {
responseStr = httpRequest.method(Method.POST).body(JSON.toJSONString(form)).execute().body();
}
} else {
log.error("无法解析请求参数配置:{}", requestConfig);
return;
}
String responseConfig = requestDemo.getResponseConfig();
// 如果需要对调用结果进行处理,如落库等,则另外用一个策略模式对url进行相应处理
// 跟参数解析器一样,不过以url为key
if (checkReportResult(responseStr, responseConfig)) {
log.info("请求[{}]成功", url);
} else {
log.info("请求[{}]失败", url);
}
} catch (Exception e) {
log.error("请求[{}]异常", url);
}
}
/**
* 解析并构建头部
*/
private Map<String, String> buildHeaders(JSONObject requestHeaderJson, JSONObject msgJson) {
Map<String, String> headers = new HashMap<>(4);
Map<String, Object> headerParamMap = parseParams(requestHeaderJson, msgJson);
if (MapUtil.isNotEmpty(headerParamMap)) {
headerParamMap.forEach((key, value) -> {
if (value != null && StringUtils.isNoneBlank(value.toString())) {
headers.put(key, value.toString());
}
});
}
return headers;
}
/**
* 解析请求参数,把Json配置转换成Map
*
* @param config 入参配置
* @param msgJson 消息体
* @return 参数
*/
private Map<String, Object> parseParams(JSONObject config, JSONObject msgJson) {
Map<String, Object> paramMap = new HashMap<>(config.size());
EvaluationContext ctx;
try {
ctx = SpringElUtil.createContext();
} catch (NoSuchMethodException e) {
throw new RuntimeException("创建SpringEL表达式上下文异常。");
}
config.forEach((paramKey, paramValueExpress) -> {
Object key;
// 字段名也支持表达式 但暂时不打算支持任意位置的,如果以后支持字段名任意位置的表达式 把条件去掉
if (paramKey.charAt(0) == '$') {
// 字段名也支持表达式
key = parseValue(ctx, paramKey, msgJson);
} else {
key = paramKey;
}
if (paramValueExpress instanceof JSONObject) {
Map<String, Object> paramValue = parseParams((JSONObject) paramValueExpress, msgJson);
paramMap.put(String.valueOf(key), paramValue);
} else if (paramValueExpress instanceof JSONArray) {
List<Map<String, Object>> paramValueList = new ArrayList<>(((JSONArray) paramValueExpress).size());
((JSONArray) paramValueExpress).forEach(
subParamValueExpress -> paramValueList.add(parseParams((JSONObject) subParamValueExpress,
msgJson)));
paramMap.put(String.valueOf(key), paramValueList);
} else {
try {
Object paramValue = parseValue(ctx, paramValueExpress, msgJson);
paramMap.put(String.valueOf(key), paramValue);
log.info("解析参数[{}]对应的表达式[{}]值为[{}].", key, paramValueExpress, paramValue);
} catch (Exception e) {
log.info("解析参数[{}]对应的表达式[{}]跳过.", key, paramValueExpress);
}
}
});
return paramMap;
}
/**
* 解析参数值的表达式
* 逻辑是${XXX}代表参数名,解析完后替换成#XXX以匹配Spring Spell表达式的语法
* 之所以这么定义,是为了区分#XXX()的方法,更方便解析参数去获取对应的值
*
* @param msgJson 消息体
* @param valueExpress 表达式
* @return 参数值
*/
private Object parseValue(EvaluationContext ctx, Object valueExpress, JSONObject msgJson) {
String varExpress = String.valueOf(valueExpress);
if (varExpress.contains(VAR_PREFIX) && varExpress.contains(VAR_SUFFIX)) {
List<String> varList = this.getVariableNames(varExpress);
for (String varName : varList) {
if (ctx.lookupVariable(varName) == null) {
// 策略模式获取定义参数值
VariableParser variableParser = VariableParseSelector.selectParser(varName);
if (variableParser == null) {
throw new RuntimeException(varName + "找不到对应的变量解析器。");
}
Object varValue = variableParser.parseVariable(varName, msgJson);
if (varName.indexOf(valueMayBeEmptyParams) != -1
&& (varValue == null
|| StringUtils.isBlank(varValue.toString()))) {
throw new RuntimeException(varName + "解析变量值失败。");
}
ctx.setVariable(varName, varValue);
}
// 将 ${oppoPhoneIdValue} 替换成 #oppoPhoneIdValue,匹配Spell表达式
varExpress = varExpress.replaceAll("\\$\\{" + varName + "}", "\\#" + varName);
}
}
if (varExpress.contains(WELL)) {
return parser.parseExpression(varExpress).getValue(ctx);
} else {
// 直接赋值的,即写死的参数
return valueExpress;
}
}
/**
* 检查请求是否正确
*
* @param responseStr 响应体
* @param responseConfig 响应配置
* @return 是否成功
*/
private boolean checkReportResult(String responseStr, String responseConfig) {
try {
if (responseConfig.contains(EQUAL)) {
String[] configs = StringUtils.split(responseConfig, EQUAL);
String resultKey = configs[0];
String expectValue = configs[1];
String[] keys = StringUtils.split(resultKey, POINT);
String resultValue = getResultValue(JSON.parseObject(responseStr), keys, 0);
return expectValue.equalsIgnoreCase(resultValue);
} else {
return responseConfig.equalsIgnoreCase(responseStr);
}
} catch (Exception e) {
log.error("根据出参配置[{}]解析响应结果[{}]产生异常[{}]。", responseConfig, responseStr, e.getMessage(), e);
return false;
}
}
/**
* 递归获取请求成功的值
* 配置是result.data.code=200这样的形式
* @param resultJson
* @param keys
* @param index
* @return
*/
private String getResultValue(JSONObject resultJson, String[] keys, int index) {
if (index >= keys.length) {
log.error("根据[{}]无法获取返回值", Arrays.toString(keys));
return "";
}
Object value = resultJson.get(keys[index]);
if (value == null || StringUtils.isBlank(value.toString())) {
return "";
}
if (value instanceof JSONObject) {
resultJson = (JSONObject) value;
return getResultValue(resultJson, keys, index + 1);
} else {
return value.toString();
}
}
/**
* 获取表达式中${}中的值
*
* @param content 变量表达式
* @return 变量列表
*/
private List<String> getVariableNames(String content) {
Matcher matcher = VAR_PATTERN.matcher(content);
List<String> variableList = new ArrayList<>();
while (matcher.find()) {
variableList.add(matcher.group(1));
}
return variableList;
}
}
参数解析器
package com.example.spring_boot_study.spring.spell;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 变量解析器选择器
*/
public class VariableParseSelector {
private static final Map<String, VariableParser> PARSER_MAP = new ConcurrentHashMap<>();
public static void registerParser(String variableName, VariableParser variableParser) {
PARSER_MAP.put(variableName, variableParser);
}
public static VariableParser selectParser(String variableName) {
return PARSER_MAP.get(variableName);
}
}
package com.example.spring_boot_study.spring.spell;
import com.alibaba.fastjson.JSONObject;
import java.util.Map;
/**
* Description: 变量解析接口
*/
public interface VariableParser {
/**
* 变量配置解析
*
* @param variableName 变量名
* @param msgJson 消息体(包含所有参数对应的参数值)
* @return 变量
*/
Object parseVariable(String variableName, JSONObject msgJson);
}
package com.example.spring_boot_study.spring.spell;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* Description:
* 参数解析器
*/
@Component
public class OppoExposeParser implements VariableParser, InitializingBean {
/**
* 有些参数值是需要调具体的service去获取的,可以在这里实现
* 也可以在封装msgJson的时候实现好。这里用到的是参数值封装好在msgJson的形式
* @param variableName 变量名
* @param msgJson 消息体
* @return
*/
@Override
public Object parseVariable(String variableName, JSONObject msgJson) {
switch (variableName) {
case "oppoPhoneIdName": {
if (true) {
return "oppoPhoneIdName";
}
return "companyName";
}
case "oppoPhoneIdValue": {
return msgJson.get("oppoPhoneIdValue");
}
case "oppoExposeTimestamp": {
return msgJson.get("oppoExposeTimestamp");
}
case "oppoAdId": {
return msgJson.get("oppoAdId");
}
case "oppoType": {
return msgJson.get("oppoType");
}
case "requestBody": {
return msgJson.get("requestBody");
}
default:
return "";
}
}
@Override
public void afterPropertiesSet() throws Exception {
VariableParseSelector.registerParser("oppoPhoneIdName", this);
VariableParseSelector.registerParser("oppoPhoneIdValue", this);
VariableParseSelector.registerParser("oppoExposeTimestamp", this);
VariableParseSelector.registerParser("oppoAdId", this);
VariableParseSelector.registerParser("oppoType", this);
VariableParseSelector.registerParser("oppoType", this);
}
}
工具类
package com.example.spring_boot_study.spring.spell;
import cn.hutool.crypto.digest.MD5;
import com.example.spring_boot_study.SpringEl.AesFunction;
import com.example.spring_boot_study.SpringEl.DateFunction;
import com.example.spring_boot_study.SpringEl.HmacSha1Function;
import com.example.spring_boot_study.SpringEl.MathFunction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import java.util.HashMap;
import java.util.Map;
/**
* Description: SpringEl获取静态Method
*/
@Slf4j
public class SpringElUtil {
/**
* IMMEDIATE - 在即时模式下,尽快编译表达式。这通常是在第一次解释评估之后。
* 如果编译的表达式失败(通常是由于类型改变,如上所述),则表达式评估的调用者将收到异常。
*/
public static final SpelParserConfiguration CONFIG = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
SpringElUtil.class.getClassLoader());
/**
* 对公共method的进行缓存 防止每次都getDeclaredMethod
* 所有方法必须都是静态方法
* 这里使用自定义函数,简化方法的调用
* 只演示ceil方法,aes方法等其他所需方法可自行实现
*/
private static final Map<String, Object> METHOD_CACHE = new HashMap<String, Object>() {
{
try {
put("ceil", Math.class.getDeclaredMethod("ceil", double.class));
// put("hmac", HmacSha1Function.class.getDeclaredMethod("encrypt", String.class, String.class));
put("aes", AesFunction.class.getDeclaredMethod("strEncodeBase64", String.class, String.class));
// put("currentTimeMillis", DateFunction.class.getDeclaredMethod("currentTimeMillis"));
// put("currentTimeSeconds", DateFunction.class.getDeclaredMethod("currentTimeSeconds"));
// put("currentStringDate", DateFunction.class.getDeclaredMethod("currentStringDate"));
} catch (NoSuchMethodException e) {
log.error("SpringEl获取方法失败", e);
}
}};
public static EvaluationContext createContext() throws NoSuchMethodException {
//这里因为安全 设置了只读模式,如需其他模式可以自行更改
// StandardEvaluationContext有注入风险,所以使用SimpleEvaluationContext
//SimpleEvaluationContext是 StandardEvaluationContext子集,功能没那么多
EvaluationContext ctx = SimpleEvaluationContext.forReadOnlyDataBinding().build();
METHOD_CACHE.forEach(ctx::setVariable);
return ctx;
}
public static void main(String[] args) throws Exception{
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SpringElUtil.createContext();
Object value = parser.parseExpression("#ceil(1.345)").getValue(context);
System.out.println(value);
}
}
package com.example.spring_boot_study.spring.spell;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
/**
* AES加密
*/
public class AesFunction {
private static final String AES_ECB_PKCS5PADDING = "AES/ECB/PKCS5Padding";
public static String strEncodeBase64(String data, String base64Key) {
final Key dataKey = new SecretKeySpec(Base64.getDecoder().decode(base64Key), "AES");
try {
Cipher cipher = Cipher.getInstance(AES_ECB_PKCS5PADDING);
cipher.init(Cipher.ENCRYPT_MODE, dataKey);
byte[] encryptData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptData).replaceAll("\r", "").replaceAll("\n", "");
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public static void main(String[] args) {
System.out.println(AesFunction.strEncodeBase64("123", "XGAXicVG5GMBsx5bueOe4w=="));
}
}
入参:
package com.example.spring_boot_study.spring.spell;
import lombok.Data;
@Data
public class RequestDemo {
/**
* 请求URL,如果需要带动态参数(非写死的参数),以${参数名形式}
*/
private String url;
/**
* 请求类型 1、Get 2、Post
*/
private Integer requestType;
/**
* 请求头配置
*/
private String requestHeader;
/**
* 请求配置
*/
private String requestConfig;
/**
* 响应配置
*/
private String responseConfig;
}
模拟入参:
RequestDemo requestDemo = new RequestDemo();
requestDemo.setRequestConfig("{\n" +
"\t\"timestamp \": \"${oppoExposeTimestamp}\",\n" +
"\t\"adId\": \"${oppoAdId}\",\n" +
"\t\"dataType\": 2,\n" +
"\t\"ascribeType\": 1,\n" +
"\t\"channel\": 1,\n" +
"\t\"type\": \"${oppoType}\",\n" +
"\t\"pkg\": \"com.credit\",\n" +
"\t\"${oppoPhoneIdName}\": \"#aes(${oppoPhoneIdValue},'XGAXicVG5GMBsx5bueOe4w==')\"\n" +
"}");
// 注意我这里构建了一个md5(String str)的方法,而我在SpringElUtil里并没有自定义md5(String str)
// 方法,所以肯定会报错,需要啥方法就要预先在SpringElUtil定义
requestDemo.setRequestHeader("{\n" +
"\t\"Content-Type\": \"application/json;charset=UTF-8\",\n" +
"\t\"signature\": \"#md5(${requestBody}+${oppoExposeTimestamp}+'e0u6fnlag06lc3pl')\",\n" +
"\t\"timestamp\": \"${oppoExposeTimestamp}\"\n" +
"}");
// 封装参数对应的值
JSONObject msgJson = new JSONObject();
msgJson.put("custNo", "123");
msgJson.put("oppoPhoneIdName", "123");
msgJson.put("oppoPhoneIdValue", "123");
msgJson.put("oppoExposeTimestamp", new Date());
msgJson.put("oppoAdId", "123");
msgJson.put("oppoType", "123");
// 可以自己写一个Controller实现具体的接口,我这里随便写的
// 下面的返回也是一样,调试时,屏蔽http调用,构造返回参数即可
// 调用真实的url,可根据具体要求设置responseConfig
requestDemo.setUrl("/test");
requestDemo.setResponseConfig("result.data.code=200");