声明即实现(二) - Java 注解的应用

1 前言

在前文《声明即实现(一) - Java 反射与代理的应用》中,为了把重点放在介绍反射和代理,因此将场景设定得相对理想化,但实际情况往往要复杂许多。

这里可以随口列出几个方案 B 可能面临的问题:

  • 方案 A 已经在项目中使用了一段时间,切换到方案 B 时,希望能保持向下兼容 —— 即仍然沿用参数形式,而不是 POJO。
  • API 的函数名(Function Name)未使用驼峰式命名(CamelCase),甚至包含子路径。
  • API 的参数未使用驼峰式命名。

上述这些问题,都可以通过引入注解(Annotation)来解决。

2 场景

首先更新一下场景,引入上面说到的诸多复杂情况。

还是服务商 FOOBAR,API 发生了如下改动:

  • 接口按功能划分到不同的子目录;
  • 请求方式分为 GETPOST,对于没有参数的接口,要求使用 GET
  • 参数名改为蛇式命名(SnakeCase);

前文中用到的几个 API,变为:

Function Path Parameters
name/set name
profile/set first_name,last_name,email,bio
avatar/set url
action/say_hello -

各 API 的返回结果并没有变化。

3 实现

3.1 定义注解

先简单介绍一下注解:注解用于在 Java 的各种元素 —— 如类(Class),类字段(Field),类方法(Method)等 —— 上,添加一些元数据。开发者可以在编译阶段或者运行期读取这些元数据,进行对应的处理。

因此,配合反射使用注解,只是注解的一种使用场景。


为了解决前面提到的问题,定义两个注解:ApiFuncApiParam

1
2
3
public @interface ApiFunc {}

public @interface ApiParam {}

默认情况下,注解在编译阶段会被丢弃,因此需要通过 java.lang.annotation.Retention 注解,让编译器将它保留到到运行期。此外,还需要通过 java.lang.annotation.Target 注解,声明每个注解的应用范围:

1
2
3
4
5
6
7
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ApiFunc {}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface ApiParam {}

最后,在注解中,定义需要的元数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ApiFunc {

/** API function path. */
String path();

}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface ApiParam {

/** API parameter name. */
String name();

}

3.2 添加注解

在前文的 FoobarClient 接口上,添加定义的注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface FoobarClient {

@ApiFunc(path = "name/set")
void setName(SetNameParams params);

@ApiFunc(path = "profile/set")
void setProfile(SetProfileParams params);

@ApiFunc(path = "action/say_hello")
String sayHello();

@ApiFunc(path = "avatar/set")
void setAvatar(SetAvatarParams params);

}

用于参数的 POJO 类,根据需要添加注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SetProfileParams {

@ApiParam(name = "first_name")
public String firstName;
@ApiParam(name = "last_name")
public String lastName;

public String email;
public String bio;

public SetProfileParams(String firstName, String lastName,
String email, String bio) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.bio = bio;
}
}

为了让方案 B 对方案 A 保持向下兼容,将原本接受参数的方法也定义到 FoobarClient 中:

1
2
3
4
5
6
7
8
9
10
11
@ApiFunc(path = "name/set")
void setName(@ApiParam(name = "name") String name);

@ApiFunc(path = "profile/set")
void setProfile(@ApiParam(name = "first_name") String firstName,
@ApiParam(name = "last_name") String lastName,
@ApiParam(name = "email") String email,
@ApiParam(name = "bio") String bio);

@ApiFunc(path = "avatar/set")
void setAvatar(@ApiParam(name = "url") String url);

3.3 处理注解

现在,所有必要的信息,都已经通过注解的方式添加到接口类上。

接下来,对代理类中的 invoke 方法进行改造,增加对注解的处理:

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
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// API function name
ApiFunc af = method.getAnnotation(ApiFunc.class);
String funcName = af != null ? af.path() : method.getName();
// API parameters
Form form = args.length > 0 ? this.createForm(method, args) : null;
return this.call(funcName, form, method.getReturnType());
}

private Form createForm(Method method, Object[] args) {
Form form = new Form();
Parameter[] params = method.getParameters();
for(int i = 0; i < params.length; i++) {
Object argValue = args[i];
// Skip null argument
if(argValue == null) {
continue;
}
// Set argument to form
ApiParam ap = params[i].getAnnotation(ApiParam.class);
if (ap != null) {
// An annotated argument
form.set(ap.name() , String.valueOf(argValue));
} else {
// A POJO argument
for(Field field : argValue.getClass().getFields()) {
ap = field.getAnnotation(ApiParam.class);
String argName = ap != null ? ap.name() : field.getName();
try {
form.set(argName, String.valueOf(field.get(argValue)));
} catch (IllegalAccessException ignored) {}
}
}
}
return form;
}

关于 “没有参数时,使用 GET 请求” 这一逻辑,直接在 call 方法中处理即可,这里不再赘述。

这里摒弃了前文创建的 FormUtils 类,改为使用新实现 createForm 的方法来创建 form—— 当然,这个方法是未优化的,仅用于展示处理注解的流程。

