Category Archives: 计算机与 Internet

FunctionalJ 0.8 版本发布

FunctionalJ是一个在java语言中提供Functional Programming功能的library,最早发布版本为0.7,这次的0.8版本较之于0.7版本有很大的差别,API重构程度很大,几乎可以用“面目全非”来形容,不过,较之于前版本,这次发布的一个亮点是文档很全面,而且改进后的API也不能说比原来的差,不过,一个类库的发布应该考虑接口的兼容性问题,而这一点上FunctionJ做得不是太好。
以下是FunctionJ0.8发布的信息:

FunctionalJ provides the following features:

  • Easily represent functions as objects
  • Code using functional programming concepts such as mapping, filtering,
    and folding
  • Use parameter binding, also known as partial application
  • Write methods that accept functions as parameters and/or return
    functions as a result (higher-order functions)
  • Replace procedural code with functional code for simpler, less
    error-prone computations
  • Define functions by implementing an interface, subclassing a base class,
    or with a reflection mechanism, according to your preference
  • Use a reflection mechanism to easily create functions that refer to
    existing constructors, instance methods, or static methods
  • No need to deal with exceptions if you don’t want to.

Summary of what is new in version 0.8:

  • Major refactoring from version 0.7 to improve the API and provide
    stronger typing
  • Now available in two versions, JDK 1.4 version and new JDK 1.5 version
    that uses generic types and other 1.5 features
  • Pluggable reflection mechanisms
  • Two reflection mechanism implementations: one that uses the standard JDK
    and one that uses cglib
  • Improved documentation.

Gotchas With some Ant Tasks

这几天没啥好写的,随便抓个主题涂鸦一下,所以,暂且以Ant的一些Task的问题作为话题。
大部分的Ant Tasks在发行版附带的manual里面都会提供相应的sample build script,但是,如果你以葫芦画瓢的把这些build script片段copy到你的build文件的话,往往会导致你的build失败。
你会说了,靠,官方文档提供的还会有错?!唬人吧你?!
不信?!上眼瞧啦~
<exec dir="D:\InstallAnywhere 5.5 Standard\" executable="build.exe" output="buildresult.txt" error="builderror.txt">
<arg value="${installAnywhere.credit.buildfile}"/>
</exec>
你说这段build script会不会成功那?!没有问题吧?!不过那,虽然理论上应该build Successfully,但,事实并非如此,不管你以这种方式运行什么命令,都会抛出该死的“…IOException:(.*),error=2”错误信息,不信你就试试,除非你采用如下方式来使用exec task:
<exec executable="D:\InstallAnywhere 5.5 Standard\build.exe" output="buildresult.txt" error="builderror.txt">
<arg value="${installAnywhere.credit.buildfile}"/>
</exec>
看出差别没?!你指定命令所在的dir根本没有用,需要通过绝对路径指定executable!不要问我为什么,反正只有这样才能正确调用,I don’t know why either.

平常工作中经常有scp操作吧?!那你看下面这个简单的task能否build成功那?!
<scp file="file location to scp" todir="user:password@server:/home/user"></scp>
铁定的,类似这样的build 失败信息你是吃定了:
BUILD FAILED
…\build.xml:11: com.jcraft.jsch.JSchException: reject HostKey: yourServerIp
不信,你看看ant的manual,他是不是告诉你这么用的?!
所以你现在也不得不信了吧?!还是听我的,在scp task的attribute里面添加一个trust="true"试一试吧!
类似的sshexec task也有这个问题,同样的解决方式。
其实如果你ant用的多了,这些小问题自然而然会冒出来烦你的,不过,google一下子,应该可以很快解决,呵呵,good luck今天就扯这么些

扩展Ant Task之sshexec

随着日方系统管理安全级别的提高,原来自动上传文件的crUploader(自己用RCP写的一个小工具)不能用了,而又不想再返回到原来那种WinScp和SecureCRT手动干预的年代,所以,转而让Ant帮我把上传,执行command,发送邮件等事情全部搞定,不过因为现在执行command的时候都需要指定用户用sudo来执行,所以,其间需要一个手动干预输入password的过程,而Ant提供的默认sshexec又不支持自动给你添入password这个动作,所以相当于将我的后继流程拦腰斩断,实在是如鲠在喉,就因为它,我就得分别手动上传并发送邮件,所以,今天痛下决心,铲除这个刺芒,so here we go…
要扩展ant task,通常都是extends org.apache.tools.ant.Task这个类,不过,既然ant已经提供了ssh的相关类,所以,我们还是extends org.apache.tools.ant.taskdefs.optional.ssh.SSHBase类比较好,呵呵,这样我们就有了:
public class SshExecWithInteractivePassword extends SSHBase
为了有助于理解,后面会给出一些代码片段,不过在此之前,我想先说一下原理比较好哈,其实也很简单,sshexec默认只是打开一个ChannelExec类型的Channel,然后为其设置要执行的command以及command执行后的输出dump到哪里,如果出错,那就抛出BuildException;说白了就是如果执行成功了,把输出打印给你,否则报错;但我们则需要除了输出信息之外,还要通过判断输出,来输入相应信息,也就是sudo后提示的password,鉴于此,我们只需要检测跟Channel相关联的InputStream,如果发现password提示,则向Channel相关联的OutputStream中write相应的password就可以了,easy,right?!
以下是Task的execute()方法的代码:
public void execute() throws BuildException {
// super.execute();
if(getHost() == null)
{
throw new BuildException("Host is required.");
}
if(getUserInfo().getName() == null)
{
throw new BuildException("Username is required.");
}
if(getUserInfo().getKeyfile() == null && getUserInfo().getPassword() == null)
{
throw new BuildException("Password or Keyfile is required.");
}
if(command == null)
{
throw new BuildException("Command is required.");
}
Session session = null;
Channel channel = null;
try
{
session = openSession();
channel = session.openChannel("exec");
((ChannelExec) channel).setPty(true);
((ChannelExec)channel).setCommand(command);
channel.connect();
executeInChannel(channel);
}
catch(Exception e)
{
throw new BuildException(e);
}
finally
{
dispose(session,channel);
}
}

很粗糙的实现,在executeInChannel(channel);方法中,我们检测输出并输入所要求的输入(哈哈,有点儿别扭,不过,相对于Channel来说,它的输入,就是我们的输出),类似于:
InputStream in = channel.getInputStream();
OutputStream out = channel.getOutputStream();
InputStream err = ((ChannelExec) channel).getErrStream();
byte tmp[] = new byte[2048];
do {
while (in.available() > 0) {
int i = in.read(tmp, 0, 2048);
String line = new String(tmp, 0, i);
log(line+"\n");
if(i>0 && PASSWORD_PATTERN.matcher(line).find())
{
out.write(super.getUserInfo().getPassword().getBytes());
out.write("\n".getBytes());
out.flush();
break;
}
}

while(err.available() > 0)
{
int i = in.read(tmp, 0, 2048);
String line = new String(tmp, 0, i);
log(line+"\n");
}
if (channel.isClosed()) {
int exitStatus = channel.getExitStatus();
if (exitStatus != 0) {
StringBuffer exp = new StringBuffer();
exp.append("Error Exit Status with Value:").append(
exitStatus).append("\n\n");
throw new BuildException(exp.toString());
}
break;
}
try {
Thread.sleep(1000L);
} catch (Exception exception) {
}
} while (true);
呵呵,我知道代码很烂,不过将就看吧,我也不会为了这么个简单的小功能去费那个劳神子,一切OK之后,打包成jar,扔到ANT-HOME/lib下,这样你就可以使用它了:
<taskdef name="dsshexec" classname="org.darrenstudio.extension.anttasks.SshExecWithInteractivePassword"/>
<dsshexec host="server" username="user" password="pwd"
command="sudo cp /home/wfq/result.txt /usr/local/" trust="true"/>
注意哦,上面的情况可不是普遍适用的,只是给你一个类似问题的提示而已,使用的话,可能要根据环境adapt一下,OK,打完收工!

