Лучшие практики для шаблонов проектирования — успешное использование шаблона прокси

Шаблоны проектирования
Лучшие практики для шаблонов проектирования — успешное использование шаблона прокси

Введение: Шаблон прокси предназначен для предоставления прокси-объекта для определенного объекта, и прокси-объект получает доступ к различным методам прокси-объекта.

Когда использовать прокси-режим

Если вы хотите выполнять вспомогательные функции вне логики метода для некоторых методов объекта (например, печать входных и выходных параметров, обработка исключений, проверка разрешений), но не хотите (или не можете) писать код этих функций в оригинал методы, то вы можете использовать режим прокси.

Развлекайтесь, используя прокси-режим

задний план

Когда мы только начинали разрабатывать модельную платформу, нам всегда были нужны какие-то функции вне бизнес-логики для отладки или статистики, например:

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 возвращается ответ в этом случае.