Java和动态代理

Java 中的动态代理是一个简单但非常有用的特性。

通常我们创建一个接口实现,然后涉及到编译。使用动态代理,我们可以在运行时实现接口列表。将创建代理对象,当在该代理实例上调用方法时,调用的方法将转发到指定的调用处理程序。

这可以有多种用途。一个常见的用例是 java 接口,我们可以使用代理并拦截对所调用方法的调用。

假设我们有一个 JDBC 连接池,并且我们想要一个类似于千分尺计数器的东西。获得连接后,计数器会增加,因此我们可以确定应用程序中获得连接的速率。

我们首先使用 Docker Compose 为 Postgresql 添加一个 docker 容器。

version: '3.1'
 
services:
  postgres:
    image: postgres
    restart: always
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - 5432:5432

以下命令运行

docker compose up

让我们将我们的依赖项添加到我们的 Java 项目中:

<dependencies>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.5.1</version>
    </dependency>
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>4.0.3</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

代码:

package com.example.gkatzioura.proxy;
 
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
 
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
 
public class Application {
 
    public static void main(String[] args) throws SQLException {
        Properties props = new Properties();
 
        props.setProperty("dataSourceClassName", "org.postgresql.ds.PGSimpleDataSource");
        props.setProperty("dataSource.user", "postgres");
        props.setProperty("dataSource.password", "postgres");
        props.setProperty("dataSource.databaseName", "postgres");
        props.put("dataSource.logWriter", new PrintWriter(System.out));
 
        HikariConfig config = new HikariConfig(props);
         
        try(HikariDataSource ds = new HikariDataSource(config)) {
            try(Connection connection = ds.getConnection()) {
                System.out.println("Should be printed after the proxy");       
            }
        }
    }
 
}

Java的DataSource接口中有getConnection :

public interface DataSource  extends CommonDataSource, Wrapper {
 
    Connection getConnection() throws SQLException;
    ...
}
我们不必创建接口实现并必须实现所有这些方法,然后将它们委托给实际的 DataSource 实例,而是使用代理并仅向感兴趣的方法添加操作,在我们的例子中为 getConnection。我们将实施一个 InvocationHandler:

package com.example.gkatzioura.proxy;
 
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
 
import javax.sql.DataSource;
 
public class GetConnectionHandler implements InvocationHandler {
 
    private final DataSource dataSource;
 
    public GetConnectionHandler(DataSource dataSource) {
        this.dataSource = dataSource;
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("getConnection")) {
            System.out.println("Called before actual method");
 
        }
 
        return method.invoke(dataSource, args);
    }
 
    public static DataSource proxy(DataSource dataSource) {
        return (DataSource) Proxy.newProxyInstance(DataSource.class.getClassLoader(), new Class[]{DataSource.class}, new GetConnectionHandler(dataSource));
    }
 
}
这是为指定接口的方法调用调用处理程序:当调用接口方法时,我们可以选择如何处理它。在我们的例子中,我们将打印一条简单的消息,然后我们将对我们的目标实例执行相应的方法。 我们还指定了一个静态工厂,它将代理实现感兴趣接口的对象。将创建一个新的代理实例,它将实现提供的接口,并且对代理实例的调用将传递给我们提供的处理程序。

重新审视我们的mai方法:

。。。。
        try(HikariDataSource ds = new HikariDataSource(config)) {
            DataSource dataSource = proxy(ds);
            try(Connection connection = dataSource.getConnection()) {
                System.out.println("Should be printed after the proxy");
            }
        }

我们用动态代理包装了 HikariDataSource,如果我们运行该程序,我们应该会看到以下输出:

Called before actual method
Should be printed after the proxy

我们创建了一个具有 DataSource 接口的代理:通过创建代理,我们提供了一个调用处理程序,该处理程序应在调用 getConnection 方法之前打印一条消息。getConnection 将由我们在 InvocationHandler 上指定的 DataSource 接口的实际实现调用。

可以在GitHub 上找到源代码。