Quartz 在 Spring 中如何动态配置时间(3)

动态调度服务实现类: 
  1. package com.sundoctor.quartz.service;  
  2.   
  3. import java.text.ParseException;  
  4. import java.util.Date;  
  5. import java.util.UUID;  
  6.   
  7. import org.quartz.CronExpression;  
  8. import org.quartz.CronTrigger;  
  9. import org.quartz.JobDetail;  
  10. import org.quartz.Scheduler;  
  11. import org.quartz.SchedulerException;  
  12. import org.quartz.SimpleTrigger;  
  13. import org.springframework.beans.factory.annotation.Autowired;  
  14. import org.springframework.beans.factory.annotation.Qualifier;  
  15. import org.springframework.stereotype.Service;  
  16.   
  17. @Service("schedulerService")  
  18. public class SchedulerServiceImpl implements SchedulerService {  
  19.   
  20.     private Scheduler scheduler;  
  21.     private JobDetail jobDetail;  
  22.   
  23.     @Autowired  
  24.     public void setJobDetail(@Qualifier("jobDetail") JobDetail jobDetail) {  
  25.         this.jobDetail = jobDetail;  
  26.     }  
  27.   
  28.     @Autowired  
  29.     public void setScheduler(@Qualifier("quartzScheduler") Scheduler scheduler) {  
  30.         this.scheduler = scheduler;  
  31.     }  
  32.   
  33.     @Override  
  34.     public void schedule(String cronExpression) {  
  35.         schedule(null, cronExpression);  
  36.     }  
  37.   
  38.     @Override  
  39.     public void schedule(String name, String cronExpression) {  
  40.         try {  
  41.             schedule(name, new CronExpression(cronExpression));  
  42.         } catch (ParseException e) {  
  43.             throw new RuntimeException(e);  
  44.         }  
  45.     }  
  46.   
  47.     @Override  
  48.     public void schedule(CronExpression cronExpression) {  
  49.         schedule(null, cronExpression);  
  50.     }  
  51.   
  52.     @Override  
  53.     public void schedule(String name, CronExpression cronExpression) {  
  54.         if (name == null || name.trim().equals("")) {  
  55.             name = UUID.randomUUID().toString();  
  56.         }  
  57.   
  58.         try {  
  59.             scheduler.addJob(jobDetail, true);  
  60.   
  61.             CronTrigger cronTrigger = new CronTrigger(name, Scheduler.DEFAULT_GROUP, jobDetail.getName(),  
  62.                     Scheduler.DEFAULT_GROUP);  
  63.             cronTrigger.setCronExpression(cronExpression);  
  64.             scheduler.scheduleJob(cronTrigger);  
  65.             scheduler.rescheduleJob(name, Scheduler.DEFAULT_GROUP, cronTrigger);  
  66.         } catch (SchedulerException e) {  
  67.             throw new RuntimeException(e);  
  68.         }  
  69.     }  
  70.   
  71.     @Override  
  72.     public void schedule(Date startTime) {  
  73.         schedule(startTime, null);  
  74.     }  
  75.   
  76.     @Override  
  77.     public void schedule(String name, Date startTime) {  
  78.         schedule(name, startTime, null);  
  79.     }  
  80.   
  81.     @Override  
  82.     public void schedule(Date startTime, Date endTime) {  
  83.         schedule(startTime, endTime, 0);  
  84.     }  
  85.   
  86.     @Override  
  87.     public void schedule(String name, Date startTime, Date endTime) {  
  88.         schedule(name, startTime, endTime, 0);  
  89.     }  
  90.   
  91.     @Override  
  92.     public void schedule(Date startTime, Date endTime, int repeatCount) {  
  93.         schedule(null, startTime, endTime, 0);  
  94.     }  
  95.   
  96.     @Override  
  97.     public void schedule(String name, Date startTime, Date endTime, int repeatCount) {  
  98.         schedule(name, startTime, endTime, 0, 0L);  
  99.     }  
  100.   
  101.     @Override  
  102.     public void schedule(Date startTime, Date endTime, int repeatCount, long repeatInterval) {  
  103.         schedule(null, startTime, endTime, repeatCount, repeatInterval);  
  104.     }  
  105.   
  106.     @Override  
  107.     public void schedule(String name, Date startTime, Date endTime, int repeatCount, long repeatInterval) {  
  108.         if (name == null || name.trim().equals("")) {  
  109.             name = UUID.randomUUID().toString();  
  110.         }  
  111.   
  112.         try {  
  113.             scheduler.addJob(jobDetail, true);  
  114.   
  115.             SimpleTrigger SimpleTrigger = new SimpleTrigger(name, Scheduler.DEFAULT_GROUP, jobDetail.getName(),  
  116.                     Scheduler.DEFAULT_GROUP, startTime, endTime, repeatCount, repeatInterval);  
  117.             scheduler.scheduleJob(SimpleTrigger);  
  118.             scheduler.rescheduleJob(name, Scheduler.DEFAULT_GROUP, SimpleTrigger);  
  119.   
  120.         } catch (SchedulerException e) {  
  121.             throw new RuntimeException(e);  
  122.         }  
  123.     }  
  124. }  
SchedulerService 只有一个多态方法schedule,SchedulerServiceImpl实现SchedulerService接口,注入org.quartz.Schedulert和org.quartz.JobDetail,schedule方法可以动态配置org.quartz.CronExpression或org.quartz.SimpleTrigger调度时间。 
Posted in Quartz 编程 | Leave a comment

Quartz 在 Spring 中如何动态配置时间(2)

四、实现动态定时任务 
  什么是动态定时任务:是由客户制定生成的,服务端只知道该去执行什么任务,但任务的定时是不确定的(是由客户制定)。 
这样总不能修改配置文件每定制个定时任务就增加一个trigger吧,即便允许客户修改配置文件,但总需要重新启动web服务啊,研究了下Quartz在Spring中的动态定时,发现 

引用
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean" > 
         <property name="jobDetail" ref="schedulerJobDetail"/> 
         <property name="cronExpression"> 
             <value>0/10 * * * * ?</value> 
         </property> 
