背景
接下去我们分析下自适应扩展点也就是代码中所对应的
if (clazz.isAnnotationPresent(Adaptive.class)) {
cacheAdaptiveClass(clazz);
这个Adaptive 注解可以加在类上也可以加在方法上,当然根据权限范围来说类的作用和控制范围大于方法
类自适应扩展点
还是回到我们最初的demo
public static void main(String[] args) {
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Job.class);
Job program = extensionLoader.getExtension("program");
program.play();
}
我们假设一个场景,在分布式场景中,我们要获取某个动态下发的value值,但是注册中心存在多种,我们的代码尝尝会这样写,当然我模拟的场景还是比较简单的,实际应用中还有更复杂的场景
//file:com.poizon.study.provider.spi.ConfigCenter
apollo=com.poizon.study.provider.spi.ApolloConfigCenter
nacos=com.poizon.study.provider.spi.NacosConfigCenter
//SPITest.java
public static void main(String[] args) {
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(ConfigCenter.class);
ConfigCenter configCenter = null;
if (apollo) {
configCenter = extensionLoader.getExtension("apollo");
} else {
configCenter = extensionLoader.getExtension("nacos");
}
configCenter.get("key");
}
好,我们继续升级,将代码抽取成util
public static void main(String[] args) {
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(ConfigCenter.class);
String value = ConfigCenterUtil.get("key", extensionLoader);
}
//ConfigCenterUtil.java
public class ConfigCenterUtil {
private static boolean apollo;
public static String get(String key, ExtensionLoader extensionLoader) {
ConfigCenter configCenter = null;
if (apollo) {
configCenter = extensionLoader.getExtension("apollo");
} else {
configCenter = extensionLoader.getExtension("nacos");
}
return configCenter.get("key");
}
}
我们再升级下,能否这个工具类也注册成为 ConfigCenter 接口的实现类
//file:com.poizon.study.provider.spi.ConfigCenter
apollo=com.poizon.study.provider.spi.ApolloConfigCenter
nacos=com.poizon.study.provider.spi.NacosConfigCenter
util=com.poizon.study.provider.spi.ConfigCenterUtil
public class ConfigCenterUtil implements ConfigCenter{
private static boolean apollo;
@Override
public String get(String key) {
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(ConfigCenter.class);
ConfigCenter configCenter = null;
if (apollo) {
configCenter = extensionLoader.getExtension("apollo");
} else {
configCenter = extensionLoader.getExtension("nacos");
}
return configCenter.get("key");
}
}
public static void main(String[] args) {
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(ConfigCenter.class);
ConfigCenter util = extensionLoader.getExtension("util");
util.get("key");
}
这样一来,我们把获取配置的实现细节全部都屏蔽到了ConfigCenterUtil这个实现类中,上层不需要关系实现,像这个类的作用就可以叫做自适应扩展类,然后在dubbo里面有一种另外的写法,将类中加上@Adaptive 标志注解,ExtensionLoader 也专门提供了获取方法getAdaptiveExtension,并且这个值是就是通过这句代码赋值的,并且一个接口只能一个自适应扩展点,多个会报错,当然也不会被抛出,因为dubbo封装了错误到map,中只有最后找不到实现合适的实现类才会吐出错误栈
if (clazz.isAnnotationPresent(Adaptive.class)) {//☆先跳过后面分析
cacheAdaptiveClass(clazz);
}
private void cacheAdaptiveClass(Class> clazz) {
if (cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException();
}
}
最后我们的版本变为
@Adaptive
public class ConfigCenterUtil implements ConfigCenter{
//.........
public static void main(String[] args) {
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(ConfigCenter.class);
ConfigCenter util = extensionLoader.getAdaptiveExtension();
String key = util.get("key");
System.out.println(key);//打印:nacos key
}
dubbo 框架里面也有类扩展点实现(ExtensionFactory),我会在后面做详细介绍。
方法自适应扩展点
像上面这样实现,的确很复杂,我们要扩展很多的类,是不是都要这样实现,答案肯定不是,我们在举一个场景,登录,现在互联网软件如雨后春笋一样多,登录的方式也不断扩从,我亲身经历了,从手机验证码登录到微信facebook等sns登录,再到后面的手机号本地登录,当然未来还会有更多的登录方式我们的代码如何更好的做扩展?
//com.poizon.study.provider.spi.Login
weixin=com.poizon.study.provider.spi.WeixinLogin
phone=com.poizon.study.provider.spi.PhoneLogin
public static void main(String[] args) {
String loginType = "weixin";
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Login.class);
if (loginType.equals("weixin")) {
extensionLoader.getExtension("weixin").doLogin();
} else if (loginType.equals("phone")) {
extensionLoader.getExtension("phone").doLogin();
}
}
上面的代码很简单,扩展起来也很方便 大不了在加if else;这种方式对于有经验的开发会选择升级为工厂模式的写法,我们试试,在试之前我们介绍下URI(统一资源定位符号),将登陆方式用URI的形式传给工厂,用这种形式来消除分支。
public static void main(String[] args) {
String loginType = "weixin";
URL url = new URL(loginType, null, (Integer) null);
doLogin(url);
}
public static void doLogin(URL url) {
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Login.class);
String protocol = url.getProtocol();
if (StringUtils.isEmpty(protocol)) {
protocol = "hupu";
}
extensionLoader.getExtension(protocol).doLogin();
}
我们观察doLogin 中的代码,如果不设置默认值“hupu”的话,是否可以认为和接口没关系,只是一种查找实现类的规范,没错dubbo 也是就是这种方式来查找实现类,将@Adaptive暴露给用户来设置,我们来看看采用dubbo自适应扩展点方法的写法
@SPI
public interface Login {
@Adaptive("protocol")
boolean doLogin(URL url);
}
public static void main(String[] args) {
String loginType = "weixin";
URL url = new URL(loginType, "8888", 80);
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Login.class);
extensionLoader.getAdaptiveExtension().doLogin(url);
//打印:do weixin login
}
也一样实现了自动选择登陆方式的功能,对比起来少了一个工厂类,那么dubbo是怎么实现的呢,我们深入源码之前先debug看看实例名称,“Login$Adapter” 这个类我们没有定义过,推测实现方式应该是动态代理,一探究竟
顺着 getAdaptiveExtension() 进去
public T getAdaptiveExtension() {
//省略..
instance = createAdaptiveExtension();
return (T) instance;
}
private T createAdaptiveExtension() {
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
}
private Class> getAdaptiveExtensionClass() {
getExtensionClasses();
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
private Class> createAdaptiveExtensionClass() {
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
ClassLoader classLoader = findClassLoader();
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
果然在最后找到了compiler.compile();将code代码编译为java对象,compiler也是扩展点,默认实现为javassist(),可以通过
@SPI("javassist") //默认扩展点javassist
public interface Compiler {
Class> compile(String code, ClassLoader classLoader);
}
Dubbo 也是推荐使用javassist 字节码的方式效率更好,jdk大家可以看看,编译还在1.6的版本
编译的过程就不深入了,我们主要看看这个code 变量的值是怎么生成的。
//org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generate
public String generate() {
StringBuilder code = new StringBuilder();
code.append(generatePackageInfo());
code.append(generateImports());
code.append(generateClassDeclaration());
Method[] methods = type.getMethods();
for (Method method : methods) {
code.append(generateMethod(method));
}
code.append("}");
return code.toString();
}
//org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generateMethod
private String generateMethod(Method method) {
String methodReturnType = method.getReturnType().getCanonicalName();
String methodName = method.getName();
String methodContent = generateMethodContent(method);
String methodArgs = generateMethodArguments(method);
String methodThrows = generateMethodThrows(method);
return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
}
//org....common.extension.AdaptiveClassCodeGenerator#generateMethodContent
private String generateMethodContent(Method method) {
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
StringBuilder code = new StringBuilder(512);
if (adaptiveAnnotation == null) {
return generateUnsupported(method);
} else {
//解释在下面
int urlTypeIndex = getUrlTypeIndex(method);
if (urlTypeIndex != -1) {
code.append(generateUrlNullCheck(urlTypeIndex));
} else {//解释在下面
code.append(generateUrlAssignmentIndirectly(method));
}
//获取@Adaptive("protocol")中的value 没有则用接口名去除驼峰
String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
boolean hasInvocation = hasInvocationArgument(method);
code.append(generateInvocationArgumentNullCheck(method));
//解释在下面
code.append(generateExtNameAssignment(value, hasInvocation));
// 封装报错信息 类似我们代码中写的throw new Exception()这样
code.append(generateExtNameNullCheck(value));
//解释在下面
code.append(generateExtensionAssignment());
// 封装返回
code.append(generateReturnAndInvocation(method));
}
return code.toString();
}
private int getUrlTypeIndex(Method method) {
int urlTypeIndex = -1;
//功能很简单,找到方法参数中是否是URL类型的参数,有返回参数位置索引
//我们回忆下doLogin 中我们把URL 参数最为第一个参数传了进来
Class>[] pts = method.getParameterTypes();
for (int i = 0; i < pts.length; ++i) {
if (pts[i].equals(URL.class)) {
urlTypeIndex = I;
break;
}
}
return urlTypeIndex;
}
//org....common.extension.AdaptiveClassCodeGenerator#generateUrlAssignmentIndirectly
private String generateUrlAssignmentIndirectly(Method method) {
Class>[] pts = method.getParameterTypes();
//如果方法没带URL类型的参数,就遍历参数的getXxx方法看是否有
//这段代码看着还是挺有意思的,和我们写的很逊色
for (int i = 0; i < pts.length; ++i) {
for (Method m : pts[i].getMethods()) {
String name = m.getName();
if ((name.startsWith("get") || name.length() > 3)
&& Modifier.isPublic(m.getModifiers())
&& !Modifier.isStatic(m.getModifiers())
&& m.getParameterTypes().length == 0
&& m.getReturnType() == URL.class) {
return generateGetUrlNullCheck(i, pts[i], name);
}
}
}
}
#这个方法太重要了高亮显示
org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generateExtNameAssignment
private String generateExtNameAssignment(String[] value, boolean hasInvocation) {
//接口中我们传入的是protocol
String getNameCode = null;
for (int i = value.length - 1; i >= 0; --i) {
//defaultExtName 留意下就是我们在@SPI("defaultExtName") 中设置的
if (null != defaultExtName) {
if (!"protocol".equals(value[i])) {
if (hasInvocation) {
getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
} else {
//如果扩展点不是protocol 则通过url.getParameter去获取
//这种写法也是支持的我们在创建对象的时候可以这样去设置
//url.addParameter("loginType", loginType);
//@Adaptive("loginType")
//boolean doLogin(URL url);
getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
}
} else {
//如果key是protocol ,则通过 url.getProtocol() 获取实现类的扩展点名称
getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
}
} else {
if (!"protocol".equals(value[i])) {
if (hasInvocation) {
getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
} else {
getNameCode = String.format("url.getParameter(\"%s\")", value[I]);
}
} else {
getNameCode = "url.getProtocol()";
}
}
}
//最后通过正则将获取到的扩展点名称赋值给 extName 变量
return String.format(CODE_EXT_NAME_ASSIGNMENT, getNameCode);
}
private static final String CODE_EXT_NAME_ASSIGNMENT = "String extName = %s;\n";
//这边还是通过正则拼装调用ExtensionLoader.getExtensionLoader(extName) 获取真正要调用的扩展点
private String generateExtensionAssignment() {
return String.format(CODE_EXTENSION_ASSIGNMENT, type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
}
static String CODE_EXTENSION_ASSIGNMENT = "%s extension = (%
最后我们dubug 看看Login 类生成的code代码,过过眼瘾
一步步验证自己的猜想。
总结
源码这块主要是多调试,一遍不行就十遍,自适应扩展点就写到这里,后面接着分析dubbo中的依赖注入。