JDK动态代理的门门道道

JDK动态代理的门门道道

JDK动态代理,核心是java.lang.reflect.Proxy这个类,它可以基于接口创建实现类(implement class)和实现类的实例(instance of implement class),创建出的实现类就叫做代理类,创建出的实例就是代理实例。还有一些其它的知识点,读者可自行阅读jdk源码中Proxy类的注释说明。

本文聚焦于Proxy代理的两种用法:

  1. 动态生成整个代理逻辑
  2. 对某个实现类的实例做增强

并将举例说明这两种用法的实际应用。

1. 动态生成整个代理逻辑

有时,我们希望通过接口内的方法签名和返回值,动态判断其业务执行逻辑并实现之。这种情况下,接口往往没有现成的实现,需要我们使用Proxy配合InvocationHandler来代理实现这个接口的行为。

实际上,Mybatis的Mapper接口就是这样一种代理模式。我们看一下使用Mybatis的时候的代码,通过代码理解一下这个典型场景:

// Demo.java
import lombok.Data;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.defaults.DefaultSqlSessionFactory;

public class Demo {
    public static void main(String[] args) {
        SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(new Configuration());
        SqlSession sqlSession = sqlSessionFactory.openSession();
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        Student student = studentMapper.selectById(1);
        System.out.println(student);
    }

    interface StudentMapper {
        Student selectById(int id);
    }

    @Data
    static class Student {
        private int id;
        private String name;
        private int grade;  // 年级
        private int classNo;
    }
}

可以看到,mybatis中的SqlSession类有一个核心方法<T> T getMapper(Class<T> type),这个方法可以根据传入的Mapper接口,动态的生成一个代理实例,里面封装了生成SQL、执行SQL、得到结果、转换为出参的完整过程。

当然,我们当前这个代码是不能跑的,毕竟这里面没有配置好mybatis,也没有连接到任何实际的数据库。

那么,如何自己实现一个可以生成代理实例的SqlSessionFactory呢?对于上述场景,我们希望实现这样的效果:

// Demo.java
import lombok.Data;

public class Demo {
    public static void main(String[] args) {
        CustomSqlSessionFactory customSqlSessionFactory = new CustomSqlSessionFactory();
        StudentMapper studentMapper = customSqlSessionFactory.getMapper(StudentMapper.class);
        Student student = studentMapper.selectById(1);
        System.out.println(student);
    }

    interface StudentMapper {
        Student selectById(int id);
    }

    @Data
    static class Student {
        private int id;
        private String name;
        private int grade;  // 年级
        private int classNo;
    }

    static class CustomSqlSessionFactory {
        public <T> T getMapper(Class<T> clz) {
            // TODO 待实现
            return null;
        }
    }
}

我们自定义一个CustomSqlSessionFactory,在内部定义一个getMapper,希望它可以实现跟mybatis一样的功能。

终于要引出今天的主角了,我们怎么做呢?答案是可以使用Proxy类结合InvocationHandler类来实现。

利用Proxy先做一个最粗糙的版本出来:

// Demo.java
	@SuppressWarnings("all")
    static class CustomSqlSessionFactory {
        public <T> T getMapper(Class<T> clz) {
            return (T) Proxy.newProxyInstance(clz.getClassLoader(), new Class[]{clz}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    String methodName = method.getName();
                    if (methodName.startsWith("select")) {
                        // select开头,就执行sql查询
                        Connection connection = DriverManager.getConnection("jdbc:duckdb:/develop/duckdb/my_test_db1");  // 这里有张student表,结构与Student实体一致;里面有一些数据。
                        PreparedStatement pstmt = connection.prepareStatement("select * from student where id = ?");
                        pstmt.setInt(1, 1);
                        ResultSet rs = pstmt.executeQuery();
                        if (rs.next()) {
                            Student s = new Student();
                            s.setId(rs.getInt("id"));
                            s.setName(rs.getString("name"));
                            s.setGrade(rs.getInt("grade"));
                            s.setClassNo(rs.getInt("class_no"));
                            return s;
                        }
                    }
                    return null;
                }
            });
        }
    }

虽然代码还有很多值得改进的地方,尽管大部分东西是写死的,但是基础的代理过程我们已经实现了,现在main函数执行后可以正常查询出结果,打印结果如下:

Demo.Student(id=1, name=小明, grade=3, classNo=1)