cronExpression是关键,如果可以动态设置cronExpression的值,就可以顺利解决问题了。这样我们就不能直接使用org.springframework.scheduling.quartz.CronTriggerBean,需要自己实现一个动态调度服务类,在其中构建CronTrigger或SimpleTrigger,动态配置时间。 
动态调度服务接口: 
  1. package com.sundoctor.quartz.service;  
  2.   
  3. import java.util.Date;  
  4.   
  5. import org.quartz.CronExpression;  
  6.   
  7. public interface SchedulerService {  
  8.     /** 
  9.      * 根据 Quartz Cron Expression 调试任务 
  10.      * @param cronExpression  Quartz Cron 表达式,如 "0/10 * * ? * * *"等 
  11.      */  
  12.     void schedule(String cronExpression);  
  13.       
  14.     /** 
  15.      * 根据 Quartz Cron Expression 调试任务 
  16.      * @param name  Quartz CronTrigger名称 
  17.      * @param cronExpression Quartz Cron 表达式,如 "0/10 * * ? * * *"等 
  18.      */  
  19.     void schedule(String name,String cronExpression);  
  20.       
  21.     /** 
  22.      * 根据 Quartz Cron Expression 调试任务 
  23.      * @param cronExpression Quartz CronExpression 
  24.      */  
  25.     void schedule(CronExpression cronExpression);  
  26.       
  27.     /** 
  28.      * 根据 Quartz Cron Expression 调试任务 
  29.      * @param name Quartz CronTrigger名称 
  30.      * @param cronExpression Quartz CronExpression 
  31.      */  
  32.     void schedule(String name,CronExpression cronExpression);  
  33.       
  34.     /** 
  35.      * 在startTime时执行调试一次 
  36.      * @param startTime 调度开始时间 
  37.      */  
  38.     void schedule(Date startTime);    
  39.       
  40.     /** 
  41.      * 在startTime时执行调试一次 
  42.      * @param name Quartz SimpleTrigger 名称 
  43.      * @param startTime 调度开始时间 
  44.      */  
  45.     void schedule(String name,Date startTime);  
  46.       
  47.     /** 
  48.      * 在startTime时执行调试,endTime结束执行调度 
  49.      * @param startTime 调度开始时间 
  50.      * @param endTime 调度结束时间 
  51.      */  
  52.     void schedule(Date startTime,Date endTime);   
  53.       
  54.     /** 
  55.      * 在startTime时执行调试,endTime结束执行调度 
  56.      * @param name Quartz SimpleTrigger 名称 
  57.      * @param startTime 调度开始时间 
  58.      * @param endTime 调度结束时间 
  59.      */  
  60.     void schedule(String name,Date startTime,Date endTime);  
  61.       
  62.     /** 
  63.      * 在startTime时执行调试,endTime结束执行调度,重复执行repeatCount次 
  64.      * @param startTime 调度开始时间 
  65.      * @param endTime 调度结束时间 
  66.      * @param repeatCount 重复执行次数 
  67.      */  
  68.     void schedule(Date startTime,Date endTime,int repeatCount);   
  69.       
  70.     /** 
  71.      * 在startTime时执行调试,endTime结束执行调度,重复执行repeatCount次 
  72.      * @param name Quartz SimpleTrigger 名称 
  73.      * @param startTime 调度开始时间 
  74.      * @param endTime 调度结束时间 
  75.      * @param repeatCount 重复执行次数 
  76.      */  
  77.     void schedule(String name,Date startTime,Date endTime,int repeatCount);  
  78.       
  79.     /** 
  80.      * 在startTime时执行调试,endTime结束执行调度,重复执行repeatCount次,每隔repeatInterval秒执行一次 
  81.      * @param startTime 调度开始时间 
  82.      * @param endTime 调度结束时间 
  83.      * @param repeatCount 重复执行次数 
  84.      * @param repeatInterval 执行时间隔间 
  85.      */  
  86.     void schedule(Date startTime,Date endTime,int repeatCount,long repeatInterval) ;  
  87.       
  88.     /** 
  89.      * 在startTime时执行调试,endTime结束执行调度,重复执行repeatCount次,每隔repeatInterval秒执行一次 
  90.      * @param name Quartz SimpleTrigger 名称 
  91.      * @param startTime 调度开始时间 
  92.      * @param endTime 调度结束时间 
  93.      * @param repeatCount 重复执行次数 
  94.      * @param repeatInterval 执行时间隔间 
  95.      */  
  96.     void schedule(String name,Date startTime,Date endTime,int repeatCount,long repeatInterval);  
  97. }  
Posted in Quartz 编程 | Leave a comment

Quartz 在 Spring 中如何动态配置时间(1)

在项目中有一个需求,需要灵活配置调度任务时间,并能自由启动或停止调度。 
有关调度的实现我就第一就想到了Quartz这个开源调度组件,因为很多项目使用过,Spring结合Quartz静态配置调度任务时间,非常easy。比如:每天凌晨几点定时运行一个程序,这只要在工程中的spring配置文件中配置好spring整合quartz的几个属性就好。 

Spring配置文件 

引用
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> 
<property name="targetObject" ref="simpleService" /> 
<property name="targetMethod" value="test" /> 
</bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean"> 
<property name="jobDetail" ref="jobDetail" /> 
<property name="cronExpression" value="0 0/50 * ? * * *" /> 
</bean> 
<bean  id="schedulerTrigger" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> 
<property name="triggers"> 
<list>
<ref bean="cronTrigger"/>      
</list> 
</property> 
</bean> 

这种配置就是对quartz的一种简单的使用了,调度任务会在spring启动的时候加载到内存中,按照cronTrigger中定义的 cronExpression定义的时间按时触发调度任务。但是这是quartz使用“内存”方式的一种配置,也比较常见,当然对于不使用spring的项目,也可以单独整合quartz。方法也比较简单,可以从quartz的doc中找到配置方式,或者看一下《Quartz Job Scheduling Framework 》。 

但是对于想持久化调度任务的状态,并且灵活调整调度时间的方式来说,上面的内存方式就不能满足要求了,正如本文开始我遇到的情况,需要采用数据库方式集成 Quartz,这部分集成其实在《Quartz Job Scheduling Framework 》中也有较为详细的介绍,当然doc文档中也有,但是缺乏和spring集成的实例。 

