背景
在最近以流程业务为主的开发过程中,由于功能上线后发生业务变更,遇到了多版本控制问题。业务方要求新功能上线后,尚未完结的流程需按照旧版的功能继续流转,新发起的流程按照新版的功能流转。
Springboot版本约定
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.13.RELEASE</version>
</dependency>
</dependencies>
功能实现
最初的实现
当拿到这个需求后,我很快就动手了,这也就是加个if else的事儿,如果还要加新的版本,就在后面增加更多的 else if,那不要太简单:
/**
* 流程第1环节提交:多版本
*/
public void submit1(SubmitData submitData) {
String version = submitData.getVersion();
if ("v1".equals(version)) {
// v1版本的业务逻辑...
} else {
// v2版本的业务逻辑...
}
}
/**
* 流程第2环节提交:多版本
*/
public void submit2(SubmitData submitData) {
String version = submitData.getVersion();
if ("v1".equals(version)) {
// v1版本的业务逻辑...
} else {
// v2版本的业务逻辑...
}
}
写完提交收工。此时,开发同事小帅code review一看,你这代码写得太丑陋了,所有版本的逻辑写在一个方法里,后面再有需求变更只会越写越长,可读性差,不符合单一职责的原则。
我这么一听,他说的确实有点道理,不过这可难不倒我。此处调用相同的方法,但不同版本对应不同的业务逻辑,可以套用策略模式来进行改造。
升级版的实现
先定义一个顶级的接口类:FlowService
public interface FlowService {
/**
* 流程第1环节提交:多版本
*/
void submit1(SubmitData submitData);
/**
* 流程第2环节提交:多版本
*/
void submit2(SubmitData submitData);
}
再分别写出两个版本的实现类:FlowServiceImplV1
,FlowServiceImplV2
@Service
public class FlowServiceImplV1 implements FlowService {
/**
* 流程第1环节提交:多版本
*/
@Override
public void submit1(SubmitData submitData) {
// v1版本的业务逻辑...
}
/**
* 流程第2环节提交:多版本
*/
@Override
public void submit2(SubmitData submitData) {
// v1版本的业务逻辑...
}
}
///
@Service
public class FlowServiceImplV2 implements FlowService {
/**
* 流程第1环节提交:多版本
*/
@Override
public void submit1(SubmitData submitData) {
// v2版本的业务逻辑...
}
/**
* 流程第2环节提交:多版本
*/
@Override
public void submit2(SubmitData submitData) {
// v2版本的业务逻辑...
}
}
再写一个门面类做统一调用入口和不同版本的调用转发:FlowFacadeService
@Primary
@Service
public class FlowFacadeService implements FlowService {
@Resource
private ApplicationContext applicationContext;
/**
* 流程第1环节提交:多版本
*/
@Override
public void submit1(SubmitData submitData) {
String version = submitData.getVersion();
getServiceByVersion(version).submit1(submitData);
}
/**
* 流程第2环节提交:多版本
*/
@Override
public void submit2(SubmitData submitData) {
String version = submitData.getVersion();
getServiceByVersion(version).submit2(submitData);
}
/**
* 获取对应版本逻辑的service
*/
private void FlowService getServiceByVersion(String version) {
if ("v1".equals(version)) {
return applicationContext.getBean(FlowServiceImplV1.class);
} else {
return applicationContext.getBean(FlowServiceImplV2.class);
}
// 如有新增的版本逻辑,可以在此处做扩展
}
}
大功告成,写完之后,我看着这个漂亮的多版本逻辑分离代码,自信满满地提交了上去,关上电脑,心满意足地下班回家。第二天早上刚过来,看到开发同事小帅正好在code review这段代码,我便得意地凑了过去:
"怎么样,这段代码写得挺吊的吧?"
"嗯,比昨天的是好多了,但是..."
"但是什么?你别搞事,这还不行?"
"但是实现起来有点麻烦,假如以后我也要做类似需求的开发,还都要写这么多的话,那太烦了,而且每次迭代,还要记得去维护这个门面类"
"来来来,键盘给你,你来写!"
"你先别急,有没有一种可能,当我注入顶级的FlowService之后,在不写门面类的情况下,自动识别需要使用哪个版本的service?"
"你的这个想法确实很不错,但我觉得你在想屁吃"
"你可以参考Spring里HttpServletRequest的那种注入方式呀"
"你这么一说我好像有点思路了"
基于ObjectFactory实现的多版本逻辑控制的实现
要想实现同事小帅说的效果,我们有必要研究一下HttpServletRequest
的实现方式。可能有不少同学在获取HttpServletRequest
对象时,都使用过以下这种注入方式。同时你可能也已经注意到了,这里的request对象在每次发起http请求时获取到的都是不同的
@Resource
private HttpServletRequest request;
那么,上面的HttpServletRequest
对象是通过什么方式来实现这种动态注入的呢?
我们借助IDEA打断点可以发现,这里注入的对象并不是HttpServletRequest
,
而是AutowireUtils$ObjectFactoryDelegatingInvocationHandler
,
它里面有一个成员变量WebApplicationContextUtils$RequestObjectFactory
,如下所示:
我们在Spring源码中追踪一下RequestObjectFactory
,可以发现它在WebApplicationContextUtils
中进行了注册,此处用到的beanFactory.registerResolvableDependency()
方法的作用是,使得需要注入ServletRequest
类型bean的地方,都使用RequestObjectFactory
的代理对象,如下:
beanFactory.registerResolvableDependency(ServletRequest.class, new RequestObjectFactory());
beanFactory.registerResolvableDependency(ServletResponse.class, new ResponseObjectFactory());
beanFactory.registerResolvableDependency(HttpSession.class, new SessionObjectFactory());
beanFactory.registerResolvableDependency(WebRequest.class, new WebRequestObjectFactory());
再来看RequestObjectFactory
的源码,它实现了ObjectFactory
类的getObject()
方法,大致逻辑就是从当前的上下文中获取request
对象并返回(此处currentRequestAttributes()
方法的具体内容不作展开,感兴趣的同学可以研究一下源码),如下所示:
private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {
@Override
public ServletRequest getObject() {
return currentRequestAttributes().getRequest();
}
@Override
public String toString() {
return "Current HttpServletRequest";
}
}
这样一来,当我们注入HttpServletRequest
对象的时候,实际注入的是另外一个代理类。只有在我们使用的时候,这个代理类才会调用RequestObjectFactory
的getObject()
方法来提供我们真正需要使用的HttpServletRequest
对象。
AutowireUtils
中的解析注入对象方法:
public static Object resolveAutowiringValue(Object autowiringValue, Class<?> requiredType) {
if (autowiringValue instanceof ObjectFactory && !requiredType.isInstance(autowiringValue)) {
ObjectFactory<?> factory = (ObjectFactory<?>) autowiringValue;
if (autowiringValue instanceof Serializable && requiredType.isInterface()) {
// 此处生成代理对象
autowiringValue = Proxy.newProxyInstance(requiredType.getClassLoader(),
new Class<?>[] {requiredType}, new ObjectFactoryDelegatingInvocationHandler(factory));
}
else {
return factory.getObject();
}
}
return autowiringValue;
}
调用实际对象方法的逻辑由ObjectFactoryDelegatingInvocationHandler
实现
private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable {
private final ObjectFactory<?> objectFactory;
public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
this.objectFactory = objectFactory;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("equals")) {
// 对equals方法做特殊处理,只有同一个代理对象才会认为相等
return (proxy == args[0]);
}
else if (methodName.equals("hashCode")) {
// 对hashCode方法做特殊处理
return System.identityHashCode(proxy);
}
else if (methodName.equals("toString")) {
// 对toString方法做特殊处理,调用所代理的objectFactory的toString方法
return this.objectFactory.toString();
}
try {
// 其他方法调用objectFactory生产出来的对象的方法
return method.invoke(this.objectFactory.getObject(), args);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}
在了解了HttpServletRequest
的动态注入实现方式之后,我们就可以照葫芦画瓢,来对原本的代码进行改造了。
首先,定义一个用于识别版本的基础接口类,也是我们需要代理的顶级接口:
public interface ProxyService {
/**
* 版本是否匹配
*/
boolean isVersionMatch(String version);
}
同时对原本的几个FlowService进行扩展:
public interface FlowService extends ProxyService {
// 原本的方法省略,该方法用作测试
String test();
}
@Service
public class FlowServiceImplV1 implements FlowService {
/**
* 版本是否匹配
*/
@Override
public boolean isVersionMatch(String version) {
return "v1".equals(version);
}
@Override
public String test() {
return "我是:flowService1";
}
// 原本的方法省略
}
///
@Service
public class FlowServiceImplV2 implements FlowService {
/**
* 版本是否匹配
*/
@Override
public boolean isVersionMatch(String version) {
return "v2".equals(version);
}
@Override
public String test() {
return "我是:flowService2";
}
// 原本的方法省略
}
再创建一个管理版本号的上下文类VersionContextHolder
,以方便版本信息的传递和获取:
public class VersionContextHolder {
private final static ThreadLocal<String> THREAD_LOCAL = new NamedInheritableThreadLocal<>("versionContext");
public static void setVersion(String version) {
THREAD_LOCAL.set(version);
}
public static String currentVersion() {
return THREAD_LOCAL.get();
}
public static void clear() {
THREAD_LOCAL.remove();
}
}
这里为了方便演示,我们约定通过参数version来获取流程的版本信息,并通过拦截器来设置和获取:
public class ProxyVersionInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String version = request.getParameter("version");
VersionContextHolder.setVersion(version);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
VersionContextHolder.clear();
}
}
然后是我们的核心类,代理类工厂ProxyServiceBeanFactory
。
注意,根据
AutowireUtils.resolveAutowiringValue()
方法里的解析要求,这里必须实现Serializable
接口,否则无法生成代理对象:
public class ProxyServiceBeanFactory implements ObjectFactory<ProxyService>, Serializable {
private final List<ProxyService> proxyServiceList;
public ProxyServiceBeanFactory(Collection<ProxyService> proxyServiceList) {
this.proxyServiceList = new ArrayList<>(proxyServiceList);
}
@Override
public ProxyService getObject() throws BeansException {
String version = VersionContextHolder.currentVersion();
return proxyServiceList.stream()
// 通过isVersionMatch方法找到匹配的service
.filter(s -> s.isVersionMatch(version))
.findFirst()
.orElseThrow(IllegalArgumentException::new);
}
}
接着再把上面这几个类在配置类中注册一下:
@Configuration
public class ProxyServiceConfig implements BeanFactoryPostProcessor, WebMvcConfigurer {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 获取所有需要代理的服务类
Map<String, ProxyService> beans = beanFactory.getBeansOfType(ProxyService.class);
// 注册代理对象
beanFactory.registerResolvableDependency(ProxyService.class, new ProxyServiceBeanFactory(beans.values()));
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ProxyVersionInterceptor());
}
}
编写测试类TestController
进行测试:
@RestController
public class TestController {
@Resource
private FlowService flowService;
@Resource
private HttpServletRequest request;
@GetMapping("/test")
public void test() {
System.out.println(flowService.test());
}
}
尝试启动项目进行测试,然后就会出现以下报错:
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'com.xxx.demos.service.FlowService' available: expected single matching bean but found 3:
com.sun.proxy.$Proxy54@309028af,
FlowServiceImplV1,
FlowServiceImplV2
???这是为什么呢?
原来,在Spring进行依赖注入的时候,会去检查这个类型的bean在容器是否唯一,如果不是就会抛出这个异常。
if (matchingBeans.size() > 1) {
autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
if (autowiredBeanName == null) {
if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
// 此处会进行bean唯一性的校验
return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
}
else {
// In case of an optional Collection/Map, silently ignore a non-unique case:
// possibly it was meant to be an empty collection of multiple regular beans
// (before 4.3 in particular when we didn't even look for collection beans).
return null;
}
}
instanceCandidate = matchingBeans.get(autowiredBeanName);
}
要想使用具体的实现类就要注入到容器中,可是一但注入,相同类型的bean就会存在多个,导致出现上面的异常,可是不注入又无法使用框架中用到的各种bean,还得做一大堆改造,令人浑身难受。那该怎么办呢?先不要急,我们冷静分析一下,其实我们只要能注入代理类就行,别的实现类不需要注入,那有没有办法屏蔽别的实现类呢?
有!Spring中提供了AutowireCandidateResolver
进行扩展,它的isAutowireCandidate()
方法可以判断当前bean定义是否符合注入的候选集,我们只要把ProxyService
类型的实现类从候选集中剔除就行了,如下:
public class ProxyAutowireCandidateResolver implements AutowireCandidateResolver {
@Override
public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
Class<?> dependencyType = descriptor.getDependencyType();
// ProxyService类型的bean不放入候选集
if (ProxyService.class.isAssignableFrom(dependencyType)) {
return false;
}
return true;
}
}
同时上面这个类也要注册一下,我们在原先的ProxyServiceConfig
基础上改一下就行:
@Configuration
public class ProxyServiceConfig implements BeanFactoryPostProcessor, WebMvcConfigurer {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof DefaultListableBeanFactory) {
DefaultListableBeanFactory bf = (DefaultListableBeanFactory) beanFactory;
// 注册到beanFactory中
bf.setAutowireCandidateResolver(new ProxyAutowireCandidateResolver());
}
Map<String, ProxyService> beans = beanFactory.getBeansOfType(ProxyService.class);
beanFactory.registerResolvableDependency(ProxyService.class, new ProxyServiceBeanFactory(beans.values()));
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ProxyVersionInterceptor());
}
}
再次启动,没有报错,切换version参数进行测试,测试通过:
我是:flowService1
我是:flowService2
我是:flowService1
小结
在本次开发过程中,我们经历了从硬编码到使用策略模式,再到研究动态注入的方式完成了多版本流程开发的改造。新的方式不但隔离了不同版本的业务逻辑,而且对原本代码的侵入性也比较低,改造的工作量也很小。当然,如果只是简单的业务调整,那也没必要进行大刀阔斧地改造,还是要结合使用场景具体问题具体分析。本文中简化了版本信息version的设置和获取方式,读者也可以结合自己的业务进行扩展改造。