可配置输入参数的接口如何设计

个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview


作为程序员,我们绝大多数场景需要根据业务需求来设计系统,开发后端接口,这些接口通常都是在设计阶段就确定了,但是还有一些场景需要实现支持的配置的接口,包含以下功能

1、可以配置接口url

这个通常在一些论坛博客类系统中比较常见,用于设置个人主页,但其实这个又是不同概念,个人主页通常是get请求,然后通过path索引用户主页。

2、可选择接口具体实现

流程引擎、规则引擎通常就可以实现

3、输入参数和输出参数可配置

应用场景包括数据收集等

前面乱讲一通,最终回归本题——可配置输入参数的接口如何设计?

AI的回答

以下是AI的回答——可配置输入参数的接口如何设计?

设计支持可配置输入参数的接口时,主要目标是保证接口的灵活性和易用性,同时兼顾健壮性和可维护性。以下是一个通用的设计策略:

  1. 使用Map或Dictionary类型的参数
    可以定义一个Map<String, Object>作为接口的输入参数,其中键(key)表示参数名,值(value)表示参数值。这样可以根据需要动态添加或减少参数。
1
2
3
public interface MyService {
Response handleRequest(Map<String, Object> parameters);
}

这种方法简单灵活,但对于复杂的参数校验和类型转换需要额外处理。

  1. 使用泛型请求类
    定义一个包含所有可能参数的请求类,使用Java Bean或者Builder模式,通过构造方法或setters注入参数,同时可以用@JsonCreator@AllArgsConstructor等注解支持JSON序列化和反序列化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RequestParams<T> {
private String param1;
private int param2;
private T customParam; // 泛型参数,可根据需求定制

// 构造方法、getter、setter省略...

// 或者使用Builder模式
public static class Builder<T> {
// ...
public RequestParams build() { /*...*/ }
}
}

public interface MyService {
Response handleRequest(RequestParams<?> params);
}
  1. 使用策略模式
    如果参数的处理方式因参数类型而异,可以设计不同的策略类处理不同类型的参数,接口接收这些策略类作为参数。
1
2
3
4
5
6
7
public interface ParamStrategy {
void process(RequestContext context);
}

public interface MyService {
Response handleRequest(List<ParamStrategy> strategies);
}
  1. 使用命令模式
    对于复杂的操作,可以将参数封装成一个命令对象,命令对象包含所有执行操作所需的参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface Command {
void execute();
}

public class SomeCommand implements Command {
private final String param1;
private final int param2;

public SomeCommand(String param1, int param2) {
this.param1 = param1;
this.param2 = param2;
}

@Override
public void execute() {
// 使用param1和param2执行操作
}
}

public interface MyService {
Response handleRequest(Command command);
}

在设计时,请务必充分考虑参数的有效性检查、默认值设定以及错误处理机制,确保接口的健壮性。同时,良好的文档说明对于使用者来说非常重要,特别是当参数众多且复杂时。

实现思路

有了AI的帮助,思路一下子打开了是吧!

这里主要讨论Map作为参数具体该怎么实现。

若仅着眼于数据采集的目标,采用Map作为输入参数无疑是一种可行方案,然而这种方式也存在显著不足。尽管如此便捷,但数据的原始形态较为零散无序,在实际应用前仍需额外进行整理加工,因此从整体效果上看并不尽如人意。

所以还是需要一些设计的,简化流程如下,我讲的不好,也可以直接跳到下面demo看实现。

yuque_diagram

