关于编程开发中的“状态模式”,网上可以找到非常多比较详细的介绍文章,但是看过很多的文章大部分都是都是通过简单的 new 对象的方式来进行具体状态子类的实例化,都没有介绍如何实现对子类对象的有效管理,比如这篇:https://blog.csdn.net/lyabc123456/article/details/80476317。
我们都知道,在真实的Java应用开发场景中,几乎不可能采用这种 new 对象的方式,一般都是要结合Spring框架采用xml 配置或者注解的方式来进行实例化,xml方式相对显得有些笨重,本文在这里介绍一种相对方便的注解式管理方式。
首先还是要定义一个状态抽象接口:
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);
}
}
然后就是最重要的 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 extends IServiceState> 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;
}
}
其中自定义注解如下:
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 extends IServiceState> 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];
}
需要说明的是,之所以用 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;
}
}
大家可能注意到了 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来找同一个状态,相信我,遇到时你就指明白了!
好了,最后再给大家提供一种切面的方式来统一记录各个状态实例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);
}
}
}
}
这里简单介绍了我在真实项目实践中使用状态模式的一种方式,个人觉得还是非常方便的,其中由于一些原因部分代码进行了脱敏处理,可能无法直接编译通过,深表歉意,其实不限于状态模式,推广到其他设计模式也是异曲同工,欢迎感兴趣的朋友发表自己的观点,大家一起交流共同进步!