需求背景

最近公司项目提出一个需求,我们的项目原本使用dubbo作为RPC框架,现需改为springcloud+eruka方式,并能保证多种服务框架能灵活切换。以下是我结合多方资料,写下的实现办法。

实现思路

熟悉springcloud的朋友应该知道,springcloud推荐使用restful风格的http协议,在这方面springMVC提供了非常方便的支持,我们只需要实现带有@Controller注解的类,并注入到web容器中即可,又因为springcloud是支持自动装配的,所以主要任务就成了构建@Controller控制器,在控制器中调用业务逻辑代码。

其次,现有代码是基于dubbo方式实现的,为了尽可能少的改动代码,我们将使用动态生成控制器类,注入到spring容器。

详细代码

spring boot quartz 动态任务 springboot动态controller_spring

第一步、使用Javassist动态生成控制器

生成控制器类简单地说就是生成的类上带有 @Controller、@RequestMapping注解,映射的方法上添加@RequestMapping、@ResponseBody注解,方法参数按需添加注解(为了接收Content-Type=application/json的请求,我在参数上使用了@RequestBody注解),具体代码如下:

public class JavassistProxy implements Proxy {
	protected final static Logger LOG = LoggerFactory.getLogger(JavassistProxy.class);

	private static ClassPool classPool = ClassPool.getDefault();

	static {
		ClassClassPath path = new ClassClassPath(JavassistProxy.class);
		classPool.insertClassPath(path);
	}

	@Override
	public Class<?> createProxy(Class<?> interfaceClass) throws TecException {
		CtClass controllerClass = classPool.makeClass("com.xxx.xxx." + interfaceClass.getSimpleName() + "Controller");
		ClassFile ccFile = controllerClass.getClassFile();
		ConstPool constpool = ccFile.getConstPool();
		// 类上添加@RestController注解
		AnnotationsAttribute classAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
		Annotation controller = new Annotation("org.springframework.web.bind.annotation.RestController", constpool);
		classAttr.addAnnotation(controller);
		// 类上添加@RequestMapping注解
		Annotation requestMapping = new Annotation("org.springframework.web.bind.annotation.RequestMapping", constpool);
		//Mapping路径(使用代理接口的全限定名)
		ArrayMemberValue memberValue = new ArrayMemberValue(constpool);
		memberValue.setValue(new StringMemberValue[] { new StringMemberValue("/" + interfaceClass.getName(), constpool) });
		requestMapping.addMemberValue("path", memberValue);
		classAttr.addAnnotation(requestMapping);
		ccFile.addAttribute(classAttr);
		try {
		    // 添加成员变量:调度器
			controllerClass.addField(makeDispatcherField(controllerClass, constpool));
			// 添加方法
			CtMethod[] methods = makeRequestMapping(interfaceClass, controllerClass, constpool);
			for (CtMethod m : methods) {
				controllerClass.addMethod(m);
			}
			return controllerClass.toClass();
		} catch (CannotCompileException e) {
			LOG.error("create Controller:[{}] failed", controllerClass.getName(), e);
			throw TecException.SYS_APPERR("create Controller:" + controllerClass.getName() + " failed.", e);
		}
	}

	private CtField makeDispatcherField(CtClass declaring, ConstPool constpool) throws TecException {
		try {
			CtField ctField = new CtField(classPool.get(CoreTrnDispatcher.class.getName()), "coreTrnDispatcher", declaring);
			ctField.setModifiers(Modifier.PRIVATE);
			FieldInfo fieldInfo = ctField.getFieldInfo();
			// 成员变量添加@Autowired注解
			AnnotationsAttribute fieldAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
			Annotation autowired = new Annotation("org.springframework.beans.factory.annotation.Autowired", constpool);
			fieldAttr.addAnnotation(autowired);
			fieldInfo.addAttribute(fieldAttr);
			return ctField;
		} catch (CannotCompileException | NotFoundException e) {
			LOG.error("make dispatcher field failed.", e);
			throw TecException.SYS_APPERR("make dispatcher field failed.", e);
		}
	}

	private CtMethod[] makeRequestMapping(Class<?> interfaceClass, CtClass declaring, ConstPool constpool) {
		try {
			CtClass parentCtClass = classPool.get(interfaceClass.getName());
			CtMethod[] declaredMethods = parentCtClass.getDeclaredMethods();
			CtMethod[] methods = new CtMethod[declaredMethods.length];
			for (int i = 0; i < declaredMethods.length; i++) {
				// 生成方法体
				CtMethod method = generatedMetaMethod(declaredMethods[i], declaring);
				// 方法上添加注解
				MethodInfo info = method.getMethodInfo();
				AnnotationsAttribute methodAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
				// 添加 @RequestMapping注解
				Annotation requestMapping = new Annotation("org.springframework.web.bind.annotation.RequestMapping", constpool);
				//Mapping路径(使用方法名)
				ArrayMemberValue pathValue = new ArrayMemberValue(constpool);
				pathValue.setValue(new StringMemberValue[] { new StringMemberValue("/" + declaredMethods[i].getName(), constpool) });
				requestMapping.addMemberValue("path", pathValue);
				methodAttr.addAnnotation(requestMapping);
				// 添加@ResponseBody注解
				Annotation responseBody = new Annotation("org.springframework.web.bind.annotation.ResponseBody", constpool);
				methodAttr.addAnnotation(responseBody);
				//参数上注解@RequestBody
				Annotation requestBody = new Annotation("org.springframework.web.bind.annotation.RequestBody", constpool);
				ParameterAnnotationsAttribute parameterAnnotationsAttribute = new ParameterAnnotationsAttribute(constpool,ParameterAnnotationsAttribute.visibleTag);
				javassist.bytecode.annotation.Annotation[][] anno = new javassist.bytecode.annotation.Annotation[][] {{requestBody}};
				parameterAnnotationsAttribute.setAnnotations(anno);
				
				info.addAttribute(methodAttr);
				info.addAttribute(parameterAnnotationsAttribute);
				methods[i] = method;
			}
			return methods;
		} catch (CannotCompileException | NotFoundException e) {
			LOG.error("create request mapping failed for interface:[{}]", interfaceClass.getName(), e);
			throw TecException.SYS_APPERR("create request mapping failed for interface:" + interfaceClass.getName(), e);
		}
	}

