Spring aop应用之实现数据库读写分离

去年五月份的时候曾经写过一篇:Spring加Mybatis实现MySQL数据库主从读写分离,实现的原理是配置了多套数据源,相应的sqlsessionfactory,transactionmanager和事务代理各配置了一套,如果从库或数据库有多个的时候,需要配置的信息会越来越多,远远不够优雅,在我们编程界有一个规范:约定优于配置。所以就用Sping的aop实现了一个简单的数据库分离方案,具体实现代码放在了Github上,地址如下: https://github.com/bridgeli/practical-util/tree/master/src/main/java/cn/bridgeli/datasource 读者如果想使用再简单的方法就是把这个代码download下来,放到自己的项目里面,当然更优雅的方式是:打成jar包,放到项目里面了,具体打jar的方法,老夫就不在这里多说了,相信看这篇文章的读者肯定都会了。当然仅仅有这份代码,他们是不会自动生效的,既然是使用Spring的Aop实现数据库读写分离,所以肯定会有牵涉到Aop的配置了,所以在spring-mybatis.xml中有如下配置: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> <!&#8211; 配置写数据源 &#8211;> <bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="driverClassName" value="${bridgeli.jdbc.driver}" /> <property name="url" value="${bridgeli.jdbc.url}" /> <property name="username" value="${bridgeli.jdbc.username}" /> <property name="password" value="${bridgeli.jdbc.password}" /> <property name="validationQuery" value="${bridgeli.jdbc.validationQuery}" /> <property name="initialSize" value="1" /> <property name="maxActive" value="20" /> <property name="minIdle" value="0" /> <property name="maxWait" value="60000" /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> <property name="testWhileIdle" value="true" /> <property name="timeBetweenEvictionRunsMillis" value="60000" /> <property name="minEvictableIdleTimeMillis" value="25200000" /> <property name="removeAbandoned" value="true" /> <property name="removeAbandonedTimeout" value="1800" /> <property name="logAbandoned" value="true" /> <property name="filters" value="stat" /> </bean> <!&#8211; 配置读数据源 &#8211;> <bean id="parentSlaveDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="driverClassName" value="${bridgeli.jdbc.driver}" /> <property name="validationQuery" value="${bridgeli.jdbc.validationQuery}" /> <property name="initialSize" value="1" /> <property name="maxActive" value="20" /> <property name="minIdle" value="0" /> <property name="maxWait" value="60000" /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> <property name="testWhileIdle" value="true" /> <property name="timeBetweenEvictionRunsMillis" value="60000" /> <property name="minEvictableIdleTimeMillis" value="25200000" /> <property name="removeAbandoned" value="true" /> <property name="removeAbandonedTimeout" value="1800" /> <property name="logAbandoned" value="true" /> <property name="filters" value="stat" /> </bean> <bean id="slaveDataSource1" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close" parent="parentSlaveDataSource"> <property name="url" value="${bridgeli_slave1.jdbc.url}" /> <property name="username" value="${bridgeli_slave1.jdbc.username}" /> <property name="password" value="${bridgeli_slave1.jdbc.password}" /> </bean> <bean id="dataSource" class="cn.bridgeli.datasource.MasterSlaveDataSource"> <property name="targetDataSources"> <map> <entry key-ref="masterDataSource" value-ref="masterDataSource"/> <entry key-ref="slaveDataSource1" value-ref="slaveDataSource1"/> </map> </property> <property name="defaultTargetDataSource" ref="masterDataSource"/> <property name="masterSlaveSelector" ref="dataSelector"/> </bean> <bean id="dataSelector" class="cn.bridgeli.datasource.MasterSlaveSelectorByPoll"> <property name="masters"> <list> <ref bean="masterDataSource"/> </list> </property> <property name="slaves"> <list> <ref bean="slaveDataSource1"/> </list> </property> <property name="defaultDataSource" ref="masterDataSource"/> </bean> <aop:aspectj-autoproxy/> <!&#8211; mybaits 数据工厂 &#8211;> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> </bean> <!&#8211; 自动扫描所有注解的路径 &#8211;> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="cn.bridgeli.mapper" /> <!&#8211; <property name="sqlSessionFactory" ref="sqlSessionFactory" /> &#8211;> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property> </bean> <!&#8211; 数据库切面 &#8211;> <bean id="masterSlaveAspect" class="cn.bridgeli.datasource.MasterSlaveAspect"> <property name="prefixMasters"> <list> <value>save</value> <value>update</value> <value>delete</value> </list> </property> </bean> <aop:config> <aop:aspect id="c" ref="masterSlaveAspect"> <aop:pointcut id="tx" expression="execution(\* cn.bridgeli.service..\*.*(..))"/> <aop:before pointcut-ref="tx" method="before"/> </aop:aspect> </aop:config> <context:annotation-config /> <context:component-scan base-package="cn.bridgeli" /> </beans> 这样我们就很优雅的利用Spring的Aop实现了对数据库的读写分离,读的时候走slaveDataSource1这个数据源,写的时候走masterDataSource这个数据源。哎,等等,这里哪里体现了约定优于配置这一规范,他们怎么知道哪些方法走读库哪些走写库?同学你别急,仔细读读这个配置文件,你就会发现在第98行,配置了一个MasterSlaveAspect,也就是说代码里面service层(为什么是service层?)的方法以这里面配置的这些关键字打头,都将会走写库,所以当我们想让一个方法走主库的时候,必须在这个地方添加该方法的前缀或者用这里面已有的前缀,这就要求我们必须约定好走主库的方法的打头,即约定优于配置。

