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) { return null ; } public String sayHello () { return null ; } }
之后,为了传递调用失败的情况,再定义一个异常类:
1 2 3 4 5 6 7 public class FoobarException extends RuntimeException { public FoobarException (String message) { super (message); } }
接下来,开始对 setName
和 sayHello
两个方法进行实现。
不过在这之前 —— 众所周知,懒惰是程序员的第一美德 ,发送请求的代码当然不会重复写两次。因此,先在 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) { PostRequest req = new PostRequest (); req.setUrl("https://api.foo.bar/" + funcName); req.setHeader("Authorization" , this .apiKey); req.setForm(form); String response = Http.send(req); Map<String, Object> result = Json.parse(response); 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
方法传入 funcName
和 resultType
;
当然,也可以把构造 Form
的工作丢给调用者,甚至直接把 call
方法暴露给调用者,不过这样并不符合封装这个 SDK 的初衷,而且从整体来说代码量并没有减少,只是转嫁给了调用者。
最重要的是:明明定义了参数名和方法名,还要再 copy 一次给 Form.set()
和 call()
方法,这样的重复劳动很容易增加出错的机率。
接下来,就针对上面两点,逐一击破。
由于 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; } Class type = bean.getClass(); for (Field field : type.getDeclaredFields()) { if (!field.isAccessible()) { continue ; } try { Object value = field.get(bean); if (value == null ) { continue ; } 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; } for (Field field : getFieldsFromClass(bean.getClass())) { try { Object value = field.get(bean); if (value == null ) { continue ; } 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)) { fields = fieldCache.get(type); } else { 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 { 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 { String funcName = method.getName(); Class resultType = method.getReturnType(); Form form = args.length > 0 ? FormUtils.toForm(args[0 ]) : null ; 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 () ; void setAvatar (SetAvatarParams params) ; }
至此,完成,不需要对实现代码进行任何改动,只要添加相关的声明即可。这样就达到了标题中的 “声明即实现” 目标。
4 总结 方案 B 所用到的思想,就是常说的 “面向切面编程(Aspect-oriented programming)”,简称 “AOP”。
虽然常常能看到这个概念,但是如果问起它的用途,可能很多人只会想到:在调用方法(Method)的开始和结束时打印日志,记录执行时间。
希望本文能够起到一个抛砖引玉的作用,让大家挖掘出更多的可能性。
可能有人会问:怎么有点像 MyBatis?
没错,我就是借鉴了这个框架的设计思想。👻