什么是链式接口(Fluent Interface)

根据wikipedia上的定义,Fluent interface是一种通过链式调用方法来完成方法的调用,其操作分为终结与中间操作两种。[1]

下面是一个Wikipedia上的例子。

Author author = AUTHOR.as("author");
create.selectFrom(author)
.where(exists(selectOne()
.from(BOOK)
.where(BOOK.STATUS.eq(BOOK_STATUS.SOLD_OUT))
.and(BOOK.AUTHOR_ID.eq(author.ID))));

在设置多参数时,通过这种方式简化操作,提高可读性。在effective java 第2版中讲述了这种接口的一个实现方式“第2条:遇到多个构造器参数时要考虑用构建器”。通过创建一个构建类Builder来实现这种风格的接口。

例如effective java 第2版,第2条的例子。

public class NutritionFacts {
private int servingSize;
private int fat; private NutritionFacts(Builder builder) {
this.servingSize = builder.servingSize;
this.fat = builder.fat;
}
...略
public static class Builder {
private int servingSize;
private int fat; public Builder servingSize(int sS) {
servingSize = sS;
return this;
} public Builder fat(int ft) {
fat = ft;
return this.
} public NutritionFacts build() {
return NutritionFacts(this);
}
}
}

可以看到,要使用这种风格的接口,那么每次都要创建Builder,然后实现属性设置,对象创建等重复操作。因此考虑通过程序自动完成这些重复操作,在使用时仅需要指定所需要实现的接口即可。

为了实现这种风格的操作需要完成两个主要的功能。 
1.链式调用设置属性。 
2.创建最终的对象。

通过Effective java 2 中的例子可以看到可以看到,链式调用的实现,是在调用属性设置方法时,每次返回Builder来实现,方法名根据要创建的目标对象的不同而变化,对于任何一个对象的创建都是通过build方法完成。

基于JDK动态代理的实现

下面以类Student为例。

public class Student {
private int age;
private String name;
private char sex;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public char getSex() {
return sex;
}
public void setSex(char sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Student [age=" + age + ", name=" + name + ", sex=" + sex + "]";
}
}

定义Builder接口,通过类型参数T来表示最终创建的对象,而方法build来完成最终对象的创建。

public interface Builder<T> {
T build();
}

创建Student的Builder接口,接口中的方法返回StudentBuilder最终实现链式属性设置。

public interface StudentBuilder extends Builder<Student>{
StudentBuilder age(int age);
StudentBuilder name(String name);
StudentBuilder sex(char sex);
}

最终的目标是,只需要使用者定义自己的XXXBuilder,而无需自己去实现各种属性设置方法及对象创建方法来使用链式调用完成属性设置。

接下来通过JDK的动态代理机制来实现上述功能。定义FluentApi来提供链式调用功能。通过target传入使用者定义的XXXBuilder接口。

public class FluentApi<T> {
/**
* 目标接口
*/
Class<T> target; private FluentApi(Class<T> target) {
if(!target.isInterface()) {
throw new IllegalArgumentException("must be interface");
}
this.target = target;
} public static <T> FluentApi<T> target(Class<T> target) {
return new FluentApi<T>(target);
}
}

接着创建XXXBuilder接口的代理。为了实现链式调用,每次必须返回代理对象proxy本身。

public class JdkProxy implements InvocationHandler{
private final String BUILDMETHOD = "build"; Class<?> target; public JdkProxy(Class<?> target) {
this.target = target;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName());
return proxy;
}
}

在FluentApi增加Create方法创建XXXBuilder的代理。

    public T create() {
JdkProxy jdkProxy = new JdkProxy(target);
return (T)Proxy.newProxyInstance(target.getClassLoader(),
new Class[]{target},
jdkProxy);
}

经过上面简单的两步,各个方法进行链式调用功能就完成了。

public class Main {

    public static void main(String[] args) {
FluentApi.target(StudentBuilder.class)
.create()
.age(1)
.sex('m')
.name("java")
.build();
}
}

接下来实现属性的设置与对象创建。由于对象创建是在build方法调用时创建,因此调用非build方法时仅保存属性。所以需要判断方法到底是属性设置还是对象创建。

build方法仅需要与Builder接口中build方法比较即可,判断方法如下。

    boolean isBuildMethod(Method method) {
try {
Method buildMethod = Builder.class.getDeclaredMethod(BUILDMETHOD);
return method.equals(buildMethod);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("");
}
}

属性设置方法满足的条件是只有一个参数且返回类型为使用者传入的接口类型,判断方法如下。

    boolean isPropertySetter(Method method) {
return method.getParameters().length == 1 && target.isAssignableFrom(method.getReturnType());
}

这样就可以在invoke方法中判断方法的类型了。

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName());
if(isPropertySetter(method)) {
System.out.println("PropertySetter");
return proxy;
}
else if(isBuildMethod(method)) {
System.out.println("BuildMethod");
return 最终要创建的对象;
}
return null;
}

