1 前言
在前文《声明即实现(一) - Java 反射与代理的应用》中,为了把重点放在介绍反射和代理,因此将场景设定得相对理想化,但实际情况往往要复杂许多。
这里可以随口列出几个方案 B 可能面临的问题:
- 方案 A 已经在项目中使用了一段时间,切换到方案 B 时,希望能保持向下兼容 —— 即仍然沿用参数形式,而不是 POJO。
- API 的函数名(Function Name)未使用驼峰式命名(CamelCase),甚至包含子路径。
- API 的参数未使用驼峰式命名。
上述这些问题,都可以通过引入注解(Annotation)来解决。
2 场景
首先更新一下场景,引入上面说到的诸多复杂情况。
还是服务商 FOOBAR,API 发生了如下改动:
- 接口按功能划分到不同的子目录;
- 请求方式分为
GET
和 POST
,对于没有参数的接口,要求使用 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)等 —— 上,添加一些元数据。开发者可以在编译阶段或者运行期读取这些元数据,进行对应的处理。
因此,配合反射使用注解,只是注解的一种使用场景。
为了解决前面提到的问题,定义两个注解:ApiFunc
和 ApiParam
:
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 {
String path();
}
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.PARAMETER}) public @interface ApiParam {
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 { ApiFunc af = method.getAnnotation(ApiFunc.class); String funcName = af != null ? af.path() : method.getName(); 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]; if(argValue == null) { continue; } ApiParam ap = params[i].getAnnotation(ApiParam.class); if (ap != null) { form.set(ap.name() , String.valueOf(argValue)); } else { 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 {
public String funcName;
public Map<Integer, String> positionalArgs = new HashMap<>();
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;
private Map<Method, ApiFuncInfo> apiMap = new HashMap<>();
public FoobarClientHandler(String apiKey) { this.apiKey = apiKey; this.scanApis(); }
private void scanApis() { for(Method method : FoobarClient.class.getMethods()) { ApiFuncInfo info = new ApiFuncInfo(); ApiFunc af = method.getAnnotation(ApiFunc.class); info.funcName = af != null ? af.path() : method.getName(); 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) { info.positionalArgs.put(index, ap.name()); } else { Map<String, Field> pojoInfo = new HashMap<>(); 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 { return null; } }
|
这样,在代理类完成实例化的时候,所有 API 相关信息都已经存储在 apiMap
中。
最后重新实现 invoke
和 createForm
方法,改为使用预先解析出的信息构造请求:
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 { ApiFuncInfo info = this.apiMap.get(method); 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]; 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 风格,请求的方式除 GET
和 POST
外,还增加了 PUT
,DELETE
等。
在上述方案中,只需要在 ApiFunc
注解中增加 method
信息,并在 FoobarClientHandler
中进行相应的处理即可,整体架构保持不变。
4.2 解耦
FoobarClientHandler
中与实际业务相关的部分,只有 scanApis
中硬编码的 FoorbarClient.class
,和 call
方法的实现。
如果将这两者从中剥离出来,改为通过参数传入,FoobarClientHandler
就变成了一个通用化的代理类。这个代理类配合两个注解,就形成了一个通用的方案。
这里就不深入展开了,有兴趣的话,可以自行实现一下。
5 总结
反射(Reflection)、代理(Proxy)和注解(Annotation),可以算是 Java 界的三剑客,几乎所有主流的 Java 框架都用到了这些技术。
本文(和前文)通过封装 HTTP API 这一常见需求,尝试应用了一下这几项技术。如果将思路倒转一下,也可以基于本文的介绍,大概想象出那些主流框架的内部是如何实现的。