Java程序采用注解方式管理状态模式子类实例的方法

关于编程开发中的“状态模式”,网上可以找到非常多比较详细的介绍文章,但是看过很多的文章大部分都是都是通过简单的 new 对象的方式来进行具体状态子类的实例化,都没有介绍如何实现对子类对象的有效管理,比如这篇:https://blog.csdn.net/lyabc123456/article/details/80476317。

我们都知道,在真实的Java应用开发场景中,几乎不可能采用这种 new 对象的方式,一般都是要结合Spring框架采用xml 配置或者注解的方式来进行实例化,xml方式相对显得有些笨重,本文在这里介绍一种相对方便的注解式管理方式。

1、定义接口和抽象类

首先还是要定义一个状态抽象接口:

public interface IServiceState {
    /**
     * 行为处理方法
     * @param p
     * @param q
     * @return
     */
    R handle(P p, Q q);

    /**
     * 下一个要执行的状态
     * @param stateKey
     * @return IServiceState
     */
    IServiceState nextState(String stateKey);
}

另外为了简化一些通用逻辑,我们再定义一个抽象类实现接口中的部分方法:

public abstract class AbstractServiceState implements IServiceState {
    @Autowired
    protected StateContext stateContext;
    /**
     * 下一个要执行的状态
     *
     * @return IServiceState
     */
    @Override
    public IServiceState nextState(String stateKey) {
        return stateContext.getState(stateKey);
    }
}

2、Context 类的实现

然后就是最重要的 Context 类的实现:

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @Description 状态模式上下文

*/ @Component public class StateContext implements ApplicationContextAware, InitializingBean { private Logger logger = LoggerFactory.getLogger(StateContext.class); private Map> context = new HashMap(); private ApplicationContext applicationContext; public IServiceState getState(String key){ return context.get(key); } @Override public void afterPropertiesSet() throws Exception { logger.info("StateContext.afterPropertiesSet()...初始化状态实例"); initContextMap(); } @SuppressWarnings("rawtypes") private void initContextMap() { Map state = applicationContext.getBeansOfType(IServiceState.class); if (state == null || state.isEmpty()) { logger.error("StateContext.initContextMap()...state==null"); return; } for (Map.Entry entry : state.entrySet()) { IServiceState handler = entry.getValue(); String[] keys = getKey(handler); for (String key : keys) { context.put(key, handler); logger.info("StateContext.initContextMap()...#put#" + key); } } } private String[] getKey(IServiceState handler) { IServiceState target = null; try{ target = (IServiceState) AopTargetUtils.getTarget(handler); }catch (Exception e){ logger.warn("StateContext.initContextMap()...#getTarget fail#, className: {}", handler.getClass().getName(), e); return new String[0]; } Class clazz = target.getClass(); if(clazz.isAnnotationPresent(ServiceStateKey.class)){ return StringUtil.split(clazz.getAnnotation(ServiceStateKey.class).value(),","); } logger.warn("StateContext.initContextMap()...#getKey fail#, strategy no annotation, className:" + clazz.getName()); return new String[0]; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }

这里我们首先实现了 InitializingBean 接口,用于在Spring容器初始完成后调用的 afterPropertiesSet()方法开始进行我们的State实例管理,另外实现 ApplicationContextAware 接口,用于得到 applicationContext 对象的引用,通过调用下面这一方法得到实现了IServiceState 接口的所有类的实例:

Map state = applicationContext.getBeansOfType(IServiceState.class);

其中Map集合的key 为具体子类的全路径,如果我们直接使用这个Map集合来管理我们的 实例对象就会很不方便,也不灵活,为此我们需要将这个Map集合转换为一个方便我们使用的Map集合,首先我们为每个对象找一个新的key,这个key就是我们通过自定义注解的方式注入到类上面的:

@Component
@ServiceStateKey("login")
public class LoginState extends
    AbstractServiceState {
   
    @Override
    public Boolean handle(User user, Long id) {
        

        return null;
    }
}

3、自定义注解

其中自定义注解如下:

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Description: 标注状态对象的key
 * @author 
 * @date 
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceStateKey {
    public String value() default "common";
}

在 StateContext 的实现中我们看到是通过以下方式获取到类上的注解值的:

private String[] getKey(IServiceState handler) {
        IServiceState target = null;
        try{
            target = (IServiceState) AopTargetUtils.getTarget(handler);
        }catch (Exception e){
            logger.warn("StateContext.initContextMap()...#getTarget fail#, className: {}", handler.getClass().getName(), e);
            return new String[0];
        }
        
        Class clazz = target.getClass();
        if(clazz.isAnnotationPresent(ServiceStateKey.class)){
            return StringUtil.split(clazz.getAnnotation(ServiceStateKey.class).value(),",");
        }
        logger.warn("StateContext.initContextMap()...#getKey fail#, strategy no annotation, className:" + clazz.getName());
        return new String[0];
    }

4、获取动态代理原始对象

需要说明的是,之所以用 target = (IServiceState) AopTargetUtils.getTarget(handler); 这个方法来获取我们需要的 target,是因为我们通过 applicationContext 获取到的实例对象很可能是被动态代理过的对象,在这样的对象上我们是无法拿到自定义注解值的,必须通过下面的方法获取到动态代理之前的原始对象(包括JDK动态代理和Cglib代理两种方式):

import java.lang.reflect.Field;

import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.framework.AopProxy;
import org.springframework.aop.support.AopUtils;

/**
 * @Description: 获取 AOP 被代理对象
 * @author 
 * @date 
 */
public class AopTargetUtils {

    /**
     * 获取 目标对象
     * @param proxy 代理对象
     * @return
     * @throws Exception
     */
    public static Object getTarget(Object proxy) throws Exception {

        if(!AopUtils.isAopProxy(proxy)) {
            return proxy;//不是代理对象
        }

        if(AopUtils.isJdkDynamicProxy(proxy)) {
            return getJdkDynamicProxyTargetObject(proxy);
        } else { //cglib
            return getCglibProxyTargetObject(proxy);
        }
    }


    private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
        Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
        h.setAccessible(true);
        Object dynamicAdvisedInterceptor = h.get(proxy);

        Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
        advised.setAccessible(true);

        Object target = ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();

        return target;
    }


    private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
        Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
        h.setAccessible(true);
        AopProxy aopProxy = (AopProxy) h.get(proxy);

        Field advised = aopProxy.getClass().getDeclaredField("advised");
        advised.setAccessible(true);

        Object target = ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();

        return target;
    }

}

