Введение: Шаблон прокси предназначен для предоставления прокси-объекта для определенного объекта, и прокси-объект получает доступ к различным методам прокси-объекта.
Когда использовать прокси-режим
Если вы хотите выполнять вспомогательные функции вне логики метода для некоторых методов объекта (например, печать входных и выходных параметров, обработка исключений, проверка разрешений), но не хотите (или не можете) писать код этих функций в оригинал методы, то вы можете использовать режим прокси.
Развлекайтесь, используя прокси-режим
задний план
Когда мы только начинали разрабатывать модельную платформу, нам всегда были нужны какие-то функции вне бизнес-логики для отладки или статистики, например:
public Response processXxxBiz(Request request) {
long startTime = System.currentMillis();
try {
// 业务逻辑
......
} catch (Exception ex) {
logger.error("processXxxBiz error, request={}", JSON.toJSONString(request), ex)
// 生成出错响应
......
}
long costTime = (System.currentMillis() - startTime);
// 调用完成后,记录出入参
logger.info("processXxxBiz, costTime={}ms, request={}, response={}", costTime, JSON.toJSONString(request), JSON.toJSONString(response));
}
Легко видеть, что печать входных и выходных параметров, трудоемкая запись методов, перехват исключений и их обработка — все это не имеет отношения к бизнесу, бизнес-методы должны заботиться только о коде бизнес-логики. Если вы не хотите ее решать, в долгосрочной перспективе недостатки очевидны:
-
Нарушает принцип DRY (не повторяйтесь), поскольку каждый бизнес-метод будет включать код с аналогичными функциями за пределами этой бизнес-логики.
-
Нарушая принцип единой ответственности, код бизнес-логики и код дополнительных функций смешиваются вместе, что увеличивает сложность последующего обслуживания и расширения и легко приводит к взрыву класса.
Поэтому, чтобы не вызывать путаницы в будущем, мне нужен способ решения вышеуказанных проблем — очевидно, что мне нужен режим прокси: метод исходного объекта должен заботиться только о бизнес-логике, а затем прокси-объект выполняет эти вспомогательные функции. В Spring существуют различные способы реализации режима прокси.Позвольте мне поделиться моим текущим «лучшим способом» реализации режима прокси на основе Spring (если у вас есть лучший способ, добро пожаловать, чтобы просветить и обсудить)~
план
Все слышали, что у Spring есть два артефакта — IoC и AOP. АОП расшифровывается как Аспектно-ориентированное программирование: технология, реализующая проксирование программных функций с помощью предварительной компиляции (CGLib) или динамического прокси-сервера во время выполнения (JDK Proxy). Использование прокси-режима в Spring — идеальный сценарий приложения для АОП, и использование аннотаций для операций АОП стало первым выбором, потому что аннотации действительно удобны и просты в использовании. Давайте кратко рассмотрим связанные концепции Spring AOP:
-
Pointcut (указатель), укажите, при каких обстоятельствах выполнять АОП, например, когда метод помечен определенной аннотацией.
-
JoinPoint (точка соединения), точка выполнения в операции программы, такой как выполнение метода или обработка исключения; а в Spring AOP только точки соединения методов.
-
Совет (улучшение) по улучшению точки соединения (прокси): дополнительная обработка перед вызовом метода, после вызова или при возникновении исключения
-
Аспект (раздел), состоящий из Pointcut и Advice, можно понимать как: при каких обстоятельствах (Pointcut) делать какое улучшение (Advice) к какой цели (JoinPoint)
После рассмотрения концепции АОП наше решение также стало очень ясным.Для сценария агента:
-
Сначала определите аннотацию, а затем напишите соответствующую логику обработки расширения.
-
Создайте соответствующий аспект, определите pointcut на основе аннотации в аспекте и привяжите соответствующую логику обработки расширения.
-
Для метода, соответствующего pointcut (то есть метода, отмеченного этой аннотацией), используйте логику обработки связанного улучшения, чтобы улучшить его.
Определить метод улучшения процессора
Сначала мы определяем абстракцию «прокси»: обработчик расширения метода MethodAdviceHandler. После этого каждая определяемая нами аннотация привязывается к соответствующему классу реализации MethodAdviceHandler.Когда целевой метод проксируется, соответствующий класс реализации MethodAdviceHandler обрабатывает прокси-доступ к методу.
/**
* 方法增强处理器
*
* @param <R> 目标方法返回值的类型
*/
public interface MethodAdviceHandler<R> {
/**
* 目标方法执行之前的判断,判断目标方法是否允许执行。默认返回 true,即 默认允许执行
*
* @param point 目标方法的连接点
* @return 返回 true 则表示允许调用目标方法;返回 false 则表示禁止调用目标方法。
* 当返回 false 时,此时会先调用 getOnForbid 方法获得被禁止执行时的返回值,然后
* 调用 onComplete 方法结束切面
*/
default boolean onBefore(ProceedingJoinPoint point) { return true; }
/**
* 禁止调用目标方法时(即 onBefore 返回 false),执行该方法获得返回值,默认返回 null
*
* @param point 目标方法的连接点
* @return 禁止调用目标方法时的返回值
*/
default R getOnForbid(ProceedingJoinPoint point) { return null; }
/**
* 目标方法抛出异常时,执行的动作
*
* @param point 目标方法的连接点
* @param e 抛出的异常
*/
void onThrow(ProceedingJoinPoint point, Throwable e);
/**
* 获得抛出异常时的返回值,默认返回 null
*
* @param point 目标方法的连接点
* @param e 抛出的异常
* @return 抛出异常时的返回值
*/
default R getOnThrow(ProceedingJoinPoint point, Throwable e) { return null; }
/**
* 目标方法完成时,执行的动作
*
* @param point 目标方法的连接点
* @param startTime 执行的开始时间
* @param permitted 目标方法是否被允许执行
* @param thrown 目标方法执行时是否抛出异常
* @param result 执行获得的结果
*/
default void onComplete(ProceedingJoinPoint point, long startTime, boolean permitted, boolean thrown, Object result) { }
}
Чтобы облегчить использование MethodAdviceHandler, мы определяем абстрактный класс, предоставляющий некоторые общие методы.
public abstract class BaseMethodAdviceHandler<R> implements MethodAdviceHandler<R> {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 抛出异常时候的默认处理
*/
@Override
public void onThrow(ProceedingJoinPoint point, Throwable e) {
String methodDesc = getMethodDesc(point);
Object[] args = point.getArgs();
logger.error("{} 执行时出错,入参={}", methodDesc, JSON.toJSONString(args, true), e);
}
/**
* 获得被代理的方法
*
* @param point 连接点
* @return 代理的方法
*/
protected Method getTargetMethod(ProceedingJoinPoint point) {
// 获得方法签名
Signature signature = point.getSignature();
// Spring AOP 只有方法连接点,所以 Signature 一定是 MethodSignature
return ((MethodSignature) signature).getMethod();
}
/**
* 获得方法描述,目标类名.方法名
*
* @param point 连接点
* @return 目标类名.执行方法名
*/
protected String getMethodDesc(ProceedingJoinPoint point) {
// 获得被代理的类
Object target = point.getTarget();
String className = target.getClass().getSimpleName();
Signature signature = point.getSignature();
String methodName = signature.getName();
return className + "." + methodName;
}
}
Абстракция, определяющая аспект метода
Таким же образом извлекается публичная логика аспекта метода и определяется абстракция аспекта метода — определяется каждая последующая аннотация, и соответствующий аспект метода наследуется от этого абстрактного класса.
/**
* 方法切面抽象类,由子类来指定切点和绑定的方法增强处理器的类型
*/
public abstract class BaseMethodAspect implements ApplicationContextAware {
/**
* 切点,通过 @Pointcut 指定相关的注解
*/
protected abstract void pointcut();
/**
* 对目标方法进行环绕增强处理,子类需通过 pointcut() 方法指定切点
*
* @param point 连接点
* @return 方法执行返回值
*/
@Around("pointcut()")
public Object advice(ProceedingJoinPoint point) {
// 获得切面绑定的方法增强处理器的类型
Class<? extends MethodAdviceHandler<?>> handlerType = getAdviceHandlerType();
// 从 Spring 上下文中获得方法增强处理器的实现 Bean
MethodAdviceHandler<?> adviceHandler = appContext.getBean(handlerType);
// 使用方法增强处理器对目标方法进行增强处理
return advice(point, adviceHandler);
}
/**
* 获得切面绑定的方法增强处理器的类型
*/
protected abstract Class<? extends MethodAdviceHandler<?>> getAdviceHandlerType();
/**
* 使用方法增强处理器增强被注解的方法
*
* @param point 连接点
* @param handler 切面处理器
* @return 方法执行返回值
*/
private Object advice(ProceedingJoinPoint point, MethodAdviceHandler<?> handler) {
// 执行之前,返回是否被允许执行
boolean permitted = handler.onBefore(point);
// 方法返回值
Object result;
// 是否抛出了异常
boolean thrown = false;
// 开始执行的时间
long startTime = System.currentTimeMillis();
// 目标方法被允许执行
if (permitted) {
try {
// 执行目标方法
result = point.proceed();
} catch (Throwable e) {
// 抛出异常
thrown = true;
// 处理异常
handler.onThrow(point, e);
// 抛出异常时的返回值
result = handler.getOnThrow(point, e);
}
}
// 目标方法被禁止执行
else {
// 禁止执行时的返回值
result = handler.getOnForbid(point);
}
// 结束
handler.onComplete(point, startTime, permitted, thrown, result);
return result;
}
private ApplicationContext appContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
appContext = applicationContext;
}
}
На данный момент наша полка режима прокси на основе AOP настроена. Причина, по которой эта небольшая полка необходима, заключается в том, чтобы иметь возможность расширяться по горизонтали при добавлении аннотаций в будущем: каждый раз, когда вы добавляете аннотацию (XxxAnno), вам нужно только реализовать новый обработчик улучшения метода (XxxHandler) и новый аспект метода. ( XxxAspect ) без изменения существующего кода, что полностью соответствует концепции шаблона проектирования, закрытой для модификации и открытой для расширения.
Давайте реализуем наше первое улучшение на основе этой небольшой полки: запись вызова метода (запись параметров метода и продолжительности вызова).
определить аннотацию
/**
* 用于产生调用记录的注解,会记录下方法的出入参、调用时长
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokeRecordAnno {
/**
* 调用说明
*/
String value() default "";
}
Реализация процессора расширения метода
@Component
public class InvokeRecordHandler extends BaseMethodAdviceHandler<Object> {
/**
* 记录方法出入参和调用时长
*/
@Override
public void onComplete(ProceedingJoinPoint point, long startTime, boolean permitted, boolean thrown, Object result) {
String methodDesc = getMethodDesc(point);
Object[] args = point.getArgs();
long costTime = System.currentTimeMillis() - startTime;
logger.warn("\n{} 执行结束,耗时={}ms,入参={}, 出参={}",
methodDesc, costTime,
JSON.toJSONString(args, true),
JSON.toJSONString(result, true));
}
@Override
protected String getMethodDesc(ProceedingJoinPoint point) {
Method targetMethod = getTargetMethod(point);
// 获得方法上的 InvokeRecordAnno
InvokeRecordAnno anno = targetMethod.getAnnotation(InvokeRecordAnno.class);
String description = anno.value();
// 如果没有指定方法说明,那么使用默认的方法说明
if (StringUtils.isBlank(description)) {
description = super.getMethodDesc(point);
}
return description;
}
}
Реализация аспекта метода
@Aspect
@Order(1)
@Component
public class InvokeRecordAspect extends BaseMethodAspect {
/**
* 指定切点(处理打上 InvokeRecordAnno 的方法)
*/
@Override
@Pointcut("@annotation(xyz.mizhoux.experiment.proxy.anno.InvokeRecordAnno)")
protected void pointcut() { }
/**
* 指定该切面绑定的方法切面处理器为 InvokeRecordHandler
*/
@Override
protected Class<? extends MethodAspectHandler<?>> getHandlerType() {
return InvokeRecordHandler.class;
}
}
@Aspect используется, чтобы сообщить Spring, что это аспект, а затем Spring сканирует методы, соответствующие @Pointcut, при запуске собрания, а затем сплетает эти целевые методы: то есть метод с @Around в аспекте используется для улучшения целевой метод...
@Order используется для обозначения того, в каком слое должен находиться этот участок. Чем меньше число, тем внешний слой (чем раньше вы входите и чем позже конец) - участок, записываемый вызовом метода, очевидно, должен находиться в атмосфере (Редактор: Слава королю) термин, т.е. крайний), так как аспект записи вызова метода должен заканчиваться последним, мы даем небольшое число.
контрольная работа
Теперь мы можем аннотировать метод, который хочет записывать информацию о вызове во время разработки, а затем наблюдать за вызовом целевого метода через журнал. Старые правила, получите контроллер:
@RestController
@RequestMapping("proxy")
public class ProxyTestController {
@GetMapping("test")
@InvokeRecordAnno("测试代理模式")
public Map<String, Object> testProxy(@RequestParam String biz,
@RequestParam String param) {
Map<String, Object> result = new HashMap<>(4);
result.put("id", 123);
result.put("nick", "之叶");
return result;
}
}
Затем посетите: localhost/proxy/test?biz=abc¶m=test
В тот момент, когда вы видите этот вывод — успех прокси — да, это самое счастливое чувство программистов.
расширять
Предположим, мы хотим обрабатывать, когда целевой метод выдает исключение: когда выдается исключение, асинхронно отправлять информацию об исключении в почтовый ящик или DingTalk, а затем возвращать соответствующий ответ об ошибке в соответствии с типом возвращаемого значения метода.
★ Определите соответствующие аннотации
/**
* 用于异常处理的注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExceptionHandleAnno { }
★ Реализовать метод улучшения процессора
@Component
public class ExceptionHandleHandler extends BaseMethodAdviceHandler<Object> {
/**
* 抛出异常时的处理
*/
@Override
public void onThrow(ProceedingJoinPoint point, Throwable e) {
super.onThrow(point, e);
// 发送异常到邮箱或者钉钉的逻辑
}
/**
* 抛出异常时的返回值
*/
@Override
public Object getOnThrow(ProceedingJoinPoint point, Throwable e) {
// 获得返回值类型
Class<?> returnType = getTargetMethod(point).getReturnType();
// 如果返回值类型是 Map 或者其子类
if (Map.class.isAssignableFrom(returnType)) {
Map<String, Object> result = new HashMap<>(4);
result.put("success", false);
result.put("message", "调用出错");
return result;
}
return null;
}
}
Если тип возвращаемого значения — Map, то мы возвращаем соответствующий экземпляр Map в случае ошибки в вызове (реальная ситуация — вообще вернуть Response в бизнес-системе).
★ Аспект метода реализации
@Aspect
@Order(10)
@Component
public class ExceptionHandleAspect extends BaseMethodAspect {
/**
* 指定切点(处理打上 ExceptionHandleAnno 的方法)
*/
@Override
@Pointcut("@annotation(xyz.mizhoux.experiment.proxy.anno.ExceptionHandleAnno)")
protected void pointcut() { }
/**
* 指定该切面绑定的方法切面处理器为 ExceptionHandleHandler
*/
@Override
protected Class<? extends MethodAdviceHandler<?>> getAdviceHandlerType() {
return ExceptionHandleHandler.class;
}
}
Обработка исключений, как правило, является очень внутренним аспектом, поэтому мы устанавливаем для @Order значение 10 и позволяем ExceptionHandleAspect быть более внутренним (то есть ввод после и завершение до) в InvokeRecordAspect, чтобы внешний InvokeRecordAspect также мог записывать возврат при возникновении исключения. ценность. Измените тестовый метод, чтобы добавить @ExceptionHandleAnno:
@RestController
@RequestMapping("proxy")
public class ProxyTestController {
@GetMapping("test")
@ExceptionHandleAnno
@InvokeRecordAnno("测试代理模式")
public Map<String, Object> testProxy(@RequestParam String biz,
@RequestParam String param) {
if (biz.equals("abc")) {
throw new IllegalArgumentException("非法的 biz=" + biz);
}
Map<String, Object> result = new HashMap<>(4);
result.put("id", 123);
result.put("nick", "之叶");
return result;
}
}
Посетите: localhost/proxy/test?biz=abc¶m=test, аспект обработки исключений заканчивается первым:
Вызов метода заканчивается после записанного аспекта:
Ничего страшного, все так естественно, гармонично и красиво~
★ мышление
**Редактор:** Вы можете видеть, что метод onThrow InvokeRecordHandler не выполняется при возникновении исключения, почему?
**Leaf:**Поскольку InvokeRecordAspect является более внешним, чем ExceptionHandleAspect, когда внешний InvokeRecordAspect выполняется, выполнение уже является методом, который внутренний ExceptionHandleAspect проксировал, и соответствующий ExceptionHandleHandler уже «переварил» исключение, то есть ExceptionHandleAspect проксируется. методы больше не генерируют исключения.
**Редактор: **Если мы хотим ограничить количество вызовов метода в единицу времени, например, пользователь может отправить форму только один раз в течение 3 секунд, кажется, что это также может быть достигнуто с помощью рутины этого режима прокси.
**Листья:**Небольшие сцены. Сначала определите аннотации (аннотации могут включать такие параметры, как единица времени, максимальное количество вызовов и т. д.), а затем в методе onBefore обработчика аспектов метода используйте кеш для записи количества пользовательских представлений в единицу времени. превышено максимальное количество вызовов, вернуть false, то целевой метод не разрешается вызывать, то в методе getOnForbid возвращается ответ в этом случае.