APDPlat在数据库备份成功之后,会调用一个系统扩展点,用户可以方便地编写自己的包含特定业务逻辑的插件,并可配置启用哪些插件。本文以将备份文件上传到FTP服务器以实现异地容灾为例子,来说明如何编写自己的插件并配置使其生效。
1、如何编写?
我们先看看系统扩展点,即BackupFileSender接口:
/** * 备份文件发送器 * 将最新的备份文件发送到其他机器,防止服务器故障丢失数据 * @author 杨尚川 */ public interface BackupFileSender { public void send(File file); }
然后新建一个类FtpBackupFileSender,实现系统扩展点:
/** * 将备份文件发送到FTP服务器上面 * @author 杨尚川 */ @Service public class FtpBackupFileSender implements BackupFileSender{ protected final APDPlatLogger LOG = new APDPlatLogger(getClass()); @Resource(name="ftpUtils") private FtpUtils ftpUtils; @Resource(name="configurationEncryptor") private StandardPBEStringEncryptor configurationEncryptor; @Override public void send(File file) { try{ String host = PropertyHolder.getProperty("ftp.server.host"); int port = PropertyHolder.getIntProperty("ftp.server.port"); String username = PropertyHolder.getProperty("ftp.server.username"); String password = PropertyHolder.getProperty("ftp.server.password"); if(username!=null && username.contains("ENC(") && username.contains(")")){ username=username.substring(4,username.length()-1); } if(password!=null && password.contains("ENC(") && password.contains(")")){ password=password.substring(4,password.length()-1); } username = configurationEncryptor.decrypt(username); password = configurationEncryptor.decrypt(password); String dist = PropertyHolder.getProperty("log.backup.file.ftp.dir"); String database = PropertyHolder.getProperty("jpa.database"); dist = dist.replace("${database}", database); LOG.info("本地备份文件:"+file.getAbsolutePath()); LOG.info("FTP服务器目标目录:"+dist); boolean connect = ftpUtils.connect(host, port, username, password); if(connect){ boolean result = ftpUtils.uploadTo(file, dist); if(result){ LOG.info("备份文件上传到FTP服务器成功"); }else{ LOG.error("备份文件上传到FTP服务器失败"); } } }catch(Exception e){ LOG.error("备份文件上传到FTP服务器失败",e); } } }
这里有三个要点:一是跟FTP服务器相关的信息(主机、端口、用户名、密码)从配置文件config.properties或config.local.properties中获取;二是使用FtpUtils的uploadTo方法实现实际的文件上传功能;三是使用StandardPBEStringEncryptor对加密的用户名和密码进行解密(配置文件中不存放明文用户名和密码)。
先看看FTP服务器的配置信息,默认存放在config.properties中,用户可以在config.local.properties中进行重新指定以覆盖默认配置:
ftp.server.host=192.168.0.100 ftp.server.port=21 ftp.server.username=ENC(rAjYIMF6ANd2q/cTgX6SpQ==) ftp.server.password=ENC(GHVWGhan3XajaRZF8QzZKQ==)
接下来看看FtpUtils的实现:
/** * FTP操作工具 * @author 杨尚川 */ @Service public class FtpUtils { protected final APDPlatLogger LOG = new APDPlatLogger(getClass()); private FTPClient ftpClient; /** * 连接FTP服务器 * @param host FTP服务器地址 * @param port FTP服务器端口号 * @param username 用户名 * @param password 密码 * @return */ public boolean connect(String host, int port, String username, String password) { try{ ftpClient = new FTPClient(); ftpClient.connect(host, port); ftpClient.login(username, password); ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE); int reply = ftpClient.getReplyCode(); if (!FTPReply.isPositiveCompletion(reply)) { ftpClient.disconnect(); LOG.error("连接FTP服务器失败,响应码:"+reply); return false; } }catch(IOException e){ LOG.error("连接FTP服务器失败",e); return false; } return true; } /** * 上传文件到服务器上的特定路径 * @param file 上传的文件或文件夹 * @param path 上传到ftp服务器哪个路径下 * @return */ public boolean uploadTo(File file, String path){ //切换到服务器上面的合适目录 //如果对应的目录不存在,则创建 LOG.info("上传文件 "+file.getAbsolutePath()+" 到服务器路径 "+path); try{ String[] segs = path.split("/"); for(String seg : segs){ if(!ftpClient.changeWorkingDirectory(seg)){ ftpClient.makeDirectory(seg); if(!ftpClient.changeWorkingDirectory(seg)){ LOG.error("服务器目录切换错误:"+seg); return false; } } } }catch(IOException e){ LOG.error("服务器目录切换错误",e); return false; } return upload(file); } /** * 上传文件 * @param file 上传的文件或文件夹 * @return 是否上次成功 */ private boolean upload(File file) { try{ if (file.isDirectory()) { ftpClient.makeDirectory(file.getName()); ftpClient.changeWorkingDirectory(file.getName()); File[] subFiles = file.listFiles(); for (File subFile : subFiles) { if (subFile.isDirectory()) { upload(subFile); ftpClient.changeToParentDirectory(); } else { try (FileInputStream input = new FileInputStream(subFile)) { ftpClient.storeFile(subFile.getName(), input); } } } } else { try (FileInputStream input = new FileInputStream(file)) { ftpClient.storeFile(file.getName(), input); } } }catch(IOException e){ LOG.error("上传文件失败",e); return false; } return true; } }
最后看看解密,StandardPBEStringEncryptor是定义在Spring的配置文件中的,默认加密和解密的密码是config:
<bean id="configurationEncryptor" class="org.jasypt.encryption.pbe.StandardPBEStringEncryptor"> <property name="config" ref="environmentVariablesConfiguration" /> </bean> <bean id="environmentVariablesConfiguration" class="org.jasypt.encryption.pbe.config.EnvironmentStringPBEConfig"> <property name="algorithm" value="PBEWithMD5AndDES" /> <property name="password" value="config" /> </bean>
那么如何对明文的用户名和密码进行加密,然后放入到配置文件中呢?
可以使用APDPlat_Web子项目中的util.ConfigEncryptUtils类来实现:
/** *把密文放到配置文件中的时候要注意: * ENC(密文) * @author 杨尚川 */ public class ConfigEncryptUtils { private static final StandardPBEStringEncryptor ENCRYPTOR = new StandardPBEStringEncryptor(); static{ EnvironmentStringPBEConfig config = new EnvironmentStringPBEConfig(); config.setAlgorithm("PBEWithMD5AndDES"); //自己在用的时候更改此密码 config.setPassword("config"); ENCRYPTOR.setConfig(config); } public static void main(String[] args){ String plaintext="test"; String ciphertext=ENCRYPTOR.encrypt(plaintext); System.out.println(plaintext+" : "+ciphertext); } }
在main方法中指定plaintext的值为待加密的字符串,然后运行ConfigEncryptUtils类,控制台输出如下:
test : pXnEaEXPoseN4lSNTuz3eQ==
把密文放到配置文件中的时候要注意,在密文前加入ENC(,在密文后加入),形如ENC(密文),如下所示:
ftp.server.username=ENC(rAjYIMF6ANd2q/cTgX6SpQ==) ftp.server.password=ENC(pXnEaEXPoseN4lSNTuz3eQ==)
2、如何配置?
插件编写完成之后,如何配置使其生效?
在config.local.properties中指定log.backup.file.sender的值为所编写的插件的Spring bean name,可指定多个,用;分割,如下所示:
log.backup.file.sender=ftpBackupFileSender;