最近做项目过程中遇到了一个需求:要求Druid连接池在不重启服务的同时修改用户名和密码,并使之生效。要求用户名和密码不能为明文。
首先对用户名和密码的加密和解密:
第一种方案:自定义一个类继承Druid连接池的datasource,并重写其setUsername和setPassword 在方法内部使用德鲁伊自带的同居类解密用户名和密码。需要注意的是加密使用私钥,解密使用公钥。安全期间建议自定义私钥和公钥。特别注意的是:解密完成后需要调用super.setUsername和super.setPassword,才能把解密后的用户名和密码设置进去。
第二种方案:自定义一个类继承Druid连接池的DruidPasswordCallback,重写其setProperties方法,从传入的Properties中取需要的密码,进行解密,解密之后直接调用setPassword方法即可设置密码,但是DruidPasswordCallback却没有对用户名的相对设置,如果有其他可以实现的同学,留言相互学习一下。
第三种方案:重写spring的PropertyPlaceholderConfigurer类,请自行搜索实现。
其次:需要实时监控对应的properties文件的变动:
思路1:利用JDK自带的WatchService监控指定目录下文件的变动,大致代码如下
/**
* 文件变动行为枚举
*
*
*/
public enum FileAction {
DELETE("ENTRY_DELETE"), CREATE("ENTRY_CREATE"), MODIFY("ENTRY_MODIFY");
private String value;
FileAction(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
import java.io.File;
/**
* 文件操作的回调方法
*
*
*/
public abstract class FileActionCallback {
public void delete(File file) {
};
public void modify(File file) {
};
public void create(File file) {
};
}
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;
/**
* 文件夹监控
*
* @author Goofy <a href="http://www.xdemo.org/">http://www.xdemo.org/</a>
* @Date 2015年7月3日 上午9:21:33
*/
public class WatchDir {
private final WatchService watcher;
private final Map<WatchKey, Path> keys;
private final boolean subDir;
private static Long LAST_MODIFY=1000lL;
/**
* 构造方法
*
* @param file
* 文件目录,不可以是文件
* @param subDir
* @throws Exception
*/
public WatchDir(File file, boolean subDir, FileActionCallback callback) throws Exception {
if (!file.isDirectory())
throw new Exception(file.getAbsolutePath() + "is not a directory!");
this.watcher = FileSystems.getDefault().newWatchService();
this.keys = new HashMap<WatchKey, Path>();
this.subDir = subDir;
Path dir = Paths.get(file.getAbsolutePath());
if (subDir) {
registerAll(dir);
} else {
register(dir);
}
processEvents(callback);
}
@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>) event;
}
/**
* 观察指定的目录
*
* @param dir
* @throws IOException
*/
private void register(Path dir) throws IOException {
WatchKey key = dir.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
keys.put(key, dir);
}
/**
* 观察指定的目录,并且包括子目录
*/
private void registerAll(final Path start) throws IOException {
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
register(dir);
return FileVisitResult.CONTINUE;
}
});
}
/**
* 发生文件变化的回调函数
*/
@SuppressWarnings("rawtypes")
void processEvents(FileActionCallback callback) {
for (;;) {
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException x) {
return;
}
Path dir = keys.get(key);
if (dir == null) {
System.err.println("操作未识别");
continue;
}
for (WatchEvent<?> event : key.pollEvents()) {
Kind kind = event.kind();
// 事件可能丢失或遗弃
if (kind == StandardWatchEventKinds.OVERFLOW) {
continue;
}
// 目录内的变化可能是文件或者目录
WatchEvent<Path> ev = cast(event);
Path name = ev.context();
Path child = dir.resolve(name);
File file = child.toFile();
Long lastModify=file.lastModified();
if (kind.name().equals(FileAction.DELETE.getValue())) {
callback.delete(file);
} else if (kind.name().equals(FileAction.CREATE.getValue())) {
callback.create(file);
} else if (kind.name().equals(FileAction.MODIFY.getValue())
&&!lastModify.equals(LAST_MODIFY)&&file.length()>0) {
LAST_MODIFY=lastModify;
callback.modify(file);
} else {
continue;
}
// if directory is created, and watching recursively, then
// register it and its sub-directories
if (subDir && (kind == StandardWatchEventKinds.ENTRY_CREATE)) {
try {
if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
registerAll(child);
}
} catch (IOException x) {
// ignore to keep sample readbale
}
}
}
boolean valid = key.reset();
if (!valid) {
// 移除不可访问的目录
// 因为有可能目录被移除,就会无法访问
keys.remove(key);
// 如果待监控的目录都不存在了,就中断执行
if (keys.isEmpty()) {
break;
}
}
}
}
}
注意标红部分,如果不添加这些逻辑,那么当文件只修改一次,会触发两次Modify时间,这段代码的意思是,每当文件发生变动就比较一下最后修改时间和变动内容的大小,防止重复触发。
使用代码如下
public class Usage {
注入Applicationcontext;
public static void main(String[] args) throws Exception {
final File file = new File("D:\\upload");
new Thread(new Runnable() {
@Override
public void run() {
try {
new WatchDir(file, true, new FileActionCallback() {
@Override
public void create(File file) {
System.out.println("文件已创建\t" + file.getAbsolutePath());
}
@Override
public void delete(File file) {
System.out.println("文件已删除\t" + file.getAbsolutePath());
}
@Override
public void modify(File file) {
从容器中拿到指定的连接池bean
DruidDataSource dds=(DruidDataSource) applicationcontext.getBean("datasource");
Properties proerties=new Properties();
InputStream is=new FileInputStream(file);
把最新的文件读进properties中
properties.load(is);
判断此时的连接池是否已经初始化 如果初始化了就重启 如果不重启 那么setUsername setPassword
将无法执行 会报错
if(dds.isInited()){
对连接池进行重启 这一步非常重要 要确保 连接池处于未初始化状态 重启的同时会释放并关闭所有的连接,有朋友反映直接重启会因为存在存活的连接导致重启失败,又看了一下源码,发现调用close()方法会关闭所有存活的连接因此
dds.close();
dds.restart();
重新设置密码和用户名
dds.setUsername(properties.getProperty("username"));
dds.setPassword(properties.getProperty("password"));
}else{
如果连接池处于未初始化状态直接设置密码和用户名
dds.setUsername(properties.getProperty("username"));
dds.setPassword(properties.getProperty("password"));
}
最后对连接池进行初始化 使设置的用户名和密码生效
dds.init();
System.out.println("文件已修改\t" + file.getAbsolutePath());
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
System.out.println("正在监视文件夹:" + file.getAbsolutePath() + "的变化");
}
}
当初开发的时候只是调用连接池的close方法,虽然释放并关闭了连接,但是整个连接池依旧是已经初始化的状态,这个时候去设置密码和用户名会报错,经过源码跟踪发现,只有处于未初始化状态的连接池才可以重新设置用户名和密码。最后重新初始化连接池,那么德鲁伊连接池会重新初始化,并且使用的是最新的用户名和密码。
但是注意:对连接池重启会断开所有连接,为了不对业务造成影响,请在没有业务处理的时候执行密码与用户名的更新操作。
如果大家有更好的方案,或者发现不足的地方,请留言交流。