在优化代码前,先介绍一下Proxy类生成代理实例的两个要点:

  1. 调用Proxy.newProxyInstance()方法,就可以生成一个代理实例以及相对应的代理类。它需要3个参数:
    1. 第一个参数是一个classloader,指定该代理类的类加载器,通常我们使用接口同款类加载器即可;
    2. 第二个参数是代理类需要实现的接口的列表,具体对应我们的例子就是StudentMapper.class
    3. 第三个参数是InvocationHandler的实现类的实例,其核心方法是一个invoke()方法
  2. InvocationHandlerinvoke()方法会接收三个参数,JDK将在被代理的接口产生方法调用的时候,自动调用invoke()方法并传入这三个参数,它们是:
    1. 第一个参数是proxy实例,也就是我们所生成的代理实例,JDK将这个代理实例的引用传递过来,方便使用;
    2. 第二个参数是一个method实例,它指向被代理的接口产生方法调用时,具体调用的是哪个方法,比如本例中,这个方法就是selectById()方法;
    3. 第三个参数是一个args列表,它表示在调用method方法时,实际传入的实参参数列表

了解了这些知识后,实际上JDK的动态代理就可以算掌握了。接下来,我们在实战中,利用反射动态的获取方法的各类信息,以期模拟mybatis中的常用查询写法。

首先,selectById()的这个经典查询,我们如何优化呢?要组装一个这样的查询SQL并执行、返回,我们的思路大致如下:

  1. 首先SQL写出来应该是:select id, name, grade, class_no from student where id = 1;如果用prepareStmt,那查询就是where id = ?
  2. 要组装它,我们需要知道以下信息:
    1. 到底要查哪些列,需要把列名都拿到
    2. 到底要查什么表,需要把表名拿到
    3. 到底有什么查询条件,需要把传入的参数列表拿到
    4. prepareStmt在set参数的时候,需要知道参数类型,因此这里还需要把对应的传入的参数类型拿到
  3. 上述信息中:
    1. 列名需要通过方法的出参类型的实体类,反射拿到实体类的成员变量列表,即可得到;
    2. 表名无法从现有信息获取,我们可以定义一个@Table注解,放在实体类上,这样就可以反射获取它;
    3. 查询条件需要从参数列表获取,然而参数列表在runtime中往往不是source code中定义好的名字,而是诸如arg1, arg2的变量名,它无法反应实际我们想查询的条件的字段名,因此我们同样需要定义一个@Param注解,放在参数列表中,这样在invoke()中才可以反射获取到
  4. 要返回实体的实例,我们需要知道返回类型,并反射创建其实例

综合以上信息,我们需要定义两个注解@Table和@Param,分别对应表名的获取和where条件的获取。

那么,一个可以反射获取已有信息、拼装SQL、执行和返回的代码大概应该长这样:

// Table.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Table {
    String value() default "";
}


// Param.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface Param {
    String value() default "";
}


// Demo.java
	interface StudentMapper {
        Student selectById(@Param("id") int id);
    }

    @Data
    @Table("student")
    static class Student {
        private int id;
        private String name;
        private int grade;  // 年级
        private int classNo;
    }
    
    @SuppressWarnings("all")
    static class CustomSqlSessionFactory {
        public <T> T getMapper(Class<T> clz) {
            return (T) Proxy.newProxyInstance(clz.getClassLoader(), new Class[]{clz}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    String methodName = method.getName();
                    if (methodName.startsWith("select")) {
                        // select开头,就执行sql查询
                        Connection connection = DriverManager.getConnection("jdbc:duckdb:/develop/duckdb/my_test_db1");  // 这里有张student表,结构与Student实体一致;里面有一些数据。

                        // 获取select字段名列表
                        List<String> fieldList = Arrays.stream(method.getReturnType().getDeclaredFields()).map(item -> item.getName()).collect(Collectors.toList());

                        // 获取表名
                        String tblName = method.getReturnType().getAnnotation(Table.class).value();

                        // 获取查询字段列表
                        List<String> whereSegList = new ArrayList<>();
                        for (Parameter parameter : method.getParameters()) {
                            if (parameter.isAnnotationPresent(Param.class)) {
                                Param param = parameter.getAnnotation(Param.class);
                                whereSegList.add(param.value() + " = ?");
                            }
                        }

                        // 拼装SQL
                        String sql = "select " + String.join(", ", fieldList) + " from " + tblName + " where " + String.join(" and ", whereSegList);
                        PreparedStatement pstmt = connection.prepareStatement(sql);

                        // 把参数列表set进去
                        for (int i = 0; i < args.length; i++) {
                            Object arg = args[i];
                            if (arg instanceof String) {
                                pstmt.setString(i + 1, (String) arg);
                            } else if (arg instanceof Integer) {
                                pstmt.setInt(i + 1, (Integer) arg);
                            }
                            // TODO 支持更多类型
                        }

                        // 执行
                        ResultSet rs = pstmt.executeQuery();

                        // 组装返回
                        Object result = method.getReturnType().getConstructor().newInstance();
                        if (rs.next()) {
                            Field[] fields = method.getReturnType().getDeclaredFields();
                            for (int i = 0; i < fields.length; i++) {
                                Object colVal = null;
                                Field field = fields[i];
                                if (field.getType().isPrimitive()) {
                                    colVal = rs.getObject(field.getName());
                                } else if (field.getType() == String.class) {
                                    colVal = rs.getString(field.getName());
                                } else if (field.getType() == Integer.class) {
                                    colVal = rs.getInt(field.getName());
                                }
                                field.setAccessible(true);
                                field.set(result, colVal);
                            }
                        }

                        return result;
                    }
                    return null;
                }
            });
        }
    }

