技术:
springboot,maven,dubbo,zookeeper
背景:
项目的功能类似一个中转路由,通过页面可以发送请求,请求到别的项目的接口,大家都知道dubbo接口的服务提供方需要把服务注册到zookeeper上,然后服务消费方获得服务提供方提供的facade包(也就是jar包),可以作为消费者去请求提供方的服务。
这里就有一个问题,dubbo接口的服务,无论是服务提供者还是服务消费者都有几种注册方式,可以查看官网。(为什么我说消费者也是注册,是因为在dubbo-admin中可以看到)
我的理解中比较常用的应该是:
1,xml
2,properties
3,api
需求:
动态注册dubbo服务,项目不需要重启
解释一下为什么有这个需求:
因为公司本身有自己的项目,这个独立的新的项目只是为了做测试各个接口的功能,有点类似于自动化测试,并且提供页面能够让开发人员测试自己写的接口。
那么也就是说,其他开发人员并不需要关注这个独立项目的代码,配置等等一切有关于这个项目本身的东西,他们只需要使用提供的功能,到服务器上看一看请求过来以后我们自己项目的日志。
这样子就必须要做到动态加载。因为人家不可能把你的代码拉到自己的电脑上,还要改你的代码配置,然后打包,重新部署。
所以,才会有这么一个需求。
问题:
作为服务的消费者,需要获得服务提供者提供的jar包与对应的接口信息才可以请求服务,那么问题来了,项目启动之后,服务提供者新提供了几个接口,那么现在作为消费者,我需要请求这几个新的接口,那么我能怎么做?
解决方案:
一般来说我都都会采用以下方式
更新maven的pom文件,下载新的jar包,更新xml,properties,api配置,重新打包。此类做法需要重启
但是现在需求是不重启,所以不能采用常规方式,必须动态加载
页面如下
在页面右上角提供一个按钮,点击效果如下
解决方案如下:
1,在springboot项目中提供一个外置文件dubbo_consumer.properties,里面的内容是key-value,key是方法名(也就是采用xml配置时的id),value是服务提供者提供的服务的类全路径(也就是采用xml配置时的interface)
xml配置如下
<dubbo:reference id="demoServiceRemote" interface="com.alibaba.dubbo.demo.DemoService" />
作为开发,自己写的接口,复制黏贴这个应该是没有问题的。
2,在开发好一个dubbo接口一个,作为服务提供者需要提供一个facade包也就是jar包给服务消费者,这个时候,我们就需要在springboot项目中提供一个外置的文件夹来存放jar包
3,springboot中的提供一个定时任务,每隔一分钟就去扫描这两个文件夹,查看是否有更新,如果有,那么dubbo_consumer.properties文件就直接替换,新的jar包就直接加载进项目
4,通过反射的形式,动态读取dubbo_consumer.properties中的配置,然后使用api的形式来注册dubbo消费者
具体的做法如下:
1,springboot项目启动的时候是1.0版本,这个时候在固定的路径下已经有dubbo_consumer.properties和一个facade包了
2,在项目中写一个单例作为缓存(之所以选择单例,而不使用redis之类是为了减少依赖)来存放初始加载的dubbo_consumer.properties配置和注册的dubbo消费者
3,定时任务每隔一分钟就去扫描这几个路径下的文件是否有更新(注意,项目启动之后,外置的facade包如果加载了是没有办法删除的,所以每次升级就是不断往里面添加facade包,关于数量问题,只需要定时重启项目,保留最新的facade包就行了,其他的删掉)
4,如果有更新就动态加载这些配置到单例缓存,并且注册新的dubbo消费者
下面提供部分代码:
dubbo基础配置:
public abstract class DubboBaseConfig {
//应用名
public static final String APPLICATION = "test";
//连接zookeeper
public static final String ADDRESS = "127.0.0.1:2181";
//选择的协议
public static final String PROTOCOL = "zookeeper";
//zookeeper对外的端口
public static final Integer PORT = 20880;
//dubbo配置
public static final ApplicationConfig applicationConfig = new ApplicationConfig();
public static final RegistryConfig registryConfig = new RegistryConfig();
//加载参数
static {
applicationConfig.setName(APPLICATION);
registryConfig.setAddress(ADDRESS);
registryConfig.setProtocol(PROTOCOL);
registryConfig.setPort(PORT);
}
}
dubbo消费者配置:
public class DubboConsumerConfig extends DubboBaseConfig{
private DubboConsumerConfig() {};
private static final DubboConsumerConfig dubboConsumerConfig = new DubboConsumerConfig();
public static DubboConsumerConfig getInstance() {
return dubboConsumerConfig;
}
/**
*
* @param applicationConfig 当前应用配置
* @param registryConfig 连接注册中心配置
* @param cls 引用的远程服务
* @param method
*/
public void registerConsumer(ApplicationConfig applicationConfig,RegistryConfig registryConfig,Class<?> cls,String method) {
// 引用远程服务
ReferenceConfig<?> reference = new ReferenceConfig<>(); // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
reference.setApplication(applicationConfig);
reference.setRegistry(registryConfig); // 多个注册中心可以用setRegistries()
reference.setInterface(cls);
//放入缓存
AppCache.getInstance().getReferenceConfigMapCache().put(method, reference);
}
}
缓存配置:
public class AppCache {
private AppCache(){}
private static final AppCache serviceIdCache = new AppCache();
public static AppCache getInstance(){
return serviceIdCache;
}
//消费者缓存
private Map<String,ReferenceConfig<?>> referenceConfigMapCache = new HashMap<>();
public Map<String, ReferenceConfig<?>> getReferenceConfigMapCache() {
return referenceConfigMapCache;
}
public void setReferenceConfigMapCache(Map<String, ReferenceConfig<?>> referenceConfigMapCache) {
this.referenceConfigMapCache = referenceConfigMapCache;
}
}
初始化调用写法:
private void registryDubboService() {
List<DubboServiceEntity> list = AppCache.getInstance().getInitServiceIdEntityList();
for(DubboServiceEntity entity:list) {
try {
DubboConsumerConfig.getInstance().registerConsumer(DubboBaseConfig.applicationConfig, DubboBaseConfig.registryConfig, Class.forName(entity.getDubboServiceValue()), entity.getDubboServiceKey());
} catch (Exception e) {
System.out.println(e);
}
}
}
调用写法中的实体类
public class DubboServiceEntity {
private String dubboServiceKey;//消费者服务ID
private String dubboServiceValue;//消费的服务类名
public String getDubboServiceKey() {
return dubboServiceKey;
}
public void setDubboServiceKey(String dubboServiceKey) {
this.dubboServiceKey = dubboServiceKey;
}
public String getDubboServiceValue() {
return dubboServiceValue;
}
public void setDubboServiceValue(String dubboServiceValue) {
this.dubboServiceValue = dubboServiceValue;
}
@Override
public String toString() {
return "DubboServiceEntity [dubboServiceKey=" + dubboServiceKey + ", dubboServiceValue=" + dubboServiceValue
+ "]";
}
}
这样子就可以做到动态注册dubbo服务了
动态加载部分
在动态加载部分其实就是动态加载jar包
需要在绝对路径读取jar包,使用了URLClassLoader,此段代码参考如下:
package cn.fjs;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public final class JarLoaderUtil {
/** URLClassLoader的addURL方法 */
private static Method addURL = initAddMethod();
/** 初始化方法 */
private static final Method initAddMethod() {
try {
Method add = URLClassLoader.class
.getDeclaredMethod("addURL", new Class[] { URL.class });
add.setAccessible(true);
return add;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static URLClassLoader system = (URLClassLoader) ClassLoader.getSystemClassLoader();
/**
* 循环遍历目录,找出所有的JAR包
*/
private static final void loopFiles(File file, List<File> files) {
if (file.isDirectory()) {
File[] tmps = file.listFiles();
for (File tmp : tmps) {
loopFiles(tmp, files);
}
} else {
if (file.getAbsolutePath().endsWith(".jar") || file.getAbsolutePath().endsWith(".zip")) {
files.add(file);
}
}
}
/**
* <pre>
* 加载JAR文件
* </pre>
*
* @param file
*/
public static final void loadJarFile(File file) {
try {
addURL.invoke(system, new Object[] { file.toURI().toURL() });
//System.out.println("加载JAR包:" + file.getAbsolutePath());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* <pre>
* 从一个目录加载所有JAR文件
* </pre>
*
* @param path
*/
public static final void loadJarPath(String path) {
List<File> files = new ArrayList<File>();
File lib = new File(path);
loopFiles(lib, files);
for (File file : files) {
loadJarFile(file);
}
}
}
根据以上代码,再自行修改就能做到动态读取jar文件并且动态注册dubbo服务了