接下来当满足isPropertySetter时,将属性保存,因此增加一个map来保存待设置的值。Map <String,Object> properties = new HashMap <String,Object> 。其中key为属性的名字,而value为属性值。由于在定义接口XXXBuilder时约定里面的属性设置方法是与要创建的对象的各个属性名保持一致,因此取方法名作为key即可,而属性参数值作为value。

最后调用build方法时,完成对象的创建。而为了创建对象,需要获取Builder<T>接口中的类型参数,这样才能完成对象的创建。

    Class<?> getParameterType(Class<?> clzz) {
Type[] types = clzz.getGenericInterfaces();
for(Type type : types) {
if(type instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType)type;
if(paramType.getRawType() == Builder.class) {
return (Class<?>)paramType.getActualTypeArguments()[0];
}
}
}
return null;
}

由于使用者传入的XXXBuilder实现了Builder<T>接口,因此只要检查XXXBuilder所实现的接口中哪个为参数类型ParameterizedType并且参数擦出后的原始类型为Builder即可。当类型满足这些条件时,再取出参数类型,这样就获取到了所需要的对象类型,就可以创建对象实例了[3]。

接下来将properties存放的属性赋给对象的各个成员变量即可。

    private void setPropperties(Object obj) throws Exception {
for(Map.Entry<String, Object> entry : properties.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
Field field = obj.getClass().getDeclaredField(key);
boolean isAccess = field.isAccessible();
try {
field.setAccessible(true);
field.set(obj, value);
}finally {
field.setAccessible(isAccess);
}
}
}

这样一个链式调用风格小框架主体功能就完成了,在使用时使用者仅需要定义属性设置接口,并满足方法名与属性名保持一致,随后就可以通过链式调用的方式来为对象设置属性并完成创建功能,而不需要重复编写属性设置与对象创建的代码了。

参考:

[1] fluent interface定义,https://en.wikipedia.org/wiki/Fluent_interface 
[2] effective java 第2版,第2条 
[3] jdk 1.8 api参考手册

代码:

public class FluentApi<T> {
/**
* 目标接口
*/
Class<T> target; private FluentApi(Class<T> target) {
if(!target.isInterface()) {
throw new IllegalArgumentException("must be interface");
}
this.target = target;
} public static <T> FluentApi<T> target(Class<T> target) {
return new FluentApi<T>(target);
} @SuppressWarnings("unchecked")
public T create() {
JdkProxy jdkProxy = new JdkProxy(target);
return (T)Proxy.newProxyInstance(target.getClassLoader(),
new Class[]{target},
jdkProxy);
}
}
public class JdkProxy implements InvocationHandler{
private final String BUILDMETHOD = "build"; Class<?> target; Class<?> object;
Map<String,Object> properties = new HashMap<String,Object>(); public JdkProxy(Class<?> target) {
this.target = target;
this.object = getParameterType(target);
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName());
if(isPropertySetter(method)) {
properties.put(method.getName(), args[0]);
System.out.println("PropertySetter");
return proxy;
}
else if(isBuildMethod(method)) {
Object obj = object.newInstance();
setPropperties(obj);
System.out.println("BuildMethod");
return obj;
}
return null;
} boolean isPropertySetter(Method method) {
return method.getParameters().length == 1 && target.isAssignableFrom(method.getReturnType());
} boolean isBuildMethod(Method method) {
try {
Method buildMethod = Builder.class.getDeclaredMethod(BUILDMETHOD);
return method.equals(buildMethod);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("");
}
} private Class<?> getParameterType(Class<?> clzz) {
Type[] types = clzz.getGenericInterfaces();
for(Type type : types) {
if(type instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType)type;
if(paramType.getRawType() == Builder.class) {
return (Class<?>)paramType.getActualTypeArguments()[0];
}
}
}
return null;
} private void setPropperties(Object obj) throws Exception {
for(Map.Entry<String, Object> entry : properties.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
Field field = obj.getClass().getDeclaredField(key);
boolean isAccess = field.isAccessible();
try {
field.set(obj, value);
field.setAccessible(true);
}finally {
field.setAccessible(isAccess);
}
}
}
}
public interface Builder<T> {
T build();
} public interface StudentBuilder extends Builder<Student>{
StudentBuilder age(int age);
StudentBuilder name(String name);
StudentBuilder sex(char sex);
}
public class Main {

    public static void main(String[] args) {
Student student =
FluentApi.target(StudentBuilder.class)
.create()
.age(1)
.sex('m')
.name("java")
.build();
System.out.println(student);
}
}