By the Way:上次那个Gotches是我的疏忽,敲错一个字母,应该是Gotchas,意即“I’ve got you",不过,一般字典上查不到

使用springdoclet简化基于spring框架的应用开发

(simplify the development process of springframework-based applications)

Author:DarrenWang @ 2006-07-28
楔子
之前写过几个基于spring的程序,因为规模不是很大,所以,开发过程中并没有探索更有效的开发流程,仅仅是手工编辑spring-config.xml文件,虽然通过个人的细心与努力最终能够成功的完成这些程序,但是,开发过程中也难免的暴露一些问题,在无意间看到springdoclet的时候,萌生了我想写下这篇文字的念头…

在说springdoclet带来的便利之前,我还是先说说之前的开发流程,这样也好有一个比较。
在没有任何途径得知基于spring框架开发流程是一个什么样的情况下,我开始了我的第一个spring-based应用的编写,在穿梭于源代码和配置文件之间多时之后,我最终形成了类似风格:将配置文件先扔一边,直接编写应用类,如果该类依赖于其他类,那我声明他们,然后IDE直接生成相应的setter,getter方法,当所有应用类完成之后,从主入口类开始,依次添加到spring-config.xml配置文件中,因为类似于从上倒下的索引形式,通常也不会遗漏任何类,所以也可以很顺利的配置下来,加上SpringIDE的支持,这种方式也可以很好的完成我的工作。
不过,我前面说过,这个流程也存在一定的问题和不便:
首先,因为是最后一次性将类配置完成,而即使程序规模不大,那类的数量通常也不会太少,起码10几个二十几个应该有吧,加上属性等,那你在最终配置的过程中就需要尤其谨慎,因为这么些信息的组织稍有不慎就可能出错(所以,后期我有时候直接是写一个应用类就直接配置到spirng-config.xml中);
其次,虽然Eclipse等类似的IDE现在对于重构支持已经很不错,但是,他们也仅仅是对源代码级的重构支持很好而已,对于其他的相关配置等却基本没有支持,而你又不可能保证你的所有类配置到spring-config就一次通过运行(有这样的可能,不过很少),所以你需要时不时的调试并修改相应的源码,这个时候,源代码的变动势必造成配置文件的相应修改,没有了重构的支持,可想而知,遗忘或者稍有大意,错误就会更愿意频频光顾你了。
当然,说这些不是说以上流程就一无是处,而是为了找出可以解决这些问题和不便的方法,所以,下面我们来看看springdoclet是如何帮助我们避免以上的纠缠的。
我不想对springdoclet做详细的介绍,他只属于xdoclet为spring开发提供的一个task,想知道有关的更多信息,可以参考后面罗列的几个参考项,我这里只想说一下使用springdoclet后的开发流程以及与原来相比有什么便利:
使用springdoclet,我们现在就可以完全撇开手工配置spring-config.xml配置文件这一过程,而全身心的投入到应用的开发过程中。现在,我们只需要关注应用类的编写就可以了,也就是说,现在的开发流程其实也就是上面提到的流程的前半部分,唯一的差别就是,现在,我们在完成一个类之后,要相应使用springdoclet的Tag为其标注相应标志和依赖。虽然就这个差别,实际上可以随着项目规模的增长,为开发带来的更大的便利。
现在你只要关注单一的一个文件—java源代码,再也不用穿梭于源文件和配置文件,管理一个类和管理多个类加上配置文件相比,无论是工作量还是出错几率,都大大降低了;
而且,随着项目规模的增长,为了增进开发人员的协作,我们通常会将spring-config.xml按照功能或者层次等分成多个来使得开发人员的开发可以并行进行,虽然如此,依然会存在多个开发人员需要注册同一个配置文件的情况,而使用了springdoclet之后,每个开发人员只需要关注自己开发的相关应用类并标注他们就可以了,完全不可能出现多个开发人员在一点出现冲突的情况;
对于调试过程中的重构来说,因为你现在只关注源代码文件,所以,随着你对代码 的更改,对于就在眼前的javadoc,我想你不会视而不见吧?!只需要更改相应的javadoc,再也不用再跑到配置文件中搜寻相应配置并手工更改了。

补充说明
好像光说原来流程的不足以及光说基于springdoclet流程的长出有些不公平,所以,我还是再补充一下,以免有失偏颇。
其实,原来的流程也不是一无是处,既然是手工编辑,那么你就可以拥有最大的灵活性,你可以根据需要添加属性,可以像在java中应用DesignPattern那样在xml配置文件中引用类似配置结构,这些在某种程度上提高了配置文件的可读性及可维护性,而使用springdoclet,你通常就做不到这些,因为模板是写死的,他只能照章办事,呵呵,跟“制度是死的,人是活的”一个道理,虽然你可以自定义template,不过,你也会相应的付出一定的代价;
另外,springdoclet也不是对所有的spring.dtd中的结构都提供了很好的支持,就像后面要提到的,某些结构还是需要你自己来添加相应的支持;
还有就是springdoclet也不会为我们生成所有需要的配置,某些情况下,需要我们自己手动维护某些文件,虽然可能通过某些方面的努力来让他为我们生成所有的,不过,在付出的代价和取得的成果之间,最好有一个权衡。
其他的就不再赘述了,随着开发的进行,我想更多的感受会自动过来找你的。

TroubleShooting
1.springdoclet对于某些property注入默认不提供支持,这种情况下怎么办?!
类似问题可以参照后面的参考资料部分第二项《扩展XDoclet对Spring List引用注入的支持》,xdoclet框架有很好的可扩展性,你可以按照自己的需要来对其进行扩展。对于《扩展XDoclet对Spring List引用注入的支持》这篇文章来说,我觉得文中提到的对原发布包进行更改的方式欠妥,实际上,完全可以通过自定义模板或者添加merge point来实现相同的功能,对原发布包也没有侵入性,以后要share的话,也仅仅只需要share模板就可以,第三方可以通过任何途径取得xdoclet的发布。

2.springdoclet只能根据源代码中的tag生成相应的配置文件,但是,对于不属于当前项目的类,如何将其集成到generate过程中?!
其实,我想说的是,对于当前项目的源代码,你可以通过添加相应的tag来实现类的注入,springdoclet也可以根据这些tag为你生成相应的配置文件,但是,对于第三方类库来说,你无法对其源码进行任何操作(实际上,大部分都是class形式发布),这个时候,我们如何让springdoclet在生成配置文件的时候将这些类一起merge到最终的输出中那?!
我觉得这个问题本身已经不能放到code generation这个范畴,springdoclet也不能为我们做这个事情,实际上,完全可以单独设置一个配置文件,按照需要声明这些依赖类,而源码中可以通过tag标注这些依赖类的引用,最后,结合这个配置文件和最终生成的配置文件就可以了。
GOOD LUCK!
参考资料:
1. Manning《XDoclet In Action》by Craig Walls ,Norman Richards(建议读一下,里面对于Code Generation和XDoclet的一些理念对于开发和设计理念很有帮助,即使没有时间,起码读一下第一,第二,第12,13章,或者后面相应的appendix)
2.《扩展XDoclet对Spring List引用注入的支持 》from http://www.crackj2ee.com/Article/ShowArticle.asp?ArticleID=557
3.XDoclet documentation from http://xdoclet.sourceforge.net/
————————————blog_draft_20060728

谈java中的动态条件查询(Dynamic Criteria Query In Java)