一、需要构建Quartz数据库表,建表脚本在Quartz发行包的docs\dbTables目录,里面有各种数据库建表脚本,我采用的Quartz 1.6.5版本,总共12张表,不同版本,表个数可能不同。我用mysql数据库,执行了Quartz发行包的docs\dbTables\tables_mysql_innodb.sql建表。 

二、建立java project,完成后目录如下 
 

三、配置数据库连接池 
配置jdbc.properties文件 

引用
jdbc.driverClassName=com.mysql.jdbc.Driver 
jdbc.url=jdbc:mysql://localhost:3306/quartz?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true 
jdbc.username=root 
jdbc.password=kfs 
cpool.checkoutTimeout=5000 
cpool.minPoolSize=10 
cpool.maxPoolSize=25 
cpool.maxIdleTime=7200 
cpool.acquireIncrement=5 
cpool.autoCommitOnClose=true 

配置applicationContext.xml文件 

引用
<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans&quot; 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot; 
    xmlns:aop="http://www.springframework.org/schema/aop&quot; 
    xmlns:tx="http://www.springframework.org/schema/tx&quot; 
    xmlns:context="http://www.springframework.org/schema/context&quot; 
     xmlns:jee="http://www.springframework.org/schema/jee&quot; 
    xsi:schemaLocation=" 
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd 
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd 
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd 
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd 
     http://www.springframework.org/schema/jee 
       http://www.springframework.org/schema/jee/spring-jee-2.5.xsd"  > 
  
   <context:component-scan base-package="com.sundoctor"/> 

<!– 属性文件读入 –> 
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> 
<property name="locations"> 
<list> 
<value>classpath:jdbc.properties</value> 
</list> 
</property> 
</bean> 

<!– 数据源定义,使用c3p0 连接池 –> 
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> 
<property name="driverClass" value="${jdbc.driverClassName}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="initialPoolSize" value="${cpool.minPoolSize}"/>
<property name="minPoolSize" value="${cpool.minPoolSize}" />
<property name="maxPoolSize" value="${cpool.maxPoolSize}" />
<property name="acquireIncrement" value="${cpool.acquireIncrement}" /> 
    <property name="maxIdleTime" value="${cpool.maxIdleTime}"/>   
</bean>

</beans> 

这里只是配置了数据连接池,我使用c3p0 连接池,还没有涉及到Quartx有关配置,下面且听我慢慢道来。 
Posted in Quartz 编程 | Leave a comment

Quartz入门-用xml实现日程安排

 
为什么是 Quartz?
Quartz是企业级的日程安排[schedule]软件,有Unix下熟悉的CRON定义方式,也有简单的定义方式。
Quartz的存储方式可以是内存存储,也可以用数据库来实现持久化。
 
为什么是这篇文章?
使用Quartz的过程中发现入门的文章很少,看Quartz的文档是很痛苦的事情,因为你希望下午就可以交差的东东,却发现它连QuickStart都没有,除了郁闷,你别无他法。这篇文章是实战型,直接介绍如何可以最快使用上Quartz,起码,替代timer.
 
如何开始
本文介绍的是用xml启动Quartz任务的方法。
1.下载Quartz
将quartz放到yourappl/Web-INF/lib里面。
2.配置web.xml,启动Quartz服务.
在yourappl/WEB-INF/web.xml里添加以下内容。
<Servlet>
<servlet-name>QuartzInitializer</servlet-name>
<display-name>QuartzInitializerServlet</display-name>
<servlet-class>org.quartz.ee.servlet.QuartzInitializerServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
 
3.配置您的任务文件quartz_reminder.xml,建立job和trigger.
在yourappl/WEB-INF/web.xml里新建文件quartz_reminder.xml
以下例子建立两个日程安排:
    一个是每隔15分钟执行scheduling.QuartzEmail任务
    一个是星期1-58:30AM执行scheduling.QuartzDailyReminder任务
 
您可以自建QuartzEmail.class,QuartzDailyReminder.class放置在yourappl/WEB-INF/classes/scheduling里进行测试。
注意一个日程是由一个job和一个trigger组成,代表任务定义和时间定义。
<?xmlversion="1.0"encoding="UTF-8"?>
<!DOCTYPEquartzPUBLIC
"-//QuartzEntERPriseJobScheduler//DTDJobSchedulingData1.0//EN"
"
http://www.quartzscheduler.org/dtd/job_scheduling_data_1_0.dtd">
<quartz>
<job>
    <!– 每隔15分钟执行scheduling.QuartzEmail任务 –>
    <job-detail>
        <name>Job_Email</name>
        <group>DEFAULT</group>
        <job-class>scheduling.QuartzEmail</job-class>
    </job-detail>
    <trigger>
        <cron>
            <name>Trigger_Email</name>
            <group>DEFAULT</group>
http://www.mscto.com 
            <job-name>Job_Email</job-name>
            <job-group>DEFAULT</job-group>
            <cron-expression>00/15 * * * ?</cron-expression>
        </cron>
    </trigger>
</job>
<job>
    <!– 星期1-58:30AM执行scheduling.QuartzDailyReminder任务 –>
    <job-detail>
        <name>Job_Daily_Reminder</name>
        <group>DEFAULT</group>
        <job-class>scheduling.QuartzDailyReminder</job-class>
    </job-detail>
    <trigger>
        <cron>
            <name>Trigger_Daily_Reminder</name>
            <group>DEFAULT</group>
            <job-name>Job_Daily_Reminder</job-name>
            <job-group>DEFAULT</job-group>
            <cron-expression>03 08 ? * MON-FRI</cron-expression>
        </cron>
    </trigger>
</job>
</quartz>
4.配置quartz.properties,建立Quartz实例
在yourappl/WEB-INF/web.xml里新建文件quartz.properties
########################################################################################
#
#ConfigureMainSchedulerProperties
#
org.quartz.scheduler.instanceName=TestScheduler
org.quartz.scheduler.instanceId=one
 
#
#ConfigureThreadPool
#
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount= 5
org.quartz.threadPool.threadPriority=4
 
#
#ConfigureJobStore
#
org.quartz.jobStore.misfireThreshold=5000
org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore
 
