需求背景
最近公司项目提出一个需求,我们的项目原本使用dubbo作为RPC框架,现需改为springcloud+eruka方式,并能保证多种服务框架能灵活切换。以下是我结合多方资料,写下的实现办法。
实现思路
熟悉springcloud的朋友应该知道,springcloud推荐使用restful风格的http协议,在这方面springMVC提供了非常方便的支持,我们只需要实现带有@Controller注解的类,并注入到web容器中即可,又因为springcloud是支持自动装配的,所以主要任务就成了构建@Controller控制器,在控制器中调用业务逻辑代码。
其次,现有代码是基于dubbo方式实现的,为了尽可能少的改动代码,我们将使用动态生成控制器类,注入到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。
以上即为所有内容,希望对读到本篇文章的读者有所帮助。