偶然的机会,发现开发中的这个共通的问题—动态条件查询,故此决定结合自己当初的开发方式以及网上各种观点,对这个问题点作一个分析和总结,以供参考。
在我们的开发过程中,经常需要面对各种数据的查询需求,比如说检索顾客信息,根据业务视图抽取相关数据做成报表等等,而对于这些查询,有的时候查询条件是固定的,比如说检索所有的顾客;但有的时候则不然,查询条件会不固定,像用户可以根据需求选择相应的查询条件,比如这次要根据姓名查询,下次可能就会根据年龄段来查询等等,像这类问题,查询的处理就会比固定条件查询要复杂一些,所以,下面我们就对这种动态查询的情况做一个总结,以期引入更多观点来完善相应问题的解决方式。
为了说明各种方式的差异,我们需要一个实例来作为比较的标准,所以,假设我们拥有以下查询条件画面(因为只是实例,所以无论从画面还是表结构上都做了很大的简化,实际项目中要复杂的多):
————————– —
顾客姓名| | |暧|
————————– —
————————-
电话号码 | |
————————-
+————————————–+
| [X]家庭固定电话 [X]移动电话 |
| [X]亲属电话1 [X]亲属电话2 |
| [X]工作地电话1 [X]工作地电话2 |
+————————————–+

查询需求:
1.用户可以输入顾客姓名进行查询,默认查询模式为模糊查询,如果用户点击[暧]按钮,可以在[暧]-[前]-[后]-[全]四种模式中选择,分别进行模糊查询,前向匹配查询,后向匹配查询和完全匹配查询;如果用户没有输入顾客姓名,则顾客姓名不加入查询条件;
2.用户可以输入电话号码进行查询
2.1用户只是输入电话号码,而没有选择下方group中的相应电话类型,则查询条件中不加入电话号码的条件限制;
2.2用户没有输入电话号码,不管选择还是没有选择下方group中的相应电话类型,则查询条件不加入电话号码条件限制;
2.3用户输入了电话号码,并选择了下方group中的最少一项电话类型,加入电话号码和电话类型的查询条件进行检索。
3.如果用户没有添加任何查询条件,进行全检索。

查询需求的实现方式
1.SQL语句的字符串拼接(SQL String Concatenation)
这种方式是从我大学时期做兼职时代就开始的一种实现方式,在我没有找到更好的解决方式之前,也是我解决类似问题的唯一方式,缺点自然不用多说,各种条件的判断纠缠在一起,后期维护很难;但是,如果后期不会加入太多新的查询条件的话,测试完成后就基本可以不用管了(对了,说到测试,这东西也很难测试的哦!)。
下面是对于实例画面给出的一个参考拼接结果,当然不是唯一的,仅作参考:

StringBuffer criteria = new StringBuffer();
criteria.append("SELECT DISTINCT CustomerID FROM Mastercustomer as cust WHERE ");
int flag = 0;
String surnameKanji = model.getSurNameKanji();
if(!CustomerValidator.isBlank(surnameKanji))
{
switch(model.getSurNameKanjiFlag())
{
case CmpQueryState.LEFT_MATCH_STATE:
criteria.append("CUSTOMERNAME LIKE ‘"+surnameKanji+"%’ AND ");
break;
case CmpQueryState.RIGHT_MATCH_STATE:
criteria.append("CUSTOMERNAME LIKE ‘%"+surnameKanji+"’ AND ");
break;
case CmpQueryState.ALL_MATCH_STATE:
criteria.append("CUSTOMERNAME = ‘"+surnameKanji+"’ AND ");
break;
case CmpQueryState.AMBIGUOUS_MATCH_STATE:
criteria.append("CUSTOMERNAME LIKE ‘%"+surnameKanji+"%’ AND ");
break;
}
flag++;
}
String tel = model.getPhoneNum();
if(!CustomerValidator.isBlank(tel))
{
tel = telFormatter.format(tel);
int innerflag = 0;
criteria.append("( ");
if(model.isHomeTelSelected())
{
criteria.append("APPLHOMETEL = ‘"+tel+"’ OR ");
innerflag++;
}
if(model.isKin1TelSelected())
{
criteria.append("KIN1HOMETEL = ‘"+tel+"’ OR ");
innerflag++;
}
if(model.isKin2TelSelected())
{
criteria.append("KIN2HOMETEL = ‘"+tel+"’ OR ");
innerflag++;
}
if(model.isOffice1TelSelected())
{
criteria.append("APPLWRK1TEL = ‘"+tel+"’ OR ");
innerflag++;
}
if(model.isOffice2TelSelected())
{
criteria.append("APPLWRK2TEL = ‘"+tel+"’ OR ");
innerflag++;
}
if(model.isMobile1Selected())
{
criteria.append("APPLMOBILE1TEL = ‘"+tel+"’ OR ");
innerflag++;
}
//———–DELETE USELESS WORDS—————
if(innerflag == 0)
{
criteria.delete(criteria.length()-2,criteria.length());
}
else
{
criteria.delete(criteria.length()-3,criteria.length());
criteria.append(" ) AND ");
flag++;
}
}
// finally
if(flag == 0)
{
// In this way, the user select no query constraint field
// we need to delete the "WHERE" from the StringBuffer’s end
criteria.delete(criteria.length()-6 , criteria.length());
}
else
{
// here, the user select one or more query constraint field,
// we need to delete the "AND" from the StringBuffer’s end
criteria.delete(criteria.length() – 4 , criteria.length());
}
return criteria.toString();

可能一些地方还可以节俭,但你还是可以看出,这种方式是多么的复杂,不仅要维护条件的上下文,而且还要根据情况添加查询条件,我想你看到这么多的条件判断语句已经很faint了吧,呵呵,不过这还只是一个简单的查询页面,想想一个页面上几十甚至上百个的查询条件,这种方式恐怖你就可想而知了,开发效率,健壮性,可维护性,这些都是问题啊…
但,我想,有些时候,如果其他方式无法解决的话,这也只能是你的last resort了。
NOTE:这种方式虽然复杂,但是同时也可以给你最大的灵活性,“路怎么走,你看着办咯”
另外,实际项目中,出于安全性考虑,最好对SQL进行escape,以防止SQL injection攻击等,原型就是原型,我们这里不可能面面具到的。
2.iBatis的DynamicSQL支持
iBatis针对这种动态查询提供了一种DynamicSQL的支持,通过在其SQLMap中定义查询条件,减少抽取逻辑和程序之间的耦合,而且,这种SQL的组装是通过XML来完成的,通过合理的处理,相对于SQL语句拼接方式来说,开发效率上更胜一筹。
让我们来看一下相应于实例画面的查询,DynamicSQL是如何实现的吧:
<statement id="yourQuery" resultMap="yourRetMap">
SELECT DISTINCT CustomerID FROM Mastercustomer as cust
<dynamic prepend="WHERE">
<isNotEmpty property="customerName" prepend="AND">
<isEqual property="customerNameSearchMode" compareValue="0">CUSTOMERNAME LIKE ‘#customerName#%'</isEqual>
<isEqual property="customerNameSearchMode" compareValue="1">CUSTOMERNAME LIKE ‘%#customerName#'</isEqual>
<isEqual property="customerNameSearchMode" compareValue="2">CUSTOMERNAME = ‘#customerName#'</isEqual>
<isEqual property="customerNameSearchMode" compareValue="3">CUSTOMERNAME LIKE ‘%#customerName#%'</isEqual>
</isNotEmpty>
<isNotEmpty property="telNum" prepend="AND">
<iterate property="telTypeList" open="(" close=")" conjunction="OR">
telNumber=#telTypeList[]#
</iterate>
</isNotEmpty>
</dynamic>
</statement>
更多信息可以参考iBatis提供的Reference…
btw.个人还是很看中这种方式的
3.Hibernate的(Detached)Criteria或者HQL拼接
如果你的系统当前的persistence层用的是Hibernate的话,那恭喜你,在你享有hibernate当前便利的前提下,针对这种动态查询问题,你还会享有hibernate提供的(Detached)Criteria或者HQL灵活拼接的支持。
只要你将相应的SearchContext中的查询条件设置到(Detached)Criteria中,那么你就可以直接取得你需要的查询结果就可以了,所有的什么拼接啦,查询结果组装啦什么乱七八糟的事情统统全免,是不是很惬意那?!不过,前提是你的系统persistence用的是hibernate,而且,实际上,(Detached)Criteria也不是万能药,对于复杂的查询,他也依然无能为力,所以,这个时候,不好意思,你还是的求助于字符串拼接的方式,不过,这回不是SQL的拼接了,而是HQL的拼接,不过原理是一样的,这里就不做赘述了,下面只是列出使用(Detached)Criteria的实现代码片段以供参考:
DetachedCriteria detachedCriteria = DetachedCriteria.forClass(Mastercustomer.class);
// … 根据情况取得相应的Criterion,如Criterion nameCriteria = Restrictions.eq("customerName",customerName);
detachedCriteria.add(nameCriteria);
if(notEmpty(telNum))
{
Disjunction disjunction = Restrictions.disjunction();
for(int i=0,size=telTypeList.size;i<size;i++)
{
disjuction.add(Restrictions.eq("telNum",telTypeList.get(i)));
}
detachedCriteria.add(disjunction);
}
Criteria criteria = detachedCriteria.getExecutableCriteria(session);
return criteria.list();