#===========================================================================
#ConfigureSchedulerPlugins
#===========================================================================

org.quartz.plugin.triggHistory.class=org.quartz.plugins.history.LoggingTriggerHistoryPlugin
org.quartz.plugin.triggHistory.triggerFiredMessage=Trigger{1}.{0}firedjob{6}.{5}at:{4,date,HH:mm:ssMM/dd/yyyy}
org.quartz.plugin.triggHistory.triggerCompleteMessage=Trigger{1}.{0}completedfiringjob{6}.{5}at{4,date,HH:mm:ssMM/dd/yyyy}withresultingtriggerinstructioncode:{9}
org.quartz.plugin.jobInitializer.class=org.quartz.plugins.xml.JobInitializationPlugin
org.quartz.plugin.jobInitializer.fileName=/quartz_reminder.xml
org.quartz.plugin.jobInitializer.overWriteExistingJobs=false
org.quartz.plugin.jobInitializer.failOnFileNotFound=true
org.quartz.plugin.shutdownhook.class=org.quartz.plugins.management.ShutdownHookPlugin
org.quartz.plugin.shutdownhook.cleanShutdown=true
########################################################################################
好了,现在重起JSP服务器,留意logs里面的输出,就可以测试您的日程有无生效了。
Posted in Quartz 编程 | Leave a comment

Three Ways to Sync Browser Bookmarks

http://www.notebooks.com/2010/01/17/three-ways-to-sync-browser-bookmarks/

Many people use more than just one computer throughout the day. Perhaps you have a home desktop computer, a laptop, and even a work computer. Chances are, you use each of these computers to access the web. What can be frustrating is attempting to keep your bookmarks in order. On just one machine alone this can be daunting, but to have them organized across all machines is even tougher. Luckily there are some great free options out there for effortlessly keeping your bookmarks synced for whatever browser you are using, and we’ve compiled a list of three, just for you.

Google Chrome – Integrated Syncing

If you are using Google Chrome, you don’t even need to install any addition software. Google Chrome has bookmark syncing baked directly into the browser. Enabling it is as simple as plugging in your Google account information.

At the top right of the browser, click on the wrench button, then find “Synchronize my bookmarks…”

A new box will pop up which will ask for your Google account login info.

After entering your Google credentials, bookmark syncing will be handled automatically from this point on. Simply go to any other computer that you’d like to keep bookmarks in sync with, and follow the same steps with the same Google account. Bookmarks should be merged, so if you have different bookmarks on multiple computers, everything will mesh together, then you can use the Bookmark manager to organize (ctrl+shift+b in Chrome). Be sure to double check that bookmarks are merging and not being replaced, just in case you have important bookmarks on one of your computers. Carefully read the prompts as you set up the syncing.

Google stores your synced bookmarks on Google Docs, which means if you are ever on another computer that you aren’t syncing with (perhaps a public computer) you can always go to docs.google.com to get access to your bookmarks from any web connect computer (just remember to log out of your Google account when you are done!)

Mozilla Firefox – Weave

Weave is an official project from Mozilla which is designed to keep your Firefox sessions in perfect sync. Weave does a little bit more than just syncing bookmarks. Have a look at this diagram which is featured on the Weave website.

Weave is a Firefox add-on which can be easily installed by visiting the Weave website and clicking “Get it now!” You’ll need to be running the latest version of the Firefox browser (to check, go to the “Help” menu at the top of the browser, and find “Check for updates”). Once the add-on is installed, you need to set up a Weave account:

Start by opening the add-ons manager and clicking on “Options” on the Weave Sync add-on.

Click the button to create a Mozilla Weave account.

Fill in the required information and hit next. After this window, you’ll have a few more fields to fill out until everything it set up.

Once the account is set up, use the dropdown box to decide what Weave will do for you. You can choose between syncing everything, or choose “Use my custom settings” to decide exactly what gets synced. Once this is done, install the add-on in the same way on other computers that you’d like to sync with and use the account that you just created to fill out the User Name and Password fields in the first step.

Xmarks – Multi-browser bookmark syncing

If you use Firefox or Google Chrome exclusively across all of your computers, the two above options will probably work well for you. However, there are a myriad of reasons that someone might use more than one browser across multiple computers, or even on the same computer. If you find yourself in this group, Xmarks is the solution for you. Xmarks (formerly Foxmarks) started as a very good Firefox-only bookmark syncing add-on. It has since then evolved to support the four major browsers, most recently Chrome. Follow the links below to get the Xmarks add-on for your favorite browsers and start syncing bookmarks:

Again, you’ll need to install Xmarks on each computer and for each browser that you’d like to sync with. You’ll create an Xmarks account to keep everything in order, and if you find yourself on a computer which doesn’t have Xmarks installed, you can visit my.xmarks.com to access your bookmarks.

Posted in Uncategorized | Leave a comment

享受插件带来的便利 – Bookmark Syncing

其实bookmark共享已经不是什么新鲜事,但一直没有去真正的用他们。 因为若资料确实好,最好的办法还是转载在自己的blog中更为方便。但对于一些帮助性的网站,则需要bookmark比较好。最近,把Google Chrome更新到了4.x版本,突然发现它居然支持bookmark共享了。 立马设置(需要gmail帐号)并试了一下,哇,真的不错。
 
于是乎,开始看有没有安装在其他浏览器上的类似插件。Firefox的插件支持应该是数一数二的, 一搜一大把。那个更好些呢?经过不断的搜索,最后锁定了Xmark,Weave和Delicious(前两种需要创建账户,Delicious是yahoo的东西,因此也可使用yahoo邮箱代替)。这些在Firefox上使用好像都不错,但发现只有xmarks支持四大浏览器:IE,Firefox,Chrome和Safari。(唯一的遗憾是,我使用Opera,却并不用Safari。Xmarks也不支持Opera,使我不能让Opera和其他浏览器之间同步了。)但xmark并非出自知名公司之手,有些不是很放心。于是,在Firefox中,首先安装了Delicious,但觉得不是太好,因为其需要单独的管理而非是嵌在已有的bookmark上。接下来,就有去下载了Weave,安装完毕后,提示创建账户,输入数据加密私有编码,开始同步。安装完毕后,发现还是Weave好一些,省去管理两个bookmark(浏览器自身的和Delicious的)的额外工作。当然,Delicious也有他的好处,提供网站访问,这样在别人的机器上,你就可以通过登录Delicious去查看你的bookmark,而不需要把你的bookmark放到别人的机器上。
 
