Java 动态代理和依赖注入

Java
294
0
0
2023-05-27

【注】本文译自:

本文将讨论动态代理如何在 Java 平台中工作以及依赖注入如何利用此 Java 功能。本文的撰写源于我在 HK2 框架(或更确切地说是 Jersey 应用程序中的 HK2)中尝试将请求范围对象注入到单例对象中的的搜索。我本来打算将我的发现都写在一个博客里,但是我觉得这个主题太宽泛了,不适合用两行代码就能解决的问题。

首先,我将快速讨论代理模式,然后展示如何在 Java 语言中使用动态代理,最后介绍一个使用动态代理和自定义依赖项注入的示例。

代理模式

我不会过多介绍代理模式。在互联网上到处都有很好的参考。我只给出一个简短的类比,以及该模式的一些简短代码示例。

我敢肯定,你们大多数人都听过“委托投票”这个说法。当有人投票给其他人时。例如,说公司董事会成员之间有一些随意的投票。成员 B 生病住院了,因此无法参加董事会会议。所以,成员 A 代表成员 B 进行了投票。因此,在表决会议上,成员 A 只是成员 B 的委托。

代理模式的工作原理与此相同。这是一个类图(来自维基百科)。

假设 Member 是接口

 public interface Member {
    void vote();
}  

那么你有 MemberA MemberB

 public class MemberA implements Member {
    public void vote() {}
}

public class MemberB implements Member {
    public void vote() {}
}  

由于成员 B 将不存在,因此我们需要一个代理。 代理还应实现 Member 接口,并应包含对 MemberB 的引用。

 public class MemberBProxy implements Member {
    private MemberB memberB;

    public void vote {
        memberB.vote();
    }
}  

现在 MemberA 可以让 a 成为成员 B 的代理,让代理为成员 B 进行代理投票。

这可能不是最好的例子,因为代理仅对成员 B 进行表决。但是对于真正的代理,通常情况下还会发生其他事情。例如,在使用远程代理的情况下, vote() 方法实际上可能会对远程 MemberB 进行网络调用。Java平台中的一个示例就是 RMI(远程方法调用)。后面的示例将描述另一个用例,通常对于开发人员来说是透明的。

动态代理

在上面的示例中,我们终须手动编写代理类。但是,在 Java 中,随着 1.3 中引入动态代理,这不是必需的。动态代理的核心接口是 java.lang.reflect.Proxy。要使用它,我们需要组件,我们的代理接口和一个 InvocationHandler。 让我们看一个简单的例子,使用与上面相同的类比。

 Member memberBProxy = (MemeberProxy.newProxyInstance(
        Memeber.class.getClassLoader(),
        new Class[] { Member.class },
        new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) {
                return method.invoke(new MemberB(), args);
            }
        }
);  

就是这样。现在 memberBProxy Proxy 的一个实例,而不是 MemberB 的实例。如果你打印出 Member 对象的类名,实际上会看到类名是 com.sun.proxy.ProxyX ,而不是 MemberB

让我们快速地浏览一下。这是 Proxy#newProxyInstance 的签名

 newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)  

它首先需要 ClassLoader 用于定义代理,其次需要一个实现的接口,最后是 InvocationHandler InvocationHandler 只有一个需要实现的 callback 方法。

 Object invoke(Object proxy, Method method, Object[] args)  

第一个参数是实际的代理对象。你应该很少使用它。第二个参数是 java.lang.reflect.Method 。如果你有 Java 反射的经验,那么应该熟悉这个接口。 使用 Method 时,我们可以通过传递要调用该方法的对象以及任何参数(最后一行)来调用:

 return method.invoke(new MemberB(), args);  

在这里,代理将方法和传递给代理之上的方法调用的参数传递给代理。作为 InvocationHandler 的实现者,我们可以使用 Method 和method 参数做我们想做的任何事情。这里我们只是简单地,应该在 new MemberB() 对象中调用,并传递参数。

为了获得更清楚地了解情况,只需将其看作 Proxy 实例具有 Member 接口具有的所有方法。因此,当我们调用 Proxy#vote() 时,它会调用 InvocationHandler#invoke 本身、方法和传递给 vote() 的参数(如果有)。通过调用 Method 对象上的 inovoke InvocationHandler 实现只需调用该方法即可。然后, Method 对象将对实际的 MemberB 对象调用 vote()

