如何用Java进行3DES加密解密
Posted on January 5, 2008 - Filed Under Uncategorized
最近一个合作商提出使用3DES交换数据,本来他们有现成的代码,可惜只有.net版本,我们的服务器都是Linux,而且应用都是Java。于是对照他们提供的代码改了一个Java的版本出来,主要是不熟悉3DES,折腾了一天,终于搞定。
所谓3DES,就是把DES做三次,当然不是简单地DES DES DES就行了,中途有些特定的排列。这个我可不关心,呵呵,我的目的是使用它。
在网上搜索了一下3DES,找到很少资料。经过朋友介绍,找到GNU Crypto和Bouncy Castle两个Java扩充包,里面应该有3DES的实现吧。
从GNU Crypto入手,找到一个TripleDES的实现类,发现原来3DES还有一个名字叫DESede,在网上搜索TripleDES和DESede,呵呵,终于发现更多的资料了。
Java的安全API始终那么难用,先创建一个cipher看看算法在不在吧
Cipher cipher = Cipher.getInstance("DESede");
如果没有抛异常的话,就证明这个算法是有效的
突然想看看JDK有没有内置DESede,于是撇开Crypto,直接测试,发现可以正确运行。在jce.jar里面找到相关的类,JDK内置了。
于是直接用DES的代码来改&测试,最后代码变成这样
SecureRandom sr = new SecureRandom(); DESedeKeySpec dks = new DESedeKeySpec(PASSWORD_CRYPT_KEY.getBytes()); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DESede"); SecretKey securekey = keyFactory.generateSecret(dks); Cipher cipher = Cipher.getInstance("DESede"); cipher.init(Cipher.ENCRYPT_MODE, securekey, sr); return new String(Hex.encodeHex(cipher.doFinal(str.getBytes())));
需要留意的是,要使用DESede的Spec、Factory和Cipher才行
事情还没完结,合作商给过来的除了密钥之外,还有一个IV向量。搜索了一下,发现有一个IvParameterSpec类,于是代码变成这样
SecureRandom sr = new SecureRandom(); DESedeKeySpec dks = new DESedeKeySpec(PASSWORD_CRYPT_KEY.getBytes()); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DESede"); SecretKey securekey = keyFactory.generateSecret(dks); IvParameterSpec iv = new IvParameterSpec(PASSWORD_IV.getBytes()); Cipher cipher = Cipher.getInstance("DESede"); cipher.init(Cipher.ENCRYPT_MODE, securekey, iv, sr); return new String(Hex.encodeHex(cipher.doFinal(str.getBytes())));
但是,运行报错了
java.security.InvalidAlgorithmParameterException: ECB mode cannot use IV
ECB是什么呢?我的代码完全没有写ECB什么的
又上网搜索,结果把DES的来龙去脉都搞清楚了
http://www.tropsoft.com/strongenc/des.htm
ECB是其中一种字串分割方式,除了DES以外,其他加密方式也会使用这种分割方式的,而Java默认产生的DES算法就是用ECB方法,ECB不需要向量,当然也就不支持向量了
除了ECB,DES还支持CBC、CFB、OFB,而3DES只支持ECB和CBC两种
http://www.tropsoft.com/strongenc/des3.htm
CBC支持并且必须有向量,具体算法这里就不说了。合作商给的.net代码没有声明CBC模式,似乎是.net默认的方式就是CBC的
于是把模式改成CBC
Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
成功运行了
后话:
搜索的过程中,找到一个不错的讨论
http://www.lslnet.com/linux/dosc1/21/linux-197579.htm
在CBC(不光是DES算法)模式下,iv通过随机数(或伪随机)机制产生是一种比较常见的方法。iv的作用主要是用于产生密文的第一个block,以使最终生成的密文产生差异(明文相同的情况下),使密码攻击变得更为困难,除此之外iv并无其它用途。因此iv通过随机方式产生是一种十分简便、有效的途径。此外,在IPsec中采用了DES-CBC作为缺省的加密方式,其使用的iv是通讯包的时间戳。从原理上来说,这与随机数机制并无二致。
看来,向量的作用其实就是salt
最大的好处是,可以令到即使相同的明文,相同的密钥,能产生不同的密文
例如,我们用DES方式在数据保存用户密码的时候,可以另外增加一列,把向量同时保存下来,并且每次用不同的向量。这样的好处是,即使两个用户的密码是一样的,数据库保存的密文,也会不一样,就能降低猜测的可能性
另外一种用法,就是类似IPsec的做法,两部主机互传数据,保证两部机的时钟同步的前提下(可以取样到分钟或更高的单位避免偏差),用时钟的变化值作为向量,就能增加被sniffer数据的解密难度
如何用Java进行DES加密解密
Posted on January 5, 2008 - Filed Under Uncategorized
这篇其实是引子,直接贴代码,不多解释了
SecureRandom sr = new SecureRandom(); DESKeySpec dks = new DESKeySpec(PASSWORD_CRYPT_KEY.getBytes()); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); SecretKey securekey = keyFactory.generateSecret(dks); Cipher cipher = Cipher.getInstance("DES"); cipher.init(Cipher.ENCRYPT_MODE, securekey, sr); return new String(Hex.encodeHex(cipher.doFinal(str.getBytes())));
SecureRandom sr = new SecureRandom(); DESKeySpec dks = new DESKeySpec(PASSWORD_CRYPT_KEY.getBytes()); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); SecretKey securekey = keyFactory.generateSecret(dks); Cipher cipher = Cipher.getInstance("DES"); cipher.init(Cipher.DECRYPT_MODE, securekey, sr); return new String(cipher.doFinal(Hex.decodeHex(str.toCharArray())));
PageCacheFilter2
Posted on December 31, 2007 - Filed Under Uncategorized
最近翻起一年多前在上一家公司写的代码,发现也有不少工具性质的类,于是整理一下都贴出来吧
/** * 2006-5-1 * @author Sparkle */ public class PageCacheFilter2 implements Filter { private Set<String> cacheUrlSet = new HashSet<String>(); private Set<String> scacheUrlSet = new HashSet<String>(); private String baseCachePath, contentType; public void init(FilterConfig config) throws ServletException { String cacheUrl = config.getInitParameter("cacheUrl"); if (cacheUrl != null) { StringTokenizer tk = new StringTokenizer(cacheUrl); while (tk.hasMoreTokens()) { String str = tk.nextToken().trim().toLowerCase(); if (str.endsWith("?")) { str = str.substring(0, str.length() - 1); scacheUrlSet.add(str); } cacheUrlSet.add(str); } } baseCachePath = config.getInitParameter("cachePath"); contentType = config.getInitParameter("contentType"); } private int urlHashCode(HttpServletRequest request) { String uri = request.getRequestURI(); String qStr = request.getQueryString(); if (qStr != null) { uri += ('?' + qStr); } return Math.abs(uri.hashCode()); } private String cacheFilePath(HttpServletRequest request) { String uri = request.getRequestURI(); int hash = urlHashCode(request); if (scacheUrlSet.contains(uri.toLowerCase())) { String entityId = request.getParameter("entityId"); if (StringUtils.isNotBlank(entityId)) { return baseCachePath + "/article/" + subDir(entityId) + '/' + entityId + '/' + hash + ".wml"; } } return baseCachePath + "/other/" + subDir2(String.valueOf(hash)) + '/' + hash + ".wml"; } private String subDir(String str) { int length = str.length(); if (length > 4) { return str.substring(0, length - 4); } return "0"; } private String subDir2(String str) { int length = str.length(); if (length > 4) { return str.substring(length - 4, length); } return str; } private void clearCache(HttpServletRequest request, File cacheFile) { if (scacheUrlSet.contains(request.getRequestURI().toLowerCase())) { File cacheDir = cacheFile.getParentFile(); for (File file : cacheDir.listFiles()) { file.delete(); } cacheDir.delete(); } else { cacheFile.delete(); } } public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; if (!cacheUrlSet.contains(request.getRequestURI().toLowerCase())) { chain.doFilter(req, res); return; } HttpServletResponse response = (HttpServletResponse) res; File cacheFile = new File(cacheFilePath(request)); if (cacheFile.isFile()) { if (request.getParameter("refresh") != null) { clearCache(request, cacheFile); } else { try { BufferedReader reader = new BufferedReader( new FileReader(cacheFile)); response.setContentType(contentType); String line; PrintWriter printWriter = response.getWriter(); while ((line = reader.readLine()) != null) { printWriter.println(line); } printWriter.close(); return; } catch (IOException e) { // ignore response.reset(); System.out.println("read cache file error"); } } } StringWriter strWriter = new StringWriter(); boolean[] support = new boolean[1]; support[0] = true; chain.doFilter(req, new ContentResponseWrapper(response, strWriter, support)); if (support[0] && !response.isCommitted()) { String content = strWriter.getBuffer().toString(); PrintWriter printWriter = response.getWriter(); printWriter.println(content); printWriter.close(); File cacheDir = cacheFile.getParentFile(); if (!cacheDir.exists()) { cacheDir.mkdirs(); } PrintWriter writer = new PrintWriter(cacheFile); writer.println(content); writer.close(); } } public void destroy() { } }
代码点评:这段代码的作用是,对指定的url进行cache,用了ContentRewriteFilter一样的做法来获取内容,因为具体的细节有些区别,没有继承ContentRewriteFilter而是直接编写代码。先判断是否有cache文件,如果有就输出给用户,但是如果连接上有refresh参数的话,就会清掉cache文件,如果没有cache文件,就会获取Servlet or JSP输出的内容,原原本本保存到磁盘,供下次用户访问使用。如果设置URL的时候,最后一位是问号,就会进入scacheUrlSet,这个是用于文章类型,主要的不同点是有分页,如果要refresh的话,应该要把所有分页的cache文件都清除。至于类名后面有个2,是因为这个是第二个版本。
(最近在考虑用Squid+Etag来实现一样的功能)
ContentRewriteFilter
Posted on December 31, 2007 - Filed Under Uncategorized
最近翻起一年多前在上一家公司写的代码,发现也有不少工具性质的类,于是整理一下都贴出来吧
/** * 2006-4-30 * @author Sparkle */ public abstract class ContentRewriteFilter implements Filter { protected boolean wantRewrite(HttpServletRequest request) { return true; } protected abstract String rewrite(String content, HttpServletRequest request, HttpServletResponse response); public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; if (!wantRewrite(request) || res.isCommitted()) { chain.doFilter(req, res); return; } HttpServletResponse response = (HttpServletResponse) res; StringWriter strWriter = new StringWriter(); boolean[] support = new boolean[1]; support[0] = true; chain.doFilter(req, new ContentResponseWrapper(response, strWriter, support)); if (support[0] && !response.isCommitted()) { String content = strWriter.getBuffer().toString(); content = rewrite(content, request, response); PrintWriter printWriter = response.getWriter(); printWriter.println(content); printWriter.close(); } } public void destroy() { } public void init(FilterConfig config) throws ServletException { } } public class ContentResponseWrapper extends HttpServletResponseWrapper { private Writer writer; private boolean[] support; public ContentResponseWrapper(HttpServletResponse response, Writer writer, boolean[] support) { super(response); this.writer = writer; this.support = support; } @Override public ServletOutputStream getOutputStream() throws IOException { support[0] = false; return super.getOutputStream(); } @Override public PrintWriter getWriter() throws IOException { return new PrintWriter(writer); } }
代码点评:这段代码的作用是,获得Servlet or JSP输出的内容进行改写操作,这是一个基类,继承之后重载abstract rewrite即可获得输出的内容进行改写。这段代码在实际环境工作之后做了不少的修正,对response的commit状态,和是否成功获取内容都有较好的判断。中途用了一个[0]的技巧回传了一个boolean值。在公司里使用了这个代码实现了动态插入广告,个人短消息等资料,还有sessionid自动补充(!)等等功能
CryptUtil
Posted on December 31, 2007 - Filed Under Uncategorized
最近翻起一年多前在上一家公司写的代码,发现也有不少工具性质的类,于是整理一下都贴出来吧
/** * 2006-6-10 * @author Sparkle */ public class CryptUtil { /** * 将一个数字转换成0-9a-zA-z的62进制 */ public static String encodeLongToString(long i) { if (i == 0) { return "0"; } else if (i < 0) { throw new IllegalArgumentException(); } StringBuffer buffer = new StringBuffer(); while (i > 0) { buffer.insert(0, encodeIntToChar((int) (i % 62))); i /= 62; } return buffer.toString(); } /** * 将0-9a-zA-z的62进制转换成一个数字 */ public static long decodeStringToLong(String s) { long i = 0; for (int index = 0; index < s.length(); index++) { if (index > 0) { i *= 62; } i += decodeCharToInt(s.charAt(index)); } return i; } /** * 将数字作为顺序转换成单位的0-9a-zA-z<br> * 0 -> '0'<br> * 9 -> '9'<br> * 10 -> 'a'<br> * 35 -> 'z'<br> * 36 -> 'A'<br> * 61 -> 'Z' */ private static char encodeIntToChar(int i) { if (i >= 0 && i <= 9) { return (char) (i + 48); } else if (i >= 10 && i <= 35) { return (char) (i + 87); } else if (i >= 36 && i <= 61) { return (char) (i + 29); } else { throw new IllegalArgumentException(); } } /** * 将单位的0-9a-zA-z按顺序转换成数字<br> * '0' -> 0<br> * '9' -> 9<br> * 'a' -> 10<br> * 'z' -> 35<br> * 'A' -> 36<br> * 'Z' -> 61 */ private static int decodeCharToInt(char c) { if (c >= '0' && c <= '9') { return c - 48; } else if (c >= 'a' && c <= 'z') { return c - 87; } else if (c >= 'A' && c <= 'Z') { return c - 29; } else { throw new IllegalArgumentException(); } } }
代码点评:这段代码的作用是,转换出来的字符串会比16进制要短很多
Apache、Nginx、Lighttpd对比
Posted on December 18, 2007 - Filed Under 互联网
Apache
* 经典的Web服务器
* 除了慢没有别的缺点了
* 对了,Apache2对fcgi支持并不好
* 非常好用的proxy和proxy_ajp(很多人用它作为tomcat的前端)
* 不支持epoll(这年头,epoll几乎是性能的必备)
Nginx
* 速度快,占用资源少
* 杀手级的proxy和rewrite
* 非常不错的静态文件能力
* 最适合作为整个网站的前端服务(将php、svn等不同请求发送往后端apache)
* 其他功能马马虎虎
Lighttpd
* 杀手级的静态文件能力
* 杀手级的fcgi能力
* 不稳定的proxy模块
总体来说,如果你不确定应该用什么服务器,那就应该用Apache
但是稍微可以配置多个服务的情况下,做一个Nginx在最前端,然后把需要的功能转发给Apache是最好的选择
如果你打算跑fcgi,Lighttpd是不二的选择
如果你打算做图片服务器,独立的Lighttpd也是很好的选择
消失的COS
Posted on December 15, 2007 - Filed Under Uncategorized
一直在用COS作为上传组件
最近升级其中一个应用到Spring 2.5
竟然报错,找不到org.springframework.web.multipart.cos.CosMultipartResolver类
Spring 2.5把web和webmvc独立出来已经觉得有点奇怪得了
重新检查了一下,在spring-web里面找到
> org.springframework.web.multipart.commons
但是找不到
> org.springframework.web.multipart.cos
立刻查看2.5的reference,里面还在提及怎么使用COS
对比查看2.0和2.5的MultipartResolver的javadoc
在2.5发现这样一句
> There is only one concrete implementation included in Spring, as of Spring 2.5:
> org.springframework.web.multipart.commons.CommonsMultipartResolver for Jakarta Commons FileUpload
COS支持的确被去掉了,晕
一直一来,许多人的观点都是认为commons-fileupload是有内存泄露的,用COS比较保险
为了确认一下被去掉的原因,到spring forum里面search了一番,没有提及到这次的修改
在spring的changelog里面search了一番,终于找到说明的地方
> Changes in version 2.1 M1 (2007-05-13)
> removed CosMultipartResolver (as part of generally removing COS support from the Spring codebase)
但是没有说原因
晕了
看来只能试试用commons-fileupload
Linux文件句柄数量问题
Posted on December 11, 2007 - Filed Under Uncategorized
在Linux下面部署应用的时候,有时候会遇上Socket/File: Can’t open so many files的问题,其实Linux是有文件句柄限制的(就像WinXP?),而且默认不是很高,一般都是1024,作为一台生产服务器,其实很容易就达到这个数量,因此我们需要把这个值改大一些。
大概知道ulimit这个命令是相关的,上Google搜索了一下,大多数说的很含糊,也没有统一说一下,经过两个小时看了不少文章终于弄清楚ulimit相关的一些配置问题。
我们可以用ulimit -a来查看所有限制值,我只关心文件句柄数量的问题
open files (-n) 1024
这个就是限制数量
这里,有很多ulimit的文章都说的很含糊,究竟这个1024是系统的限制,还是用户的限制呢。其实,这个是用户限制来的,完整的说法,应该是当前用户准备要运行的程序的限制。
1、这个限制是针对单个程序的限制
2、这个限制不会改变之前已经运行了的程序的限制
3、对这个值的修改,退出了当前的shell就会消失
比如说,我先运行了一个程序A,然后通过ulimit修改了限制为2048,然后运行B,然后退出了shell再登录,然后运行C。那就只有B可以打开2048个句柄。
如果我们需要改变整体的限制值,或者我们运行的程序是系统启动的,应该怎么处理呢
其中一个方法,是想ulimit修改命令放入/etc/profile里面,但是这个做法并不好
正确的做法,应该是修改/etc/security/limits.conf
里面有很详细的注释,比如
* soft nofile 2048
* hard nofile 32768
就可以将文件句柄限制统一改成软2048,硬32768
这里涉及另外一个问题,什么是软限制,什么是硬限制
硬限制是实际的限制,而软限制,是warnning限制,只会做出warning
其实ulimit命令本身就有分软硬设置,加-H就是硬,加-S就是软
默认显示的是软限制,如果修改的时候没有加上的话,就是两个一起改
配置文件最前面的一位是domain,设置为星号代表全局,另外你也可以针对不同的用户做出不同的限制
修改了,重新登录用ulimit一开就立刻生效了,不过之前启动过的程序要重新启动才能使用新的值。我用的是CentOS,似乎有些系统需要重启才能生效。
ulimit其实就是对单一程序的限制
那系统总限制呢
其实是在这里,/proc/sys/fs/file-max
可以通过cat查看目前的值,echo来立刻修改
另外还有一个,/proc/sys/fs/file-nr
只读,可以看到整个系统目前使用的文件句柄数量
查找文件句柄问题的时候,还有一个很实用的程序lsof
可以很方便看到某个进程开了那些句柄
也可以看到某个文件/目录被什么进程占用了(umount不了就可以看谁的问题了)
为什么选择trac
Posted on December 10, 2007 - Filed Under 互联网
在一个开发团队里面,bug tracker工具是很重要的。软件一定有bug,我们需要一个跟踪bug的工具,谁报告的bug?详细情况怎么样?别人能不能还原?
需要修正吗?优先度?已经修正了吗?等等。虽然我说了那么多需求,但实际上太全面的功能也会导致复杂性,呵呵。
最近需要在公司部署一个bug tracker,目前可以选择的工具也有不少
1、Bugzilla
功能非常强大,定制性也很强,Mylyn支持最好
可惜,安装困难,板式不直观
=====
正好提到Mylyn,其实我的选择,很大程度受到Mylyn的支持情况影响
2、JIRA
功能强大,定制性也非常强
可惜太复杂了
3、Mantis
我一直对这个软件印象非常好
清晰的版面,用上去就非常舒服
可惜新版本迟迟未release
另一个重要的原因是Mylyn对他支持非常差
4、Trac
Mylyn官方支持三个软件,Bugzilla,JIRA,Trac
因此我也在这三个软件中挑选了很久
Trac的安装比较麻烦(当然后来知道并不麻烦)
功能也中规中矩,有最基本的功能(其实我也不想让软件做太多功能,始终团队的能力为主)
定制能力很差(其实我不需要定制)
同时还包含一个wiki(正好还在讨论公司内部要不要弄一个wiki)
对mysql支持不太好(其实sqlite的效果也很不错,备份也方便)
认证模式刚好可以跟svn一样(太棒了)
真是越用越喜欢,呵呵
sexy Quartz
Posted on November 28, 2007 - Filed Under Uncategorized
一个系统里面经常需要做一些定时任务,比如说定时清空今日得分,或者定时清理临时文件。简单的定时任务很容易实现,用线程或者用Timer就可以了,但是始终需要自己写大量代码才能实现复杂的需求。
于是便有Quartz。不过,Quartz太久没有更新了,而且它太复杂。由于我的系统是基于Spring构建的,所以我希望能使用Spring支持的scheduling类库,可惜Spring只支持commonj和Quartz,正确来说,在Java界,并没有别的scheduling类库了,而commonj只是一个interface,没有具体的实现,似乎在Weblogic之类的里面有实现。
当然,也有另外一个选择,也是轻量级的脚本语言常用的做法,就是使用Linux的crontable,可以实现比较复杂的定时。不过,脚本语言调用数据库并不是很方便(应该说我们的团队技术累积上的问题),如果用crontable启动Java,每次启动的成本又比较高。
在评估过各种方案之后,我还是选择了使用Quartz,首先从Spring的辅助类开始入手吧。
题外话,在一个集群的环境里面(也就是多个Tomcat的环境下),定时任务应该是独立的应用,也就是不应该在每一个Tomcat里面都启动Quartz或者定时线程。另外,在Tomcat的应用里面,也是尽量不要使用线程,有可能一点点小错误就会导致整个Tomcat崩溃(其实我们还是使用很多的,呵呵)。
根据Quartz的使用行为,一个任务我们至少需要一个Job、一个JobDetail、一个Trigger(真复杂)
JobDetail jobDetail = new JobDetail("myJob", // job name sched.DEFAULT_GROUP, // job group DumbJob.class);// the java class to execute Trigger trigger = TriggerUtils.makeDailyTrigger(8, 30); trigger.setStartTime(new Date()); trigger.setName("myTrigger"); sched.scheduleJob(jobDetail, trigger);
首先!!我在这里要明确一个事情。Job类是没有状态的!!
这是什么概念呢,就是说,你实现的一个Job(例如上面的代码的DumbJob),并不是由你自己new出来的,留意一下new JobDetail的代码,传入的参数是DumbJob.class,而不是一个具体的job实例。Quartz帮你吧Job new一份出来,并且调用相应的接口,并没有别的功能。
这里会带来一个什么问题呢,我们先来看看Spring的辅助类。
Spring有两个辅助类可以产生JobDetail类,需要留意的是,Spring并不辅助产生Job类,也就是Spring认为Job类不需要管理。
我们先看看第一个,JobDetailBean
<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailBean"> <property name="jobClass" value="example.ExampleJob" /> <property name="jobDataAsMap"> <map> <entry key="timeout" value="5" /> </map> </property> </bean>
不知道大家有没有看出问题在哪里。property jobClass是一个类名,并不是一个实例名!也就是跟Quartz的调用一样,是Quartz负责帮你new一个example.ExampleJob类出来,也就是说你不能对Job类进行任何形式的注入(IOC),比如说,我们的example.ExampleJob是一个DAO,需要传入DataSource进行DB操作,没辙。
因此,Spring提供了另外一个JobDetail辅助类MethodInvokingJobDetailFactoryBean
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="targetObject" ref="exampleBusinessObject" /> <property name="targetMethod" value="doIt" /> </bean>
你可以留意到,property targetObject是一个ref,指向的是一个常规的Spring管理的Bean。
但是!
MethodInvokingJobDetailFactoryBean很不友好。首先,它是通过反射调用的,而不是Interface,因此我们必须要看了Spring的xml才能知道谁被调用了,你还可能会写一大堆property targetMethod=doIt,而且Job Interface是会传入一个JobExecutionContext,这个被miss了。
其次,如果我们需要大量的Job的话(因为我就是做一个专门用来定时的应用),Spring的配置文件会变得非常臃肿,我希望Job和JobDetail不需要Spring专门管理,只要他是一个Spring管理的Bean,并且实现了Job这个接口就ok了。
这里补充一个事情,我们跳过了Trigger的部分,每一个JobDetail必须配备一个相应的Trigger,因此配置文件是你之前想象中的两倍那么大,而且你还得给每一个Bean命名一个ID,而这个类你以后都不会用到。
我的目标是:
1、只要是实现了Job接口的Spring管理的Bean,自动加入scheduling,根本不用关心JobDetail的存在,也不会有注入的问题
2、所有Job均使用CronTrigger,并且通过配置文件设定Cron Expressions
通过研究MethodInvokingJobDetailFactoryBean和Quartz的代码,我明白到JobDetail是有状态的,而MethodInvokingJobDetailFactoryBean正是利用这点来实现具体效果的,于是便有了我一下这些辅助代码
首先
,需要一个DummyJob,由于Quartz的主入口始终是Job类
public class DummyJob implements Job { public void execute(JobExecutionContext context) throws JobExecutionException { Job job = (Job) context.getMergedJobDataMap().get("methodInvoker"); if (job != null) { job.execute(context); } } }
jobDataMap就是JobDetail存储状态的地方,DummyJob唯一要做的就是,知道实际的Job类,并且调用它
接下来是戏玉了
Map<String, Job> jobMap = context.getBeansOfType(Job.class); for (Map.Entry<String, Job> entry : jobMap.entrySet()) { String taskName = entry.getKey(); String cronExpression = props.getProperty(taskName); if (cronExpression == null) { logger.warn("[{}] don't have a cronExpression", taskName); continue; } try { Trigger trigger = new CronTrigger(taskName + "Trigger", null, cronExpression); JobDetail jobDetail = new JobDetail(taskName + "Job", null, DummyJob.class); jobDetail.getJobDataMap() .put("methodInvoker", entry.getValue()); scheduler.scheduleJob(jobDetail, trigger); } catch (ParseException e) { logger.error("", e); } catch (SchedulerException e) { logger.error("", e); } }
从Spring context里面读取所有实现了Job的类遍历,props是从文件里面读取相应的cronExpression配置。
JobDetail jobDetail = new JobDetail(taskName + "Job", null, DummyJob.class); jobDetail.getJobDataMap() .put("methodInvoker", entry.getValue());
这两句是关键
于是,Quartz变得更sexy了
« go back — keep looking »