但我的四大浏览器(IE,Firefox,Chrome和Opera)之间还是有问题,怎么办?看虑再三,豁出去了,装xmark。Xmark与Weave非常象,估计Mozilla是抄了xmark一把。Xmark的好处是你可以选择不需要同步密码。这样,就不会把重要信息从家里机器同步到公司机器了。Xmarks也具有和Delicious一样的网页浏览bookemark的功能。
 
但很可惜,xmark目前,并不支持Opera。其他的几个bookmark插件也无法在Opera上运行。无奈,只好手工同步Opera和其他浏览器啦。不过,一旦把Opera的bookmark同步到Firefox, 那么,在哪里都可以继续使用我的bookmarks了。
 
哈,一切搞定。看着不同机器上的brower互相同步bookmark,哇,好爽!以后基本上再也不用手工处理他们了。
 
有点心动没有?试一下吧!
 
目前,我使用的浏览器有: IE, Firebox,Opera和Chrome。为什么装这么多?各有各的好处。
IE, FireFox和Opea均支持金山词霸或有道(新发现10.5不支持取词了, 但速度快了很多)。公司的IE还是6.0,好慢好烂,即使IE8,速度也排名最后。基本处于闲置状态,除非要运行公司的烂应用时才使用。
Opera 10.10支持金山词霸, 运行速度要比IE快的多,且关闭后或异常终止时,重新打开时,会象Firxefox一样恢复原来浏览的网页。但对网页渲染,有时有些问题,不如Firefox。
Firefox对金山词霸的支持还好,运行速度比较快,且关闭后或异常终止时,重新打开,网页会恢复到原来的样子。对网页的渲染与IE基本相同。
Chrome好像根本不支持,至少目前我还没有找到好办法。Chrome支持网页恢复功能,且在浏览历史中会显示每个浏览窗口中关闭的tab历史。是平常使用最多的浏览器,其速度绝对是快。但在某些网页的渲染上,没有Firefox和Opera好。
Posted in Uncategorized | Leave a comment

Maven 2常见命令

Maven2 的运行命令为 : mvn ,
常用命令为 : 
 mvn archetype:create :创建 Maven 项目 
 
 mvn compile :编译源代码
 mvn test-compile :编译测试代码
 mvn test : 运行应用程序中的单元测试
 mvn site : 生成项目相关信息的网站
 mvn clean :清除目标目录中的生成结果
 mvn package : 依据项目生成 jar 文件
 mvn install :在本地 Repository 中安装 jar
 mvn eclipse:eclipse :生成 Eclipse 项目文件
生成项目
    建一个 JAVA 项目 : mvn archetype:create -DgroupId=com.demo -DartifactId=App
    建一个 web 项目 : mvn archetype:create -DgroupId=com.demo -DartifactId=web-app -DarchetypeArtifactId=maven-archetype-webapp
生成 Eclipse 项目
 普通 Eclipse 项目执行 : mvn eclipse:eclipse
 Eclipse WTP 项目执行 : mvn eclipse:eclipse –Dwtpversion=1.0 ( wtp1.0 以上版本均可用)
 
pom.xml 文件基本节点介绍
 <project > :文件的根节点 .
 <modelversion > : pom.xml 使用的对象模型版本 .
 <groupId > :创建项目的组织或团体的唯一 Id.
 <artifactId > :项目的唯一 Id, 可视为项目名 .
 <packaging > :打包物的扩展名,一般有 JAR,WAR,EAR 等 
 <version > :产品的版本号 .
 <name > :项目的显示名,常用于 Maven 生成的文档。 
 <url > :组织的站点,常用于 Maven 生成的文档。 
 <de.ion > :项目的描述,常用于 Maven 生成的文档。

Posted in Maven | Leave a comment

What is Spring Roo?

Spring出来了一个叫做Roo的东东,一直没有时间去看看,引文Spring出品的东西一向来说,都是珍品诸多的,但从spring的官网上,居然没办法了解Roo到底是个什么玩意(投资放大了,但文档却做得越来越差了)。好在Roo实在是一个小玩意,下载还不到3M,于是马上下载下来,一读为快。
  • 下载
  • 解压
  • 运行 roo.bat 进入到roo shell,就可以一步一步的按照提示进行了。(这个做得不错,居然没有文档,也可以按照提示进行操作)