就是这样。如你所见,动态代理很容易实现。

动态代理和自定义注入示例

  • 获取 GitHub 项目(

我将在这里尝试解释的是在依赖项注入(DI)框架中如何使用动态代理。DI 中动态代理的主要用例之一是处理范围。例如,您有一个处于单例作用域中的服务或控制器,这意味着每个应用仅创建一个。该单例服务依赖于请求范围内的服务器,这意味着应为每个请求创建一个服务器。类可能看起来像这样(这完全是虚构的—没有特定的框架)。

 @Controller(scope = "singleton")
public class AppController {
    @Inject
    SingletonService service;
}

@Singleton
public class SingletonService {
    @Inject
    RequestScopedService service;
}

@RequestScoped
public class RequestScopedService {}  

这里的问题是,在启动时创建 SingletonService 时,需要执行所有注入。但是在启动时没有请求,因此当前应该没有绑定到请求的 RequestScopedService 。另一个问题是我们如何管理哪个请求获取哪个 RequestScopedService 。也许我们可以在 SingletonService 中添加一个 setter,在其中我们可以为每个请求设置一个新的 RequestScopedService 。但这是行不通的,因为 SingletonService 将被并发访问,就像一些服务器的工作方式一样(每个请求一个线程)。

这是动态代理发挥作用的地方。This is where dynamic proxies come to the rescue. When the当启动创建 SingletonService 时我们将注入服务的 Proxy ,而不是注入实际的 RequestScopedService 。当从 from inside the SingletonService , 内部对 RequestScopedService 进行调用时,实际上将在 Proxy 上进行调用,该代理将调用委派给 InvocationHandler#invoke 方法,该方法实现对从 ThreadLocal 获得的 RequestScopedService 的调用。每次处理一个请求时,都会在 ThreadLocal 中设置一个新的 RequestScopedService ,这个请求将在一个单独的线程中处理。如果你听说过 “线程本地代理”一词,那么这几乎就是它的工作原理。

让我们尝试实现这些。我们甚至将实现自己的依赖注入。我们将实现一个简单的服务器框架,该框架允许用户实现一个自定义 RequestHandler ,它可以注入我们的 SingletonService ,而 SingletonService 又依赖于 RequestScopedService 。以下是类图。(接下来,最好从上面的链接中获取 GitHub 项目)。

如前所述,用户将能够实现自定义 RequestHandler 并注入我们的 SingletonService 。在项目中,有一个默认的实现,它仅只返回来自 SingletonService 的消息作为 Response

 public class DefaultRequestHandler implements RequestHandler {
    
    @Inject
    private SingletonService singletonService;

    @Override
    public Response handleRequest(Request request) {
        return new Response(singletonService.getMessage());
    }  
}  

然后用户创建 Server 传入实现类给构造器。

Server server = new Server ( DefaultRequestHandler . class );

在服务器构造函数中,您将看到两件事,用户定义的 RequestHandler 类验证,以及创建 SingletonService 。验证在这里并不重要,这是 SingletonService 的创建。

 private static SingletonService initSingletonService() {
    Service proxyService = (Service) Proxy.newProxyInstance(
            Service.class.getClassLoader(), new Class[]{Service.class},
            new ServiceInvocationHandler());
    return new SingletonService(proxyService);
}  

我们要做的第一件事是创建 Service 类的代理。这里是 ServiceInvocationHandler

 public class ServiceInvocationHandler implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) .. {
        Service service = ThreadLocalService.get();
        return method.invoke(service, args);
    }
}  

它没有太大作用。它只是从 ThreadLocalService 检索 Service ,并在服务上调用代理方法。稍后我们将看到, RequestScopedService 的实例被设置为 ThreadLocal

然后,使用 SingletonService 创建 Service 。所以现在,当 SingletonService 调用 Service 上的方法时,代理将查找线程本地 Service 并将调用委托给 方法。

服务器引导程序就是这样。现在我们进入运行时和请求处理。下面是来自请求处理流的序列图。