这是一个相对初始版本来说,更好的动态代理实现,它利用了反射获取一切所需信息,一切都是动态的。此时,如果我们在StudentMapper接口中,再定义一个根据姓名查询的方法Student selectByName(@Param("name") String name),那么我们定义好方法之后,就可以立即使用它了,不需要在额外实现,这与mybatis的使用方法完全一样,如下:

// Demo.java
	interface StudentMapper {
        Student selectById(@Param("id") int id);
        Student selectByName(@Param("name") String name);  // 新增一个方法,定义好签名和返回值即可,其它什么都不用动了
    }
    
	// main函数调用一下看看
	public static void main(String[] args) throws Exception {
        CustomSqlSessionFactory customSqlSessionFactory = new CustomSqlSessionFactory();
        StudentMapper studentMapper = customSqlSessionFactory.getMapper(StudentMapper.class);
        Student student1 = studentMapper.selectById(1);
        System.out.println(student1);

        Student student2 = studentMapper.selectByName("小李");
        System.out.println(student2);
    }

// 打印结果是:
// Demo.Student(id=1, name=小明, grade=3, classNo=1)
// Demo.Student(id=2, name=小李, grade=3, classNo=2)

可以看到,我们已经实现了动态、灵活的Mapper查询代理,它可以用于任何返回单条结果的查询。

实际上,返回多条的查询(使用Collection<>或其子类包装)、返回Map<String, Object>的查询,甚至是返回List<Map<String, Object>>的查询,都可以通过反射或反射+注解来组装、返回。读者有兴趣可以自行实现。

笔者这里有一个粗略的实现,供参考,代码仓库地址见最下面附录。

2. 对实现类的实例做增强

若接口有一些实现类,它们的功能一切正常。在某一次需求迭代中,我们需要对接口的某个方法做一些增强,并期望可以写一处代码就自动应用在这个接口的所有实现类的实例上。简而言之,我们一般把这种场景叫做“增强”。

增强隐含了两个要素:

  1. 不改动已有逻辑
  2. 在已有逻辑之前、之后,可以额外执行一些逻辑

是不是很眼熟,这实际上就是切面做的事情。我们还是从简单的例子出发,一步步分析它。现在有一个饮料接口,以及两个工作一切正常的实现类,如下:

// Demo2.java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Demo2 {
    public static void main(String[] args) {
        Drink milkDrink = new MilkDrink();
        Drink colaDrink = new ColaDrink();
        System.out.println(milkDrink.drink());
        System.out.println(colaDrink.drink());
    }

    static interface Drink {
        String drink();
    }

    // 有两个实现类,它们工作一切正常
    static class MilkDrink implements Drink {
        public String drink() {
            return "Drink Milk!";
        }
    }

    static class ColaDrink implements Drink {
        public String drink() {
            return "Drink Cola!";
        }
    }
}

上面的两个实现类,都可以正常工作,打印各自的饮料。

现在,有一个新需求来了:喝饮料的步骤要细化,喝前要打开包装,喝完要清理垃圾。如果挨个去改实现类的实现,当然也是可以的。但显然这么做不符合DRY原则,对每一种饮料,我们要做的事情是不变的。那么,我们还是请出我们的Proxy代理,来实现这个需求。