3.4 优化

优化基于两项原则:

  • 尽可能减少反射的使用;
  • 尽可能在初始化阶段完成费时的操作;

因此,这里选择在代理类的构造阶段,扫描整个接口类,将所有声明的 API 和相关参数解析并缓存下来。

首先定义类 ApiFuncInfo,用于存储解析的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final class ApiFuncInfo {

/** API function */
public String funcName;

/**
* Argument at parameter:
* Key => parameter index.
* Value => argument name.
*/
public Map<Integer, String> positionalArgs = new HashMap<>();

/**
* Arguments in POJO.
* Key => parameter index.
* Value => POJO info map.
* Key => argument name.
* Value => POJO field.
*/
public Map<Integer, Map<String, Field>> pojoArgs = new HashMap<>();

}

修改 FoobarClientHandler,在构造函数(constructor)中增加扫描接口的逻辑:

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
public class FoobarClientHandler implements InvocationHandler {

private String apiKey;

/** Store all API information. */
private Map<Method, ApiFuncInfo> apiMap = new HashMap<>();

public FoobarClientHandler(String apiKey) {
this.apiKey = apiKey;
this.scanApis();
}

private void scanApis() {
// Scan all methods on FoobarClient
for(Method method : FoobarClient.class.getMethods()) {
ApiFuncInfo info = new ApiFuncInfo();
// Store function name
ApiFunc af = method.getAnnotation(ApiFunc.class);
info.funcName = af != null ? af.path() : method.getName();
// Scan function parameters
Parameter[] params = method.getParameters();
for(int index = 0; index < params.length; index ++) {
Parameter param = params[index];
ApiParam ap = param.getAnnotation(ApiParam.class);
if(ap != null) {
// An annotated parameter
info.positionalArgs.put(index, ap.name());
} else {
// A POJO parameter
Map<String, Field> pojoInfo = new HashMap<>();
// Scan POJO fields
Class<?> paramType = param.getType();
for(Field field : paramType.getFields()) {
ap = field.getAnnotation(ApiParam.class);
String argName = ap != null ? ap.name() : field.getName();
pojoInfo.put(argName, field);
}
info.pojoArgs.put(index, pojoInfo);
}
}
apiMap.put(method, info);
}
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// TODO
return null;
}
}

这样,在代理类完成实例化的时候,所有 API 相关信息都已经存储在 apiMap 中。

最后重新实现 invokecreateForm 方法,改为使用预先解析出的信息构造请求:

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
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Get API info from map
ApiFuncInfo info = this.apiMap.get(method);
// Build form
Form form = args.length > 0 ? this.createForm(info, args) : null;
return this.call(info.funcName, form, method.getReturnType());
}

private Form createForm(ApiFuncInfo info, Object[] args) {
final Form form = new Form();
for(int i = 0; i < args.length; i++) {
final Object argValue = args[i];
// Skip null argument
if(argValue == null) {
continue;
}
if(info.positionalArgs.containsKey(i)) {
form.set(info.positionalArgs.get(i), String.valueOf(argValue));
} else if(info.pojoArgs.containsKey(i)) {
Map<String, Field> pojoInfo = info.pojoArgs.get(i);
pojoInfo.forEach((name, field) -> {
try{
form.set(name, String.valueOf(field.get(argValue)));
} catch (IllegalAccessException ignored) {}
});
}
}
return form;
}

至此,一个完善的方案 B——“方案 B-EX” 诞生了。

调用方法不需要任何改动,也实现了方案 B 对方案 A 的向下兼容:从方案 A 切换到方案 B,只需要修改创建 FoobarClient 的一行代码即可。

4 延伸

4.1 扩展注解

本文的定位依然是抛砖引玉,因此上述示例代码并不能 100% 覆盖所有场景,但是文中的思路,是可以应对大部分场景的。

假定服务商又把 API 改成了 RESTful 风格,请求的方式除 GETPOST 外,还增加了 PUTDELETE 等。

在上述方案中,只需要在 ApiFunc 注解中增加 method 信息,并在 FoobarClientHandler 中进行相应的处理即可,整体架构保持不变。

4.2 解耦

FoobarClientHandler 中与实际业务相关的部分,只有 scanApis 中硬编码的 FoorbarClient.class,和 call 方法的实现。

如果将这两者从中剥离出来,改为通过参数传入,FoobarClientHandler 就变成了一个通用化的代理类。这个代理类配合两个注解,就形成了一个通用的方案。

这里就不深入展开了,有兴趣的话,可以自行实现一下。

5 总结

反射(Reflection)、代理(Proxy)和注解(Annotation),可以算是 Java 界的三剑客,几乎所有主流的 Java 框架都用到了这些技术。

本文(和前文)通过封装 HTTP API 这一常见需求,尝试应用了一下这几项技术。如果将思路倒转一下,也可以基于本文的介绍,大概想象出那些主流框架的内部是如何实现的。