	public CtMethod generatedMetaMethod(CtMethod ctMethod, CtClass declaring) throws NotFoundException, CannotCompileException {
		CtMethod method = new CtMethod(ctMethod.getReturnType(), ctMethod.getName(), ctMethod.getParameterTypes(), declaring);
		StringBuilder builder = new StringBuilder();
		builder.append("{\n");
		builder.append("com.xxx.xxx.Context respContext = null;\n");
		builder.append("try{\n");
		builder.append("com.xxx.xxx.Context reqContext = com.xxx.xxx.Context.createContext($1);\n");
		builder.append("respContext = coreTrnDispatcher.execute(reqContext);\n");
		builder.append("}").append("catch(Throwable t){\n");
		builder.append("throw new RuntimeException(t);").append("}\n");
		builder.append("return ($r)com.xxx.xxx.BeanUtil.Map2Bean(respContext,").append(ctMethod.getReturnType().getName())
		        .append(".class);\n}");
		method.setBody(builder.toString());
		return method;
	}
}

注:在参数注解这里,由于方法参数可有多个,每个参数又可以有多个注解,所以注解定义是个二维数组,第一维表示第几个参数,第二维表示该参数的第几个注解。由于我的接口需求上只有一个入参,也只需要在这个参数上添加@RequestBody,所以简单写成以下形式

javassist.bytecode.annotation.Annotation[][] anno = new javassist.bytecode.annotation.Annotation[][] {{requestBody}};

又注:CoreTrnDispatcher、com.xxx.xxx.Context、TecException、com.xxx.xxx.BeanUtil.Map2Bean()是公司内部基础包类,仿照这段代码的如果编译ClassNotFoundException错误请更改为自己的实现用法。

第二步 使用工厂类调用代理生成方法

这一步比较鸡肋,只是为了预留多种动态代理的实现方式,当前我只实现了javassist一种,本步骤跟本文主旨关系不大,可以直接跳过。

public class ControllerRegisterFactory {
	protected final static Logger LOG = LoggerFactory.getLogger(ControllerRegisterFactory.class);

	public static Class<?> regist(Class<?> interfaceClass) throws TecException {
		String proxyType = System.getProperty("com.xxx.xxx.springmvc.proxy", "javassist");
		Proxy proxy = null;
		switch (proxyType) {
			case "javassist":
				proxy = new JavassistProxy();
				break;
			case "cglib":
				break;
			case "jdk":
				break;
			default:
				proxy = new JavassistProxy();
				break;
		}
		return proxy.createProxy(interfaceClass);
	}

}

第三步 注入控制器

本文使用xml自定义标签的方式注入控制器(如何使用自定义标签注入Bean请读者另搜博文)
springmvc.xsd限定文件

<?xml version="1.0" encoding="UTF-8"?>
<schema targetNamespace="http://www.xxx.xxx.com/xxx/springmvc"
	xmlns="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://www.xxx.xxx.com/xxx/springmvc"
	xmlns:spring="http://www.springframework.org/schema/beans">
	
	<import namespace="http://www.springframework.org/schema/beans" />
	<import namespace="http://www.springframework.org/schema/tool" />
	
    <element name="controller" type="tns:ServiceType"></element>
    
    <complexType name="ServiceType">
    	<attribute name="interface" type="string" use="required"></attribute>
    	<attribute name="ref" type="string" ></attribute>
    	<attribute name="version" type="string"></attribute>
    </complexType>

</schema>

注:本文暂只用到interface属性,另外两个属性ref和version用不上,可以删除

ContextNamespaceHandler继承NamespaceHandlerSupport类,用于解析自定义标签

public class ContextNamespaceHandler extends NamespaceHandlerSupport {

	@Override
	public void init() {
		registerBeanDefinitionParser("controller", new ControllerBeanDefinitionParser());
	}
}

ControllerBeanDefinitionParser实现BeanDefinitionParser接口,用于注入Bean。通过root.setBeanClass(ControllerRegisterFactory.regist(c))将动态生成的Controller类设置为Bean,然后注入到Spring容器。

public class ControllerBeanDefinitionParser implements BeanDefinitionParser {

	private final static String INTERFACE_ATTRIBUTE = "interface";

	@Override
	public BeanDefinition parse(Element element, ParserContext parserContext) {
		RootBeanDefinition root = new RootBeanDefinition();
		//获取interface属性
		String interfaceName = element.getAttribute(INTERFACE_ATTRIBUTE);
		try {
			Class c = Class.forName(interfaceName);
			root.setBeanClass(ControllerRegisterFactory.regist(c));
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		//注册ControllerBean
		parserContext.getRegistry().registerBeanDefinition(interfaceName, root);
		return root;
	}

}

总结

本文是博主为了实现根据服务接口动态生成并注入Web控制器而写的详细代码说明,主要目的是为了表述如何动态生成springMVC的控制器类,以及如何将该类注入到Spring容器中。本文代码依赖的一些jar包及版本:spring-webmvc:4.3.23.RELEASE、javassist:3.24.1-GA。
以上即为所有内容,希望对读到本篇文章的读者有所帮助。