基于JDK动态代理实现的接口链式调用(Fluent Interface)工具的更多相关文章

  1. 设计模式之jdk动态代理模式、责任链模式-java实现

    设计模式之JDK动态代理模式.责任链模式 需求场景 当我们的代码中的类随着业务量的增大而不断增大仿佛没有尽头时,我们可以考虑使用动态代理设计模式,代理类的代码量被固定下来,不会随着业务量的增大而增大. ...

  2. 基于JDK动态代理和CGLIB动态代理的实现Spring注解管理事务(@Trasactional)到底有什么区别。

    基于JDK动态代理和CGLIB动态代理的实现Spring注解管理事务(@Trasactional)到底有什么区别. 我还是喜欢基于Schema风格的Spring事务管理,但也有很多人在用基于@Tras ...

  3. Spring -- <tx:annotation-driven>注解基于JDK动态代理和CGLIB动态代理的实现Spring注解管理事务(@Trasactional)的区别。

    借鉴:http://jinnianshilongnian.iteye.com/blog/1508018 基于JDK动态代理和CGLIB动态代理的实现Spring注解管理事务(@Trasactional ...

  4. SpringBoot27 JDK动态代理详解、获取指定的类类型、动态注册Bean、接口调用框架

    1 JDK动态代理详解 静态代理.JDK动态代理.Cglib动态代理的简单实现方式和区别请参见我的另外一篇博文. 1.1 JDK代理的基本步骤 >通过实现InvocationHandler接口来 ...

  5. JDK 动态代理分析

    Java的代理有两种:静态代理和动态代理,动态代理又分为 基于jdk的动态代理 和 基于cglib的动态代理 ,两者都是通过动态生成代理类的方法实现的,但是基于jdk的动态代理需要委托类实现接口,基于 ...

  6. 从静态代理,jdk动态代理到cglib动态代理-一文搞懂代理模式

    从代理模式到动态代理 代理模式是一种理论上非常简单,但是各种地方的实现往往却非常复杂.本文将从代理模式的基本概念出发,探讨代理模式在java领域的应用与实现.读完本文你将get到以下几点: 为什么需要 ...

  7. 动态代理之 JDK 动态代理

    动态代理 动态代理源于设计模式中的代理模式,代理模式的主要作用就是使代理对象完成用户的请求,屏蔽用户对真实对象的访问.通过代理对象去访问目标对象来控制原对象的访问. 代理模式的最典型的应用就是 Spr ...

  8. 面试造火箭系列,栽在了cglib和jdk动态代理

    "喂,你好,我是XX巴巴公司的技术面试官,请问你是张小帅吗".声音是从电话那头传来的 "是的,你好".小帅暗喜,大厂终于找上我了. "下面我们来进行一 ...

  9. 017 Java中的静态代理、JDK动态代理、cglib动态代理

    一.静态代理 代理模式是常用设计模式的一种,我们在软件设计时常用的代理一般是指静态代理,也就是在代码中显式指定的代理. 静态代理由业务实现类.业务代理类两部分组成.业务实现类负责实现主要的业务方法,业 ...

随机推荐

  1. 汇编代码中db,dw,dd的区别

    db定义字节类型变量,一个字节数据占1个字节单元,读完一个,偏移量加1 dw定义字类型变量,一个字数据占2个字节单元,读完一个,偏移量加2 dd定义双字类型变量,一个双字数据占4个字节单元,读完一个, ...

  2. bzoj 1658: [Usaco2006 Mar]Water Slides 滑水

    题解: 很神奇的做法,把点分成入度大于出度和入度小于出度两种. 然后入度大于出度的点必须走到某个点,所以排序贪心. #include<stdio.h> #include<iostre ...

  3. 如何离线安装GitHub for windows?

    此文献给xp用户和被墙用户. 今天群里(GitHub家园 225932282)有人说GitHub for windows安装不上,错误提示如下,看了下感觉应该是被墙了,我试了试下面的网址,没问题,所以 ...

  4. 【BZOJ】2760: [JLOI2011]小A的烦恼【字符串模拟】

    2760: [JLOI2011]小A的烦恼 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 406  Solved: 258[Submit][Statu ...

  5. ZOJ 3632 K - Watermelon Full of Water 优先队列优化DP

    K - Watermelon Full of Water Time Limit:3000MS     Memory Limit:65536KB     64bit IO Format:%lld &am ...

  6. Mac OSX系统下通过ProxyChains-NG实现终端下的代理

    项目主页:https://github.com/rofl0r/proxychains-ng 官方说明: proxychains ng (new generation) - a preloader wh ...

  7. gridview DataFormatString 属性设置须知

    设置 DataFormatString 进行格式化数据时默认情况下是不会起作用的还有设置HtmlEncode = "false" 具体为什么?以下几点1. 在GridView中的a ...

  8. 活动(Activity)

    一.用Log打印日志 Log.d("HelloWorldActivity", "onCreate execute"); 二.Toast用法 Toast.make ...

  9. HDU 4716 A Computer Graphics Problem (水题)

    A Computer Graphics Problem Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (J ...

  10. Windows界面编程第四篇 异形窗体 高富帅版 ---博客

    http://blog.csdn.net/morewindows/article/details/8451638