具体实现Demo

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author wnhyang
* @date 2023/11/29
**/
@RestController
@RequestMapping("/risk")
@RequiredArgsConstructor
@Slf4j
public class RiskController {

private final RiskService riskService;

@PostMapping("/{name}/sync")
public String syncRisk(@PathVariable("name") String name, @RequestBody Map<String, Object> params) {

log.info("syncRisk {}", name);
log.info("param {}", params);

return riskService.syncRisk(name, params);
}

}

Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* @author wnhyang
* @date 2023/11/29
**/
@Service
@Slf4j
@RequiredArgsConstructor
public class RiskServiceImpl implements RiskService {

/**
* 模拟服务配置
*/
private static final Map<String, Set<InputFieldVO>> SERVICE_CONFIG = Map.of(
"publicInterface", new HashSet<>(Arrays.asList(
new InputFieldVO().setId(1L).setParamName("eventId").setDisplayName("事件标识").setName("N_S_eventId").setRequired(true).setType(0),
new InputFieldVO().setId(2L).setParamName("appName").setDisplayName("应用标识").setName("N_S_appName").setRequired(true).setType(0),
new InputFieldVO().setId(3L).setParamName("customerId").setDisplayName("客户号").setName("N_S_customerId").setRequired(true).setType(0),
new InputFieldVO().setId(5L).setParamName("age").setDisplayName("年龄").setName("N_N_age").setRequired(true).setType(1),
new InputFieldVO().setId(4L).setParamName("money").setDisplayName("金额").setName("N_F_money").setRequired(true).setType(2),
new InputFieldVO().setId(6L).setParamName("transTime").setDisplayName("交易时间").setName("N_D_transTime").setRequired(true).setType(3),
new InputFieldVO().setId(7L).setParamName("transType").setDisplayName("交易类型").setName("N_E_transType").setRequired(true).setType(4),
new InputFieldVO().setId(8L).setParamName("isPayee").setDisplayName("是否收款人").setName("N_B_isPayee").setRequired(true).setType(5),
new InputFieldVO().setId(9L).setParamName("event").setDisplayName("事件标识").setName("N_S_event").setRequired(true).setType(0)
)));

@Override
public String syncRisk(String name, Map<String, Object> params) {

// 通过SERVICE_CONFIG取name判空
log.info("name {}", name);
log.info("params {}", params);

Set<InputFieldVO> fields = SERVICE_CONFIG.get(name);
if (fields == null) {
return "服务未找到";
}
// 使用增强for循环代替迭代器
for (InputFieldVO field : fields) {
Object value = params.get(field.getParamName());
if (value != null) {
// 设置field的值
field.setValue(value);
log.info("field {}", field);
}
}

return fields.toString();
}

@Override
public String asyncRisk(String name, Map<String, Object> params) {
log.info("name {}", name);

return null;
}
}

InputFieldVO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class InputFieldVO {

/**
* 参数名
*/
private String paramName;

/**
* 是否必填
*/
private Boolean required;

/**
* 值,默认null
*/
private Object value;

/**
* 自增编号
*/
private Long id;

/**
* 显示名
*/
private String displayName;

/**
* 字段名
*/
private String name;

/**
* 字段类型
*/
private Integer type;

/**
* 字段分组
*/
private Long groupId;

/**
* 描述
*/
private String description;

/**
* 默认值
*/
private String defaultValue;

/**
* 是否动态字段 N(normal)和D(dynamic)
*/
private Boolean dynamic = Boolean.FALSE;

}

FieldTypeEnum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* @author wnhyang
* @date 2023/12/5
**/
@AllArgsConstructor
@Getter
public enum FieldTypeEnum {

/**
* 字符型
*/
STRING(0, "S", "字符型"),

/**
* 整数型
*/
NUMBER(1, "N", "整数型"),

/**
* 小数型
*/
FLOAT(2, "F", "小数型"),

/**
* 日期型
*/
DATE(3, "D", "日期型"),

/**
* 枚举型
*/
ENUM(4, "E", "枚举型"),

/**
* 布尔型
*/
BOOLEAN(5, "B", "布尔型");


private final Integer code;

private final String name;

private final String desc;

}

测试

请求http://localhost:8080/risk/publicInterface/sync

1
2
3
4
5
6
7
8
9
10
11
{
"eventId": "手机交易",
"appName": "手机",
"customerId": "123456",
"age": "24",
"money": "3.14159",
"transTime": "2024-03-01 20:35:45",
"transType": "2",
"isPayee": "true",
"event": "true"
}

日志打印如下

image

小结

就先这样草草结束了,还有以下需要说明一下。

1、字段类型区分的目的是为了便于后续规则或其他计算类方法能直接使用整数或浮点类字段,而且这类字段还可以通过比较大小来区分,当然枚举和布尔类也有其他用处。

2、动态字段是为脚本类字段预留的。

3、以上Demo没有加入数数据库,这个可以设计一下。

写在最后

拙作艰辛,字句心血,望诸君垂青,多予支持,不胜感激。


个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview