1. Какова модель агентства?
Шаблон прокси — относительно простой для понимания шаблон проектирования. Проще говоря, мы используем прокси-объекты для замены доступа к реальным объектам, чтобы мы могли предоставлять дополнительные функциональные операции и расширять функции целевого объекта без изменения исходного целевого объекта.
Основная функция режима прокси — расширение функциональности целевого объекта. Например, вы можете добавить некоторые пользовательские операции до и после выполнения метода целевого объекта.
Например: если вы просите Сяохун помочь вам задать вопросы, Сяохун можно рассматривать как прокси-объект, действующий от вашего имени, а поведение (метод) агента заключается в том, чтобы задавать вопросы.
Режим прокси имеет два метода реализации: статический прокси и динамический прокси.Давайте сначала посмотрим на реализацию режима статического прокси.
2. Статический прокси
В статическом прокси мы улучшаем каждый метод целевого объекта вручную (код будет продемонстрирован позже), что очень негибко (например, как только в интерфейс добавляется новый метод, и целевой объект, и прокси-объект должны быть изменено) и хлопотно (вам нужно написать отдельный прокси-класс для каждого целевого класса). Реальных сценариев применения очень мало, и почти нет сценариев, в которых статические прокси используются в повседневной разработке.
Выше мы говорили о статических прокси с точки зрения реализации и применения.С уровня JVM статические прокси превращают интерфейсы, классы реализации и прокси-классы в реальные файлы классов во время компиляции.
2.1. Этапы реализации статического прокси:
- Определите интерфейс и класс его реализации;
- Создайте прокси-класс, который также реализует этот интерфейс.
- Внедрите целевой объект в прокси-класс, а затем вызовите соответствующий метод целевого класса в соответствующем методе прокси-класса. В этом случае мы можем защитить доступ к целевому объекту через прокси-класс и делать то, что хотим, до и после выполнения целевого метода.
Показано ниже через код!
2.2 Случай 1
1. Определите интерфейс для отправки текстовых сообщений.
public interface SmsService {
String send(String message);
}
2. Реализовать интерфейс отправки текстовых сообщений.
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
3. Создайте прокси-класс, а также реализуйте интерфейс для отправки текстовых сообщений.
public class SmsProxy implements SmsService {
private final SmsService smsService;
public SmsProxy(SmsService smsService) {
this.smsService = smsService;
}
@Override
public String send(String message) {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method send()");
smsService.send(message);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method send()");
return null;
}
}
4. Фактическое использование
public class Main {
public static void main(String[] args) {
SmsService smsService = new SmsServiceImpl();
SmsProxy smsProxy = new SmsProxy(smsService);
smsProxy.send("java");
}
}
После запуска приведенного выше кода консоль выводит:
перед методом send()
отправить сообщение: Java
после метода send()
Из вывода видно, что мы добавили SmsServiceImpl
методы send()
.
2.2 Случай 2
package com.company.proxy.v07;
import java.util.Random;
/**
* 问题:我想记录坦克的移动时间
* 最简单的办法:修改代码,记录时间
* 问题2:如果无法改变方法源码呢?
* 用继承?
* v05:使用代理
* v06:代理有各种类型
* 问题:如何实现代理的各种组合?继承?Decorator?
* v07:代理的对象改成Movable类型-越来越像decorator了
*
*/
public class Tank implements Movable {
/**
* 模拟坦克移动了一段儿时间
*/
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Tank t = new Tank();
TankTimeProxy ttp = new TankTimeProxy(t);
TankLogProxy tlp = new TankLogProxy(ttp);
tlp.move();
}
}
class TankTimeProxy implements Movable {
Movable m;
public TankTimeProxy(Movable m) {
this.m = m;
}
@Override
public void move() {
long start = System.currentTimeMillis();
m.move();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
class TankLogProxy implements Movable {
Movable m;
public TankLogProxy(Movable m) {
this.m = m;
}
@Override
public void move() {
System.out.println("start moving...");
m.move();
long end = System.currentTimeMillis();
System.out.println("stopped!");
}
}
interface Movable {
void move();
}
результат операции
3. Динамический прокси
По сравнению со статическими прокси, динамические более гибкие. Нам не нужно создавать отдельный прокси-класс для каждого целевого класса, и нам не нужно реализовывать интерфейс.Мы можем напрямую проксировать класс реализации ( механизм динамического прокси CGLIB ).
С точки зрения JVM, динамический прокси динамически генерирует байт-код класса во время выполнения.
Говоря о динамических прокси, следует упомянуть фреймворки Spring AOP и RPC, реализация которых основана на динамических прокси.
Динамические прокси сравнительно редко используются в нашей повседневной разработке, но это практически обязательная технология в рамках фреймворка. После изучения динамических агентов нам также очень полезно понять и изучить принципы различных фреймворков.
Что касается Java, существует множество способов реализации динамического прокси, например динамический прокси JDK , динамический прокси CGLIB и так далее.
guide-rpc-framework
использует динамический прокси JDK.Давайте сначала рассмотрим использование динамического прокси JDK.
Кроме того, хотя руководство-rpc-framework
Динамический прокси-сервер CGLIB не используется.Здесь мы кратко представим его использование и сравнение с динамическим прокси-сервером JDK .
3.1. Механизм динамического прокси JDK
3.1.1. Введение
InvocationHandler
Интерфейсы и классы являются основой механизма динамического прокси Java Proxy
.
Proxy
Наиболее часто используемый метод в классе: newProxyInstance()
, этот метод в основном используется для создания прокси-объекта.
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
......
}
Этот метод имеет всего 3 параметра:
- loader : загрузчик классов, используемый для загрузки прокси-объектов.
- интерфейсы : некоторые интерфейсы, реализованные прокси-классом;
- h : объект, реализующий
InvocationHandler
интерфейс;
Чтобы реализовать динамический прокси, необходимо также реализовать InvocationHandler
собственную логику обработки. Когда наш динамический прокси-объект вызывает метод, вызов этого метода будет перенаправлен методу, реализующему InvocationHandler
класс интерфейса invoke
.
public interface InvocationHandler {
/**
* 当你使用代理对象调用方法的时候实际会调用到这个方法
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
invoke()
Метод имеет следующие три параметра:
- прокси : динамически генерируемый прокси-класс
- метод : соответствует методу, вызываемому объектом прокси-класса
- args : параметры текущего метода
То есть: когда прокси-объект, который вы создаете через Proxy
класс, вызывает метод, он фактически вызывает метод класса, реализующего интерфейс . newProxyInstance()
InvocationHandler
invoke()
Вы можете invoke()
настроить логику обработки метода, например, что делать до и после выполнения метода.
3.1.2. Шаги по использованию динамического прокси-класса JDK
- Определите интерфейс и класс его реализации;
- Настройте InvokeHandler и перепишите метод вызова. В методе вызова мы будем вызывать собственный метод (метод прокси-класса) и настраивать некоторую логику обработки;
- Создайте прокси-объект с помощью метода Proxy.newProxyInstance(ClassLoader loader,Class<?>[]interfaces,InfectionHandler h);
3.1.3 Пример кода 1
Возможно, это немного пусто и сложно для понимания. Позвольте мне привести вам пример. Давайте попробуем!
1. Определите интерфейс для отправки текстовых сообщений.
public interface SmsService {
String send(String message);
}
2. Реализовать интерфейс отправки текстовых сообщений.
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
3. Определите динамический прокси-класс JDK.
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author shuang.kou
* @createTime 2020年05月11日 11:23:00
*/
public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return result;
}
}
invoke()
Метод: когда наш динамический прокси-объект вызывает собственный метод, на самом деле вызывается метод invoke()
, а затем invoke()
метод вызывает собственный метод прокси-объекта от нашего имени.
4. Получите фабричный класс прокси-объекта.
public class JdkProxyFactory {
public static Object getProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类的类加载
target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个
new DebugInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler
);
}
}
getProxy()
: В основном Proxy.newProxyInstance()
получить прокси-объект определенного класса с помощью методов.
5. Фактическое использование
SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
smsService.send("java");
После запуска приведенного выше кода консоль выводит:
перед отправкой метода отправить
сообщение: Java
после отправки метода
3.1.4. Пример кода 2
Отображение кода
package com.company.proxy.v08;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Random;
/**
* 问题:我想记录坦克的移动时间
* 最简单的办法:修改代码,记录时间
* 问题2:如果无法改变方法源码呢?
* 用继承?
* v05:使用代理
* v06:代理有各种类型
* 问题:如何实现代理的各种组合?继承?Decorator?
* v07:代理的对象改成Movable类型-越来越像decorator了
* v08:如果有stop方法需要代理...
* 如果想让LogProxy可以重用,不仅可以代理Tank,还可以代理任何其他可以代理的类型 Object
* (毕竟日志记录,时间计算是很多方法都需要的东西),这时该怎么做呢?
* 分离代理行为与被代理对象
* 使用jdk的动态代理
*/
public class Tank implements Movable {
/**
* 模拟坦克移动了一段儿时间
*/
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Tank tank = new Tank();
//reflection 通过二进制字节码分析类的属性和方法
Movable m = (Movable)Proxy.newProxyInstance(Tank.class.getClassLoader(),
new Class[]{Movable.class}, //tank.class.getInterfaces()
new LogHander(tank)
);
m.move();
}
}
class LogHander implements InvocationHandler {
Tank tank;
public LogHander(Tank tank) {
this.tank = tank;
}
//getClass.getMethods[]
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("method " + method.getName() + " start..");
Object o = method.invoke(tank, args);
System.out.println("method " + method.getName() + " end!");
return o;
}
}
interface Movable {
void move();
}
результат операции:
3.2. Механизм динамического прокси CGLIB
3.2.1. Введение
Одна из наиболее серьезных проблем динамического прокси JDK заключается в том, что он может проксировать только те классы, которые реализуют интерфейсы.
Чтобы решить эту проблему, мы можем использовать механизм динамического прокси CGLIB, чтобы избежать ее.
CGLIB
(библиотека генерации кода) — это библиотека генерации байт-кода на основе ASM, которая позволяет нам изменять и динамически генерировать байт-код во время выполнения. CGLIB реализует прокси посредством наследования. Многие известные платформы с открытым исходным кодом используют CGLIB, например модуль AOP в Spring: если целевой объект реализует интерфейс, по умолчанию используется динамический прокси-сервер JDK, в противном случае используется динамический прокси-сервер CGLIB.
MethodInterceptor
Интерфейсы и классы являются ядром механизма динамического прокси CGLIB Enhancer
.
Вам необходимо настроить MethodInterceptor
и переопределить intercept
методы intercept
, чтобы перехватывать методы, расширяющие прокси-класс.
public interface MethodInterceptor
extends Callback{
// 拦截被代理类中的方法
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable;
}
- obj : проксируемый объект (объект, который необходимо улучшить)
- метод : перехваченный метод (метод, который необходимо улучшить)
- args : входные параметры метода
- прокси : используется для вызова исходного метода
Вы можете Enhancer
динамически получить прокси-класс через класс.Когда прокси-класс вызывает метод, фактически MethodInterceptor
вызывается метод intercept
.
3.2.2. Шаги по использованию динамического прокси-класса CGLIB
- определить класс;
- Настраивайте
MethodInterceptor
и переписывайтеintercept
методыintercept
для перехвата методов, расширяющих класс прокси,invoke
аналогично методам в динамическом прокси JDK; - Создание прокси-классов через
Enhancer
классы ;create()
3.2.3 Примеры кода
В отличие от JDK, динамический прокси не требует дополнительных зависимостей. CGLIB
( Библиотека генерации кода ) на самом деле является проектом с открытым исходным кодом. Если вы хотите использовать его, вам необходимо вручную добавить связанные зависимости.
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
1. Реализуйте класс, который использует Alibaba Cloud для отправки текстовых сообщений.
package github.javaguide.dynamicProxy.cglibDynamicProxy;
public class AliSmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
2. Кастомизация (перехватчик метода)
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 自定义MethodInterceptor
*/
public class DebugMethodInterceptor implements MethodInterceptor {
/**
* @param o 代理对象(增强的对象)
* @param method 被拦截的方法(需要增强的方法)
* @param args 方法入参
* @param methodProxy 用于调用原始方法
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object object = methodProxy.invokeSuper(o, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return object;
}
}
3. Получите прокси-класс
import net.sf.cglib.proxy.Enhancer;
public class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) {
// 创建动态代理增强类
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new DebugMethodInterceptor());
// 创建代理类
return enhancer.create();
}
}
4. Фактическое использование
AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class);
aliSmsService.send("java");
После запуска приведенного выше кода консоль выводит:
перед отправкой метода отправить
сообщение: Java
после отправки метода
3.3 Сравнение динамического прокси JDK и динамического прокси CGLIB
- Динамический прокси-сервер JDK может проксировать только классы, реализующие интерфейсы, или непосредственно прокси-интерфейсы, тогда как CGLIB может проксировать классы, которые не реализуют никаких интерфейсов. Кроме того, динамический прокси-сервер CGLIB перехватывает вызовы методов прокси-класса, создавая подкласс проксируемого класса, поэтому он не может проксировать классы и методы, объявленные как конечные типы.
- Что касается эффективности этих двух методов, в большинстве случаев динамический прокси JDK лучше.С обновлением версии JDK это преимущество становится более очевидным.
4. Сравнение статического и динамического прокси
- Гибкость: динамические прокси более гибки. Им не нужно реализовывать интерфейсы. Они могут напрямую проксировать классы реализации и не требуют создания прокси-класса для каждого целевого класса. Кроме того, в статическом прокси-сервере после добавления нового метода в интерфейс необходимо изменить и целевой объект, и прокси-объект, что очень хлопотно!
- Уровень JVM: статический прокси превращает интерфейс, класс реализации и прокси-класс в реальные файлы классов во время компиляции. Динамический прокси динамически генерирует байт-код класса во время выполнения и загружает его в JVM.
5. Резюме
В этой статье в основном представлены две реализации режима прокси: статический прокси и динамический прокси. Охватывает фактическую борьбу между статическим и динамическим прокси-сервером, разницу между статическим и динамическим прокси-сервером, разницу между динамическим прокси-сервером JDK и динамическим прокси-сервером Cglib и т. д.
Справочная статья: Подробное объяснение режима прокси-сервера Java_Блог Джеральда Ньютона - блог CSDN_Подробное объяснение режима прокси-сервера Java