以上就是我本人对于这种动态查询条件相关问题解决方式的几点认识,如有谬误,还望指正。希望以上文字可以为大家解决相关问题提供一定的思路和解决问题的方向,如果大家还有什么更好的解决方式,也可以放到网上与大家共享,毕竟现在是互联网的时代 :->

篇后语
感谢Sun Java Forums 和javaEye Forum中的开发者共享他们的观点,使我能够可以了解更多相关信息并促成这篇文字的诞生,同时也要感谢万维网和google的支持,没有他们,我也无法顺利的形成这篇文字并将其与大家分享…
参考文献:
1《iBatis SqlMap Developer Guide 2.0》
2《Hibernate Reference》
----------blog-draft-200060620

反射及FunctionJ的应用(Apply Java Reflection and FunctionJ)

不知道大家在日常开发过程中有没有碰到类似的场景,即根据一个条件,后面会跟着多个(通常是两个)处理逻辑。我想,或多或少的会碰到这样的场景吧!反正,我回头想想,加上近期工作,也不少地方遇到了,所以,就想总结一下类似问题,能够列为best practice当然就更好了。
以近来做的一个报表为例吧,这个报表逻辑很简单,根据顾客支付利息的方式是先期支付还是后期支付或者是按月支付方式来打印不同的报表样式,这样总结到上面一般的情况就是,根据支付方式这一个条件,后继处理需要首先根据这种方式取得相应的报表样式(处理1),然后根据这种支付方式实现相应的报表render逻辑(处理2)。
伪代码的形式可能是:
// get paymethod in your own way
if(paymethod==1)
{
//get template for paymethod 1
// render report for paymethod 1
}
if(paymethod == 2)
{
// get template for paymethod 2
// render report for paymethod2
}
// …etc.
我想这个是最一般的实现了,但是,最一般的实现通常也是最有改进余地的实现,所以,让我们来看一下如何对这个进行改进吧!
我们的目的是去除代码中的条件判断,而实现这个目的,我这里给出2个实现途径:
1-使用反射(java reflection)支持
2-使用FunctionJ的支持
不过,在此之前,我还是先说一下配置问题吧,为了简单,我们直接将条件和相应的处理映射放到properties文件中,这样,我们有了类似于如下内容的mapping-config.xml:
1=templateLocation4PayMethod1,renderReport4PayMethod1
2=templateLocation4PayMethod2,renderReport4PayMethod2