这里吧 roo 支持的命令一一列出来,各位看官可以对 roo 的功能有一个初步的了解:
* */ – End of block comment
* /* – Start of block comment
* // – Inline comment markers
* ; – Inline comment markers
* add dependency – Adds a new dependency to the Maven project object model (POM)
* add field boolean – Adds a private boolean field to an existing Java source file
* add field date jdk – Adds a private date field to an existing Java source file
* add field date jpa – Adds a private JPA-specific date field to an existing Java source file
* add field email template – insert a MailTemplate field into an existing type
* add field number – Adds a private numeric field to an existing Java source file
* add field reference jpa – Adds a private reference field to an existing Java source file (ie the ‘many’ side of a many-to-one)
* add field set jpa – Adds a private Set field to an existing Java source file (ie the ‘one’ side of a many-to-one)
* add field string – Adds a private string field to an existing Java source file
* configure email template – configure a template for a SimpleMailMessage
* configure logging – Configure logging in your project
* database properties – Shows database configuration details
* database remove – Removes a particular database property
* database set – Changes a particular database property
* date – Displays the local date and time
* development mode – Switches the system into development mode (greater diagnostic information)
* exit – Exits the shell
* generate class file – Creates a new Java source file in any project path
* help – Shows system help
* hint – Provides step-by-step hints and context-sensitive guidance
* insert field – Inserts a private field into the specified file
* install email provider – Install a Email provider in your project
* install finder – Install finders in the given target (must be an entity)
* install jms – Install a JMS provider in your project
* install security – Install Spring Security into your project
* install web flow – Install Spring Web Flow configuration artifacts into your project
* list finders for – List all finders for a given target (must be an entity
* metadata for id – Shows detailed information about the metadata item
* metadata for type – Shows detailed metadata for the indicated type
* metadata summary – Shows statistics on the metadata system
* metadata trace – Traces metadata event delivery notifications (0=none, 1=some, 2=all)
* new controller automatic – Create a new automatic Controller (ie where we maintain CRUD automatically)
* new controller manual – Create a new manual Controller (ie where you write the methods)
* new dod – Creates a new data on demand for the specified entity
* new integration test – Creates a new data on demand for the specified entity
* new java file – Creates a new Java source file in SRC_MAIN_JAVA
* new mock test – Creates a mock test for the specified entity
* new persistent class jpa – Creates a new JPA persistent entity in SRC_MAIN_JAVA
* new selenium test – Creates a new Selenium test for a particular controller
* new test file – Creates a new Java source file in SRC_TEST_JAVA
* property file details – Shows the details of a particular properties file
* property file remove – Removes a particular properties file property
* property file set – Changes a particular properties file property
* props – Shows the shell’s properties
* quit – Exits the shell
* remove dependency – Removes an existing dependency from the Maven project object model (POM)
* script – Parses the specified resource file and executes its commands
* update jpa – Update the JPA persistence provider in your project
* version – Displays shell version
** Type ‘hint’ (without the quotes) and hit ENTER for step-by-step guidance **
我还没有对Roo进行深入的研究,初步的感觉是:
1、Roo的 TAB  做的真是很好,TAB在Bash中有一定的帮助作用,但仅限于有限的文件名自动完成,而Roo的命令行自动完成有点接近于IDE中的代码自动完成了。
2、Hint 做得也真好,居然,在没有文档和帮助的情况下,我可以(近乎)熟练的操作一个命令行工具。要记得,当初我学习Unix还是花了些功夫的。
3、最后的感觉是:Roo好像就是一个基于命令行的IDE,其功能无非就是一个IDE中会有的项目向导功能,包括命令行自动完成,也是IDE中的一些概念而已,本质上倒没有什么突破。而且,从实际投入而言,对于最终用户来说,一个命令行的Roo无论如何从使用便利性而言,是不如像eclipse之类的IDE的。不过,开发IDE所需时间、复杂度要大得多,Roo这种模式倒不失为一种IDE的快速开发原型吧。
4、Roo的本质是一个代码自动生成的工具,代码自动生产到底能够多大程度的应用到企业开发中,对项目开发效率能有多大提高,我觉得有些怀疑。作为教学用途,或者快速原型,倒是不错的选择。
Posted in Spring 编程 | Leave a comment

使用Spring Roo ,感受ROR式的开发

Roo是一种 Spring 开发的辅助工具,使用命令行操作来生成自动化项目,操作非常类似于rails 
我这里使用spring tool suit来开发一个demo项目 

首先新建个spring template project,然后选择template类型为Roo Wep App (based on Roo 1.0.0.RC1) 
这样,便会通过roo的命令自动生成一个标准的maven web项目,里面含有最基本的Spring 配置 

建好项目后,我们可以在project上运行roo shell。 通过hint命令,我们可以很快的了解各种操作提示 

首先,设置数据库 

  1. install jpa -provider HIBERNATE -database MYSQL  

roo会帮你在配置文件applicationContext.xml中配置好了相应的数据源配置,并且在 pom.xml中已经添加jpa和数据库驱动等相关的依赖。 

然后新建一个domain 

  1. new persistent class jpa -name ~.domain.Employee -testAutomatically  

这时roo会帮我们生成Employee类,这时Employee类还没有任何字段,我们可以通过roo shell往里面加入一些字段 

  1. add field string -fieldName name -notNull -sizeMax 200  
  2. add field date jpa -fieldName birth -type java.util.Date  
  3. ……  

最终生成的Employee如下: 

  1. @Entity  
  2. @RooEntity  
  3. @RooJavaBean  
  4. @RooToString  
  5. public class Employee {  
  6.   
  7.     @NotNull  
  8.     @Size(max = 200)  
  9.     private String name;  
  10.   
  11.     @Temporal(TemporalType.TIMESTAMP)  
  12.     private Date birth;  
  13. }  

你会发现,怎么没有get set呢?我们反编译看下编译后的class代码 。
经过反编译生成的class发现,通过domain类上面的roo注释,自动在类编译时加入get set,甚至我们发现了findAllEmployees,persist,remove,哈,这不是我们梦寐以求的充血模型吗? 

原来,roo在为我们创建domain的时候自动为同一个domain创建了4个aspect(.aj files),并在编译期动态为domain切入代码. 
4个aspect分别是: 
domainName_Roo_Configurable.aj(配置) 
domainName_Roo_Entity.aj(加入orm持久) 
domainName_Roo_JavaBean.aj(为字段生成get set) 
domainName_Roo_ToString.aj(重写toString) 

可见,使用roo我们彻底摆脱了在daomain里对各个属性写get set,并且使domain摇身一变为充血。 

下一步,就是建Controller了,同样是一条语句 

  1. new controller automatic -formBackingObject ~.domain.Employee -name ~.web.CommentController  

你会发现,EmployeeController,spring mvc的配置以及curd的jsp页面全部生成了 
EmployeeController代码如下: 

  1. @RooWebScaffold(automaticallyMaintainView = true, formBackingObject = Employee.class)  
  2. @RequestMapping("/employee/**")  
  3. @Controller  
  4. public class EmployeeController {  
  5. }  

在这里出现了一行@RooWebScaffold注解,做过rails的人会想,这不是rails里面的scaffold吗?没错,通过@RooWebScaffold注解,EmployeeController自动获得了curd的所有功能。 

同样是生成controllerName_Roo_Controller.aj在编译期织入代码 

我们来看看生成的EmployeeController_Roo_Controller.aj: 

  1. package com.javaeye.web;  
  2.   
  3. privileged aspect EmployeeController_Roo_Controller {  
  4.       
  5.     @org.springframework.web.bind.annotation.RequestMapping(value = "/employee", method = org.springframework.web.bind.annotation.RequestMethod.POST)      
  6.     public java.lang.String EmployeeController.create(@org.springframework.web.bind.annotation.ModelAttribute("employee") com.javaeye.domain.Employee employee, org.springframework.validation.BindingResult result) {      
  7.         if (employee == nullthrow new IllegalArgumentException("A employee is required");          
  8.         for(javax.validation.ConstraintViolation<com.javaeye.domain.Employee> constraint : javax.validation.Validation.buildDefaultValidatorFactory().getValidator().validate(employee)) {          
  9.             result.rejectValue(constraint.getPropertyPath(), null, constraint.getMessage());              
  10.         }          
  11.         if (result.hasErrors()) {          
  12.             return "employee/create";              
  13.         }          
  14.         employee.persist();          
  15.         return "redirect:/employee/" + employee.getId();          
  16.     }      
  17.       
  18.     @org.springframework.web.bind.annotation.RequestMapping(value = "/employee/form", method = org.springframework.web.bind.annotation.RequestMethod.GET)      
  19.     public java.lang.String EmployeeController.createForm(org.springframework.ui.ModelMap modelMap) {      
  20.         modelMap.addAttribute("employee"new com.javaeye.domain.Employee());          
  21.         return "employee/create";          
  22.     }      
  23.       
  24.     @org.springframework.web.bind.annotation.RequestMapping(value = "/employee/{id}", method = org.springframework.web.bind.annotation.RequestMethod.GET)      
  25.     public java.lang.String EmployeeController.show(@org.springframework.web.bind.annotation.PathVariable("id") java.lang.Long id, org.springframework.ui.ModelMap modelMap) {      
  26.         if (id == nullthrow new IllegalArgumentException("An Identifier is required");          
  27.         modelMap.addAttribute("employee", com.javaeye.domain.Employee.findEmployee(id));          
  28.         return "employee/show";          
  29.     }      
  30.       
  31.     @org.springframework.web.bind.annotation.RequestMapping(value = "/employee", method = org.springframework.web.bind.annotation.RequestMethod.GET)      
  32.     public java.lang.String EmployeeController.list(org.springframework.ui.ModelMap modelMap) {      
  33.         modelMap.addAttribute("employees", com.javaeye.domain.Employee.findAllEmployees());          
  34.         return "employee/list";          
  35.     }      
  36.       
  37.     @org.springframework.web.bind.annotation.RequestMapping(method = org.springframework.web.bind.annotation.RequestMethod.PUT)      
  38.     public java.lang.String EmployeeController.update(@org.springframework.web.bind.annotation.ModelAttribute("employee") com.javaeye.domain.Employee employee, org.springframework.validation.BindingResult result) {      
  39.         if (employee == nullthrow new IllegalArgumentException("A employee is required");          
  40.         for(javax.validation.ConstraintViolation<com.javaeye.domain.Employee> constraint : javax.validation.Validation.buildDefaultValidatorFactory().getValidator().validate(employee)) {          
  41.             result.rejectValue(constraint.getPropertyPath(), null, constraint.getMessage());              
  42.         }          
  43.         if (result.hasErrors()) {          
  44.             return "employee/update";              
  45.         }          
  46.         employee.merge();          
  47.         return "redirect:/employee/" + employee.getId();          
  48.     }      
  49.       
  50.     @org.springframework.web.bind.annotation.RequestMapping(value = "/employee/{id}/form", method = org.springframework.web.bind.annotation.RequestMethod.GET)      
  51.     public java.lang.String EmployeeController.updateForm(@org.springframework.web.bind.annotation.PathVariable("id") java.lang.Long id, org.springframework.ui.ModelMap modelMap) {      
  52.         if (id == nullthrow new IllegalArgumentException("An Identifier is required");          
  53.         modelMap.addAttribute("employee", com.javaeye.domain.Employee.findEmployee(id));          
  54.         return "employee/update";          
  55.     }      
  56.       
  57.     @org.springframework.web.bind.annotation.RequestMapping(value = "/employee/{id}", method = org.springframework.web.bind.annotation.RequestMethod.DELETE)      
  58.     public java.lang.String EmployeeController.delete(@org.springframework.web.bind.annotation.PathVariable("id") java.lang.Long id) {      
  59.         if (id == nullthrow new IllegalArgumentException("An Identifier is required");          
  60.         com.javaeye.domain.Employee.findEmployee(id).remove();          
  61.         return "redirect:/employee";          
  62.     }      
  63.       
  64.     @org.springframework.web.bind.annotation.InitBinder      
  65.     public void EmployeeController.initBinder(org.springframework.web.bind.WebDataBinder binder) {      
  66.         binder.registerCustomEditor(java.util.Date.classnew org.springframework.beans.propertyeditors.CustomDateEditor(new java.text.SimpleDateFormat("yyyy-M-d"), false));          
  67.     }      
  68.       
  69. }  

这样,Controller里不用写一行代码,就自动拥有了curd,而且,最新的roo使用了spring3.0,还支持了rest 

好了,就这么简单,一个domain,一个Controller,我们就可以发布到tomcat中运行了。 
不到5分钟,我们就可以生成了一个可运行的项目,是不是很有ror式的感觉啊?赶快来试试吧

Posted in Spring 编程 | Leave a comment

Spring roo

 http://nottiansyf.javaeye.com/blog/593113

比较新的东西,其实可以当作一个spring project的最佳实践进行学习,其中包含了许多内容,也让我感觉多了学习的乐趣.

内容摘自官方文档
类似代码生成器,应该说更高级一些,与Rails,Grails的命令台功能类似
1:下载Release版的压缩包,解压缩后在环境变量中设置对应的home与path
2:在cmd命令台中,运行roo执行操作
可以通过安装基于Eclipse的SpringSource Tool Suite’s工具集,为roo开发提供便利

十分钟内创建Web应用
hint命令.提供了类似向导的工具,提供了step by step的方式,指导创建一个应用.也可以加入指定的类型参数,获取指定的帮助信息,如hint controllers

project命令,用于在当前目录创建一个roo project,
persistence命令,用于创建数据库相关文件,
    persistence setup –provider HIBERNATE –database MSSQL //这里为选择的数据类型
    然后根据提示修改对应的src/main/resources/database.properties,完成数据库的细节设置
entity命令,用于创建基础的POJO对象,
entity –class ~.Timer –testAutomatically //TImer为类名
field命令,用于为该Pojo对象添加属性
field string –fieldName message –notNull
controller命令,为pojo对象提供基于Spring MVC的功能,采用最新的RESTful风格
    controller all –package ~.web //表示将会在当前目录下创建web子目录,存放controller文件
selenium test命令,用于为类创建对应的测试代码
selenium test –controller ~.web.TimerController
perform命令,用于执行不同的操作,如测试.打包,以及转换成能够使用IDE导入的方式,
通过执行perform,也可以自动下载所需的依赖jar包

roo> perform tests
roo> perform package
roo> perform eclipse
如果出现jar缺失,比如对应的sql数据库jdbc包,可以通过手动在pom.xml下加入依赖的方式,提供扩展    
打包后,就可以直接将war包放入到tomcat中运行,其中注意访问的url与war包名字一致

Roo Samples Script 一些Roo目录下的脚本
应该使用用于将一系列的命令,保存在脚本中,然后通过命令进行批量执行这些命令
script –file filename.roo
clinic.roo:  — 包含了一个完整的宠物商店的例子
vote.roo: 包含了Spring Security的例子
RSVP: 详细的使用了除去Spring本身功能的,Spring roo中的其他设置选项,(其实也没有那么多东西)

安装数据库时,选择会根据JPA标准显示三个,比较常用的应该就是hibernate,比较传统的hibernate使用,生成的代码更偏向JPA标准模式
开发过程中,可以使用内置的内存数据库HYPERSONIC_IN_MEMORY,由maven提供插件支持

创建类时,可以通过~.根据工程的top package目录进行子目录的设置,而类名为最后的一个字符串(注意)
如entity –class ~.domain.Topping 设置类名为Topping

除了hint向导外,也提供了help命令,用于完整的command帮助
project 目录下的log.roo文件,将会记录所执行过的command
在生成Java代码时,roo应该是通过Java类与AspectJ类的方式,同时生成最终功能代码,使用AOP的方式编程

在创建pojo属性的同时,可以提供为其添加基于jsr303的 annotation的验证
在对class操作后,roo会自动跳转后续都为其进行操作,可以通过在操作时,设置不同的–class属性进行选择
如 class –class Base 就可以跳转到对应的Base类

建立实体类的一对多关系
field set –fieldName toppings –element ~.domain.Topping
为当前类添加一个属性toppings,表示拥有多个的Topping对象
主要为set的使用,以及后续的element属性

在roo project中,同样也可以使用maven进行测试,打包等操作

创建web层
controller命令的使用
controller all –package ~.web //创建所有entity的controller,并且放置在子web目录下,并且会自动生成所需的web.xml等配置文件,以及添加spring MVC和URL rewrite,Tiles

可以使用mavan的命令,直接对roo项目进行部署测试
‘mvn tomcat:run, 或者 mvn jetty:run

为程序添加安全支持 spring Securing
security setup命令:进行安装,注意必须在创建Web layer后,才可以进行添加.
会添加对应的applicationContext-security.xml文件
在页面中,meun.jspx可以使用<sec:authorize ifAllGranted="ROLE_ADMIN"> 对菜单进行配置
在controller中,可以通过注解进行配置权限
@RooWebScaffold(path = "pizzaorder",
automaticallyMaintainView = true,
formBackingObject = PizzaOrder.class,
delete=false,
update=false)
用于配置roo移除该controller的删除和更新的方法
http://localhost:8080/pazzashop/login 用于提供用户登录

定义UI界面
一般情况下,如果对entity对象进行了修改,会自动印象对应的controller与jsp界面,可以通过@RooWebScaffold(automaticallyMaintainView=false)配置,用于取消自动关联

Selenium Tests  Selenium测试
自动生成对应的脚本,使用如下命令执行测试
selenium test –controller ~.web.ToppingController

也可以通过执行mvn selenium:selenese,使用mvn进行测试,区别在于.会打开firefox查看测试结果

Backups and Deployment 备份与部署
backup命令,用于创建一个保存了当前项目代码,日志和脚本记录的备份zip文件
perform package命令,用于生成部署所需的war包

区别于原来的dao service controller层次架构,roo的项目中,只有entity(类似dao)以及web层,通过使用jpa的底层,解决了在云计算平台上扩展的问题

Critical Technologies
roo peoject 中的两个重要技术, AspectJ与Spring

roo项目中,会针对class生成对应的*_Roo_*.aj文件,一个或多个, 成为AspectJ ITD文件,由Roo进行自动管理
所生成的.java类中,代码一般都比较简单,不过都带有许多注解,其中roo开头的注解将不会被编译到class代码中

当你重写了java文件的toString方法后,会自动删除对应的entity_Roo_ToString.aj文件,如果要重新使用自动生成的toString方法,可以通过annotation进行配置
@RooToString(toStringMethod="generatedToString"),但是注意这里生成的新的toString方法名为
generatedToString

Entity Layer 类(Domain层)
可以通过jpa的annotation对其进行修改,比如对其添加非空验证,修改其映射的表名等

@RooJavaBean 用于提供pojo对象的get和set方法,如果手动编写了get和set方法,那将会自动跳过
@RooEntity 为entity提供对应的JPA操作类

Web Layer
使用spring mvc提供了web的基本脚手架,包含了REST风格的URL rewrite,Apache Tiles,Spring JavaScript.以及一键式的命令为其添加Spring Security

Usage and Conventions 用法与公约

Add-On 插件安装和删除
下载后放置在$ROO_HOME/add-ons目录下,使用addon cleanup command,还包括$ROO_HOME/work的使用..(文档有些纠结)

在spring roo的project中,将一个class进行了分解,如entity类,通过修改分离get/set
,toString,以及对应的Dao方法,分别放在不同的类中,这样便于代码的阅读,在编译后,生成对应的代码….这个需要研究一下
同理,在controller中,分解成两个类,一个包含了传统的controller方法,则另外一个可以用于扩展

Base Add-Ons
目前插件开发的文档并没有写出,可能还未开发,不过如果要学习,就必须先开始了目前Roo
project中所使用的框架

目前roo中集成了 Jms email,jpa,spring security,selenium.spring web flow 
 

Posted in Spring 编程 | Leave a comment