面试造飞机,入职拧螺丝。
一直以来,对于设计模式和一些底层原理,每每听到总会不自觉的流露出膜拜的感觉,尤其是在拜读一些大牛和框架源码的时候,心里总是在想: 一样是9年义务教育……
然而现实中也有很多人一直孜孜不倦的学习,给自己充电,只是不管是视频教学还是书本上看到的知识,读来总觉耳目一新,当合上书本真正在工作当中遇到问题的时候,大部分人的颅内活动:马什么梅?? 道理都明白,只是缺乏实战,或者说是实战的时候恍如段誉一般,可能空有一身本领临敌之际却没法得心应手。今天笔者从一个一年开发经验的角度记录一次工作当中的"造飞机"的经历。
最近公司在研发一个体育赛事系统,我正好有幸参与,与另外一名前端哥们一起研发IOS移动端的赛事数据记录系统,刚开始理解需求、界面搭建、基础框架搭建等等,一切顺风顺水,到了中途,接到领导一个需求,具体的要求是这样的:整场赛事需要对于数据进行实时的记录,根据场上运动员的事件和现场工作人员的操作ipad来记录比赛的情况,比如一个球员有进球、警告、失球、红黄牌、点球等事件,每一个事件可能仅仅是做一个事件展示,暗示譬如进球这种又要去额外的操作比分,换人这种又需要去修改运动员状态,事件又需要具有基本的CRUD,我刚开始觉得这都不算难吧,正打算开始大刀阔斧的开干的时候,老大意味深长的跟我说:后端只做一个接口,而且每种事件的增删等等都是在这个接口里,仅通过客户端参数变化而动态的切换处理策略,后续需要所有事件做一个统一的监控拦截。 我:……
按照惯有的做法是, 我一个个事件提供一个个接口给前端,这样子按顺序的做下来倒也思路清晰,但是现在要浓缩到一个接口里面去完成,我就开始犯难了, 不过为了卑微的涨薪,跟它刚!
根据不同的参数去动态的选取对应的执行逻辑? 这不就是跟策略模式一样吗,策略模式就是面向接口式的调用,通过对类的扩展来实现不同的实现方式,这也是跟设计模式原则的开闭原则非常吻合的一个方案,根据实际需求可以抽取出一个通用的顶层接口:
/**
* @description: 事件策略接口
* @author: xjr
* @create: 2020-05-12 10:37
**/
public interface EventStrategy {
/**
*
* @description 事件收集
* @param events:
* @author xiejiarong
* @date 2020年05月12日 10:42
* @throws BizException 业务异常
*/
void collect(List<BizUnitAction> events) throws BizException;
/**
*删除事件策略
* @param events: 要删除的事件
* @author xiejiarong
* @date 2020年05月12日 10:47
* @throws BizException 业务异常
*/
void delete(List<BizUnitAction> events) throws BizException;
/**
* 修改事件(修改时间)
* @param events: 修改的事件
* @author xiejiarong
* @date 2020年05月12日 11:27
* @throws BizException 业务异常
*/
void update(List<BizUnitAction> events) throws BizException;
/**
*
* 校验客户端参数
* @param bizUnitActions: 客户端传递事件数组参数
* @author xiejiarong
* @date 2020年05月11日 14:42
* @throws BizException 业务异常
*/
static void checkAction(List<BizUnitAction> bizUnitActions) throws BizException{
boolean flag=bizUnitActions.stream()
.anyMatch(data->{
return ParamUtils.isEmpty(data.getRscUnitCode()) || ParamUtils.isEmpty(data.getTeamCode()) || ParamUtils.isEmpty(data.getAthleteCode())
|| ParamUtils.isEmpty(data.getAction()) || ParamUtils.isEmpty(data.getActionTime()) || ParamUtils.isEmpty(data.getBib())
|| ParamUtils.isEmpty(data.getHomeAway()) || ParamUtils.isEmpty(data.getScorea()) ||ParamUtils.isEmpty(data.getScoreh());
});
if (flag){
throw new BizException("事件参数存在部分为空,请重新确认");
}
}
/**
*
* 统一入口方法
* @param bizUnitActions: 客户端传递事件数组参数
* @author xiejiarong
* @date 2020年05月11日 14:42
* @throws BizException 业务异常
*/
void proceed(List<BizUnitAction> bizUnitActions, EventStrategyEnum strategyEnum)throws BizException;
}
不管是黄牌、红牌、进球还是乌龙球等等事件,无非就是收集、删除、更新等方式,另外提供了一些静态的方法作为客户端参数校验行为。所以抽取了三个抽象方法用于后面的扩展。 对外仅提供一个proceed方法作为统一入口。 有了接口之后,再在具体实现类和接口中间提供一个父级抽象类(这也是诸多优秀开源框架采取的一个做法,对外提供提供,内部提取一个抽象类,扩展通过继承抽象类的形式,将一些通用的方法和属性都放置在抽象类中,实现解耦和封装的思想)
/**
* @program:
* @description: 计时计分事件录入处理器扩展接口
* @author: xjr
* @create: 2020-05-09 11:12
**/
@Setter
@Getter
@Slf4j
public abstract class AbstractEventHandler implements EventStrategy {
protected final ThreadLocal<Map<String,Object>> CURRENT_EVENT=new ThreadLocal<>();
@Autowired
protected BizUnitActionManager actionManager;
@Autowired
protected BizActionRelationManager relationManager;
@Autowired
protected BizUnitAthleteManager bizUnitAthleteManager;
@Autowired
protected BizParticipantManager participantManager;
@Autowired
protected BizParticpantSuspendedManager bizParticpantSuspendedManager;
@Autowired
protected BizUnitResultManager bizUnitResultManager;
@Autowired
protected BizUnitActionMapper actionMapper;
@Autowired
protected PlatformTransactionManager txManager;
@Autowired
protected BizUnitPeriodsManager bizUnitPeriodsManager;
@Autowired
protected BasEventUnitManager basEventUnitManager;
//事件枚举
protected ActionEnum handlerAction;
protected final String RSC_UNIT_CODE="rscUnitCode";
protected final String EVENT_TEAM_CODE="eventTeamCode";
protected final String RELATIONS="relations";
protected final String ACTIONS="actions";
protected EventStrategyEnum strategyEnum;
protected volatile LocalDateTime now;
/**
*
* @description 事件录入执行前置,查询所关联的事件列表
* @author xiejiarong
* @date 2020年05月09日 14:39
* @throws BizException 业务异常
*/
protected void beforeExecute() throws BizException{
///由于公司保密,此处省略一堆代码
log.info("系统时间:{},事件{}执行{}开始", LocalDateTime.now(),this.handlerAction.getValue(),this.strategyEnum.getValue());
}
/**
*
* @description 事件录入执行后置(默认采集,子类可自行扩展)
* @author xiejiarong
* @date 2020年05月09日 14:39
*/
protected void afterExecute() throws BizException {
///由于公司保密,此处省略一堆代码
log.info("系统时间{},事件{}执行{}结束",now,this.handlerAction.getValue(),this.strategyEnum.getValue());
CURRENT_EVENT.remove();
}
@Override
public synchronized void proceed(List<BizUnitAction> bizUnitActions, EventStrategyEnum strategyEnum)throws BizException{
this.now=LocalDateTime.now();
this.strategyEnum=strategyEnum;
Map<String,Object> contextParam=new HashMap<>(3);
contextParam.put(ACTIONS,bizUnitActions.get(0));
contextParam.put(RSC_UNIT_CODE,bizUnitActions.get(0).getRscUnitCode());
contextParam.put(EVENT_TEAM_CODE,bizUnitActions.get(0).getTeamCode());
this.CURRENT_EVENT.set(contextParam);
TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());
try {
this.beforeExecute();
strategyEnum.execute(this,bizUnitActions);
this.afterExecute();
}catch (Exception e){
txManager.rollback(status);
throw new BizException(e.getMessage());
}
txManager.commit(status);
}
/**
*
* @description 公共的比分修改操作
* @param action:
* @author xiejiarong
* @date 2020年05月13日 10:47
*/
protected void updateGoal(BizUnitAction action) throws BizException{
///由于公司保密,此处省略一堆代码
}
@Override
public void update(List<BizUnitAction> events) throws BizException{
///由于公司保密,此处省略一堆代码
}
/**
*
* @description 更新球员禁赛状态 通用
* @author xiejiarong
* @date 2020年05月18日 14:34
*/
public void updateAthleteStatus(){
///由于公司保密,此处省略一堆代码
}
(大家自动忽略相关业务逻辑代码)
讲解一下思路:首先AbstractEventHandler抽象类实现了EventStrategy顶层接口,保留了collect和delete的收集和删除方法,因为业务需求更新的操作仅仅是对于时间进行更新,所以这边对于update做了一个默认的实现。CURRENT_EVENT为ThreadLocal类型,内部使用一个Map结构保存了赛事单元编码和赛事信息等等基本数据,避免多线程的情况出现线程安全问题。使用@Autowired注解事先注入了所有的业务组件提供扩展使用,ActionEnum为事件的枚举类型,作为控制台开启日志输出和后续的客户端参数的适配,EventStrategyEnum是具体的行为策略,后面会讲到。 封装了一些公用的方法,比如修改比分和更新运动状态等等公共方法。在proceed入口方法中模仿了代理模式,主要是为了适应现有的业务逻辑,对于进球这种事件必然需要有一个地方失球的事件等,所以before和after就是为了在处理各自的业务逻辑的同时无需去关注关联事件的记录,同时也支持开发者自己重写。
有了事件的策略,上面说的行为策略是什么呢? 虽说事件有多种,但是每个事件还是有共同的行为:增删改。 同样的一个事件客户端如何根据传入的参数不同去执行不同的重写策略呢? 这边我使用了枚举+抽象方法的方式去实现。
/*
* @description: 事件行为枚举
* @author: xjr
* @create: 2020-05-12 11:13
**/
@Getter
@AllArgsConstructor
public enum EventStrategyEnum {
DELETE("DELETE","删除"){
@Override
public void execute(EventStrategy strategy,List<BizUnitAction> actions) throws BizException{
checkParam(actions);
strategy.delete(actions);
}
},
COLLECT("COLLECT","采集") {
@Override
public void execute(EventStrategy strategy, List<BizUnitAction> actions) throws BizException {
strategy.collect(actions);
}
},
UPDATE("UPDATE","更新") {
@Override
public void execute(EventStrategy strategy, List<BizUnitAction> actions) throws BizException {
checkParam(actions);
strategy.update(actions);
}
};
private String code;
private String value;
public abstract void execute(EventStrategy strategy, List<BizUnitAction> actions) throws BizException;
public void checkParam(List<BizUnitAction> actions) throws BizException{
BizUnitAction removeRcAction=actions.get(0);
if (ParamUtils.isEmpty(removeRcAction.getId())){
throw new BizException("该策略需要传递id");
}
}
}
EventStrategyEnum 作为行为策略,其实是以枚举的方式定义了增删改的操作类型以此来根据参数与客户端适配。可以看到 该枚举中的execute方法为具体的调用之时的判断,入参为我们定义的顶层接口EventStrategy,同时接收一个list作为客户端参数。 不同的枚举对象如COLLECT、DELETE、UPDATE等,都重新给了execute方法,根据动态传入的接口实现(对应我们后续实现的扩展类)去执行不同实现中重写的方法 ,比如客户端传递COLLECT参数,我们就可以根据名字获取到对应的枚举COLLECT,而COLLECT重写的execute逻辑就是去执行对应扩展的collect收集方法。
有了大概的一个框架之后,领导又跟我说,之前说的那个全局权限拦截现在需要加进来,由于PC和Ipad使用的是同一个接口,为了防止客户端旧数据覆盖新数据,需要区分当前操作的数据是否为最新数据,相当于实现一个乐观锁的功能。 听到这里相信不少同学应该已经知道这边是需要使用代理去完成的,毕竟你不可能在每一个后续的扩展中的开头写一个校验,结尾写一个校验成功的代码,这样子的代码可读性极差,而且基本都是复制粘贴的。既然是用代理模式,那么AOP就是不错的实现方案,可是既然一开始我们的设计原则就是要实现一个通用、扩展性强、符合开闭原则的方式,如果使用AOP去拦截接口的统一入口方法是可以实现,但是这样子业务逻辑难免就一刀切掉了,换个简单的说法:现在的业务是要实现一个乐观锁的功能,aop中就去做这样一个东西拦截,但是需求可能一直在变,而且这个拦截的功能未必就是需要开启的,这样子我们就需要一个方案要符合通用性、可扩展性、并且可以根据需求不停的转换我们的拦截策略,而且提供一个全局的开关。那,就干吧。
由于一开始我们的就是面向接口变成,所以这边的代理方式我们就选择JDK动态代理。
为了实现开关的功能,我选择自己自定义一个注解,实现spring @component注册组件的功能。
自定义注解虽然可以实现注册组件的功能,但是考虑到每一个组件最终是一个代理类,使用spring的beandefinition功能难以实现,采用FactoryBean–spring的工厂bean来实现对象的代理创建。
使用自定义注解要实现组件注册功能,就必须要借助spring的扩展接口–ImportBeanDefinitionRegistrar来获取注册类,并且为了让spring知道我们的自定义注解需要被扫描到,借助继承ClassPathBeanDefinitionScanner的方式来做一个bean扫描功能。
由于我们的项目是springboot工程,所以我也吸取了其他第三方依赖的enable思想,提供一个启用bena扫描注册注解,并且注解中可选择不同的拦截策略和扫描的注解类型。
这是我最终的项目结构:
- 首先是启动类提供一个开关注解
/**
* @description: 开启事件策略
* @author: xjr
* @create: 2020-05-15 14:50
**/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
//import导入一个ImportBeanDefinitionRegistrar的实现类,实现动态注册bean功能
@Import(EventStrategyBeanConfiguration.class)
public @interface EnableEventStrategy {
//具体扫描的包名
@AliasFor("value")
String implementsPackage() default "";
@AliasFor("implementsPackage")
String value() default "";
//扫描的注解类型
Class<?> strategyAnnotation() default Strategy.class;
//可自己制定拦截的代理策略,但必须是EventStrategyProxyParent的实现
Class<? extends EventStrategyProxyParent> proxyStrategy() default EventStrategyProxy.class;
}
自定义的bean注解:
/**
* @description: 策略实现类注解
* @author: xjr
* @create: 2020-05-15 14:27
**/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Strategy {
@AliasFor("beanId")
String value() default "";
@AliasFor("value")
String beanId() default "";
//该属性可控制bean是否使用代理模式创建,默认true,false就是spring普通bean
boolean isIntercept() default true;
}
重头戏
实现注册bean功能的配置类(enable注解import的类):
/**
* @program: fb-ovr-ts
* @description: 策略类bean实现配置
* @author: xjr
* @create: 2020-05-15 14:33
**/
@Slf4j
public class EventStrategyBeanConfiguration implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
log.info("strategy is ready o registry");
StandardAnnotationMetadata standardAnnotationMetadata= (StandardAnnotationMetadata) annotationMetadata;
AnnotationAttributes annotationMap= AnnotationAttributes.fromMap(standardAnnotationMetadata.getAnnotationAttributes(EnableEventStrategy.class.getName()));
String packageName=annotationMap.getString("value");
Assert.notNull(packageName,"the value of eventstrategy must be not null or empty");
ClassPathStrategyScanner strategyScanner=new ClassPathStrategyScanner(beanDefinitionRegistry);
strategyScanner.setAnnotationClass(annotationMap.getClass("strategyAnnotation"));
strategyScanner.setInterceptorProxy(annotationMap.getClass("proxyStrategy"));
strategyScanner.registerFilters();
strategyScanner.scan(packageName);
}
}
可以看出,该类会读取启动类上的enable注解,拿到定义的扫描包名和制定的拦截class、需要过滤的bean的注解,扩展性还是不错的。(这里吸取了mybatis与spring继承时的原理)
最后构造了一个ClassPathStrategyScanner扫描器对象,执行scan扫描,后续所有的bean注册逻辑都在scan中。
扫描类 ClassPathStrategyScanner:
/**
* @description: 策略扫描器
* @author: xjr
* @create: 2020-05-15 15:07
**/
@Setter
@Getter
@Slf4j
public class ClassPathStrategyScanner extends ClassPathBeanDefinitionScanner {
private Class<? extends Annotation> annotationClass;
private Class<? extends EventStrategyProxyParent> interceptorProxy;
public ClassPathStrategyScanner(BeanDefinitionRegistry registry) {
super(registry, false);
}
/**
*
* 自定义扫描策略
* @author xiejiarong
* @date 2020年05月16日 10:47
* @throws
*/
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitionHolders=super.doScan(basePackages);
if (beanDefinitionHolders.isEmpty()) {
log.warn("No straggly bean was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
this.processBeanDefinitions(beanDefinitionHolders);
}
return beanDefinitionHolders;
}
/**
*
* 自定义扫描策略bean的注解
* @author xiejiarong
* @date 2020年05月15日 15:41
*/
public void registerFilters() {
if (this.annotationClass != null) {
this.addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
}
}
/**
*
* @description 动态注册代理bean或者普通bean(懒加载方式)
* @param beanDefinitions:待注册的bean
* @author xiejiarong
* @date 2020年05月15日 15:57
*/
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions){
beanDefinitions.forEach(data->{
GenericBeanDefinition definition= (GenericBeanDefinition) data.getBeanDefinition();
String beanClassName=definition.getBeanClassName();
Strategy strategy= null;
Class strategyClass= null ;
try {
strategyClass = Class.forName(beanClassName);
strategy = (Strategy) strategyClass.getAnnotation(Strategy.class);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if (strategy.isIntercept()){
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
definition.getConstructorArgumentValues().addGenericArgumentValue(interceptorProxy.getName());
definition.getConstructorArgumentValues().addGenericArgumentValue(strategy.value());
definition.setBeanClass(EventStrategyFactoryBean.class);
}
definition.setAutowireMode(2);
definition.setLazyInit(false);
this.getRegistry().registerBeanDefinition(strategy.value(),definition);
});
}
}
该类持有两个引用类型分别指向需要扫描的类的注解类型、需要生成代理的拦截策略类型,重写了父类的构造,接收一个BeanDefinitionRegistry注册器并且不使用spring默认扫描策略(扫描带有@component注解的类);重写了父类的doscan方法,使用enable注解传递进来的bean的注解class去扫描;processBeanDefinitions方法接受一个Set参数,set容器中存放了BeanDefinitionHolder类型对象(对于BeanDefinitionHolder不清楚的同学可以看一下spring相关的文章),该set参数是父类scan方法扫描获取得到的最终的所有符合我们要求的class,然后根据拿到的beanDefinition,循环遍历,判断bean上的注解的isIntercept值是否为true,为false则不使用代理模式创建。为true则拿到该bean的ID和className,连同enable上传递过来的拦截策略Class一同用有参构造参数方式传入我们后面实现的FactoryBean的实现类。(因为spring不允许构造函数参数为Class类型,所以这边使用字符串的方式)
生成代理的bean:EventStrategyFactoryBean
/**
* @program: fb-ovr-ts
* @description: 事件策略工厂bean
* @author: xjr
* @create: 2020-05-15 15:50
**/
public class EventStrategyFactoryBean implements FactoryBean<EventStrategy>, ApplicationContextAware, InitializingBean {
private Class<EventStrategy> strategyBeanClass;
private Class<? extends EventStrategyProxyParent> proxyClass;
private ApplicationContext applicationContext;
private String beanName;
/**
*
* jdk动态代理
* @author xiejiarong
* @date 2020年05月16日 10:47
* @throws Exception
*/
@Override
public EventStrategy getObject() throws Exception {
return getProxyBean();
}
@Override
public Class<?> getObjectType() {
return strategyBeanClass;
}
@Override
public boolean isSingleton() {
return true;
}
/**
*
* jdk动态代理
* @author xiejiarong
* @param beanName 要实例化的bean名字
* @param proxyClass 需要被代理的对象Class
* @param strategyBeanClass 自定义的代理拦截策略Class
* @date 2020年05月16日 10:47
* @throws Exception
*/
public EventStrategyFactoryBean(Class<EventStrategy> strategyBeanClass,Class<? extends EventStrategyProxyParent> proxyClass,String beanName){
this.strategyBeanClass=strategyBeanClass;
this.proxyClass=proxyClass;
this.beanName=beanName;
}
/**
*
* jdk动态代理
* @author xiejiarong
* @date 2020年05月16日 10:47
* @throws Exception
*/
public EventStrategy getProxyBean() throws IllegalAccessException, InstantiationException {
AutowireCapableBeanFactory autowireCapableBeanFactory=this.applicationContext.getAutowireCapableBeanFactory();
EventStrategy eventStrategy=autowireCapableBeanFactory.createBean(strategyBeanClass);
EventStrategyProxyParent proxy=proxyClass.newInstance();
proxy.setEventStrategy(eventStrategy);
proxy.setBeanName(this.beanName);
proxy.init();
return (EventStrategy) Proxy.newProxyInstance(this.getClass().getClassLoader(),new Class[]{EventStrategy.class},proxy);
}
/**
*
* factoryBean初始化之前获得spring上下文
* @author xiejiarong
* @date 2020年05月16日 10:47
* @throws Exception
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
/**
*
* factoryBean初始化之后设置父拦截抽象类上下文
* @author xiejiarong
* @date 2020年05月16日 10:47
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
EventStrategyProxyParent.applicationContext=this.applicationContext;
}
}
EventStrategyFactoryBean实现了三个接口,分别为FactoryBean、ApplicationContextAware、InitializingBean,这三个为spring框架为我们提供的三个扩展性接口,一般第三方依赖要与spring做继承都或多或少会使用到。 实现了ApplicationContextAware的类会在spring实例化完该bean的时候,传入applicationContext上下文; 实现了InitializingBean的bean则是在初始化的完毕的时候调用,这边我使用它将实例化完之后获取到的上下文引用赋值给了后面的父拦截代理类的静态属性;这边重点要理解FactoryBean。
FactoryBean和BeanFactory看似很像,实则不同。 以factory结尾的标识这个是bean工厂,用来管理所有的bean对象的:而以bean结尾的表示这个是工厂bean,它是一个bean,拥有产出其他bean的能力;一般像mybatis这种框架在与spring做集成的时候,大量使用了该接口,这也就是为什么mybatis中只需要一个接口没有实现,但是却可以注入到spring中,实现xml文件中的sql调用。
FactoryBean首先是一个bean,它本身会被spring注册,但是在geBean的时候,如果你定义的bean名字是A,那么实际获取出来的不是A的示例,而是你实现的FactoryBean中的getObject方法返回的类型,如果需要取出实际的A对象,则需要在geBean的时候,在bean的名字前面加上&;
仔细观察EventStrategyFactoryBean的时候,你会发现,我这边getObject返回的是我们的顶层接口EventStrategy的代理类,代理类必须继承我们定义的EventStrategyProxyParent,该类实际实现了InvocationHandler,拥有jdk动态代理能力,后面会讲。代理会持有我们的实际的bean的对象引用,在具体调用的时候加入前置和后置的方式实现拦截,但是我们的bean这边是通过传进来的Class通过反射的方式实例化的,在前面我们的父抽象类中标注了@autowired的字段并不会注入进来(因为这边不是通过spring来创建的),所以我这里用到了AutowireCapableBeanFactory这个接口。AutowireCapableBeanFactory是spring中一个非常重要的接口,它实现了beanFactory,它最大的功能可以让不是spring的bean同样拥有spring的依赖注入功能。
父代理:
/**
* @program: fb-ovr-ts
* @description: 事件动态代理父抽象类
* @author: xjr
* @create: 2020-05-16 13:44
**/
@Getter
@Setter
@Slf4j
public abstract class EventStrategyProxyParent implements InvocationHandler {
public static volatile Map<String,String> operateMap=new ConcurrentHashMap<>(2);
public static volatile ApplicationContext applicationContext;
protected EventStrategy eventStrategy;
protected String beanName;
public static final String OPERATOR_SOURCE="";
/**
*
* 代理类公共代理方法(拦截接口方法proceed)
* @author xiejiarong
* @date 2020年05月16日 10:47
* @throws BizException
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("proceed")){
before(method,args);
Object result=null;
try {
result =method.invoke(eventStrategy,args);
}catch (InvocationTargetException e){
log.info("异常是:",e.getTargetException());
throw new BizException(e.getTargetException().getMessage());
}
after(method,args);
return result;
}else{
return method.invoke(eventStrategy,args);
}
}
/**
*
* 代理类组装组件
* @author xiejiarong
* @date 2020年05月16日 10:47
*/
protected void makeUpComponents(){
((AbstractEventHandler)this.eventStrategy).setHandlerAction(ActionEnum.getByCode(this.beanName));
}
/**
*
* 代理类初始化
* @author xiejiarong
* @date 2020年05月16日 10:47
* @throws BizException
*/
public final void init(){
makeUpComponents();
}
/**
*
* 获取当前请求上下文的token
* @author xiejiarong
* @date 2020年05月16日 10:47
* @throws BizException
*/
public static String getCurrentToken(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getHeader("Authorization");
}
/**
*
* 前置操作(留待扩展)
* @author xiejiarong
* @date 2020年05月16日 10:47
* @throws BizException
*/
protected abstract void before(Method method,Object ... args) throws BizException;
/**
*
* 后置操作(留待扩展)
* @author xiejiarong
* @date 2020年05月16日 10:47
* @throws BizException
*/
protected abstract void after(Method method,Object ... args) throws BizException;
}
该类核心方法invoke,定义了只拦截proceed方法,也就是我们一开始定义的统一入口方法。这边有一个注意的地方就是,在代理接口的时候,被代理的对象内部抛出的异常,会被InvocationTargetException吃掉,因为代理是利用方法的反射Invoke执行的额,而invoke方法只会捕捉IllegalAccessException,IllegalArgumentException,InvocationTargetException三种异常,当抛出的异常不满足前两种的时候,默认都会被第三个异常所消化掉,会导致我们自定义的异常没有办法被全局异常捕获,客户端会不知道我们异常的具体信息,所以需要在代理执行的时候使用try catch手动捕获异常再抛出。该类保留了before和after等前置后置拦截抽象方法,为了之后的扩展留有很大的余地,可以理解为编程式AOP。
具体代理拦截实现:EventStrategyProxy
/**
* @description: 事件策略默认代理
* @author: xjr
* @create: 2020-05-15 15:54
**/
@Getter
@Setter
@Slf4j
public class EventStrategyProxy extends EventStrategyProxyParent {
private String token;
private String rscUnitCode;
/**
*
* 前置操作 当前策略为校验上次请求token为本次是否一致,不一致择抛出异常,强制客户端刷新
* @author xiejiarong
* @date 2020年05月16日 10:47
* @throws BizException
*/
@Override
protected void before(Method method,Object ... args) throws BizException {
List<BizUnitAction> bizUnitActions= (List<BizUnitAction>) args[0];
if (CollectionUtils.isEmpty(bizUnitActions)){
throw new BizException("未传递事件");
}
EventStrategy.checkAction(bizUnitActions);
this.token=getCurrentToken();
this.rscUnitCode=bizUnitActions.get(0).getRscUnitCode();
// 赛事单元编码作为key,值为上次操作者的token
ifModifiedOrNot(rscUnitCode);
}
/**
*
* 后置操作(当前策略为每次处理完请求之后把当前token保存到全局map中)
* @author xiejiarong
* @date 2020年05月16日 10:47
* @throws BizException
*/
@Override
protected void after(Method method,Object ... args) throws BizException {
operateMap.put(this.rscUnitCode,this.token);
}
public static void ifModifiedOrNot(String rscUnitCode) throws BizException{
String token=EventStrategyProxyParent.getCurrentToken();
if (operateMap.containsKey(rscUnitCode)){
if (!token.equals(operateMap.get(rscUnitCode))){
throw new BizException(409,"数据已经修改,请刷新后重试");
}
}
}
}
该实现十分简单,pc端和IOS端在操作事件的时候,都会先经过代理拦截,代理会根据当前请求的赛事单元编码去判断父类中的map中是否存在此key,不存在则直接放行,存在则继续判断当前请求头token和map key对应的value是不是此token,不是就抛出异常,前端监听409状态码,然后自动去刷新页面。
万事俱备,开始我们的扩展实现
这边我只选择进球和黄牌事件扩展,分别创建两个AbstractEventHandler的实现:
/**
* @description: 进球事件录入
* @author: xjr
* @create: 2020-05-09 14:32
**/
@Strategy("GOAL")
@Slf4j
public class GoalEventHandler extends AbstractEventHandler {
@Override
public void collect(List<BizUnitAction> events) throws BizException {
///由于公司保密,此处省略一堆代码
}
@Override
public void delete(List<BizUnitAction> events) throws BizException {
///由于公司保密,此处省略一堆代码
}
}
/**
* @description: 黄牌采集事件处理器
* @author: xjr
* @create: 2020-05-11 14:20
**/
@Strategy("YC")
public class YcEventHandler extends AbstractEventHandler {
@Override
public void collect(List<BizUnitAction> events) throws BizException {
///由于公司保密,此处省略一堆代码
}
}
@Override
public void delete(List<BizUnitAction> events) throws BizException {
///由于公司保密,此处省略一堆代码
}
}
可以看到,我们的实现逻辑比较简单,就是不同的事件的录入更新删除事件,如果有额外的逻辑处理则在各自的类中实现。
都封装好之后,此时controller只需要提供一个接口:
/**
* @description: ///由于公司保密,此处省略具体名字
* @author: xjr
* @create: 2020-05-09 15:51
**/
@RestController
@RequestMapping("/pad/unit/event")
@Api(tags = "///由于公司保密,此处省略具体名字")
@Slf4j
public class UnitEventController implements ApplicationContextAware {
private final BizUnitAthleteManager bizUnitAthleteManager;
private final BizUnitResultManager bizUnitResultManager;
private final PadCommonService padCommonService;
private ApplicationContext applicationContext;
public UnitEventController(BizUnitAthleteManager bizUnitAthleteManager, BizUnitResultManager bizUnitResultManager, PadCommonService padCommonService) {
this.bizUnitAthleteManager = bizUnitAthleteManager;
this.bizUnitResultManager = bizUnitResultManager;
this.padCommonService = padCommonService;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
@PostMapping("/{action}/{policy}")
@ApiOperation(value = "事件采集统一入口",notes = "///由于公司保密,此处省略具体名字)
@ApiImplicitParams({
@ApiImplicitParam(name = "action",value = "事件枚举(放在请求路径中)特别注意的是,换人战术是InOut",required = true,dataType = "String",paramType = "path"),
@ApiImplicitParam(name = "policy",value = "事件策略枚举(放在请求路径中) 参考值:DELETE 删除 COLLECT 录入 UPDATE 更新",required = true,dataType = "String",paramType = "path"),
@ApiImplicitParam(name = "actions",value = "具体事件数组(除换人之外,都为单条数据的数组)",required = true,dataTypeClass = List.class,paramType = "body")
})
public ResultVO event(@RequestBody List<BizUnitAction> actions ,@PathVariable String action,@PathVariable String policy) throws BizException {
EventStrategy eventStrategy= (EventStrategy) this.applicationContext.getBean(action);
EventStrategyEnum strategyEnum= Arrays.asList(EventStrategyEnum.values()).stream()
.filter(data->data.getCode().equals(policy))
.findFirst()
.orElseThrow(()->new BizException("找不到对应的策略执行器,请确认policy参数是否合法"));
eventStrategy.proceed(actions, strategyEnum);
return ResultVO.success("事件"+strategyEnum.getValue()+"成功");
}
}
可以看到,除了常规的参数校验,使用@PathVariable 获取reset接口路径参数,约定policy为增删改查行为策略,action为具体的事件名称,参数统一使用list接收,controller通过实现ApplicationContectAware接口方式取得上下文对象,接收客户端参数的同时动态的获取bean工厂中的bean代理,同时根据policy名取得对应的行为枚举,入口还是proceed。
最终我们的启动类:`
@EnableAsync
@EnableSwagger
@EnableCaching
@SpringBootApplication
@ComponentScan("包名忽略")
@MapperScan("包名忽略")
@EnableEventStrategy("包名忽略")//公司保密
public class CmsApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(CmsApplication.class);
app.run(args);
}
}
让我们启动启动项目,打断点试一下:
成功进入断点,继续往下:
成功扫描了带有@Strategy注解的bean。接下来我们模拟一次进球事件请求:
此时发现,第一次请求的时候获取bean的时候才进入factoryBean的getObject方法,也就是说实现了懒加载的方式。
最终get出来的bean,注入了抽象父类当中的spring组件,并且可以从断点中明确的知道这是一个代理类。
继续往下执行,最终来到了我们父代理类,可以看到此时代理类起到了效果:
具体的代理拦截前置实现:
试着将我们的而启动类注解注释:
重新启动项目,同样的请求:
可以看到此时找不到对应的时间处理,也就是找不到我们对应的扩展事件实现,这便起到一个开关的功能。
如果此时,客户突然改变需求,不需要作校验,只是在每次请求之前我都需要记录一条日志,那么我们只需要创建一个类去实现EventStrategyProxyParent,并且在启动类注解中指定我们的拦截策略:
/**
* @description:
* @author: xjr
* @create: 2020-05-21 21:24
**/
@Getter
@Setter
@Slf4j
public class EventStrategyLogProxy extends EventStrategyProxyParent {
@Override
protected void before(Method method, Object... args) throws BizException {
//此处模拟日志操作
log.info("我是前置动作,现在我要记录日志.");
}
@Override
protected void after(Method method, Object... args) throws BizException {
}
}
@EnableAsync
@EnableSwagger
@EnableCaching
@SpringBootApplication
@ComponentScan("包名忽略")
@MapperScan("包名忽略")
@EnableEventStrategy(value = "包名忽略",proxyStrategy = EventStrategyLogProxy.class)
public class CmsApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(CmsApplication.class);
app.run(args);
}
}
重新启动项目,再以同样的请求:
此时拦截策略自动切换成日志打印,而之前的权限校验丝毫没有改动,我们仅仅只是增加了一个策略类,并且指定它作为拦截策略,这跟开闭原则提及的新的功能需求应该在不改动原有业务情况之下实现拓展完全吻合。
这次实验到这边就结束了,根据这次的项目经历,对于spring和设计模式的结合使用有了更为深刻的认识,假设使用传统的多接口方式,势必会导出存在各种复制粘贴式的代码,一旦项目结构巨大,便使维护更加困难,对于之前的面试中不断提到的底层原理的重要性更是不言而喻,所以有时候我们不提倡重复造轮子,可是你起码得知道轮子的结构,多学习一些原理和实践,对于自己的技术和工作效率肯定会起到关键性的作用。