声明即实现(一) - Java 反射与代理的应用

1 背景

不论是做客户端开发,还是服务端开发,都常常会遇到调用其他服务的情况。被调用的服务,很多会提供基于 HTTP 协议的 API 接口。除少数服务商会提供封装好的 SDK 外,大多数时候需要我们自己按照 API 文档去封装。

本文基于封装 HTTP API 这一常见需求,介绍一下 Java 中反射(Reflection)和代理(Proxy)的实际应用。

2 场景

假定有一个服务商 FOOBAR,按照如下规范提供 HTTP API:

  • API 地址: https://api.foo.bar/<functionName>
  • 请求方式: POST
  • 参数编码: application/x-www-form-urlencoded

像大多数服务提供者一样,调用 FOOBAR 的 API 也需要鉴权,方式是在请求头 Authorization 中传入 API Key。

所有 API 都以 JSON 格式返回响应数据,且 JSON 满足如下规范:

  • 固定包含布尔类型的字段 state,值为 true 时表示调用成功,反之为失败;
  • 当调用成功时,包含 data 字段,值为对应的调用结果,类型视调用的函数而定;
  • 当调用失败时,包含字符串类型的 error 字段,值为错误信息;

现在需要调用 FOOBAR 的两个 API:

Function Name Parameters Result
setName name
sayHello string

3 实现

3.1 方案 A

首先,定义一个 FoobarClient 类(Class),用于封装所有对远端服务的调用:

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

private String apiKey;

public FoobarClient(String apiKey) {
this.apiKey = apiKey;
}

public void setName(String name) {
// TODO
return null;
}

public String sayHello() {
// TODO
return null;
}

}

之后,为了传递调用失败的情况,再定义一个异常类:

1
2
3
4
5
6
7
public class FoobarException extends RuntimeException {

public FoobarException(String message) {
super(message);
}

}

接下来,开始对 setNamesayHello 两个方法进行实现。

不过在这之前 —— 众所周知,懒惰是程序员的第一美德,发送请求的代码当然不会重复写两次。因此,先在 FoobarClient 中封装一个通用的发送请求方法:

在这段代码中,处理 HTTP 请求,解析 JSON 和转换类型,都使用了伪代码,仅用于说明处理流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private <T> T call(String funcName, Form form, Class<T> resultType) {
// Make request
PostRequest req = new PostRequest();
req.setUrl("https://api.foo.bar/" + funcName);
req.setHeader("Authorization", this.apiKey);
req.setForm(form);
// Send request
String response = Http.send(req);
// Parse response
Map<String, Object> result = Json.parse(response);
// Check result
boolean state = (Boolean) result.get("state");
if(state) {
return BeanUtils.convert(result.get("data"), resultType);
} else {
throw new FoobarException((String)result.get("error"));
}
}

有了 call 方法之后,就可以很简单的写出两个 API 的实现:

1
2
3
4
5
6
7
8
9
public void setName(String name) {
Form form = new Form();
form.set("name", name);
this.call("setName", form, Void.class);
}

public String sayHello() {
return this.call("sayHello", null, String.class);
}

至此,对 FOOBAR 提供的服务 API 封装完成,调用方式如下:

1
2
3
4
5
public static void main(String[] args) {
FoobarClient client = new FoobarClient("Deja Vu");
client.setName("world");
System.out.println( client.sayHello() );
}

事情当然不会就这么结束,“野生的 Deja Vu 出现了”—— 需求变更,增加对一个 API 的调用:

Function Name Parameters Result
setProfile firstName,lastName,email,bio

不过这并不是什么难事,有了封装好的 call 方法,可以很容易地写出新 API 的实现:

1
2
3
4
5
6
7
8
9
public void setProfile(String firstName, String lastName,
String email, String bio) {
Form form = new Form();
form.set("firstName", firstName);
form.set("lastName", lastName);
form.set("email", email);
form.set("bio", bio);
call("setProfile", form, Void.class);
}

3.2 方案 B

方案 A 看起来已经足够完美,但是,只要懒惰还是第一美德,就不能阻止程序员继续偷懒寻找更精简的写法。

回顾方案 A,这些地方看起来仍是在重复劳动:

  • 构造 Form,传入参数;
  • call 方法传入 funcNameresultType

当然,也可以把构造 Form 的工作丢给调用者,甚至直接把 call 方法暴露给调用者,不过这样并不符合封装这个 SDK 的初衷,而且从整体来说代码量并没有减少,只是转嫁给了调用者。

最重要的是:明明定义了参数名和方法名,还要再 copy 一次给 Form.set()call() 方法,这样的重复劳动很容易增加出错的机率。

接下来,就针对上面两点,逐一击破。

3.2.1 自动生成 Form—— 反射的应用

由于 Java 在编译时不保留形参名,这就导致在运行时,无法通过反射拿到方法的形参名。具体来说,就是无法拿到 setName 的参数名称 ——name

因此,需要通过下列任一方法保留参数名:

  • 在形参前使用注解(Annotation);
  • 将所有参数封装到一个 POJO 中,API 方法接受 POJO 作为形参;

是不是很眼熟?没错,Spring Framework 的 Controller 就是这么干的。

这里选择方法 2,因为它相对灵活 —— 不要求调用者传入所有参数,未设置的参数将使用默认值或空值。

首先将参数封装成 POJO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SetNameParams {
public String name;
public SetNameParams(String name) {
this.name = name;
}
}

public class SetProfileParams {
public String firstName;
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;
}
}