December 31, 2016 · 2 min · 297 words · Bridge Li

Blowfish加密算法Java版简单实现

前几天网上突然出现流言:某东发生数据泄露12G,最终某东在一篇声明中没有否认,还算是勉强承认了吧,这件事对于一般人有什么影响、应该怎么做已经有一堆人说了,所以就不凑热闹了,咱来点对程序猿来说实际点的,说一个个人认为目前比较安全的加密算法:Blowfish。 上代码之前,先说几点Blowfish加密算法的特点: 对称加密,即加密的密钥和解密的密钥是相同的; 每次加密之后的结果是不同的(这也是老夫比较欣赏的一点); 可逆的,和老夫之前的文章介绍的md5等摘要算法不一样,他是可逆的; 速度快,加密和解密的过程基本上由ADD和XOR指令运算组成; 免费,任何人都可以免费使用不需要缴纳版权费; BlowFish 每次只能加密和解密8字节数据; 接下来就是最重要的部分,Blowfish加密算法的实现: package cn.bridgeli.encrypt; public enum BlowfishManager { BRIDGELI_CN("bridgeli_cn!@#$abc123_"); private BlowfishManager(String secret) { this.blowfish = new Blowfish(secret); } private Blowfish blowfish; public Blowfish getBlowfish() { return blowfish; } /** * 解密 * @param sCipherText * @return */ public String decryptString(String sCipherText){ return this.getBlowfish().decryptString(sCipherText); } /** * 加密 * @param sPlainText * @return */ public String encryptString(String sPlainText){ return this.getBlowfish().encryptString(sPlainText); } public static void main(String[] args) { String encryptString = BlowfishManager.BRIDGELI_CN.encryptString(10 + ""); System.out.println(encryptString); String decryptString = BlowfishManager.BRIDGELI_CN.decryptString(encryptString); System.out.println(decryptString); } } 这是对外的接口,使用起来非常简单,对用户很友好,下面是算法的具体实现: ...

December 18, 2016 · 16 min · 3241 words · Bridge Li

Dubbo服务telnet调试法

公司的RPC的服务使用的是阿里巴巴的dubbo,老夫之前曾经写过一篇如何在测试环境远程调试dubbo服务,详情请参考这篇,但一直对如何调试线上dubbo服务不得法,不得已每次都需要写一个web服务调一下看数据,前一段时间经新来的一个同事提示可以使用Telnet调试,网上搜了一下资料,发现真的很爽,以下是学习笔记。 需要说明的是:Dubbo2.0.5以上版本服务提供端口支持telnet命令,不过应该没有公司使用2.0.5以下版本吧。 进入调试模式 telnet localhost 20880 即:telnet + ip + 端口,这个不用解释,使用dubbo的肯定都知道 ls 使用上一个命令之后,敲一下回车,就进入dubbo的telnet调试服务了,然后就可以使用ls命令了,这个命令有几个用法: ①. 显示服务列表 ls ②. 显示服务详细信息列表 ls -l ③. 显示服务的方法列表 ls XxxService ④. 显示服务的方法详细信息列表 ls -l XxxService ps 这个命令主要是看连接信息,也有如下几个用法: ①. 显示服务端口列表 ps ②. 显示服务地址列表 ps -l ③. 显示端口上的连接信息 ps 20880 ④, 显示端口上的连接详细信息 ps -l 20880 cd 这个缺省服务,主要有以下两个用法: ①. 改变缺省服务,当设置了缺省服务,凡是需要输入服务名作为参数的命令,都可以省略服务参数 cd XxxService ②. 取消缺省服务 cd / pwd 显示当前缺省服务 trace 这个命令顾名思义:跟踪,具体有以下用法: ①. 跟踪1次服务任意方法的调用情况 trace XxxService ②. 跟踪10次服务任意方法的调用情况 ...