其中,等号前面是以payMethod的值作为key,出于其他情况考虑,你也可以使用其他的作为key,只要保证唯一性就可以;等号后面第一项为对应的模板的位置,第二项为对应的报表render方法,这个方法通常在实现类里实现。
(注:以上配置也可以通过Jakarta commons Configuration来实现或者自己写一个实现也可以)
东风有了之后,我们开始行船啦。
在主程序逻辑中,我们现在就不需要if判断语句了,让我们来看这种条件判断是如何去除的吧!
1-使用反射的方式(异常处理这里忽略)
Properties mapping = new Properties();
mapping.load(getClass().getResourceAsStream("mapping-config.properties");
// get PayMethod in your own Way
// get Properties with your PayMethod as Key to Properties
InputStream templateIns = getClass().getResourceAsStream(properties[0]);// get Template For payMethod
String renderMethod = properties[1];
Method method = getClass().getDeclaredMethod(renderMethod,new Class[]{InputStream.class,other.class});
method.invoke(this,new Object[]{templateIns,otherObjects});// invoke render method for corresponding paymethod
// DONE!
这里省略掉一些读取配置信息的代码,只保留了可以说明问题的部分,在这里,我们首先load配置文件,然后,根据取得的PayMethod取得相应的Property值(这里要对取得的值进行相应处理,因为2个值是以一个字符串的形式返回的),之后,根据取得的模板位置取得模板,根据取得的方法名取得Method实例,最后,反射调用Method就可以了,只是,传入的参数要根据你的实现来决定。
(注:取得多个Property值最简单的方式就是用String的split方法来分割,你可以根据情况给出一个通用的实现,extends java.util.Properties类或者delegate it)
2-使用FunctionJ的方式
这个小东西很早以前就在TSS上看到过,不过,没有怎么用,因为给我的感觉他虽然提出了一些概念性的东西,但是,实现的话,实际上也只是在reflection基础上进行的封装,不过,如果你不想用reflection或者你想体验一下functionJ提供给你的功能的话,这部分你可能会感兴趣。(项目位置在http://functionalj.sourceforge.net/)
加载配置文件和加载模板的部分是相同的,我们只是列出方法调用部分的代码以示差别:
// …same as above codes
String renderMethod = properties[1];
Function f = new InstanceFunction(CurrentClass.class, renderMethod, this);
f.addParameters(new Object[]{parameters}).call();
// DONE!
怎么样?!没啥大的差别吧,呵呵,其实他的大部分实现类都是扩展的ReflectionFunction,这就是我为什么说他也仅仅是对reflection进行了一下封装。不过,你可以通过fuctionJ了解一些概念性的东西。
OK,我们通过以上两种方式都去除了多重的条件判断,目的达到,虽然看起来没有省去太多代码,不过,如果你的条件判断语句很多的话,你就不会这么说了,而且,这种方式的可扩展性很好,不妨试试?!:-)
----------------blog-draft-20060705

由工程的组织结构所引发的…

昨天刘石过来找我,想让我在原来领受书project的基础上给他重新建立一个工程(project),然后他好在原来的基础上进行开发,而不用说一切从头开始,这样也可以重用原来的一些services,虽然当时我认为没有必要为了一个新的文书发送功能就新建一个project,并且认为直接在原来 project下进行开发也没有什么不妥,不过,后来在其一再要求下,我还是copy了一份receipt功能,然后share到了CVS服务器上。
后来想想,实际上,我觉得我当时的想法是恰当的。造成现在这种形势的原因,我想是因为credit组一贯的工作作风引起的,呵呵,不是说有多么的罪大恶极,不过,确实可以通过其他方式来让工程的组织结构更加合理一些,冗余也可以削减不少。
原来只要有一个新的功能,如果不是BackOffice相关的,就会独立的为其建立一个新的project,但通常的情况下,这种项目结构组织方式存在一定的局限性,最突出的一点就是会造成很大一部分冗余的存在。
我们以现在的BackOffice工程组织来看,他分成了三个独立的project,而project的划分也仅仅是根据源码完成的功能大体完成的,因为三个工程独立存在,他们拥有自己的classpath,而这三个独立的classpath中的entry要毫无交叉,这种情况可能少之又少,事实也的确如此,三个project的classapth中存在很多个重复的dependencies,单从这一点,我们来看会造成什么问题:
第一个,最明显的情况,每个project拥有自己的一份classpath拷贝,相同的dependencies不能共享,这是很明显的冗余情况,还有就是,像上面的为刘石copy工程的做法,services代码的重复存在等,都是冗余的表现,而你要为这些冗余付出存储空间和管理的代价;
第二个,各个工程的dependencies中,即使存在相同的entry,但有可能这些entry的版本不一样,比如,project1中的 hibernate可能是2.0.x版本,而project2中则可能是hibernate2.1.x版本,这些dependencies如果最后一同发布的话,在一个classpath中就会存在hibernate的2个不同版本,而通常的项目都是默认的classloader从单一的classpath来完成类文件的加载,这个时候,势必造成类文件的版本冲突,为系统的稳定运行埋下隐患;
第三个,虽然从某个角度来说,使得项目一依赖与项目二是合理的,但如果存在项目二反向依赖项目一的情况的话,现在的工程组织结构又凸现出不合适的地方,比如项目发布的时候,会根据project1里的config文件发布不同版本,而project2中后期发现需要访问这个config文件来实现相应功能,同时project2原来并不依赖project1(project1依赖project2),这个时候project2就陷入某种两难的境地(最少2中方式解决,但都不是很合适)。
而这些问题其实可以通过很简单的一个结构组织的调整得以解决:类似以上情况,我们完全没有必要将一个项目分成多个来管理,而是只管理一个项目就可以了,唯一需要改变的就是,为可以模块化的功能提供单独的 source folder!这样,所以的dependency现在可以统一到一个classpath进行管理,而以上的冗余也不会存在,就算原来各个工程中的不同版本类库冲突的问题,现在也因为只需要在当前classpath中维护单一版本的类库而得以解决。(题外话:实际上,解决第二种问题的方式,可能因为各个项目环境的不同而不同,虽然,可以通过我上面说的合并方式可以解决,但不能说解决了所有情况下的类库冲突问题,因为有些时候,需要依赖不同版本类库的情况是存在的。不过,即使遇到这种情况也没有必要烦躁,呵呵,依然可以解决 ,比如,你可以写一些相应的自定义classloader来分别加载这些类,或者,为了偷懒(或许说为了避免重新发明轮子更确切一些哈),你可以借助于OSGi,你都用EclipseIDE开发了,不会连他的插件体系如何实现的都不知道吧?!一个道理的啊,呵呵,如果觉得adapt这个体系到你的需求不容易,你也可以借助于objectWeb的oscar,他也是OSGi的一个实现,应该可以帮助你解决相应问题)
其实,问题的解决通常并不一定就只存在一条解决的路,这条路可能你很熟悉,知道能够达到你的终点,但是,不要排斥其他的路线,或许,还有比现在你知道的路更近,或者更好走的路也未定。不要让定式思维禁锢你,条条大路通罗马,为什么不找一条最适合你走的路那?!
Apple的理念:THINK DIFFERENT!
--------------blog_draft_20060726

CREDIT项目阶段反思

CREDIT从2004年开发至今,也快2年的时间了,现日方想对这个系统的SERVICE进行提取总结以便以WEB Service的形式开放并贩卖,所以,之前的系统在某种意义上已经处于维护阶段,当然也会时不时添加新的功能,但开发的主体从现在开始应该转向web应用的开发上去,在这种背景下,有必要对原来的系统从设计到实现上给出一个反思,以便更好的进行以后的工作,提出系统的缺陷不是为了诋毁当前系统,而是在这些缺陷的基础上,给出一些反思和解决方案,从而在以后的系统设计和实现中更好的改进并消灭类似的问题点,进而可以打造更加健壮,更加易于维护的系统。
因为思绪较为跳跃(好听一点儿就是活跃,呵呵,其实就是意识流),所以,罗列的条目可能不会按照一个较为有条理的顺序展示;同时,可能个人经验和阅历上的不足,以下罗列的可能是个人的偏颇见解,全作不完全参考。
1-数据库设计
当前情况:
数据库schema的设计,从业务上来说,应该没有什么大的缺陷,因为是日本人给出的设计,并且他们对于业务较为熟悉,所以,没有太多纰漏;但,这并不是说他们设计的数据库schema就是完美的,像今天上午讨论的事件表和贷付禁止表,应该说就存在问题,尤其是贷付禁止,个人感觉,当前的设计纯粹就是一个履历表的作用了;
除此之外,虽然日本人在后面陆续加入了一些Timestamp型的字段来记录一些操作信息,但这些字段除了这个作用,也就没有起到其他作用了,尤其是,各个表在根本没有考虑到最基本的乐观锁的概念,从而导致某个时期同步问题很多,尤其对金额字段进行操作的时候,虽然说通过某些手段暂时解决,但也不可避免的存在某些风险,比如陈旧数据覆盖最新数据等情况。
Alternative:
针对同步的问题,添加乐观锁,比如hibernate就提供了对该概念的最基本支持;
像贷付禁止表的问题,应该分化该表的作用,重新为其添加履历表,而不是将禁止和解除的记录全都放在这里,解除和禁止的时候添加和删除相应记录,并添加到履历表,分化贷付禁止表的作用;
其他的就不好再说了,毕竟,我也没做过太多DB Design,呵呵
2-系统基础架构
当前情况:
2004年项目刚开始的时候,徐敬琪搭建的完全基于SWT的底层框架,提供了窗体等的生命周期管理和他们之间交互的消息机制,所有窗体需要实现InnerFrameClient自定义接口,以便底层框架可以控制其生命周期中的各种situations;总得来说这个底层架构打的挺成功,就是添加窗体的话,可能配置的地方很多,在操作上有些繁琐。
Alternative:
其实,我们开发CREDIT的时候,Eclipse已经是3.0.1的版本了,当时RCP(Rich Client Platform),已经可以用于实际生产环境,但当时可能开发进度太紧,更不没有足够时间调研,而且当年也是刚接触SWT/JFace,所以,没有发现这个好东东,如果以后开发类似应用的话,完全可以使用RCP作为底层架构,没有必要再自己开发一套出来,而且,自己开发出来的想要重用也很困难,需要花费更多精力去重构他,有如此好的Wheel可以用,何乐而不为那?!
3-版本的更新
当前情况:
系统每次添加新的功能之后,通过了开发人员和测试组成员的完全(其实根本不可能)测试之后,需要使用InstallAnywhere来重新打包发布新的安装文件,之后将安装文件部署到发布的Location供操作员下载并重新安装使用;每次繁琐的发布过程暂且不提(使用我后来提供的Ant脚本后,繁琐程度降低了不少),光是源文件和各个版本之间的管理就够人头疼的,虽然每次发布完版本后,现在都是打上tag,但是,依然存在一定风险,比如,从当前tag开始,开发人员已经开始了下面新功能的开发,而每次源代码编写还没有经过测试和核对的情况下,大部分开发人员就将这些代码commit到cvs,而且源代码文件很多,当因为进度吃紧,而只是想发布一部分新的功能的时候,开发人员更不就分不清那个源文件该替换,该替换的源文件又应该从那个commit替换回来,往往将整个的package都update一遍,这样就有可能导致功能和数据库schema等不同方面之之间的不一致问题的产生。到现在没有出现太大的问题,个人感觉实在是万幸。
Alternative:
其实,我们可以使用Java Web Start来部署我们的应用,这样,就不用说每次都重新打包发布新的安装文件,而通过顶点来统一客户端的部署和更新;如果这种方式还不能满足需要的情况下,同时底层框架又使用RCP的话,我们可以使用RCP的update机制(也就是Eclipse的update机制),每次有新的功能发布,我们将这些新功能实现和打包为不同的plugins,并部署到update Site上面,这样,也可以很好的解决同一部署和更新的问题,而不用劳烦操作员动不动就需要重新安装新的客户端,也可以减少人为的错误。
4-系统配置的管理
当前情况:
所有的配置文件,不管什么格式的,properties或者xml格式的,没有一个统一的访问接口,比如xml通过我们自己实现的Configurator来读取,properties直接通过Properties类load进来等等,当然,像我们的Configurator类通过xpath来处理资源的读取可以很好的处理其请求,但,我们没有处理多个配置文件统一访问的情况,而且,如果将不同的配置需求分开,本身可以更便于管理。
Alternative:
可以对当前的Configurator等配置utilities类进行整合,以提供不同配置文件的统一访问机制,但因为Jakarka Commons Configuration实际上已经为我们做了这部分工作,所以,还是不要再自己发明轮子的好。通过Configuraton接口,可以对ConfigurationFactory加载的不同配置资源进行统一的访问,岂不easy?!不过,我在作demo的时候发现,好像他对于XML的attribute的访问有些限制,当然,可以通过其他方式解决;
除此之外,如果使用RCP,我们可以通过他提供的Preference机制来进行系统的配置。
5-质量保证
当前情况:
当前的CREDIT的质量保证,可以说,开发人员并没有起到太多他应该起到的作用,完全由测试组人员担当了最主要一道也是最后一道质量防线,开发人员在将功能编码调试通过之后,通常就直接将系统仍给了测试人员进行测试,这无法为测试人员增加了太多的负担,而开发人员在忽视了自身的一些职责,因为,软件的质量的第一步也是最基本的质量考核应该从开发人员的代码开始进行审核,代码是所有质量保证的基础,没有高质量的代码,也很难说能够有高质量的系统,这就好比购买笔记本一样,为什么那么些人宁愿多花银子购买ibm等国外厂商的产品而不愿购买国内更为便宜的本子那?!其实,笔记本的基本构件和原理都差不多,但是,国外能够生产出高质量的构件,并将这些构件以严谨科学的态度实验后组装在一起,但国内的厂家可能就只是买回来构件,不管这些构件是否兼容就直接组装出售了(没有贬低国内厂家的意思,我也希望国货自强,但首先应该接触浮躁)。
Alternative:
针对开发流程进行改进,让开发人员充分发挥其主导作用,与其让测试人员帮你被动的防御,还不如我们主动的防御来的效果更好一些。例如,使用TDD(Test-Driven Development)进行开发,每次在实现一个新的功能点或者单个的功能类的时候,我们首先编写针对这个功能点和类的单元测试(Unit-Test),以单元测试作为我们思考的Prototype,并不断加以重构和改进(要知道,单元测试是使用我们功能类的第一个地方,在这里,如果这个功能类易于使用,设计优良,那么,将这个类加入到你的系统之后,他可以以同样的高姿态展现自己并发挥作用),而不是说当这个功能点或者功能类开发完成之后,为了验证才去些他的单元测试,这个时候,你的头脑中已经下意识的形成了一个观念,你写的Unit-Test也只是对你认为的流程进行的测试,即使有什么问题,你也不会测出什么结果的,这样的单元测试或许会偶然发挥作用,但已经没有什么意义了。
所以,有了单元测试作为第一重保障之后,开发人员就可以对功能类大胆的进行重构和改进,因为你有自己的防线啊!在所有的代码通过Unit-Test之后,我们就可以move到下一步,进行系统的集成测试等,当这些白盒测试都通过之后,我们才会将我们信心十足的系统开放给测试组人员,让他们为系统作黑盒测试,为系统的质量保证和安全奠定最终基点。
6-认证和授权
当前情况:
从JAAS(Java Authorization Authentication Service) Adapt过来的一个自定义解决方案,对于认证方面的需求要求不是很高,所以基本满足要求,但授权方面,可能策略较为复杂,也不能很好的处理,所以,这个框架没有发挥完全的作用,但现在工作的很好。
Alternative:
我现在也没有想出更好的替代方案,第一印象当然就是JAAS啦,除此之外,像基于Spring的Acegi安全框架啦,OpenSymphony的OSUser(未发布正式版)啦等等,都不是可以很好的集成到现在的系统中,或者说很好的集成到Standalone形式的应用中,如果其他人有更好的建议,希望可以分享你的观点。

[entry topic="系统异常体系"]
当前情况:
无论是从数据访问层来看还是从业务层来看,CREDIT系统都没有一个设计合理的异常体系结构来支撑,或者说根本就没有什么异常体系;数据访问层只是单纯的以单一的一个自定义的DaoException(Checked Exception)来向上层抛出,业务层根本就无法根据异常来判断应该进行什么样的处理,进而导致在业务层也以单一的自定义ServiceException来标志业务处理异常,可以想像,最终导致的结果就是以粗暴到何种简单的程度向用户进行反馈,“系统发生内部错误,请与系统管理员联系…”
Alternative:
对于一个设计良好的系统来说,一个好的异常体系可以为系统的健壮性提供很好的保证,如果系统可以重新实现的话(当前系统因为力包稳定,而且重构的范围很大,导致困难重重,所以,基本不存在重构的可能),在数据访问层,可以考虑引入Spring提供的数据访问层现成的异常体系,或者根据情况,给出一套自己的实现也可以;而业务层则需要结合CREDIT系统业务逻辑和当前系统情况,从总体上给出一个设计良好,分类详细的适合当前系统的业务层异常体系结构,使客户端代码可以根据这个良好的体系结构为用户提供相应的反馈,使得系统的健壮性和用户友好度可以更进一步。
[/entry]
----------------以下为系统实现细节中的部分反思-----------------
7-数据访问
当前情况:
徐敬琪针对hibernate和jdbc两种数据访问方式给出不同的父类实现,子类要继承相应的父类以便访问相应资源,目前系统中使用并没有出现什么大的问题,但个人在使用中依然能感觉有些不便之处:不管使用哪一个实现类,只要你使用他,就必须记得dispose掉,否则数据库资源就无法释放了,像资源释放等问题,我想不应该扔给调用方来关注;在jdbc的数据访问类中,你依然要处理那些与你的需求基本不相关的Statement,Connection等,这无疑增加的代码的出错几率,而且不易维护(较多的分散)。
Alternative:
使用Spring提供的HibernateTemplate和JdbcTemplate可以很好的进行数据访问操作,无须开发人员较多关注不必要的关注点,像资源释放,Connection和Statement等的取得等问题,统统屏蔽,对调用方保持透明,使调用方可以更多关注于数据逻辑的处理。
8-事务控制
当前情况:
在HibernateDao和JdbcDao数据访问对象的基础上,提供基于Session,Connection一级的事务控制,开发人员在事物的开始和提交等事物点的处理上不尽统一,造成代码混乱,很不容易维护,这我是亲眼目睹的,真的可以用惨不忍睹来形容;
Alternative:
可以使用Spring提供的事务抽象层,以统一的方式管理事务控制,虽然针对Hibernate和Jdbc,Spring提供了PlatformTransactionManager的两个不同实现:HibernateTransactionManager和DataSourceTransactinManager,但是,前者可以统一管理后者的事务,所以,依然较为单一的方式管理事务。
事务管理一级只限制于业务层,Data Access不应该纠缠于事务,针对不同的业务层Service,根据业务粒度,可以通过暴露相应的事务控制方法或者暴露相应的业务实现类来统一事务的管理策略,总之,Data Access层绝对不在应该掺和事务管理代码,一切事务现在全部归业务层统一管理。
9-Job Commit
当前情况:
对于一些长时间运行的job,通常的做法当然是为其另起一个线程,所以,CREDIT中徐敬琪自定义提供了IProgressTask类作为job的实现接口,并提供一个job运行的context,即IProgressTaskSubmitter。由IProgressTaskSubmitter来处理job的运行,其实原理没有什么特殊之处,但在具体使用上个人认为存在一些不便之处:首先,异常处理不是很优雅,通过int型返回值来表明不同的job运行结果和状态,显然不如强类型的异常更能表明问题;其次,不管job是成功了还是失败,都需要开发人员释放workbench的忙碌状态等,显然,“包装”力度不够,开发人员不应该关注他们不需要关注的关注点;如果使用不当,还可能造成死锁,我碰到过类似情况。
Alternative:
其实,当前的实现有reinvent the wheel之嫌,因为JFace已经提供了ProgressMonitorDialog,通过这个dialog来run接口IRunnableWithProgress的实现类job就可以了,如果觉得说他有些方面不能定制或者还不能满足你的要求,那你也可以通过直接使用org.eclipse.jface.operation.ModalContext类来run相应的job实现也可以。
10-表格数据的显示和处理
当前情况:
使用徐敬琪自定义开发的XTable组件现实表格类的数据,主要是SWT的table不能很好的按照日方要求表现相应的数据,使用XTable,可以基本满足日方的数据表现需求。但在个人使用过程中依然觉得存在一些瑕疵,比如,表格定义的时候,通过反射表明数据和表格之间的对应关系,造成定义XTable的时候较为繁琐(当然,copy Code的方式可以省去不少烦恼),而且,数据对应是通过方法明和返回值类型的对应实现的,稍微不甚就会频发异常,这在CREDIT组员中是经常碰到的现象;
Alternative:
如果要满足日方数据的表现需求,现在实在没有太好的选择,只有自己去自定义Table实现,现在虽然SWT/JFace的table有了一定的增强,但某些方面依然很弱,比如Swing中Table的Render功能,SWT/JFace的Table对这方面的支持就很少,或者说根本没有什么支持(自定义除外)。当然KTable作为SWT/JFace的一个自定义Table也提供了许多自定义功能,但还是有些地方不尽如人意,不过,大多数情况下已经很可以了。

恩,目前就想到这些东西,如果有新的,随时补充…
以上愚见,欢迎拍砖!

How to use SSH in Java Programmatically

在Java程序中使用SSH(How to use SSH in Java Programmatically)
—by Darren.Wang

这个标题不知道能不能表达我的意思,实际上我只是想总结一下可以通过哪些方式或者途径来达到在Java程序中使用SSH相关功能(任务)的目的。前几天有更多free time,所以,为了简化credit的管理工具正式版的发布上传过程,简单实现了一个基于SWT界面的上传应用程序,要完成的功能其实也很简单,但是为了提高上传速度和数据传输的安全性,所以,上传分成几个阶段同时使用SSH来保证上传过程的安全性,之于说上传的阶段等细节不属于我今天要描述的重点,重点是如何在Java中使用SSH,尤其是远程登录到Linux,并执行Shell命令。
现从同事的一个需求说起,他手头的任务中包括检查某台Linux机器的磁盘空间等情况,并随同Email发送。当然别人也给他提出了多种解决方法,不能说不好,但在我看了与程序的集成性上面差一些,所以我觉得给他写一个Utility(坐我旁边,不帮都不行,呵呵)。
实际上,实现原理很简单,直接SSH登录那台Linux机器执行df命令就可以了,其他信息,像当前目录拥有的文件列表等,运行ls
-ls命令,这些我想大家都很清楚,那么在Java中我们是如何实现类似功能的那?!
可能有人以前做过类似功能,那他一定听说过JSch或者说OpenSSH(当然,我们会用他的Java实现安ganymed),对,我们也只是为JSch提供了一个简单的Wrapper而已。
先来看看这个Wrapper是什么样子的吧,然后我再详细说一下这个程序的设计和实现细节:
package org.darrenstudio.ssh;

import java.io.InputStream;
import java.util.Properties;

import org.apache.commons.lang.Validate;
import org.darrenstudio.ssh.callback.SSHExecCallback;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;

public class SSHExecutor {
private JSch jsch;
private Session session;
private boolean login;

public synchronized void login(SSHLoginOptions options) throws SSHExecException
{
try
{
if(jsch == null)
jsch =new JSch();
session = jsch.getSession(options.getUsername(),options.getHost(),options.getPort());
session.setPassword(options.getPassword());
Properties prop = new Properties();
prop.setProperty("StrictHostKeyChecking","no");//StrictHostKeyChecking: ask | yes | no
session.setConfig(prop);
session.connect();
login = true;
}
catch(Exception e)
{
throw new SSHExecException(e);
}
}

public synchronized void execute(String command,SSHExecCallback callback) throws SSHExecException
{
if(!login)
throw new SSHExecException("login first before executing the remote command!");
Validate.notEmpty(command);
Channel channel = null;
try
{
channel =session.openChannel("exec");
((ChannelExec)channel).setCommand(command);
InputStream in=channel.getInputStream();
// OutputStream out=channel.getOutputStream();
InputStream err = ((ChannelExec)channel).getErrStream();

// to retrieve the interactive password request information, this pty is a must
((ChannelExec)channel).setPty(true);

channel.connect();

byte[] tmp=new byte[2048];
while(true)
{
while(in.available() > 0)
{
int i=in.read(tmp, 0, 2048);
String line = new String(tmp, 0, i);
callback.dumpConsole(line);
}

while(err.available() > 0)
{
int size = err.read(tmp,0,2048);
String line = new String(tmp,0,size);
callback.dumpErrStream(line);
}

if(channel.isClosed())
{
int exitStatus = channel.getExitStatus();
if(exitStatus != 0)
throw new SSHExecException("Error Exit Status with Value:"+exitStatus);
break;
}
try{Thread.sleep(1000);}catch(Exception ee){}
}
}
catch(Exception e)
{
throw new SSHExecException(e);
}
finally
{
if(channel != null)
{
channel.disconnect();
channel = null;
}
}

}

public synchronized void dispose()
{
if(session != null)
{
session.disconnect();
session = null;
}
login = false;
}
}

我们给出一个Executor,他负责为我们执行Shell命令,他首先要求我们登录到要执行命令的Linux机器(即login方法),然后,如果登录成功,client端就可以调用execute方法来执行相应的shell命令,执行后,在finally中dispose掉该Executor。
对于login方法来说,因为需要提供login相关信息,而且这些信息参数较多,3-4个,当然,相对来说也不是很多,但是,我们还是采用将他们归并到一个参数类的做法(我想Effective Java大家都读过),这就有了我们的SSHLoginOptions类:
public class SSHLoginOptions implements Serializable {

private static final long serialVersionUID = -8018206086412607771L;

private String host;
private String username;
private String password;
private int port = 22;

public SSHLoginOptions(String host,String username,String password)
{
this(host,username,password,22);
}
public SSHLoginOptions(String host,String username,String password,int port)
{
this.host = host;
this.username = username;
this.password = password;
this.port = port;
}

public String getHost() {
return host;
}

public void setHost(String host) {
this.host = host;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public int getPort() {
return port;
}

public void setPort(int port) {
this.port = port;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String toString() {
return new ToStringBuilder(this).append("host", host).append(
"username", username).append("password", password).append(
"port", port).toString();
}
}
如果登录不成功的话(可能因为网络不通等原因),我们需要抛出一个异常以便告诉Client端该事件,并终止以下步骤,所以,我们采用抛出自定义的SSHExecException:
public class SSHExecException extends NestableException {

private static final long serialVersionUID = -2804917566444475128L;

public SSHExecException(String cause)
{
super(cause);
}
public SSHExecException(Throwable t)
{
super(t);
}
public SSHExecException(String cause,Throwable t)
{
super(cause,t);
}
}
(这里定义为一个Checked异常,实际上,感觉定义为unchecked异常更恰当一些,因为如果失败,Client端也做不了什么)
登录成功后,login标志被置为true,这样execute方法才可以被调用。
execute方法有两个参数,第一个参数为String类型,表示将被执行Shell命令;第二个参数较为特殊,他是我们自己定义的一个接口:
public interface SSHExecCallback {
void dumpConsole(String line);
void dumpErrStream(String errline);
}
这个接口的实现负责处理Linux的正常输出和Error输出,至于说如何处理这些输出,你可以按照自己的需要给出自己的实现,比如,只是简单的打印到控制台:
public class DefaultSSHExecCallback implements SSHExecCallback {

public void dumpConsole(String line) {
System.out.println(line);
}

public void dumpErrStream(String errline) {
System.err.println(errline);
}

在execute方法一开始,我们会检查是否登录成功,如果没有,那同样会抛出SSHExecException,以示说该类没有为Shell命令的执行准备好相应的状态,从而阻止随后的不安全操作。
之后,我们会打开一个Exec Channel,通过这个Channel来执行Shell命令,这可以很容易的从Executor的源码中看出来,如果执行过程中出现异常,我们会抛给Client端我们的自定义异常,当然,不管执行成功或者失败与否,我们都会关掉该Channel以释放连接,否则,主程序会挂在那里。在execute方法中,唯一需要关注的一个地方就是((ChannelExec)channel).setPty(true);这一句,如果没有他,那你的控制台将什么东西都没有,你将得不到任何想要的信息。
为了说命令执行完成后释放资源,我们给出一个dispose方法,这也是很自然的,这里不再赘述。

下面是该类的一个TestCase,大家可以很容易看出该类的使用,很简单。
public class SSHExecutorTest extends TestCase {

private SSHExecutor executor;

public static void main(String[] args) {
junit.textui.TestRunner.run(SSHExecutorTest.class);
}

protected void setUp() throws Exception {
super.setUp();
executor = new SSHExecutor();
SSHLoginOptions loginOptions = new SSHLoginOptions("m.livedoor.cn","root","zxcv1234");
executor.login(loginOptions);
}

protected void tearDown() throws Exception {
super.tearDown();
executor.dispose();
executor = null;
}

public void testExecuteWithCommandUname() throws SSHExecException
{
String command = "uname";
GenericSSHExecCallback callback = new GenericSSHExecCallback();
executor.execute(command,callback);
assertEquals("The Operating System of m.livedoor.cn should be Linux","Linux",StringUtils.trimToEmpty(callback.getOutput()));
}
}

至此,我们的Wrapper类就算完成了,让我们回过头来看看,我们能归纳出什么东西。
到目前为止,我所可以提供的相关信息有两类,一类就是执行Scp相关操作,一类就是基于SSH的Shell命令的执行,那么要完成这两类功能,现在有什么东西可以让我们避免去重新发明轮子那?!
对于Scp相关任务来说,除了前面blog曾经提到过的通过Ant来实现外,你也可以通过JSch来完成,不要忘了,Ant的Scp Task也是通过JSch完成的,除此之外,OpenSSH的Java实现—ganymed也可以很容易的实现scp功能,而且,代码也看起来很简洁:
/**
* @author darrenwang
* @since 1.0
*/
public class SCPExecutor {
private Connection connection;
private boolean login;

public synchronized void login(SSHLoginOptions options) throws SSHExecException
{
try
{
connection = new Connection(options.getHost());
connection.connect();
login = connection.authenticateWithPassword(options.getUsername(),options.getPassword());
if(!login)
throw new SSHExecException("Authentication failed.");
}
catch(Exception e)
{
throw new SSHExecException(e);
}
}

public synchronized void doScp(File file, String todir) throws SSHExecException
{
if(!login)
throw new SSHExecException("login() first before executing the scp task!");
try
{
SCPClient client = connection.createSCPClient();
client.put(file.getAbsolutePath(),todir);
}
catch(Exception e)
{
throw new SSHExecException(e);
}
}
public synchronized void dispose()
{
if(connection != null)
{
connection.close();
connection = null;
}
login = false;
}
}
除了Scp,那剩下很大一部分任务可能都是基于SSH的Shell命令执行啦,这个同样,你可以通过Ant,JSch和ganymed来实现,这里就不做赘述了,因为通过上面的SSHExecutor和以前的blog,你可以很容易的给出实现。

OK,今天就写这些了,《地海传说》,我来啦…

Scp With Ant API programmatically

    Scp With Ant API programmatically
                by Darren.Wang
   
    原来Credit项目在正式发布一个版本的时候,发布的正式安装文件都需要通过好几步才能上传到正式的下载服务器上(当然如果不是为了提高上传的速度,直接从本地拖到目的地也不是不可以),所以,需要在WinScp和SecureCRT之间来回切换,而且需要手工干预,故此就萌生了自动化这个上传过程的想法。正好这几日手头事情不多,故此,着手此事。
    为了避免Reinvent the wheel的危险,当然一开始会google一下,如果谁也正在做或者已经做过此事,那你应该已经发现了解决这种问题的lib—-JSch,因为我们需要scp嘛!不过,JSch只是提供了一些Sample Code,但对于初手或者说想短期完成一定工作的人来说,很明显的,研读code的时间不会很多(也或许是我懒,不愿读code的借口,hoho)。而且,他提供的Sample中大部分也是需要Swing等接口的交互,短期内也无法转化为我用,所以转而求助于Ant,因为我知道Ant提供有Scp的task,所以,底层肯定提供有实现该task的API可用。
    在研究了后面参考资料的2,3之后,“达伦”就开始了他的Credit Release Upload Console的开发工作。
    如果按照原来的上传流程手册进行的话,我在远程执行scp的时候遇到了问题,因为我无法在程序中自动根据server的反馈提供相应的response,比如如果SSH登录到一台远程机的话,我要在这台远程机器上执行scp user@server:/directory . 命令的话,他会提示需要输入password,但这个交互过程我不知道在ant的API中如何实现调用,可能SSHUserInfo是做这个工作的,但没有细看,因为返回的异常只是给出一个code 1,就表示错误,更多的信息一点儿没有,一看就知道没有戏,没有详细的StackTrace,我可猜不出来到底哪里出了问题,所以,最终转换了一下思路直接全部执行scp操作,而不是使用SSHExec来完成部分功能。
    至于说界面什么的就不想赘述了,只是说一下在java如何使用Ant API执行Scp操作。
    具体步骤可以简单总结为:
    1-构造一个Dummy的Project对象,然后project.init();
    2-实例化一个Scp对象,调用其setProject方法,设置上面的Dummy Project;
    3-设置其他properties of Scp Task,比如 setAuthProperties(Scp scp)方法设置一些登录的认证信息,设置task的TaskName等;
    4-设置上传的File或者FileSet以及要上传到的目的地;
    5-调用scp.execute()执行;
    这样我们可以得到下面类似的代码:
    public void scp(File file)

    {

        Scp scp = getScpClient();

        setAuthProperties(scp);

        // set copy task properties

        scp.setFile(file.getAbsolutePath());

        scp.setTodir("username:password@mserver:/usr/local/credit");

        // execute the scp task

        scp.execute();

    }

    private void setAuthProperties(Scp scp)

    {

        // set SSH properties

        scp.setHost("yourserver");

        scp.setUsername("yourusername");

        scp.setPassword("yourpwd");

        scp.setTrust(true);

    }

    private Scp getScpClient()

    {

        // create a dummy project object for the task

        Project project = new Project();

        project.init();

        // create our Scp task and init it

        Scp scp = new Scp();

        scp.setProject(project);

        scp.setTaskName("scp");

        scp.setTaskType("scp");

        return scp;

    }
   
    当然为了让以上代码可用,你还需要将必须的jar包含到你的classpath中,这些jar包括ant.jar以及ant-jsch.jar,还有最终的jsch(suffix).jar,因为ant的scp也是使用了JSch的API实现的。
    有了这些,你可以随便从哪台机器scp文件到另一台机器了。
   
    REFERENCE:
    1. JSch Samples
    2. Invoking Apache Ant programmatically(http://www-128.ibm.com/developerworks/websphere/library/techarticles/0502_gawor/0502_gawor.html)
    3. Ant Reference: Using Ant Tasks Outside of Ant