然后实现方法 toForm,用于将任意 POJO 转换成 Form 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class FormUtils {
public static Form toForm(Object bean) {
Form form = new Form();
if(bean == null) {
return form;
}
// Get bean type
Class type = bean.getClass();
// Traverse all fields
for(Field field : type.getDeclaredFields()) {
// Skip private field
if(!field.isAccessible()) { continue; }
try {
Object value = field.get(bean);
// Skip null value
if(value == null) { continue; }
// Append to form
form.set(field.getName(), String.valueOf(value));
} catch (Exception ignored) {}
}
return form;
}
}

就像所有介绍反射的文章说的那样,反射的性能相对差一些,因此这里使用 Map 作缓存,减少反射的次数。

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
public abstract class FormUtils {

private final static Map<Class, List<Field>> fieldCache = new HashMap<>();

public static Form toForm(Object bean) {
Form form = new Form();
if(bean == null) {
return form;
}
// Travel all fields
for(Field field : getFieldsFromClass(bean.getClass())) {
try {
Object value = field.get(bean);
// Skip null value
if(value == null) { continue; }
// Add to form
form.set(field.getName(), String.valueOf(value));
} catch (Exception ignored) {}
}
return form;
}

private static List<Field> getFieldsFromClass(Class type) {
List<Field> fields;
synchronized (type) {
if(fieldCache.containsKey(type)) {
// Get fields from cache
fields = fieldCache.get(type);
} else {
// Reflect fields and store in cache
fields = new LinkedList<>();
for(Field field : type.getDeclaredFields()) {
if(!field.isAccessible()) { continue; }
fields.add(field);
}
fieldCache.put(type, fields);
}
}
return fields;
}

}

至此,自动生成 Form 的工作完成。虽然看起来代码量增加了,但这些代码是可复用的,随着对接的 API 增多,总体代码量就会有显著的减少。

3.2.2 自动传入方法名 —— 代理的应用

再来解决自动传入方法名的需求,这里使用代理(Proxy)来实现。

首先将 FoobarClient 改为接口(Interface):

1
2
3
4
5
public interface FoobarClient {
void setName(SetNameParams params);
void setProfile(SetProfileParams params);
String sayHello();
}

接下来,创建 FoobarClientHandler 类,它需要实现 java.lang.reflect.InvocationHandler 接口,并且储存调用 FOOBAR 服务用的 API Key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FoobarClientHandler implements InvocationHandler {

private String apiKey;

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

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

}

这个类是代理机制的核心,所有对接口方法的调用,都会调用这个类的 invoke 方法,它的角色就像方案 A 中的 call 方法。

首先,把方案 A 中封装好的 call 方法整个复制到 FoobarClientHandler 中来,然后在 invoke 中,通过三行代码准备好 call 所需要的所有参数:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Use method name as functionName
String funcName = method.getName();
// Get method result type
Class resultType = method.getReturnType();
// Convert argument to form
Form form = args.length > 0 ?
FormUtils.toForm(args[0]) : null;
// Call the "call" method
return this.call(funcName, form, resultType);
}

最后,创建 FoobarClientBuilder 类,通过 Java 的代理 API,生成 FoobarClient 接口的代理实例:

1
2
3
4
5
6
7
8
9
10
11
public abstract class FoobarClientBuilder {

public static FoobarClient build(String apiKey) {
return (FoobarClient) Proxy.newProxyInstance(
FoobarClientBuilder.class.getClassLoader(),
new Class[]{FoobarClient.class},
new FoobarClientHandler(apiKey)
);
}

}

创建代理也属于一种反射操作,应尽量减少次数。这里依然使用 Map 缓存创建的 FoobarClient,优化后的 FoobarClientBuilder 如下:

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

private static final Map<String, FoobarClient> clientCache = new HashMap<>();

public static FoobarClient build(String apiKey) {
FoobarClient client;
if( clientCache.containsKey(apiKey) ){
client = clientCache.get(apiKey);
} else {
client = (FoobarClient) Proxy.newProxyInstance(
FoobarClientBuilder.class.getClassLoader(),
new Class[]{FoobarClient.class},
new FoobarClientHandler(apiKey)
);
clientCache.put(apiKey, client);
}
return client;
}

}

至此,通过方案 B 对 FOOBAR 的服务 API 封装完成,调用方式如下:

1
2
3
4
5
public static void main(String[] args) {
FoobarClient client = FoobarClientBuilder.build("Deja Vu");
client.setName(new SetNameParams("world"));
System.out.println( client.sayHello() );
}

当然,需求就是为了改变而存在的,这次又要新增对一个 API 的调用:

Function Name Parameters Result
setAvatar url

在方案 B 中,需要作如下改动。

首先新增一个 SetAvatarParams 类:

1
2
3
4
5
6
public class SetAvatarParams {
public String url;
public SetAvatarParams(String url) {
this.url = url;
}
}

之后在 FoobarClient 中,增加新 API 的声明:

1
2
3
4
5
6
7
public interface FoobarClient {
void setName(SetNameParams params);
void setProfile(SetProfileParams params);
String sayHello();
// New API
void setAvatar(SetAvatarParams params);
}

至此,完成,不需要对实现代码进行任何改动,只要添加相关的声明即可。这样就达到了标题中的 “声明即实现” 目标。

4 总结

方案 B 所用到的思想,就是常说的 “面向切面编程(Aspect-oriented programming)”,简称 “AOP”。

虽然常常能看到这个概念,但是如果问起它的用途,可能很多人只会想到:在调用方法(Method)的开始和结束时打印日志,记录执行时间。

希望本文能够起到一个抛砖引玉的作用,让大家挖掘出更多的可能性。


可能有人会问:怎么有点像 MyBatis?

没错,我就是借鉴了这个框架的设计思想。👻