November 27, 2016 · 1 min · 164 words · Bridge Li

VIM常用命令

上个周苹果公司悍然发布了新版mac,消灭了功能键,包括ESC,终于使下面这个段子成为了事实:问,如何生成一段随机数?答:让一个非开发人员退出vim。哈哈,现在开发人员是不是也可以产生随机字符串了?发现自己作为一个vim党,竟然对很多vim常用的命令都不知道,今天就记一下笔记,让自己这个vim党称呼实至名归。 首先要说明的是,基本的vim命令像A、I、O进入编辑模式,ESC进入命令模式,“:”进入末行模式,以及常用的什么dd,yy,p等都认为大家已经熟练掌握,就不说了。 替换字符 :%s/oldchar/newchar/g 这个命令同样可以解决: 注1. 在windows记事本下的文件放到Linux下时,行末多出来一个^M,这个问题,直接把oldchar换成\r,newchar传承空就可以了。 注2. 在windows记事本下的文件放到Linux下时,行末多出来一个^@,这个问题,直接把oldchar换成先摁ctrl+v,然后摁ctrl+2,newchar传承空就可以了。 注3. 在windows记事本下的文件放到Linux下时,行末多出来一个^A,这个问题,直接把oldchar换成先摁ctrl+v,然后摁ctrl+A,newchar传承空就可以了。 注4. oldchar也可以用正则表达式,之前一直不知道怎么在每一行的行末加东西,其实如此简单而已。 加密文件 进入末行模式,然后输入大写的X,然后输入密码,保存退出即可,这样的话今后每次打开都需要输入密码才行,否则就是一堆乱码。 undo和redo 这个比较简单,undo直接摁u,redo是ctrl+r 简单的移动光标 0 数字零,到行头 ^ 到本行第一个不是blank字符的位置 $ 到本行行尾 g_ 到本行最后一个不是blank字符的位置 /pattern 搜索 pattern 的字符串,如果搜索出多个匹配,可按n键到下一个 . (小数点) 可以重复上一次的命令 N<command> 重复某个命令N次 :N 到第N行 gg 到第一行。(陈皓注:相当于1G,或 :1) G 到最后一行 块操作: ctrl-v 块操作,典型的操作: 0、ctrl-v、ctrl-d、I、ESC ^ 到行头 ctrl-v 开始块操作 ctrl-d 向下移动 (你也可以使用hjkl来移动光标,或是使用%,或是别的) I 插入,然后输入 ESC 来为每一行生效。 自动提示 在输入模式下,你可以输入一个词的开头,然后按 ctrl-p或是ctrl-n,自动补齐功能就出现了 可视化选择: v,V,ctrl-v ctrl-v,我们可以使用 v 和 V。一但被选好了,你可以做下面的事: J 把所有的行连接起来(变成一行) < 或 > 左右缩进 = 自动给缩进 窗口分屏浏览 :He 在下边分屏浏览目录 :He! 在上分屏浏览目录 :Ve 在左边分屏间浏览目录 :Ve! 要在右边则是 多页签(tab page) 在末行模式下,输入: ...

November 6, 2016 · 1 min · 144 words · Bridge Li

我看拉勾一拍之系统架构