// Demo2.java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Demo2 {
    public static void main(String[] args) {
        Drink milkDrink = new MilkDrink();
        Drink colaDrink = new ColaDrink();
        System.out.println(milkDrink.drink());
        System.out.println(colaDrink.drink());
		
		// 从工厂拿实例
        System.out.println(DrinkFactory.getDrink("Milk").drink());
        System.out.println(DrinkFactory.getDrink("Cola").drink());
    }

    static interface Drink {
        String drink();
    }

    // 有两个实现类,它们工作一切正常
    static class MilkDrink implements Drink {
        public String drink() {
            return "Drink Milk!";
        }
    }

    static class ColaDrink implements Drink {
        public String drink() {
            return "Drink Cola!";
        }
    }

    // 新需求:喝饮料步骤要细化,喝前要打开包装,喝完要清理垃圾
    // 这个只是封装,实际上直接写在invoke()里也是一样的
    static class AroundDrinkAction {
        public String beforeDrink() {
            return "拆开包装";
        }

        public String afterDrink() {
            return "清理垃圾";
        }
    }

    // 更新代理之后,搞一个工厂来创建对象,屏蔽内部的代理细节
    static class DrinkFactory {
        private final static AroundDrinkAction AROUND_DRINK_ACTION = new AroundDrinkAction();
        private DrinkFactory() {}

        public static Drink getDrink(String drink) {
            AroundDrinkInvocationHandler around;
            switch (drink) {
                case "Milk":
                    around = new AroundDrinkInvocationHandler(new MilkDrink(), AROUND_DRINK_ACTION);
                    break;
                case "Cola":
                    around = new AroundDrinkInvocationHandler(new ColaDrink(), AROUND_DRINK_ACTION);
                    break;
                default:
                    around = null;
                    break;
            }
            return (Drink) Proxy.newProxyInstance(Demo2.class.getClassLoader(), new Class[]{Drink.class}, around);
        }
    }

    static class AroundDrinkInvocationHandler implements InvocationHandler {
        private final Drink drink;
        private final AroundDrinkAction aroundDrinkAction;

        public AroundDrinkInvocationHandler(Drink drink, AroundDrinkAction aroundDrinkAction) {
            this.drink = drink;
            this.aroundDrinkAction = aroundDrinkAction;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            StringBuilder sb = new StringBuilder();
            sb.append(aroundDrinkAction.beforeDrink());
            sb.append("\n");
            sb.append(method.invoke(drink, args));
            sb.append("\n");
            sb.append(aroundDrinkAction.afterDrink());
            sb.append("\n");
            return sb.toString();
        }
    }

}

// main函数执行输出如下:
// Drink Milk!
// Drink Cola!
// 拆开包装
// Drink Milk!
// 清理垃圾
//
// 拆开包装
// Drink Cola!
// 清理垃圾
//

代理的相关API在第一节中都有介绍,此处不多赘述。这里除了代理机制外,我们还引入了一个工厂,用来屏蔽构造代理实例的细节,直接返回相应的代理实例,最终暴露给客户端使用的部分只需要DrinkFactory工厂就够了。

这个使用方法实际上更常见,通过对具体实例做一层代理,我们可以实现对任何接口方法的增强。Spring框架中的AOP在老版本中也是使用的这个方式对bean实例做的AOP切面增强。顺便一提,springboot较新版本已经统一默认走cglib代理(一种基于字节码生产目标类的子类的代理工具),所以实际上我们甚至不再需要定义interface Service。这个更详细的内容,我会单独放在cglib代理的专题文章中探讨。

3. 附录:依赖和运行环境说明

pom.xml中的依赖如下:

// pom.xml
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.19</version>
    </dependency>

    <dependency>
      <groupId>org.duckdb</groupId>
      <artifactId>duckdb_jdbc</artifactId>
      <version>1.2.1</version>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.30</version>
    </dependency>

    <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-all</artifactId>
      <version>5.8.26</version>
    </dependency>
  </dependencies>

编译和运行时:JDK21

数据库使用了duckdb,如果读者习惯使用mysql等数据库,请自行替换依赖,并配置对应的jdbc连接。另,若DriverManager无法找到引入的jdbc Driver,请显式装载一下:

Class.forName("jdbc Driver的完全限定名")

文章中提到的完整的CustomSqlSessionFactory实现可以参考这里:github仓库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值