APDPlat提供了web接口的数据库备份与恢复,支持手工操作和定时调度,可下载备份文件到本地,也可把备份文件发送到异地容错,极大地简化了数据库的维护工作。
设计目标:
1、多数据库支持
2、横切关注点隔离
3、异地容错
下面阐述具体的设计及实现:
1、为了支持多数据库,统一的接口是不可避免的,如下所示:
/** * 备份恢复数据库接口 * @author 杨尚川 */ public interface BackupService { /** * 备份数据库 * @return 是否备份成功 */ public boolean backup(); /** * 恢复数据库 * @param date * @return 是否恢复成功 */ public boolean restore(String date); /** * 获取已经存在的备份文件名称列表 * @return 备份文件名称列表 */ public List<String> getExistBackupFileNames(); /** * 获取备份文件存放的本地文件系统路径 * @return 备份文件存放路径 */ public String getBackupFilePath(); /** * 获取最新的备份文件 * @return 最新的备份文件 */ public File getNewestBackupFile();}
对于各个不同的数据库来说,有一些通用的操作,如对加密的数据库用户名和密码的解密操作,还有接口定义的备份文件存放的本地文件系统路径,用一个抽象类来实现接口中的通用方法以及其他通用方法如decrypt:
/** *备份恢复数据库抽象类,抽象出了针对各个数据库来说通用的功能 * @author 杨尚川 */ public abstract class AbstractBackupService implements BackupService{ protected final APDPlatLogger LOG = new APDPlatLogger(getClass()); protected static final StandardPBEStringEncryptor encryptor; protected static final String username; protected static final String password; //从配置文件中获取数据库用户名和密码,如果用户名和密码被加密,则解密 static{ EnvironmentStringPBEConfig config=new EnvironmentStringPBEConfig(); config.setAlgorithm("PBEWithMD5AndDES"); config.setPassword("config"); encryptor=new StandardPBEStringEncryptor(); encryptor.setConfig(config); String uname=PropertyHolder.getProperty("db.username"); String pwd=PropertyHolder.getProperty("db.password"); if(uname!=null && uname.contains("ENC(") && uname.contains(")")){ uname=uname.substring(4,uname.length()-1); username=decrypt(uname); }else{ username=uname; } if(pwd!=null && pwd.contains("ENC(") && pwd.contains(")")){ pwd=pwd.substring(4,pwd.length()-1); password=decrypt(pwd); }else{ password=pwd; } } @Override public String getBackupFilePath(){ String path="/WEB-INF/backup/"+PropertyHolder.getProperty("jpa.database")+"/"; path=FileUtils.getAbsolutePath(path); File file=new File(path); if(!file.exists()){ file.mkdirs(); } return path; } @Override public File getNewestBackupFile(){ Map<String,File> map = new HashMap<>(); List<String> list = new ArrayList<>(); String path=getBackupFilePath(); File dir=new File(path); File[] files=dir.listFiles(); for(File file : files){ String name=file.getName(); if(!name.contains("bak")) { continue; } map.put(name, file); list.add(name); } if(list.isEmpty()){ return null; } //按备份时间排序 Collections.sort(list); //最新备份的在最前面 Collections.reverse(list); String name = list.get(0); File file = map.get(name); //加速垃圾回收 list.clear(); map.clear(); return file; } @Override public List<String> getExistBackupFileNames(){ List<String> result=new ArrayList<>(); String path=getBackupFilePath(); File dir=new File(path); File[] files=dir.listFiles(); for(File file : files){ String name=file.getName(); if(!name.contains("bak")) { continue; } name=name.substring(0, name.length()-4); String[] temp=name.split("-"); String y=temp[0]; String m=temp[1]; String d=temp[2]; String h=temp[3]; String mm=temp[4]; String s=temp[5]; name=y+"-"+m+"-"+d+" "+h+":"+mm+":"+s; result.add(name); } //按备份时间排序 Collections.sort(result); //最新备份的在最前面 Collections.reverse(result); return result; } /** * 解密用户名和密码 * @param encryptedMessage 加密后的用户名或密码 * @return 解密后的用户名或密码 */ protected static String decrypt(String encryptedMessage){ String plain=encryptor.decrypt(encryptedMessage); return plain; } }
下面来看一个MySQL数据库的实现:
/** *MySQL备份恢复实现 * @author 杨尚川 */ @Service("MYSQL") public class MySQLBackupService extends AbstractBackupService{ /** * MySQL备份数据库实现 * @return */ @Override public boolean backup() { try { String path=getBackupFilePath()+DateTypeConverter.toFileName(new Date())+".bak"; String command=PropertyHolder.getProperty("db.backup.command"); command=command.replace("${db.username}", username); command=command.replace("${db.password}", password); command=command.replace("${module.short.name}", PropertyHolder.getProperty("module.short.name")); Runtime runtime = Runtime.getRuntime(); Process child = runtime.exec(command); InputStream in = child.getInputStream(); try(OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(path), "utf8");BufferedReader reader = new BufferedReader(new InputStreamReader(in, "utf8"))){ String line=reader.readLine(); while (line != null) { writer.write(line+"\n"); line=reader.readLine(); } writer.flush(); } LOG.debug("备份到:"+path); return true; } catch (Exception e) { LOG.error("备份出错",e); } return false; } /** * MySQL恢复数据库实现 * @param date * @return */ @Override public boolean restore(String date) { try { String path=getBackupFilePath()+date+".bak"; String command=PropertyHolder.getProperty("db.restore.command"); command=command.replace("${db.username}", username); command=command.replace("${db.password}", password); command=command.replace("${module.short.name}", PropertyHolder.getProperty("module.short.name")); Runtime runtime = Runtime.getRuntime(); Process child = runtime.exec(command); try(OutputStreamWriter writer = new OutputStreamWriter(child.getOutputStream(), "utf8");BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "utf8"))){ String line=reader.readLine(); while (line != null) { writer.write(line+"\n"); line=reader.readLine(); } writer.flush(); } LOG.debug("从 "+path+" 恢复"); return true; } catch (Exception e) { LOG.error("恢复出错",e); } return false; } }
这里的关键有两点,一是从配置文件db.properties或db.local.properties中获取指定的命令进行备份和恢复操作,二是为实现类指定注解@Service("MYSQL"),这里服务名称必须和配置文件db.properties或db.local.properties中jpa.database的值一致,jpa.database的值指定了当前使用哪一种数据库,如下所示:
#mysql db.driver=com.mysql.jdbc.Driver db.url=jdbc:mysql://localhost:3306/${module.short.name}?useUnicode=true&characterEncoding=UTF-8&createDatabaseIfNotExist=true&autoReconnect=true db.username=ENC(i/TOu44AD6Zmz0fJwC32jQ==) db.password=ENC(i/TOu44AD6Zmz0fJwC32jQ==) jpa.database=MYSQL db.backup.command=mysqldump -u${db.username} -p${db.password} ${module.short.name} db.restore.command=mysql -u${db.username} -p${db.password} ${module.short.name}
有了接口和多个实现,那么备份和恢复的时候究竟选择哪一种数据库实现呢?BackupServiceExecuter充当工厂类(Factory),负责从多个数据库备份恢复实现类中选择一个并执行相应的备份和恢复操作,BackupServiceExecuter也实现了BackupService接口,这也是一个典型的外观(Facade)设计模式,封装了选择特定数据库的逻辑。
定时调度器和web前端控制器也是使用BackupServiceExecuter来执行备份恢复操作,BackupServiceExecuter通过每个实现类以@Service注解指定的名称以及配置文件db.properties或db.local.properties中jpa.database的值来做选择的依据,如下所示:
/** *执行备份恢复的服务,自动判断使用的是什么数据库,并找到该数据库备份恢复服务的实现并执行 * @author 杨尚川 */ @Service public class BackupServiceExecuter extends AbstractBackupService{ private BackupService backupService=null; @Resource(name="backupFileSenderExecuter") private BackupFileSenderExecuter backupFileSenderExecuter; /** * 查找并执行正在使用的数据的备份实现实例 * @return */ @Override public boolean backup() { if(backupService==null){ backupService=SpringContextUtils.getBean(PropertyHolder.getProperty("jpa.database")); } boolean result = backupService.backup(); //如果备份成功,则将备份文件发往他处 if(result){ backupFileSenderExecuter.send(getNewestBackupFile()); } return result; } /** * 查找并执行正在使用的数据的恢复实现实例 * @param date * @return */ @Override public boolean restore(String date) { if(backupService==null){ backupService=SpringContextUtils.getBean(PropertyHolder.getProperty("jpa.database")); } return backupService.restore(date); } }
关键是这行代码backupService=SpringContextUtils.getBean(PropertyHolder.getProperty("jpa.database"));
2、在记录备份恢复日志的时候,如果每种数据库的实现类都要粘贴复制通用的代码到备份和恢复方法的开始和结束位置,那么四处就飘散着重复的代码,对易读性和可修改性都是极大的破坏。
AOP是解决这个问题的不二之选,为了AOP能工作,良好设计的包结构、类层级,规范的命名都是非常重要的,尤其是这里的BackupServiceExecuter和真正执行备份恢复的实现类有共同的方法签名(都实现了BackupService接口),所以把他们放到不同的包里有利于AOP。
使用AOP首先要引入依赖:
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>${aspectj.version}</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>${aspectj.version}</version> </dependency>
其次是要在spring配置文件中指定启用自动代理:
<aop:aspectj-autoproxy />
最后就可以编写代码实现日志记录:
/** * 备份恢复数据库日志Aspect * org.apdplat.module.system.service.backup.impl包下面有多个数据库的备份恢复实现 * 他们实现了BackupService接口的backup方法(备份数据库)和restore(恢复数据库)方法 * @author 杨尚川 */ @Aspect @Service public class BackupLogAspect { private static final APDPlatLogger LOG = new APDPlatLogger(BackupLogAspect.class); private static final boolean MONITOR_BACKUP = PropertyHolder.getBooleanProperty("monitor.backup"); private BackupLog backupLog = null; static{ if(MONITOR_BACKUP){ LOG.info("启用备份恢复日志"); LOG.info("Enable backup restore log", Locale.ENGLISH); }else{ LOG.info("禁用备份恢复日志"); LOG.info("Disable backup restore log", Locale.ENGLISH); } } //拦截备份数据库操作 @Pointcut("execution( boolean org.apdplat.module.system.service.backup.impl.*.backup() )") public void backup() {} @Before("backup()") public void beforeBackup(JoinPoint jp) { if(MONITOR_BACKUP){ before(BackupLogType.BACKUP); } } @AfterReturning(value="backup()", argNames="result", returning = "result") public void afterBackup(JoinPoint jp, boolean result) { if(MONITOR_BACKUP){ after(result); } } //拦截恢复数据库操作 @Before(value="execution( boolean org.apdplat.module.system.service.backup.impl.*.restore(java.lang.String) ) && args(date)", argNames="date") public void beforeRestore(JoinPoint jp, String date) { if(MONITOR_BACKUP){ before(BackupLogType.RESTORE); } } @AfterReturning(pointcut="execution( boolean org.apdplat.module.system.service.backup.impl.*.restore(java.lang.String) )", returning = "result") public void afterRestore(JoinPoint jp, boolean result) { if(MONITOR_BACKUP){ after(result); } } private void before(String type){ LOG.info("准备记录数据库"+type+"日志"); User user=UserHolder.getCurrentLoginUser(); String ip=UserHolder.getCurrentUserLoginIp(); backupLog=new BackupLog(); if(user != null){ backupLog.setUsername(user.getUsername()); } backupLog.setLoginIP(ip); try { backupLog.setServerIP(InetAddress.getLocalHost().getHostAddress()); } catch (UnknownHostException e) { LOG.error("无法获取服务器IP地址", e); LOG.error("Can't get server's ip address", e, Locale.ENGLISH); } backupLog.setAppName(SystemListener.getContextPath()); backupLog.setStartTime(new Date()); backupLog.setOperatingType(type); } private void after(boolean result){ if(result){ backupLog.setOperatingResult(BackupLogResult.SUCCESS); }else{ backupLog.setOperatingResult(BackupLogResult.FAIL); } backupLog.setEndTime(new Date()); backupLog.setProcessTime(backupLog.getEndTime().getTime()-backupLog.getStartTime().getTime()); //将日志加入内存缓冲区 BufferLogCollector.collect(backupLog); LOG.info("记录完毕"); } }
3、怎么样才能异地容错呢?将备份文件保存到与服务器处于不同地理位置的机器上,最好能多保存几份。除了能自动把备份文件传输到异地服务器上面,用户也可以从web界面下载。
APDPlat使用推模型来发送备份文件,接口如下:
/** * 备份文件发送器 * 将最新的备份文件发送到其他机器,防止服务器故障丢失数据 * @author 杨尚川 */ public interface BackupFileSender { public void send(File file); }
有了统一的接口,就可以有灵活的实现方式,如通过HTTP、FTP、SOCKET等方式发送到异地机房。
在上面的BackupServiceExecuter类中我们已经看到,当备份成功之后就会调用BackupFileSenderExecuter的send方法发送备份文件,如下:
boolean result = backupService.backup(); //如果备份成功,则将备份文件发往他处 if(result){ backupFileSenderExecuter.send(getNewestBackupFile()); }
BackupFileSenderExecuter的设计和BackupServiceExecuter类似,不过策略不一样,如果配置有多个Sender,那么会调用所有的Sender,达到拥有多个副本的目的。BackupFileSenderExecuter利用配置项log.backup.file.sender的值来指定启用哪些Sender,并依次调用各个Sender的send方法来完成文件的发送,如下所示:
log.backup.file.sender=localBackupFileSender;
这里 localBackupFileSender是LocalBackupFileSender的Spring Bean名称。
/** *执行备份文件的发送服务,根据配置文件来判断使用哪些发送器,并按配置的前后顺序依次调用 * @author 杨尚川 */ @Service public class BackupFileSenderExecuter implements BackupFileSender, ApplicationListener{ protected final APDPlatLogger LOG = new APDPlatLogger(getClass()); private static final List<BackupFileSender> backupFileSenders = new LinkedList<>(); @Override public void send(File file) { for(BackupFileSender sender : backupFileSenders){ sender.send(file); } } @Override public void onApplicationEvent(ApplicationEvent event){ if(event instanceof ContextRefreshedEvent){ LOG.info("spring容器初始化完成,开始解析BackupFileSender"); String senderstr = PropertyHolder.getProperty("log.backup.file.sender"); if(StringUtils.isBlank(senderstr)){ LOG.info("未配置log.backup.file.sender"); return; } LOG.info("log.backup.file.sender:"+senderstr); String[] senders = senderstr.trim().split(";"); for(String sender : senders){ BackupFileSender backupFileSender = SpringContextUtils.getBean(sender.trim()); if(backupFileSender != null){ backupFileSenders.add(backupFileSender); LOG.info("找到BackupFileSender:"+sender); }else{ LOG.info("未找到BackupFileSender:"+sender); } } } } }
看一个备份文件发送者示例LocalBackupFileSender:
/** * 将备份文件从本地一个目录复制到另一个目录 * @author 杨尚川 */ @Service public class LocalBackupFileSender implements BackupFileSender{ protected final APDPlatLogger LOG = new APDPlatLogger(getClass()); @Override public void send(File file) { try { String dist = PropertyHolder.getProperty("log.backup.file.local.dir"); LOG.info("备份文件:"+file.getAbsolutePath()); LOG.info("目标目录:"+dist); FileUtils.copyFile(file, new File(dist,file.getName())); } catch (IOException ex) { LOG.info("LocalBackupFileSender失败", ex); } } }
相关推荐
APDPlat提供了应用容器、多模块架构、代码生成、安装程序、认证授权、备份恢复、数据字典、web service、系统监控、操作审计、统计图、报表、机器绑定、防止破解、数据安全、内置搜索、数据转换、maven支持、WEB...
APDPlat应用级产品开发平台是一个专门用于构建企业级应用程序的高效开发工具,它集成了多种UML(统一建模语言)设计图,旨在帮助开发者以更清晰、规范的方式规划和实现软件项目。该平台包含27张不同的UML设计图,...
该平台是一款基于Java的APDPlat应用级产品开发平台,由2902个文件组成,涵盖1750个GIF图像、282个Java源文件、269个PNG图像、187个JavaScript文件、156个CSS样式表、34个JSP页面、31个XML配置文件、30个PSD设计文件...
word分词是一个Java实现的中文分词组件,提供了多种基于词典的分词算法,并利用ngram模型来消除歧义。 能准确识别英文、数字,以及日期、时间等数量词,能识别人名、地名、组织机构名等未登录词。 同时提供了Lucene...
【开源项目案例访谈整理版:APDPlat1】是一个关于APDPlat开源项目的详细介绍,APDPlat是一个专为JAVA开发者设计的应用级产品开发平台。该项目自2008年起由杨尚川发起,至今仍在持续维护,并在2012年4月9日在GitHub上...
该项目为APDPlat应用级产品开发平台的设计源码,采用Java和JavaScript为主要编程语言,辅以CSS、HTML等前端技术。源码总文件数为2878个,其中包含1750个GIF图片、268个PNG图片、237个Java源文件、187个JavaScript...
中文分词词典 适合最大正向匹配算法使用 共计548389条词语
在“标签”中提到了“lucene”,这再次强调了这个工具是与Apache Lucene紧密相关的。Lucene支持多种操作,如创建、更新和删除索引,以及复杂的查询语法,这个工具可能是为了辅助这些操作,提供一种便捷的方式来诊断...
7. **响应式设计**:EXTJS支持移动设备,使用`Ext.viewport`和`Ext.layout.container.Box`等布局管理器,可以实现跨平台的响应式设计,确保在不同设备上都能提供良好的用户体验。 在压缩包文件`ysc-APDPlat-5e7a864...
4. APDPlat:APDPlat是一个应用级产品开发平台,提供各种模块以加速基于B/S多层架构的信息管理系统开发。 5. ArduBlock:作为Arduino平台的图形化编程工具,ArduBlock降低了非编程人员使用硬件实现创新想法的门槛。...
在给定的代码中,使用了`org.apdplat.word.segmentation.Word`类,表明使用了APDPlat分词库。 2. **构建词频矩阵**:对于每个文本,统计每个词的出现次数,生成一个词频向量。在代码中,`...
##HtmlExtractor是为大规模分布式环境设计的,采用主从架构,主节点负责维护抽取规则,从节点向主节点请求抽取规则,当抽取规则发生变化,主节点主动通知从节点,从而能实现抽取规则变化之后的实时动态生效。...
中文分词,word分词,附件是编译好的word-1.2.jar版本 maven依赖: <groupId>org.apdplat <artifactId>word <version>1.2 </dependency>