今年年中的时候由公司平台部转组到Alpha项目中心负责公司一拍项目组的技术研发工作,到现在已经快有将近半年的时间了,随着对系统的越来越熟悉,对原有系统的架构也越来越感到有些不合理的地方,随着自己水平的提升感觉对架构也有了一点自己的理解,所以今天就借这个机会说说自己的不成熟的建议。 一. 原有的架构 俗话说,一图胜千言,直接上图: 解释一下这几个系统分别的作用: 后台管理系统不用说了,管理C和B可见的内容; C端用户系统,是对C可见的一个系统,一拍是一个招聘系统,所以就是对候选人操作的后台; B端用户系统,是对B可见的一个系统,通俗点讲就是HR操作的后台; Dubbo系统,是对兄弟部门和B提供服务的一个系统; Recommend系统,其实也是一个Dubbo系统,区别在于Dubbo系统是对外提供各种服务的,而Recommend系统是发现全站系统用户的行为,然后对用户的行为进行分析,甄选出一部分C端用户作为一拍的现在用户; msgpush系统,是用netty做的一个实时消息推送的IM服务,目前主要是给后台管理用户和C端候选人聊天的一个系统; 其中:1. 在我们接手之前B和C是同一个系统,也就是说B是我们这半年新加的一个系统;2. Recommend和msgpush系统也同样是我们这半年新增的一个系统 二. 系统架构存在的问题 目前后台管理系统、C端用户系统、Dubbo系统各自独立,这样存在的问题: 最主要的是各自分别操作数据库,这样只要底层数据库发送变动,那么三个系统操作数据库的地方都要同步修改三次; 操作数据库的地方代码冗余,很多地方都一样,这样一个地方出bug,三个地方要同步修改,然后都要上线; 后台管理系统采用分层的模式分模块而不是根据业务分模块,这样每次上线service和dao都要先deploy jar到maven私服; 当时为了快速迭代,Recommend系统也是单独操作操作数据库,不过还好用了后台管理系统的dao这个jar包,但是首先根本不需要这么重的一个jar包,其次jar出bug了,有时候他也需要重新上线啊,不然这个jar包就会一直很旧,当然只要不涉及到他操作数据库的地方出bug,你不改也是可以的; 目前C和B虽然已经分开,但如果用户激增,横向扩展依然不合理,只能整体加机器,而不能针对性对某些模块单独加机器; 代码中存在的问题:不知道什么原因大量的逻辑被写在了controller层,导致代码可复用性差; 由于之前后台管理系统和其他系统不是同一个团队开发的,命名各有各的风格,代码不仅冗余还同样一个类名字不一样; 很多系统日志配置的也有问题,错误日志和最基本的业务日志没有区分开,目前在将就用; 系统中的jar依赖不仅存在循环依赖,而且加入了大量的自己不需要的依赖,导致各种jar冲突出问题; 综合以上问题,我个人认为这是一个:可维护性、可扩展性不高的系统。 三. 我的个人思考 同样先来一张图,来总体说明一下我的想法: 整体来说只有相对独立的实时消息推送系统不懂,然后把其余的各个业务层抽象成微服务,采用公司目前使用比较成熟的dubbo作为rpc框架,controller层只负责业务转发不负责逻辑的一个简单系统,这样带来的好处: Dubbo系统作为核心的业务系统,分别对兄弟部门、后台管理系统、C端用户系统、B端用户系统、Recommend系统提供服务,如果用户量增加,不仅可以整体增加Dubbo系统的机器,也可以把调用量大的接口单独拆出来,部署到另外的机器上,实现隔离,不会因某个接口调用量大,导致整个系统不可用,而且把数据库的底层操作也放到了这个Dubbo系统中,这样就可以避免数据库修改,要修改多处的问题; 抽象出来的这个dubbo系统,不仅可以解决后台系统和其他系统命名不一样的问题,而且和可以解决不同团队造成的coding style不一的问题,一举多得; 由于后台管理系统、C端用户系统、B端用户系统、Recommend系统都调用Dubbo系统,所以他们的controller层讲极其简单,很多业务逻辑类似的东西全部放到了Dubbo系统里面,代码的可复用性提高了不少; 抽象出来的逻辑都统一放到了dubbo中,这样系统如果有bug,这样就做到了一个地方修改,这样多个地方就可以同时生效; 由于业务都在Dubbo系统里面这样同时也避免了曾经出现了,后台管理系统和BC系统使用的缓存不一致,导致缓存出问题的这种低级bug; 由于controller层简单没有逻辑,这样就可以避免目前由于后台管理系统单机,修改一个业务逻辑bug重启系统,导致后台不可用的问题,因为只需要重启dubbo就行了; 这样controller层变得很轻,只需要一个简单的servlet容器,对机器的要求会降低不少; 四. 备注 由于我工作时间不长,见过的系统更有限,所以对系统架构几乎没有什么经验,这些只是我个人的一点很粗浅的理解,例如把dubbo做的那么重,虽然可扩展性提高了不少,但其实也不知道算不算合理,因为调用rpc服务,肯定会增加网络IO延时,所以这些算是我个人的抛砖引玉吧,一方面希望对同样和我一样没有经验的小伙伴能有所帮助,另一方面希望有经验的小伙伴能留言交流 五. 总结 以上便是我个人对拉勾一拍所有的核心系统进行了审视后的一番分析,如果这些核心系统架构的重构真的达到自己的理想状况这将是一番浩大的工程,对于高速发展的互联网公司来说,这就是一边驾驶者一辆高速前进的汽车,一边对这辆汽车进行换轮胎换发动机,先不说工作量的问题,难度程度也可见一斑。