5、支持多个key对应一个实例

大家可能注意到了 getKey() 方法中的这几行代码:

if(clazz.isAnnotationPresent(ServiceStateKey.class)){
     return StringUtil.split(clazz.getAnnotation(ServiceStateKey.class).value(),",");
}

没错,我们可以通过逗号分隔的方式为一个类指定多个注解的值,这样一个state实例就可以通过多个key来找到, 方便我们在两种或多种状态下使用同一种状态处理逻辑,比如:

@Component
@ServiceStateKey("login,index")
public class LoginState extends
    AbstractServiceState {
   
    @Override
    public Boolean handle(User user, Long id) {
        

        return null;
    }
}

这种设计你看起来可能会觉得没必要,会认为相同的处理逻辑就用相同的状态key来找不就好了?我只能说,具体的应用场景会比你想象的要复杂,有时候为了代码的简洁,你会不得不用不同的key来找同一个状态,相信我,遇到时你就指明白了!

6、AOP日志记录

好了,最后再给大家提供一种切面的方式来统一记录各个状态实例log的方法,可以记录下调用耗时超过多长时间的方法,以及可以通过开关的方式选择是否打印方法的详细入参或返回值。

Spring配置中别忘了加上以下注解:

    
    
    
    
    
    

日志增强切面代码:

import com.alibaba.fastjson.JSON;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

/**
 * @author 
 * @Description: 日志切面
 * @date 
 */
@Aspect
@Service
public class LogRecord {
    /**
     * The Constant logger.
     */
    private static final Logger logger = LoggerFactory.getLogger(LogRecord.class);
    private static final Long MAX_TIME = 100L;

    @Around("(execution(* com.test.state.handler.*.*(..)) "
        + "|| execution(* com.test.strategy.operation.*.*(..)))")
    public Object aroundHandler(ProceedingJoinPoint joinPoint) throws Throwable {
        long beginTime = System.currentTimeMillis();
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        try {
            if (LogSwitcher.ASPECT_LOG_PARAMS_PRINT){
                logger.info("LogRecord.aroundHandler begin call ClassName={}, MethodName={}, params={}", className, methodName, JSON.toJSON(joinPoint.getArgs()));
            }else{
                logger.info("LogRecord.aroundHandler begin call ClassName={}, MethodName={}", className, methodName);
            }
            Object proceed = joinPoint.proceed(joinPoint.getArgs());

            if (LogSwitcher.ASPECT_LOG_RESULT_PRINT){
                logger.info("LogRecord.aroundHandler end call ClassName={}, MethodName={}, result={}", className, methodName, JSON.toJSONString(proceed));
            }
            return proceed;
        } catch (Throwable e) {
            logger.error("LogRecord.aroundHandler exception: ClassName={}, MethodName={}, params={}", className, methodName,
                JSON.toJSON(joinPoint.getArgs()));
            logger.error("LogRecord.aroundHandler exception = {}", e.getMessage(), e);
            throw e;
        } finally {
            //记录调用大于100耗秒时间的类和方法,便于性能问题排查追踪
            long endTime = System.currentTimeMillis();
            long methodCostTime = endTime - beginTime;
            if (methodCostTime > MAX_TIME) {
                logger.warn("LogRecord.aroundHandler long time processor, ClassName: {}, MethodName: {}, proceed time: {} ms", className, methodName, methodCostTime);
            }
        }
    }
}

7、结尾

这里简单介绍了我在真实项目实践中使用状态模式的一种方式,个人觉得还是非常方便的,其中由于一些原因部分代码进行了脱敏处理,可能无法直接编译通过,深表歉意,其实不限于状态模式,推广到其他设计模式也是异曲同工,欢迎感兴趣的朋友发表自己的观点,大家一起交流共同进步!

你可能感兴趣的:(编码心得,设计模式)