首先, Main 程序调用 Server#sendRequest(Request) 并传入一个新的 Request 对象。 Request 对象仅具有客户端的名称。

当我们在 Server 上调用 sendRequest 时,它所做的就是将请求添加到 BlockingQueue

 public void sendRequest(Request request) {
    try {
        requests.put(request);
    } catch (InterruptedException ex) {
        throw new RuntimeException(ex);
    }
}  

服务器启动时,它会不断轮询 BlockingQueue ,等待新的 Request

 public void startServer() {
    executors.submit(new Runnable() {
        @Override
        public void run() {
            while (true) {
                try {
                    Request request = requests.take();
                    if (request.isShutdownTrigger()) {
                        break;
                    }
                    executors.submit(new RequestProcessor(userDefineHandler,
                                                          singletonService,
                                                          request));
                } catch (InterruptedException ex) {
                    throw new RuntimeException(ex);
                }
            }
            System.out.println("Server shutdown!");
        }
    });  
}  

当接收到 Request 时, Server 将创建一个新的 RequestProcessor ,传入 Request 对象、 SingletonService 对象和使用定义的 RequestHandler 类。如果你查看 RequestProcessor run() 方法,你将看到以下内容

 private void setThreadLocalService() {
    ThreadLocalService.set(new RequestScopedService(request));
}

@Override
public void run() {
    setThreadLocalService();
    RequestHandler handler = initInjections();
    Response response = handler.handleRequest(request);
    System.out.println(response.getMessage());
}  

因此,处理器要做的第一件事就是将 RequestScopedService 设置为 ThreadLocalService 。 然后使用一些反射实例化 RequestHandler ,如 initInjections() 方法所示

 RequestHandler initInjections() {
    try {
        for (Field field : handlerCls.getDeclaredFields()) {
            if (field.isAnnotationPresent(Inject.class)
                    && field.getType() == SingletonService.class) {
                return createHandler(field, handlerCls);
            }
        }

        Constructor[] cons = handlerCls.getConstructors();
        for (Constructor con : cons) {
            if (con.isAnnotationPresent(Inject.class)
                    && con.getParameterCount() == 1
                    && con.getParameterTypes()[0] == SingletonService.class) {
                return (RequestHandler) con.newInstance(singletonService);
            }
        }
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
    throw new RuntimeException("RequestHandler could not be created.");
}  

该方法只是检查我们是否应该进行字段注入或构造函数注入。它确保使用 @Inject 注解字段或构造函数。如果字段带有注解,并且字段类型为 SingletonService ,我们将使用反射使用 SingletonService 设置字段。进程构造函数注入也会发生类似事件。

RequestHander 做的最后一件事就是简单地调用 RequestHandler handleRequest of ,它返回一个 Response ,然后处理器打印出 Response 消息。这就是处理单个请求的过程。

如果运行 Main 类,应该会看到类似以下内容的信息

 Message: Hello Kobe
  meta-info:
    service class: com.sun.proxy.$Proxy2
    service id: 1
    thread name: pool-1-thread-2


Message: Hello Lebron
  meta-info:
    service class: com.sun.proxy.$Proxy2
    service id: 2
    thread name: pool-1-thread-3
... three more  

您应该注意的第一件事是,服务类确实是 Proxy 实例,而不是 RequestScopedService 。只要正在处理请求,底层的 RequestScopedService 都将保持不变。因此所有在 Service 内部的服务上所做的将总是被委托给与特定线程相关联的实际 RequestScopedService

就这样了。

总结

我们介绍了代理模式的一些基础知识,并了解了它如何使用包装器或委托模型来调用底层对象。然后,我们讨论了动态代理,以及如何用 Java 语言实现它。最后,我们通过一个示例说明了动态代理是如何使用作用域依赖注入的。如果需要将较小作用域的对象注入到更大作用域的对象中,则需要代理较小作用域的对象,以便不同的线程能够访问自己的较小作用域的对象实例。

这个示例与你在现实生活中使用的一些东西相去甚远,但我希望你能更好地理解我们讨论的两个主题(动态代理和依赖注入)的组全背后发生的事情。