October 23, 2016 · 1 min · 46 words · Bridge Li

Dubbo远程debug方法

公司项目的rpc服务基于阿里巴巴的dubbo架构,开发dubbo项目的时候测试只能跑junit test,但实际工作中由于很多时候junit test写的不全,出了问题只能再加日志分析原因(典型的没事找事型),这次和公司移动端的推送联调IM服务,发现他们已经把老夫之前听说的远程debug用在了实际工作中,刚好趁此机会实验了一把,以下是笔记,以待自己和需要的朋友参考。 dubbo服务的设置 我们自己观察dubbo的start.sh和start.bat这两个脚本会发现有如下两端代码 ①. start.sh JAVA_DEBUG_OPTS="" if [ "$1" = "debug" ]; then JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n " fi ②. start.bat if ""%1"" == ""debug"" goto debug if ""%1"" == ""jmx"" goto jmx java -Xms64m -Xmx1024m -XX:MaxPermSize=64M -classpath ..\conf;%LIB_JARS% com.alibaba.dubbo.container.Main goto end :debug java -Xms64m -Xmx1024m -XX:MaxPermSize=64M -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n -classpath ..\conf;%LIB_JARS% com.alibaba.dubbo.container.Main goto end 也就是说,脚本已经支持远程debug,只需要的在启动的时候传入一个参数 debug 即可,其余的几乎不用做任何修改 eclipse的设置 当我们把远程的服务以支持debug的模式启动之后,就需要把本地的项目也起来了,否则怎么debug呢,本地的设置其实非常简单,一张图搞定 看了这张图,我相信不用我多说了,远程远程debug如此简单

August 14, 2016 · 1 min · 65 words · Bridge Li

多线程应用之批量数据处理

我们都知道多线程是为了加快数据处理的,但至于怎么用,因为在工作中,我一直很少用,所以对多线程不是很了解。之前处理一个功能时,由于没有经验,导致速度很慢,前一段时间经老大提示,可以用多线程解决,突然发现原来多线程可以这么用可以来处理这一类问题,今天记录一下,作为笔记也作为一个给读者的参考,好了先说一下问题:公司的业务的业务不仅分模块而且是分库分表的,这样就导致一个问题,当我们要查询一个数据时,不能连表查询,不能只通过一个接口获得数据,最容易想到的常规做法就是: public List<Data> queryDatas() { List<Data> datas = queryDataFromDB(); if(null != datas && datas.size() > 0) { for(Data data : datas) { Object object = getObjectFromDb(data.getId()); data.setAttr1(object.getAttr); } } return datas; } 这么做,虽然可以满足业务需求,但效率实在是太低了,尤其是列表数据越大时,如果不只一个属性要这么做时,速度是会慢到要死人的。所以经老大提示参考同事的实现就采用了如下方法: private static final ExecutorService executor = Executors.newFixedThreadPool(20); public List<Data> queryDatas() { List<Data> datas = queryDataFromDB(); if(null != datas && datas.size() > 0) { batchSetAttr(datas); } return datas; } private boolean batchSetAttr(final List<Data> datas) { final CompletionService<Data> completionService = new ExecutorCompletionService<>(executor); for (final Data data : datas) { completionService.submit(new Callable<Data>() { @Override public Data call() throws Exception { Object object = getObjectFromDb(data.getId()); data.setAttr1(object.getAttr); return data; } }); } try { for (int i = 0, size = datas.size(); i < size; i++) { Future<Data> future = completionService.take(); Data d = future.get(); } } catch (InterruptedException e) { logger.error("InterruptedException", e); return false; } catch (ExecutionException e) { logger.error("ExecutionException", e); return false; } return true; } 利用多线程批量查询,返回时一一设置值,最终达到提高速度的目的。 最后需要说明一点:线程池的大小,大家可以根据自己的实际情况来设置,并不是越大越好;

July 24, 2016 · 1 min · 148 words · Bridge Li

DFA算法应用之敏感词过滤

