一、背景
在与大型分布式系统协作完成功能实现时,如何实现50个参数及以上的API调用,其中还有嵌套参数场景,并支持业务默认参数配置实现、还需支持API参数按业务需要进行添加和删除。
二、方案设计
我们知道,在接口联调阶段我还需要对接口字段进行分析核对然后进行业务匹配,最后完成接口调用和功能实现。但是只能应对参数较少的接口API,且一般都是在程序中硬编码写死的。那么该如何实现超多参数配置扩展接口调用?我们可以从以下方案入手。
- 1.基于远程的API参数定义对应的DTO,使用jackson的注解配置映射好相应的属性名称,属性参数一定要是全集的。
- 2.远程API调用我们选择使用OpenFeign框架,声明式接口开发提高效率、完善的日志体系有助于问题排查、同时应用SpringMvc注解完成参数映射。
- 3.定义API全集参数的默认值,使用键值对的方式,可以使用配置文件或使用快码表配置等。
- 4.定义API默认参数(业务上必填参数)的列表,同上可以选择配置文件或快码表存储。
- 5.设计一个参数比对映射的API,为全集和默认参数比对映射提供支持。场景说明:业务只会录入默认的参数值,其他的参数没有录入则为空,所有需要和从全集参数中获取默认值。同样,如果默认参数列表添加也只能从全集参数列表中选择添加。
- 6.接口参数中存在多级嵌套属性,通过实现转换的API完成映射。
三、代码实现
参考代码中Map映射 是关键点。另外:属性1.属性2.属性3(goods.spec.params.height)类似格式的key转换多层级需要使用递归逻辑处理。
- 1、代码结构主要分为两部分,远程API服务端和API调用方客户端。
- 2、模拟远程API代码,这里主要体现参数DTO,多且嵌套组合(3级嵌套)
@RestController
@RequestMapping("/api/order")
public class OrderController {
@PostMapping("/create")
public String create(@RequestBody OrderDTO dto){
System.out.println("下单成功,"+ dto);
return dto.getOrderId();
}
}
@Data
public class OrderDTO {
@JsonProperty("order-id")
private String orderId;
@JsonProperty("order-name")
private String orderName;
private String attr3;
private String attr4;
private String attr5;
// 可以有N个参数
@JsonProperty("order-n")
private String attrN;
private GoodsDTO goods;
}
@Data
public class GoodsDTO {
@JsonProperty("goods-id")
private String goodsId;
@JsonProperty("goods-name")
private String goodsName;
@JsonProperty("goods-color")
private String goodsColor;
@JsonProperty("goods-attr3")
private String goodsAttr3;
@JsonProperty("goods-attr4")
private String goodsAttr4;
@JsonProperty("goods-attr5")
private String goodsAttr5;
@JsonProperty("goods-attr-n")
private String goodsAttrN;
private SpecDTO spec;
}
@Data
public class SpecDTO {
@JsonProperty("spec-name")
private String specName;
@JsonProperty("spec-code")
private String specCode;
// 如高,宽等参数 弱类型
private Map<String,Object> params;
}
- 3、客户端代码
// 引入Openfeign依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
//定义服务端api调用客户端接口
@FeignClient(name = "Order",url = "http://localhost:8123")
public interface OrderClientService {
@PostMapping("/api/order/create")
String createOrder(OrderDTO dto);
}
// 客户端调用代码
@RestController
@RequestMapping("api/test")
public class TestController {
@Autowired
OrderClientService clientService;
@Autowired
Environment environment;
@PostMapping("/create")
public String create(){
String property = environment.getProperty("order-name");
System.out.println(property);
Properties allParam =(Properties) ((StandardServletEnvironment) environment).getPropertySources().get("order-all").getSource();
Properties pageSubmitParam =(Properties) ((StandardServletEnvironment) environment).getPropertySources().get("order-default").getSource();
ParamUtils.updateParam(allParam,pageSubmitParam);
OrderDTO dto = ParamUtils.toEntity(allParam,OrderDTO.class);
String order = clientService.createOrder(dto);
return "下单成功" + order;
}
}
//通过配置文件实现全集默认 order-all.properties
order-id=order-01
order-name=购买手机
attr3=属性3
attr4=属性4
attr5=属性5
attr-n=属性5
goods.goods-id=goods-01
goods.goods-name=iphone
goods.goods-color=红色
goods.goods-attr3=goods-attr3
goods.goods-attr4=goods-attr4
goods.goods-attr5=goods-attr5
goods.goods-attr-n=goods-attr-n
goods.spec.spec-name=手机规格
goods.spec.spec-code=C003
goods.spec.params.height=10cm
goods.spec.params.weight=500g
//模拟页面提交的参数 order-default.properties
order-id=order-02
order-name=购买手机-iPhone13
goods.goods-id=goods-02
goods.goods-name=iphone13
goods.goods-color=黄色
goods.spec.spec-name=手机规格
goods.spec.spec-code=C004
// 核心工具类
@Component
@PropertySource(name = "order-all",value = {"order-all.properties"},encoding = "UTF-8")
@PropertySource(name = "order-default",value = {"order-default.properties"},encoding = "UTF-8")
public class ParamUtils {
private static final ObjectMapper mapper;
static{
//创建ObjectMapper对象
mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.registerModule(new JavaTimeModule());
}
/**
* 页面提交的参数覆盖全集参数
*/
public static void updateParam(Properties allParam, Properties pageSubmitParam){
pageSubmitParam.forEach((key,value)->{
if(StringUtils.hasLength(allParam.getProperty(key.toString()))){
allParam.put(key,value);
}
});
}
/**
* 嵌套对象字符goods.spec.params.height转换为嵌套对象,递归处理
*
* @param key 对象字符
* @param value 值
* @param parent 父对象
*/
public static void formatChildObject(String key,Object value,Map<String,Object> parent){
if(key.contains(".")){
String currKey = key.split("\\.")[0];
Map<String,Object> child = new HashMap<>();
if(parent.containsKey(currKey)){
Object oldChild = parent.get(currKey);
if(!(oldChild instanceof Map)){
throw new RuntimeException(currKey+"属性存在多个,不能重复,转换失败!");
}
child = (Map<String,Object>)oldChild;
}
String nextKey = key.substring( key.indexOf(".")+1 );
formatChildObject(nextKey,value,child);
}else{
parent.put(key,value);
}
}
/**
* 将参数转换为对象dto
*
* @param allParam api参数
* @param clazz api参数模型
* @param <T> api参数模型
* @return api参数
*/
public static <T> T toEntity(Properties allParam,Class<T> clazz){
Map<String,Object> newMap = new HashMap<>();
allParam.forEach((key,value)->{
formatChildObject(key.toString(),value,newMap);
});
try {
String str = mapper.writeValueAsString(newMap);
return mapper.readValue(str,clazz);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
}
四、总结
1.嵌套对象不支持嵌套列表和数组
2.可以通过JSR303校验实现参数格式的有效性