公司在做一个社区应用,由于我朝特色,众所周知社区应用有一个很重要的就是要进行敏感词的过滤,这块由一个同事负责,听他说,有一个算法叫DFA,可以做这个,个人比较感兴趣,就到网上查了一些资料,有一篇文章写的特别好,老夫的这篇文章就是把其核心的部分(就是怎么应用,老夫一直有一个观点,理论弱于实践,理论懂得再多不会用一点用没有,所以老夫认为应用是核心)摘出来,留作笔记,如果有想了解其原理的,请点击下方的参考资料,好了,既然是应用那么就直接上代码了: package cn.bridgeli.dfa; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; public class SensitivewordFilter { @SuppressWarnings("rawtypes") private Map sensitiveWordMap = null; public static int minMatchTYpe = 1; // 最小匹配规则 public static int maxMatchType = 2; // 最大匹配规则 /** * 初始化敏感词库 */ public SensitivewordFilter() { sensitiveWordMap = new SensitiveWordInit().initKeyWord(); } /** * 判断文字是否包含敏感字符 * * @param txt * 文字 * @param matchType * 匹配规则 1:最小匹配规则,2:最大匹配规则 * @return 若包含返回true,否则返回false */ public boolean isContaintSensitiveWord(String txt, int matchType) { boolean flag = false; for (int i = 0; i < txt.length(); i++) { int matchFlag = this.CheckSensitiveWord(txt, i, matchType); // 判断是否包含敏感字符 if (matchFlag > 0) { flag = true; } } return flag; } /** * 获取文字中的敏感词 * * @param txt * 文字 * @param matchType * 匹配规则&nbsp;1:最小匹配规则,2:最大匹配规则 * @return */ public Set<String> getSensitiveWord(String txt, int matchType) { Set<String> sensitiveWordList = new HashSet<String>(); for (int i = 0; i < txt.length(); i++) { int length = CheckSensitiveWord(txt, i, matchType); if (length > 0) { sensitiveWordList.add(txt.substring(i, i + length)); i = i + length &#8211; 1; // 减1的原因,是因为for会自增 } } return sensitiveWordList; } /** * 替换敏感字字符 * * @param txt * @param matchType * @param replaceChar \* 替换字符,默认\* */ public String replaceSensitiveWord(String txt, int matchType, String replaceChar) { String resultTxt = txt; Set<String> set = getSensitiveWord(txt, matchType); // 获取所有的敏感词 Iterator<String> iterator = set.iterator(); String word = null; String replaceString = null; while (iterator.hasNext()) { word = iterator.next(); replaceString = getReplaceChars(replaceChar, word.length()); resultTxt = resultTxt.replaceAll(word, replaceString); } return resultTxt; } /** * 获取替换字符串 * * @param replaceChar * @param length * @return */ private String getReplaceChars(String replaceChar, int length) { String resultReplace = replaceChar; for (int i = 1; i < length; i++) { resultReplace += replaceChar; } return resultReplace; } /** * 检查文字中是否包含敏感字符,检查规则如下:<br> * * @param txt * @param beginIndex * @param matchType * @return,如果存在,则返回敏感词字符的长度,不存在返回0 */ @SuppressWarnings({ "rawtypes" }) public int CheckSensitiveWord(String txt, int beginIndex, int matchType) { boolean flag = false; // 敏感词结束标识位:用于敏感词只有1位的情况 int matchFlag = 0; // 匹配标识数默认为0 char word = 0; Map nowMap = sensitiveWordMap; for (int i = beginIndex; i < txt.length(); i++) { word = txt.charAt(i); nowMap = (Map) nowMap.get(word); // 获取指定key if (nowMap != null) { // 存在,则判断是否为最后一个 matchFlag++; // 找到相应key,匹配标识+1 if ("1".equals(nowMap.get("isEnd"))) { // 如果为最后一个匹配规则,结束循环,返回匹配标识数 flag = true; // 结束标志位为true if (SensitivewordFilter.minMatchTYpe == matchType) { // 最小规则,直接返回,最大规则还需继续查找 break; } } } else { // 不存在,直接返回 break; } } if (matchFlag < 2 || !flag) { // 长度必须大于等于1,为词 matchFlag = 0; } return matchFlag; } public static void main(String[] args) { SensitivewordFilter filter = new SensitivewordFilter(); System.out.println("敏感词的数量:" + filter.sensitiveWordMap.size()); String string = "太多的伤感情怀也许只局限于饲养基地 荧幕中的情节,主人公尝试着去用某种方式渐渐的很潇洒地释自杀指南怀那些自己经历的伤感。" + "然后法轮功 我们的扮演的角色就是跟随着主人公的喜红客联盟 怒哀乐而过于牵强的把自己的情感也附加于银幕情节中,然后感动就流泪," + "难过就躺在某一个人的怀里尽情的阐述心扉或者手机卡复制器一个人一杯红酒一部电影在夜三级片 深人静的晚上,关上电话静静的发呆着。"; Set<String> set = filter.getSensitiveWord(string, 1); System.out.println("语句中包含敏感词的个数为:" + set.size() + "。包含:" + set); } } 这个主要是应用,DFA的核心是下面: ...

May 2, 2016 · 4 min · 710 words · Bridge Li

记一次线上操作bug

身为程序猿,可以说天天都会遇到bug,今天没为什么记下这次bug呢?说来惭愧,因为这次bug是由于自己不仔细没有仔细检查没有测试就对线上数据下手造成的,一方面是记下这个bug的由来,修复方法和犯下的失误的地方,另一方面也是留下记录警示自己操作线上数据一定要小心再小心,还有就是不要对自己过于自信,测试很重要。 先说一下bug的缘由,19号晚上我们上线了一个新功能,有一个功能模块是另外一个同事负责的,所以对其实现不是很了解,但数据导入有老夫负责,所以数据导入的时候,有一个结束时间没有考虑清楚,只有日期没有时间(产品经理和另一位同事当时也没有给我说),所以数据库里面结束时间变成了默认的“00:00:00”,本来修起来应该很简单,读出来update一下时间就好了,但由于是部门间的协作,比较麻烦,就考虑用SQL解决,所以就写出了如下的SQL: CREATE TABLE t_goods_bak AS SELECT REPLACE(a.endtime,&#8217;00:00:00&#8242;,&#8217;23:59:59&#8242;) end_time,a.* FROM t_goods a; ALTER TABLE \`commercialization\`.\`t_goods_bak\` CHANGE \`id\` \`id\` INT(11) DEFAULT 0 NOT NULL FIRST, CHANGE \`end_time\` \`end_time\` DATETIME CHARSET utf8 COLLATE utf8_general_ci NOT NULL AFTER \`endtime\`, CHANGE \`price\` \`price\` DECIMAL(10,2) NOT NULL COMMENT &#8216;商品单价&#8217; AFTER \`end_time\`; ALTER TABLE \`commercialization\`.\`t_goods_bak\` DROP COLUMN \`endtime\`; ALTER TABLE \`commercialization\`.\`t_goods_bak\` CHANGE \`end_time\` \`endtime\` VARCHAR(19) CHARSET utf8 COLLATE utf8_general_ci DEFAULT &#8221; NOT NULL COMMENT &#8216;商品失效时间&#8217;; DROP TABLE \`t_goods_bak\`; RENAME TABLE \`commercialization\`.\`t_goods_bak\` TO \`commercialization\`.\`t_goods\`; 整体思想就是新建一张表,在新建这张表的时候,把数据修对,修对的数据放在了新添加的end_time字段,然后把这张新表t_goods_bak修改成和原来的表一致,最后把原表删除,再把这张表改一下名字,就达到了替换以前表的目的,所以就OK,看到这里也许有同学已经发现问题了:先别OK,你这新表没主键啊!!! 对,老夫当时就没有多想,以为就此OK了,所以就出现bug了,因为我没有仔细看SQL语句(这些SQL除了,第一句之外都是自动生成的),新表根本没主键,这还不是问题的关键,仔细看第二句SQL,id字段默认是0,所以所有插入的数据,默认值都是0,因为没有自增,这就是最为关键的两个问题。所以综上所述,关于修这个bug,老夫至少忘了如下几件事: ...

April 24, 2016 · 1 min · 98 words · Bridge Li

Spring和websocket整合应用示例(下)

在上篇中,我们已经实现了websocket,但还有一个核心的业务实现类没有实现,这里我们就实现这个业务核心类,因为老夫参与的这个系统使用websocket发送消息,所以其实现就是如何发送消息了。 NewsListenerImpl的实现 package cn.bridgeli.websocket; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import cn.bridgeli.DateUtil; import cn.bridgeli.enumeration.PlatNewsCategoryType; import cn.bridgeli.model.PlatNewsVo; import cn.bridgeli.model.SearchCondition; import cn.bridgeli.quartz.impl.TimingJob; import cn.bridgeli.service.PlatNewsService; import org.apache.commons.lang.StringUtils; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.socket.TextMessage; import java.io.IOException; import java.util.Date; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @Description : 站内消息监听器实现 * @Date : 16-3-7 */ @Component public class NewsListenerImpl implements NewsListener{ private static final Logger logger = LoggerFactory.getLogger(NewsListenerImpl.class); Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create(); //线程池 private ExecutorService executorService = Executors.newCachedThreadPool(); //任务调度 private SchedulerFactory sf = new StdSchedulerFactory(); @Autowired private PlatNewsService platNewsService; @Override public void afterPersist(PlatNewsVo platNewsVo) { logger.info("监听到有新消息添加。。。"); logger.info("新消息为:"+gson.toJson(platNewsVo)); //启动线程 if(null != platNewsVo && !StringUtils.isBlank(platNewsVo.getCurrentoperatoremail())){ //如果是定时消息 if(platNewsVo.getNewsType() == PlatNewsCategoryType.TIMING_TIME.getCategoryId()){ startTimingTask(platNewsVo); //定时推送 }else{ //立即推送 executorService.execute(new AfterConnectionEstablishedTask(platNewsVo.getCurrentoperatoremail())); } } } @Override public void afterConnectionEstablished(String email) { logger.info("建立websocket连接后推送新消息。。。"); if(!StringUtils.isBlank(email)){ executorService.execute(new AfterConnectionEstablishedTask(email)); } } /** * @Description : 如果新添加了定时消息,启动定时消息任务 * @param platNewsVo */ private void startTimingTask(PlatNewsVo platNewsVo){ logger.info("开始定时推送消息任务。。。"); Date timingTime = platNewsVo.getTimingTime(); if(null == timingTime){ logger.info("定时消息时间为null。"); return; } logger.info("定时推送任务时间为:"+DateUtil.date2String(timingTime)); JobDetail jobDetail= JobBuilder.newJob(TimingJob.class) .withIdentity(platNewsVo.getCurrentoperatoremail()+"定时消息"+platNewsVo.getId(), "站内消息") .build(); //传递参数 jobDetail.getJobDataMap().put("platNewsService",platNewsService); jobDetail.getJobDataMap().put("userEmail",platNewsVo.getCurrentoperatoremail()); Trigger trigger= TriggerBuilder .newTrigger() .withIdentity("定时消息触发"+platNewsVo.getId(), "站内消息") .startAt(timingTime) .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(0) //时间间隔 .withRepeatCount(0) //重复次数 ) .build(); //启动定时任务 try { Scheduler sched = sf.getScheduler(); sched.scheduleJob(jobDetail,trigger); if(!sched.isShutdown()){ sched.start(); } } catch (SchedulerException e) { logger.info(e.toString()); } logger.info("完成开启定时推送消息任务。。。"); } /** * @Description : 建立websocket链接后的推送线程 */ class AfterConnectionEstablishedTask implements Runnable{ String email ; public AfterConnectionEstablishedTask(String email){ this.email = email; } @Override public void run() { logger.info("开始推送消息给用户:"+email+"。。。"); if(!StringUtils.isBlank(email)){ SearchCondition searchCondition = new SearchCondition(); searchCondition.setOperatorEmail(email); JSONArray jsonArray = new JSONArray(); for(PlatNewsCategoryType type : PlatNewsCategoryType.values()){ searchCondition.setTypeId(type.getCategoryId()); int count = platNewsService.countPlatNewsByExample(searchCondition); JSONObject object = new JSONObject(); object.put("name",type.name()); object.put("description",type.getDescription()); object.put("count",count); jsonArray.add(object); } if(null != jsonArray && jsonArray.size()>0){ UserSocketVo userSocketVo = WSSessionLocalCache.get(email); TextMessage reMessage = new TextMessage(gson.toJson(jsonArray)); try { if(null != userSocketVo){ //推送消息 userSocketVo.getWebSocketSession().sendMessage(reMessage); //更新推送时间 userSocketVo.setLastSendTime(DateUtil.getNowDate()); logger.info("完成推送新消息给用户:"+userSocketVo.getUserEmail()+"。。。"); } } catch (IOException e) { logger.error(e.toString()); logger.info("站内消息推送失败。。。"+e.toString()); } } } logger.info("结束推送消息给"+email+"。。。"); } } } 这个类就是websocket的核心业务的实现,其具体肯定和业务相关,由于业务的不同,实现肯定不同,因为老夫参与的系统是发送消息,所以里面最核心的一句就是: ...

April 4, 2016 · 2 min · 304 words · Bridge Li