|
|
2008年2月12日
出自伏尔泰的名句。在许多优秀的程序员的编程习惯中,寻找解决方案就如同是寻找女友一般,总是力求完美,如果一个解决方案不够完美的话,那么宁可舍弃也不会降低标准而采用。但是这样的结果很有可能会是一叶障目,不见森林。
举个实际的例子来说,tapestry5的服务动态加载就是一个典型。
大家知道,在Tapestry中,页面模板的动态加载一直是其的一个宣传点。也就是说,页面模板如果修改了,不需要重新启动应用服务器,修改即时生效。但是,Tapestry却一直没有实现服务的动态加载,也就是说服务的类修改了,就必须重新启动服务器。为什么呢,因为追求完美。
其实很久以前在Tapestry的作者的头脑中,包括许多的用户都想在Tapestry中加入服务动态加载的功能,但是最后作者却放弃了。原因是因为,在Tapestry的架构中,服务比页面要复杂的多,服务不是单独存在的,他会被Inject到许多其他的服务,而这些服务又会被inject到其他的服务中,最后导致你很难界定哪些类需要被重新加载。
但是最近有一个人提出一个建议,为什么要实现完整的动态加载呢。其实只实现服务的实现类的动态加载就已经很好了。
确实,如果保持接口不变,只在接口具体实现修改的时候加载,确实大大简化了问题,因为这样变化范围就被限定在单个类之内了。虽然这样并不完美,因为如果改变了对外接口,比如,添加了一个新的方法,或者改变了方法的签名,还是需要重新启动服务器。可是这样也已经在很大程度上提升了用户的使用满意度。
正如有句古语所说:退一步海阔天空。也在此提醒自己在以后的开发过程中,尽量拓展自己的思维,不要一下子钻到牛角尖里。
2月25日,IBatis3发布了Beta10,在主页上宣称,一个月来Beta9的公测都没有收到真正的Bug提交。
所有收到的问题都是新的功能要求。
所以,如果没有意外的话,Beta10将会用于投票表决是否作为GA(General Available)版本。
在教程的第四部分(http://www.blogjava.net/usherlight/archive/2009/06/20/283396.html)我们已经粗略地介绍了Tapestry的前缀,其中提及最常用的两种literal和prop。这里顺便再介绍一下其他的几个前缀.
1、context,这个经常在引用图片的时候用到。比如:<img src="${context:images/icon.png}"/> 这样就能够在页面上显示在web-inf/images/icon.png
2、message,这个是需要I18N的应用必用的前缀。${message:some-key}
3、var,用于定义生成一个临时变量。比如:<li t:type="loop" source="1..10" value="var:index">${var:index}</li>。如果没有加这个前缀var:的话,就需要在页面类中添加一个property: index以及相应的getter和setter。
我们这次介绍自定义一个cycle前缀,这个可以用于在表格中显示不同背景的行(也就是斑马条)。
比如:
<t:grid .... rowClass="cycle:line1,line2">
...
</t:grid>
或者
<t:loop ...>
<div class="${cycle:line1,line2}">aaaa</div>
</tloop>
自定义prefix一般来说是3个步骤,
1、定义一个BindingFactory,这个需要实现BindingFactory接口
2、定义一个Binding继承AbstractBinding
3、注册这个Binding
看一下具体的prefix的类:
1 import java.util.ArrayList;
2 import java.util.List;
3
4 import org.apache.tapestry5.Binding;
5 import org.apache.tapestry5.BindingConstants;
6 import org.apache.tapestry5.ComponentResources;
7 import org.apache.tapestry5.ioc.Location;
8 import org.apache.tapestry5.services.BindingFactory;
9 import org.apache.tapestry5.services.BindingSource;
10
11 /** *//**
12 * Implementation of the cycle: binding prefix -- we parse list of bindings
13 * and generate delegate bindings for each element<br>
14 * default binding is literal, other bindings can be used by specifying prefix.<br>
15 * example: "cycle:prop:name,prop:lastName,sth,sth else"
16 */
17 public class CycleBindingFactory implements BindingFactory {
18 private final BindingSource _bindingSource;
19
20 public CycleBindingFactory(BindingSource source) {
21 this._bindingSource = source;
22 }
23
24 public Binding newBinding(String description, ComponentResources container, ComponentResources component,
25 String expression, Location location)
26 {
27 List<Binding> delegates = new ArrayList<Binding>();
28 String[] bindingNames = expression.split(",");
29
30 for (String bindingName : bindingNames) {
31 String defaultBinding = BindingConstants.LITERAL;
32
33 Binding binding = _bindingSource.newBinding(description, container, component, defaultBinding, bindingName, location);
34 delegates.add(binding);
35 }
36
37 CycleBinding cycleBinding = new CycleBinding(delegates);
38 container.addPageLifecycleListener(cycleBinding);
39
40 return cycleBinding;
41 }
42 }
1 import java.util.List;
2
3 import org.apache.tapestry5.Binding;
4 import org.apache.tapestry5.internal.bindings.AbstractBinding;
5 import org.apache.tapestry5.runtime.PageLifecycleListener;
6
7
8 public class CycleBinding extends AbstractBinding implements PageLifecycleListener {
9 private final List<Binding> delegates;
10 private int index = 0;
11
12 public CycleBinding(List<Binding> delegates) {
13 this.delegates = delegates;
14 }
15
16 public Object get() {
17 Object ret = delegates.get(index).get();
18 index ++;
19 if(index>=delegates.size()) index = 0;
20 return ret;
21 }
22
23 @Override
24 public boolean isInvariant() {
25 return false;
26 }
27
28 @Override
29 public Class<Object> getBindingType() {
30 return Object.class;
31 }
32
33
34 public void containingPageDidDetach() {
35 index=0;
36 }
37
38 public void containingPageDidAttach() {/**//*not interested*/}
39
40 public void containingPageDidLoad() {/**//*not interested*/}
41 }
Binding和BindingFactory写好了,注册后就可以使用了,注册的过程是在AppModel中添加以下一段代码:
1 public static void contributeBindingSource(
2 MappedConfiguration<String, BindingFactory> configuration,
3 BindingSource bindingSource
4 )
5 {
6 configuration.add("cycle",new CycleBindingFactory(bindingSource));
7 }
1. 先开始看SpringSide吧。
主要看寒冬日志版3.2.1开始有的JMS演示和WebService演示
一直觉得EJB好像已经日薄西山了,但是实际上生命力还挺顽强的。
另外,还有Web Service, SOA这个概念还是有人相信的。
所以,延伸项目有:JMS, MQ
我记得好像是Appfuse的作者曾经这样评价过Tapestry:只要你真正掌握了Tapestry,你的开发效率将会得到极大的提高。为什么呢?我认为他这样说的一个重要原因就是Tapestry的组件机制。Tapestry提供了非常便利的组件定义机制,随着Tapestry的组件不断积累,Tapestry的开发将会变得越来越简单。
本文就用一个实例来看一下Tapestry中是如何添加一个自定义组件的。
Tapestry的内置组件只提供了checkbox,而且只能返回一个boolean,用于表明是否被选中。
比如,要进行一个群众喜爱的水果调查,选项有: 苹果,葡萄,桃子,香蕉...,就需要对应每个选项设置一个布尔型变量,显得比较繁琐。
这里我们将添加一个组件用于将一组checkbox集中起来返回一个逗号分隔的字符串值。
通过查看Tapestry中的checkbox的源码(已经附在文章的后面)可以知道,Tapestry可以很容易地通过Request来获取Form中的变量的值。
遇到的问题:
Tapestry的checkbox组件不允许设置相同的name,如果name相同,Tapestry会自动在name后面添加后缀来使之不同。
If a component renders multiple times, a suffix will be appended to the to id to ensure uniqueness(http://tapestry.apache.org/tapestry5.1/tapestry-core/ref/org/apache/tapestry5/corelib/components/Checkbox.html)。如果各checkbox的name不同,我们无法通过request来获得一组checkbox的值。
思路:
在页面模板中不使用tapestry的checkbox组件,而使用Html的checkbox,这样可以避免tapestry自动修改checkbox的name。
添加一个新的tapestry组件,来映射接受所有同名的checkbox的值,并把值返回给tapestry页面中对应的变量。这个组件需要有一个属性,这个属性的值就是所有同组checkbox的name,这样,这个组件就可以通过Request来获取所有相同name的checkbox的值。
代码:
1 public class CheckBoxGroup extends AbstractField {
2
3 @SuppressWarnings("unused")
4 @Parameter(required = true, autoconnect = true)
5 private String value;
6
7 @Parameter(required = true, autoconnect = true)
8 private String groupName;
9
10 @Inject
11 private Request request;
12
13 @SuppressWarnings("unused")
14 @Mixin
15 private RenderDisabled renderDisabled;
16
17 @Inject
18 private ComponentResources resources;
19
20 @BeginRender
21 void begin(MarkupWriter writer)
22 {
23 writer.element("input", "type", "checkbox",
24 "name", groupName,
25 "id", getClientId(),
26 "style", "display:none");
27
28 resources.renderInformalParameters(writer);
29
30 decorateInsideField();
31 }
32
33 @AfterRender
34 void after(MarkupWriter writer)
35 {
36 writer.end(); // input
37 }
38
39 @Override
40 protected void processSubmission(String elementName)
41 {
42 String elementValue = "";
43 String[] valueArray = request.getParameters(groupName);
44 if ( valueArray != null && valueArray.length > 0 ) {
45 elementValue = valueArray[0];
46 for ( int i = 1; i < valueArray.length; i ++ ) {
47 elementValue += "," + valueArray[i];
48 }
49 }
50 value = elementValue;
51 }
52 }
组件的使用:
-----tml------
<t:CheckBoxGroup t:groupName="literal:bookId" t:value="selectedBooks"/>
<t:loop source="bookList" value="book" encoder="encoder">
<div><input type="checkbox" name="bookId" value="${book.id}"/> ${book.name}</div>
</t:loop>
注意checkBoxGroup的groupName和其他checkbox的name必须一致,checkBoxGroup的value的值就是页面中的变量名
-----java-----
@SuppressWarnings("unused")
@Property
private final ValueEncoder<Book> encoder = new ValueEncoder<Book>() {
public String toClient(Book value) {
return String.valueOf(value.getId());
}
public Book toValue(String clientValue) {
return bookDao.getBook(Integer.parseInt(clientValue));
}
};
public List<Book> getBookList() {
return bookDao.getBooks();
}
@SuppressWarnings("unused")
@Property
private Book book;
@SuppressWarnings("unused")
@Property
private String selectedBooks;
在tapestry5中,在页面之间传递基本有3种方法
1、存放在Session中
2、使用@Persist进行持久化
3、使用页面context来传递参数。
其中1和2都需要将数据存放在Session中,相对来说系统的开销比较大。尤其是多用户高并发情况下,对于性能可能会有一定的影响。
使用页面Context来传递则需要在开发时写一些代码,增加了一些开发量,显得没有前两种方法方便。
第3种方法的实现是需要在页面中添加onActivate和onPassivate方法来完成页面参数的传递。
我们先来看一下其背后的故事。
举个例子,比如说我们有两个页面,第一个是查询条件输入页面input,另一个是查询结果输出页面output。input页面中有两个查询条件,起始时间dateFrom和终止时间dateTo
在Input.java中,我们可以很直观地这样写:
@InjectPage
private Output output;
@Property
private String dateFrom;
@Property
private String dateTo;
Object onFormSubmit() {
output.setDateFrom(dateFrom);
output.setDateTo(dateTo);
return output;
}
首先使用注解注入output页面,然后在表单的提交事件中,返回output,这样就在程序中定义了返回页面,而不是使用配置文件的方式。
但是这样的实现却不能正确运行,原因是因为Tapestry5的使用了页面池技术,页面在每次渲染前都是从页面池中随机获取一个页面,而从页面池中取得的页面,所有的属性都是被清空了的。
也就是说在上例中,虽然我们注入了output页面,但是此页面马上就被放入了页面池,而且其中的属性值马上就被清空了。这就是引入onActivate和onPassivate这丙个方法的原因。tapestry5在清空属性前会首先查看是否包含onPassivate方法,如果有,就把其返回值保存起来,然后从页面池中取得页面后,再把刚才保存的值作为参数传递给onActivate方法。
这就是方法3的基本原理,但是无论是在官方的文档或是示例或者网上其他的应用中,可以发现大部分都是使用单个参数的,比如说id。这也很容易理解,因为onPassivate的方法的返回值只能有一个。
在Tapestry5的官方文档中,只有一句非常简要的话介绍了如果传递多个文档的方法: The activation context may consist of a series of values, in which case the return value of the method should be an array or a List. (参见:http://tapestry.apache.org/tapestry5.1/guide/pagenav.html)。
但是这并不是说只要在onPassivate中把参数的值加入到List中,返回一个List,而在onActivate中接受一个List参数,然后就可以得到其中的参数了,因为Tapestry5把参数传给onActivate的方法其实是通过将参数作为HttpRequest中的参数的。如果试图使用上述方法就是得到一个“无法将List转换成String的错误”
所以方法应该是这样的,在Output中:
private List<String> paramList;
public void setParamList(List<String> paramList) {
this.paramList = paramList;
}
public List<String> getParamList() {
return paramList;
}
List<String> onPassivate() {
return paramList;
}
void onActivate(String dateFrom, String dateTo) {
this.dateFrom = dateFrom;
this.dateTo = dateTo;
}
private String dateFrom;
private String dateTo;
在Input页面中,需要把onFormSubmit改一下:
Object onFormSubmit() {
List<String> list = new ArrayList<String>();
output.setParamList(list);
return output;
}
其中,需要注意的是output中的onActivate方法,基参数的顺序必须和List中放入的参数顺序一致。
在上一篇中我们研究了如何实现SpringSecurity中Jsp Tag的<security:authorize ifAllGranted="ROLE_SUPERVISOR">的功能。这一次我们一起研究一下如何实现在Tapestry5.1中添加一个Filter来对所有的操作进行权限的过滤控制。
在SpringSecurity中,我们一般是在application-context.xml中,添加一个SpringSecurity的Filter,然后在另外一个xml中详细配置如何根据Url的规则进行权限的控制。而Tapestry的哲学是尽量减少Xml中的配置(其IOC容器也基本上是借鉴Guice而不Spring的),所以我们也是在代码中实现权限规则的控制。
总体上来看,可以用两种方式来实现url规则,一种是Request级别的Filter,一种是页面组件级别的Filter,如果是Request级别的话,可以从Request对象中获取Url路径,这样就与SpringSecurity基本一样了。本文主要介绍页面组件级别的Filter,从中我们也可以体会到Tapestry5.1中的IOC容器的强大和便利。
这就是Filter的代码,这个Filter必须实现ComponentRequestFilter接口。值得注意的是其构造函数所需要用到的4个参数,这4个参数都是Tapestry5本身自有的服务,所以我们什么也不用做,Tapestry5自动会将服务的实例注入进来,这就是Tapestry-IOC的威力。
ComponentRequestFilter接口一共有4个方法需要实现,具体代码如下:
1 public class RequiresLoginFilter implements ComponentRequestFilter {
2
3 private final PageRenderLinkSource renderLinkSource;
4
5 private final ComponentSource componentSource;
6
7 private final Response response;
8
9 private final ApplicationStateManager appStateManager;
10
11 public RequiresLoginFilter(PageRenderLinkSource renderLinkSource,
12 ComponentSource componentSource, Response response,
13 ApplicationStateManager appStateManager
14 ) {
15 this.renderLinkSource = renderLinkSource;
16 this.componentSource = componentSource;
17 this.response = response;
18 this.appStateManager = appStateManager;
19 }
20
21 public void handleComponentEvent(
22 ComponentEventRequestParameters parameters,
23 ComponentRequestHandler handler) throws IOException {
24
25 if (dispatchedToLoginPage(parameters.getActivePageName())) {
26 return;
27 }
28
29 handler.handleComponentEvent(parameters);
30
31 }
32
33 public void handlePageRender(PageRenderRequestParameters parameters,
34 ComponentRequestHandler handler) throws IOException {
35 if (dispatchedToLoginPage(parameters.getLogicalPageName())) {
36 return;
37 }
38 handler.handlePageRender(parameters);
39
40 }
41
42 private boolean dispatchedToLoginPage(String pageName) {
43 Component page = componentSource.getPage(pageName);
44
45 if (page.getClass().isAnnotationPresent(RequiresLogin.class)) {
46 if ( ! appStateManager.exists(Authentication.class)) {
47 redirect();
48 return true;
49 }
50 Authentication auth = appStateManager.get(Authentication.class);
51 if ( auth == null ) {
52 redirect();
53 return true;
54 }
55
56 if ( ! auth.isLoggedIn()) {
57 redirect();
58 return true;
59 }
60
61 RequiresLogin requireLogin = page.getClass().getAnnotation(
62 RequiresLogin.class);
63 String ifNotGranted = requireLogin.ifNotGranted();
64 String ifAllGranted = requireLogin.ifAllGranted();
65 String ifAnyGranted = requireLogin.ifAnyGranted();
66 boolean permitted = auth.checkPermission(ifNotGranted, ifAllGranted, ifAnyGranted);
67 if ( ! permitted ) {
68 return true;
69 }
70 }
71
72 return false;
73 }
74
75 private void redirect() {
76 Link link = renderLinkSource.createPageRenderLink("Logout");
77
78 try {
79 response.sendRedirect(link);
80 } catch (Exception e) {
81 }
82 }
83
84 }
在ComponentRequestFilter中,我们无法使用@SessionState注解来直接注入Session中的变量,但是我们可以通过ApplicationStateManager来取得。
现在我们需要把刚定义的Filter注册到系统中,很简单,只要在AppModule中添加以下函数就行了:
1 public static void contributeComponentRequestHandler(
2 OrderedConfiguration<ComponentRequestFilter> configuration) {
3 configuration.addInstance("RequiresLogin", RequiresLoginFilter.class);
4 }
5
从本例子中我们可以看到Tapesty Ioc容器使用的便利性,也认识到了Ioc容器在Tapestry体系中的重要性
IBatis2中提供了3种DataSource的配置:JNDI, Apache DBCP, IBatis自带的SimpleDataSource。但在IBatis3中只提供了两种DataSource: UNPOOLED, POOLED。
如果要实现自定义的DataSource,就需要通过扩展DataSourceFactory。本文就演示一下这个过程。
准备工作:Connection Pool的选择,通过搜索发现目前比较流行的免费数据库连接池主要有3种:Apache DBCP, C3P0, Proxool。
看了一下,Proxool的最新版本是0.9.1(2008-08-23), C3P0的最新版本是0.9.1.2(2007-05-21), DBCP最新版本是1.2.2(2007-04-04)
好像这3个项目都已经挺长时间没有更新了。但是总体评价上C3P0无论从稳定上还是效率上都要好一点。
(具体这3个项目谁更优秀,并不是本文的重点,本文主要是介绍一下如何在IBatis3中自定义数据源)
大致步骤:
1、实现org.apache.ibatis.datasource.DataSourceFactory接口,主要是2个方法
a、public DataSource getDataSource() 如何具体地得到一个数据源
b、public void setProperties(Properties properties) 如何设置数据源的参数属性
2、实现javax.sql.DataSource,这个就是提供给DataSourceFactory的实例
3、在IBatis3中引用新加入的数据源
1. 从代码中可以看出,IBatis3与IBatis2不同,不再通过一个Configuration类来进行数据源属性的设置,而是使用反射机制直接调用数据源的方法来设置参数。
这就要求配置文件中的参数名称必须与数据源类中的方法名匹配.
1 public class C3p0DataSourceFactory implements DataSourceFactory {
2
3 private DataSource dataSource;
4
5 public C3p0DataSourceFactory() {
6 dataSource = new C3p0DataSource();
7 }
8
9 public DataSource getDataSource() {
10 return dataSource;
11 }
12
13 public void setProperties(Properties properties) {
14 Properties driverProperties = new Properties();
15 MetaObject metaDataSource = MetaObject.forObject(dataSource);
16 for (Object key : properties.keySet()) {
17 String propertyName = (String) key;
18 if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) {
19 String value = properties.getProperty(propertyName);
20 driverProperties.setProperty(propertyName
21 .substring(DRIVER_PROPERTY_PREFIX_LENGTH), value);
22 } else if (metaDataSource.hasSetter(propertyName)) {
23 String value = (String) properties.get(propertyName);
24 Object convertedValue = convertValue(metaDataSource,
25 propertyName, value);
26 metaDataSource.setValue(propertyName, convertedValue);
27 } else {
28 throw new DataSourceException("Unkown DataSource property: "
29 + propertyName);
30 }
31 }
32 if (driverProperties.size() > 0) {
33 metaDataSource.setValue("driverProperties", driverProperties);
34 }
35 }
36
37 @SuppressWarnings("unchecked")
38 private Object convertValue(MetaObject metaDataSource, String propertyName,
39 String value) {
40 Object convertedValue = value;
41 Class targetType = metaDataSource.getSetterType(propertyName);
42 if (targetType == Integer.class || targetType == int.class) {
43 convertedValue = Integer.valueOf(value);
44 } else if (targetType == Long.class || targetType == long.class) {
45 convertedValue = Long.valueOf(value);
46 } else if (targetType == Boolean.class || targetType == boolean.class) {
47 convertedValue = Boolean.valueOf(value);
48 }
49 return convertedValue;
50 }
51
52 private static final String DRIVER_PROPERTY_PREFIX = "driver.";
53 private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX
54 .length();
55
56 }
57
2. 数据源类,其中的一堆setter就是用于设置属性的。
1 public class C3p0DataSource implements DataSource {
2
3 private ComboPooledDataSource dataSource;
4 public C3p0DataSource() {
5 this.dataSource = new ComboPooledDataSource();
6 }
7
8 public Connection getConnection() throws SQLException {
9 return dataSource.getConnection();
10 }
11
12 public Connection getConnection(String username, String password)
13 throws SQLException {
14 return dataSource.getConnection(username, password);
15 }
16
17 public PrintWriter getLogWriter() throws SQLException {
18 return dataSource.getLogWriter();
19 }
20
21 public int getLoginTimeout() throws SQLException {
22 return dataSource.getLoginTimeout();
23 }
24
25 public void setLogWriter(PrintWriter out) throws SQLException {
26 dataSource.setLogWriter(out);
27 }
28
29 public void setLoginTimeout(int seconds) throws SQLException {
30 dataSource.setLoginTimeout(seconds);
31 }
32
33
34 public synchronized void setDriver(String driver) {
35 try {
36 dataSource.setDriverClass(driver);
37 } catch (Exception e) {
38 }
39 }
40
41 public void setUrl(String url) {
42 dataSource.setJdbcUrl(url);
43 }
44
45 public void setUsername(String username) {
46 dataSource.setUser(username);
47 }
48
49 public void setPassword(String password) {
50 dataSource.setPassword(password);
51 }
52
53 public void setInitialPoolSize(int initialPoolSize) {
54 dataSource.setInitialPoolSize(initialPoolSize);
55 }
56
57 public void setMaxPoolSize(int maxPoolSize) {
58 dataSource.setMaxPoolSize(maxPoolSize);
59 }
60
61 public void setMinPoolSize(int minPoolSize) {
62 dataSource.setMinPoolSize(minPoolSize);
63 }
64
65 public void setPreferredTestQuery(String preferredTestQuery) {
66 dataSource.setPreferredTestQuery(preferredTestQuery);
67 }
68
69 public void setPoolPingQuery(String poolPingQuery) {
70 dataSource.setPreferredTestQuery(poolPingQuery);
71 }
72 }
3. 在配置文件Configuration.xml中,可以先定义数据源的别称,然后就象POOLED和UNPOOLED一样使用别称来引用数据源。
<Configuration>
...
<typeAlias>
<typeAlias type="com.test.datasource.C3p0DataSourceFactory" alias="C3P0"/>
</typeAlias>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="C3P0">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="poolPingQuery" value="${pingquery}"/>
</dataSource>
</environment>
</environments>
...
<Configuration>
最近在使用tapestry5.1.0.5开发项目的时候,突然报错:
Exception in thread "main" java.lang.ClassFormatError: Invalid length 561 in LocalVariableTable in class file
在网上搜索后,发现有人也有同样的错误,解决方法有两种:
http://mail-archives.apache.org/mod_mbox/tapestry-users/200909.mbox/%3Cecd0e3310909040909id5275beld935fc60d54d490a@mail.gmail.com%3E
其中一个人的错误原因是在其类路径中有不同版本的javassists的jar文件。
另一个的解决方法是使用eclipse自带的jdk来编译java类。
而我自己仔细检查了类路径中的文件,并没有重复的javassists,不过我觉得问题应该就在javassists上,
因为这显然是javassists在操作class文件时报的错误,
我去网上搜索这方面的信息,发现有好几个人都和我一样在使用javassists3.11.0GA版本的时候,会出现这个错误。
后来,我改用Tapestry5中自带的javassists3.9.0GA后,问题消失了。
这次经验教训是并不是所有最新的东西都是最好的。合适的才是最好的。
IBatis3的Beta8版本已经发布了,在官方网站上声称目前的版本已经非常稳定,只有4个已知的问题,其中2个是非功能性的。作者宣称,这样的状况使它对于近期发布GA版本充满信心。
那么IBatis3与IBatis2相比,究竟变化在哪里呢?
最重要的变化是IBatis3中引入了接口绑定(Interface Binding)的概念。在IBatis2中,没有应用Java5的泛型,所以需要大量使用强制类型转换,比如:
Employee employee = (Employee)sqlMapper.queryForList("getEmployee", 5);
//...and...
List employees = sqlMapper.queryForList("listAllEmployees");
但是在IBatis3中,方法改变成:
MapperFactory factory = someConfiguration.buildMapperFactory();
EmployeeMapper employeeMapper = factory.getMapper (EmployeeMapper.class);
Employee emp = empMapper.getEmployee(5);
//...and...
List<Employee> employees = empMapper.listAllEmployees();
所以IBatis3至少需要使用Java5以上的版本。上面代码中,EmployeeMapper是一个自定义的接口(注意,开发人员只需要定义一个接口,不需要提供具体的实现)
public interface EmployeeMapper {
Employee getEmployee (int employeeId);
List<Employee> listAllEmployees();
}
这样就行了,IBatis会自动为你生成接口的具体实现。是不是感觉有点酷?
Tapestry中并没有类似于Spring Security这样的专门的权限框架。对此Tapestry的作者Lewis认为主要是用户对于权限的要求实在太多变化了。他认为很难抽象出一个通用的权限框架来满足所有的用户,所以他干脆就不费事去做这件事了。但其实我们很容易就能利用Tapestry已有的工具来完成类似于SpringSecurity的功能。
本文主要介绍如何实现类似于SpringSecurity的jsp tag的功能。在Tapestry中,利用Components实现这一点非常容易。
其基本原理是Tapestry5中一个页面或者组件的渲染生成过程是基于一个状态机和队列完成的。这样,渲染生成过程就被细分成了很多个小模块,我们可以非常容易地覆写这些小模块。具体内容详见官方文档:http://tapestry.apache.org/tapestry5.1/guide/rendering.html。如果权限校验不通过,我们就可以控制不显示组件的内容。
我们这里就是主要依赖这个过程来实现在页面这一层面对权限进行校验和控制。
代码主要包含两大部分,一个组件和一个用于权限控制的服务。
参考了Tapestry-Spring-Security的实现,我也将组件命名为IfRole(当然,我们也可以和Tapestry-Spring-Security一样,也再生成一个IfLoggedIn组件)。权限控制的服务我命名为:AuthenticationService。
主要的实现思路:
将AuthenticationService申明为SessionState变量。这样这个变量就可以在所有的页面和组件之间很方便地共享了。一般情况下,是在登录页面对AuthenticationService进行赋值,而在退出页面清空AuthenticationService这个变量。
代码(这部分代码完全根据应用的需求进自行更改):
AuthenticationService的代码:
 public class AuthenticationService {
private List<String> privilegeList;
// privilegeList 的getter and setter

public boolean checkPermission(String ifNotGranted, String ifAllGranted,
 String ifAnyGranted) {
if (((null == ifAllGranted) || "".equals(ifAllGranted))
&& ((null == ifAnyGranted) || "".equals(ifAnyGranted))
 && ((null == ifNotGranted) || "".equals(ifNotGranted))) {
return false;
}

 if ((null != ifNotGranted) && !"".equals(ifNotGranted)) {
StringTokenizer st = new StringTokenizer(ifNotGranted, ",");
 while (st.hasMoreTokens()) {
String value = st.nextToken();
 if (privilegeList.contains(value)) {
return false;
}
}
}

 if ((null != ifAllGranted) && !"".equals(ifAllGranted)) {
StringTokenizer st = new StringTokenizer(ifAllGranted, ",");
 while (st.hasMoreTokens()) {
String value = st.nextToken();
 if (!privilegeList.contains(value)) {
return false;
}
}
}

 if ((null != ifAnyGranted) && !"".equals(ifAnyGranted)) {
StringTokenizer st = new StringTokenizer(ifAnyGranted, ",");
 while (st.hasMoreTokens()) {
String value = st.nextToken();
 if (privilegeList.contains(value)) {
return true;
}
}
return false;
}

return true;
}
}
IfRole的代码(这个类需要放在Components目录下):
 public class IfRole {
 /** *//**
* A comma-separated list of roles is supplied to one or more of the
* following parameters. If none are supplied, the default behavior is to
* forbid access. Behavior should be self-explanatory.
*/
@Parameter(required = false, defaultPrefix = "literal")
private String ifAllGranted;

@Parameter(required = false, defaultPrefix = "literal")
private String ifAnyGranted;

@Parameter(required = false, defaultPrefix = "literal")
private String ifNotGranted;

 /** *//**
* An alternate {@link Block} to render if the test parameter is false. The default, null, means
* render nothing in that situation.
*/
@Parameter(name = "else")
private Block elseBlock;

private boolean test;
@SessionState
private AuthenticationService auth;

 private boolean checkPermission() {
return auth.checkPermission(ifNotGranted, ifAllGranted, ifAnyGranted);
}
 void setupRender() {
test = checkPermission();
}

 /** *//**
* Returns null if the test method returns true, which allows normal
* rendering (of the body). If the test parameter is false, returns the else
* parameter (this may also be null).
*/
 Object beginRender() {
return test ? null : elseBlock;
}

 /** *//**
* If the test method returns true, then the body is rendered, otherwise not. The component does
* not have a template or do any other rendering besides its body.
*/
 boolean beforeRenderBody() {
return test;
}
}
示例:
1. 在登录页面:
@SessionState
private Authentication auth;
......
// if user name and password is valid:
auth.setPrivliegeList(.....);
2. 在需要权限控制的页面模板中:
<t:ifRole ifAllGranted="admin">
administrator can see this block
</t:ifRole>
与现在最流行的SSH相比较,Tapestry能够完全替代其中Struts2和Spring,但是他还是需要一个ORM的框架。IBatis由于比较低的学习曲线,也受到很多人的喜爱。尤其是在IBatis3中引入了许多新的概念和想法,使用更加安全和便利。
本文主要介绍如何将Tapestry5.1和IBatis3进行整合。
简要步骤:
1. 准备工作
2. 数据库的建立
3. POJO的建立
4. IBatis相关配置文件的创建
5. Tapestry相关代码的完成
概要说明:
1、准备工作。这一部分是比较简单的,Eclipse之类的开发环境是必需的。Tapestry5.1、IBatis3(目前还是Beta7)、数据库(我使用的是MySql)的下载安装。
2、数据库的建立,由于是示例,所以数据库的建立也非常简单,只有一张User表,3个字段,Id,Name,Password
3、com.sample.User类,对应数据库表的3个字段,生成User类
4、IBatis配置文件:Configuration.xml,UserMapper.xml,jdbc.properties的生成, 前两个必需,最后一个可选.
5、在AppModule里,使用build方法, 添加服务生成IBatis3的SqlSessionFactory, 在需要使用SqlSessionFactory的地方,使用@InjectService注入即可
详细说明:
1、大家到各自的网站上下载相应的包好了。我只罗列一下我所用到的Lib:
antlr-runtime-3.1.1.jar
commons-codec-1.3.jar
commons-lang-2.4.jar
ibatis-3-core-3.0.0.216.jar
javassist.jar
log4j-1.2.14.jar
mysql-connector-java-5.0.5.jar
slf4j-api-1.5.10.jar
slf4j-log4j12-1.5.10.jar
stax2-api-3.0.1.jar
tapestry-core-5.1.0.5.jar
tapestry-ioc-5.1.0.5.jar
tapestry5-annotations-5.1.0.5.jar
woodstox-core-lgpl-4.0.7.jar
2、Create Table
DROP TABLE IF EXISTS `test`.`user`;
CREATE TABLE `test`.`user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
`password` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
3、
package com.sample.model;
public class User {
private long id;
private String name;
private String password;
// getter and setter ....
}
4、我把Configuration.xml和UserMapper.xml都放在src目录下,这样在部署的时候,就是生成在classes,也就是类路径的根目录下。
Configuration.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//ibatis.apache.org//DTD Config 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties">
</properties>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="poolPingEnabled" value="${pingenable}"/>
<property name="poolPingQuery" value="${pingquery}"/>
<property name="poolPingConnectionsNotUsedFor" value="${pingnotusetime}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
</configuration>
UserMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="com.sample.model.UserMapper">
<select id="selectUser" parameterType="int" resultType="com.sample.model.User">
select * from user where id = #{id}
</select>
</mapper>
jdbc.properties:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost/test?autoReconnect=true
jdbc.username=root
jdbc.password=root
pingenable=true
pingquery=SELECT 1
pingoldertime=0
pingnotusetime=3600000
5、
package com.sample.web.services;
public class AppModule {
public static SqlSessionFactory buildSqlSessionFactory() {
try {
String resource = "Configuration.xml";
Reader reader = Resources.getResourceAsReader(resource);
return new SqlSessionFactoryBuilder().build(reader);
} catch (Exception e) {
logger.warn("failed to build SqlSessionFactory: ", e);
return null;
}
}
private static Logger logger = LoggerFactory.getLogger(AppModule.class);
}
package com.sample.model;
public interface UserMapper {
public User selectUser(int id);
}
package com.pc.sample.web.pages;
public class Layout {
@InjectService("SqlSessionFactory")
private SqlSessionFactory sqlMapper;
public String getUserName() {
if ( sqlMapper == null ) {
return "null-mapper";
}
SqlSession session = sqlMapper.openSession();
try {
UserMapper userMapper = session.getMapper(UserMapper.class);
if ( userMapper == null ) {
return "null-userMapper";
}
User user = userMapper.selectUser(1);
if ( user == null ) {
return "null-user";
}
return user.getName();
} catch (Exception e) {
return "exception-" + e.getMessage();
} finally {
session.close();
}
}
}
几个注意事项:
1,
因为我的IBatis的配置文件Configuration.xml是放在类路径的根目录下,所以在初始化SqlSessionFactory的时候,直
接用String resource =
"Configuration.xml";就行了,否则需要添加相应的路径,比如:把Configuration.xml与User类放在一起,也就是在
com.sample.model这个package中,那么就要写成:String resource =
"com/sample/model/Configuration.xml";
同样,在Configuration.xml中,指定UserMapper.xml的规则也是这样的。
2,UserMapper的使用。Mapper的使用是IBatis3中才有的新功能,也是IBatis用户指南中推荐使用的方式。因为这样使用的话,就完全避免了类型的强制转换,实现了类型安全。
需要注意的是UserMapper只是一个接口。我们不需要提供这个接口的具体实现。IBatis3会自动生成一个具体的实例。
其中的方法名必须与UserMapper.xml中的select语句的id一样。在我的例子中是selectUser.
另外,此方法的返回值的类型必须与UserMapper.xml中配置的returnType一致。
最后要提醒的是UserMapper.xml中的namespace必须是UserMapper的全类名,在本例中就是com.sample.model.UserMapper
here is a summary of key features in Spring 3.0 overall:
* Spring expression language (SpEL): a core
expression parser for use in bean definitions, allowing for references
to nested bean structures (e.g. properties of other beans) as well as
to environmental data structures (e.g. system property values) through
a common #{…} syntax in property values.
* Extended support for annotation-based components:
now with the notion of configuration classes and annotated factory
methods (as known from Spring JavaConfig). Spring also allows for
injecting configuration values through @Value expressions now,
referring to configuration settings via dynamic #{…} expressions or
static ${…} placeholders.
* Powerful stereotype model: allows for creating
'shortcut' annotations through the use of meta-annotations, e.g. for
default scopes and default transactional characteristics on custom
stereotypes. Imagine a custom @MyService annotation indicating
@Service, @Scope("request") and @Transactional(readOnly=true) through a
single annotation.
* Standardized dependency injection annotations:
Spring 3.0 comes with full support for the JSR-330 specification for
Dependency Injection in Java – annotation-driven injection via @Inject
and its associated qualifier and provider model, as an alternative to
Spring's own @Autowired and co.
* Declarative model validation based on constraint annotations:
Spring-style setup of a JSR-303 Bean Validation provider (such as
Hibernate Validator 4.0). Comes with an annotation-driven validation
option in Spring MVC, exposing a unified view on constraint violations
through Spring’s binding result facility.
* Enhanced binding and annotation-driven formatting:
Converter and Formatter SPIs as an alternative to standard
PropertyEditors. Formatting may be driven by annotations in a style
similar to JSR-303 constraints, e.g. using @DateTimeFormat. Also, check
out the new mvc namespace for convenient setup of formatting and
validation in Spring MVC.
* Comprehensive REST support: native REST
capabilities in Spring MVC, such as REST-style request mappings, URI
variable extraction through @PathVariable parameters, and view
resolution driven by content negotiation. Client-side REST support is
available in the form of a RestTemplate class.
* Rich native Portlet 2.0 support: Spring MVC fully
supports Portlet 2.0 environments and Portlet 2.0’s new event and
resource request model. Includes specialized mapping facilities for
typical portlet request characteristics: @ActionMapping,
@RenderMapping, @ResourceMapping, @EventMapping.
* Object/XML Mapping (OXM): as known from Spring
Web Services, now in Spring Framework core. Marshalling and
Unmarshaller abstractions with out-of-the-box support for JAXB 2,
Castor, etc. Comes with integration options for XML payloads in Spring
MVC and Spring JMS.
* Next-generation scheduling capabilities: new
TaskScheduler and Trigger mechanisms with first-class cron support.
Spring 3.0 comes with a convenient task namespace and also supports
@Async and @Scheduled annotations now. This can be executed on top of
native thread pools or server-managed thread pools.
Beyond those big themes, there are hundreds of refinements in the
details which you will particularly appreciate when upgrading from
Spring 2.5. Check the changelog and the javadocs…
In terms of system requirements, Spring 3.0 covers a broad range of
environments. For two key characteristics, Spring 3.0 supports Java SE 5 and above and Servlet 2.4 and above, e.g. Tomcat 5.x and 6.x, also retaining compatibility with common enterprise servers such as WebSphere 6.1 and WebLogic 9.2
(which are formally still based on J2EE 1.4). At the same time, we
support GlassFish v3 already – adapting to Java EE 6 API level in
Spring as well.
As a consequence, Spring 3 brings brand-new component model features, and also standards like JSR-330 injection and JSR-303 validation, to established production environments – without having to upgrade your server installation! All you have to do is to upgrade the application libraries of your Spring-powered application to Spring 3.0…
Enjoy – and watch out for follow-up posts about specific Spring 3 features, as well as for samples running on Spring 3.0!
struts2的文件上传对文件大小的限制,缺省值是2m,也就是说缺省情况下,最大只能上传2m的文件。根据文档所说需要对fileUpload这个拦截器的一个参数maximunSize进行设置
<interceptor-ref name="fileUpload">
<param name="maximumSize">1000000</param>
<param name="allowedTypes">image/gif,image/jpeg,image/jpg,image/png</param>
</interceptor-ref>
但是我设置了之后并没有作用。
后来,仔细查看日志后才发现错误是commons-fileupload里面的文件大小限制引起了错误。
在struts.xml中,添加
<constant name="struts.multipart.maxSize" value="16777216"/>
解决问题!
摘要: JavaRebel是一个工具,主要是用于热加载,比如说在Tomcat之类的应用服务器中,更新了class或者某些资源文件,使用了JRebel之后,就不需要重新启动应用服务器。这对于开发的人来说,是特别方便的。当然Java也提供了HotSpot的JVM,但是如果你修改的类中有方法名称变动的话,HotSpot就无能为力了,必须要重要启动应用服务器。
这里有一点先声明一下,本文只是破解仅限于学习和研究... 阅读全文
目前从实际应用来看,ORM的老大自然是Hibernate,可是iBatis因为相对比较直观、学习曲线相对较低,因而也赢得了不少用户的青睐。
本文主要介绍作为iBatis辅助工具的iBator的使用方法。
iBator是一个iBatis相关代码的自动生成工具。
1、安装iBator的插件
在Eclipse中,使用添加站点的方法,输入网址http://ibatis.apache.org/tools/ibator,进行iBator的安装。
2、建议不要直接在使用iBatis的项目里直接使用iBator,推荐另外单独建立一个项目来生成。比如,建立一个项目叫:IbatorPrj
3、右键点击IbatorPrj这个项目,如果刚才的插件安装正确的话,就会看到一个“Add iBATOR to the build path”的选项,点击一下。
4、创建iBator的配置文件。下面是我的例子,大家在实际使用的过程中,需要根据自己的情况进行相应的修改。
主要就是数据库JDBC库的路径、数据库驱动的类名、项目的名称、包名等。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ibatorConfiguration
PUBLIC "-//Apache Software Foundation//DTD Apache iBATIS Ibator Configuration 1.0//EN"
"http://ibatis.apache.org/dtd/ibator-config_1_0.dtd">
<ibatorConfiguration>
<classPathEntry location="c:\javaLibs\MySql\mysql-connector-java-5.0.6-bin.jar" />
<ibatorContext id="SampleiBator" targetRuntime="Ibatis2Java5">
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost/sample" userId="root" password="admin">
</jdbcConnection>
<javaTypeResolver>
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>
<javaModelGenerator targetPackage="com.sample"
targetProject="IbatorPrj\src">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<sqlMapGenerator targetPackage="com.sample.xml"
targetProject="IbatorPrj\src">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>
<daoGenerator type="GENERIC-CI" targetPackage="com.sample.dao"
targetProject="IbatorPrj\src">
<property name="enableSubPackages" value="true" />
</daoGenerator>
<table schema="sample" tableName="tab1" domainObjectName="JavaBean1">
<property name="useActualColumnNames" value="false" />
<generatedKey column="ID" sqlStatement="MySql" identity="true" />
</table>
</ibatorContext>
</ibatorConfiguration>
5、配置文件生成完毕后,右键点击这个文件,选择“Generate iBatis Artifact”,然后你就在配置的文件夹下找到自动生成的文件了。
摘要: Tapestry IoC容器从历史上来说,是从从HiveMind继承发展而来,但是HiveMind和目前大红大紫的Spring都不能满足Tapestry的一些特定的需求,所以全新开发了一套IoC的容器。
其核心思想就是使用Java代码自身来解决依赖注入而不是由Xml之类的配置文件来完成,这和Guice的思想是非常相似的,Lewis也承认从Guice那里借鉴了不少。
另外需要说明一下的是,Tapesty还从中国的一个非常古老但又充满哲理的游戏--围棋中借鉴了一些术语和思想。大意是围棋中经常要把棋子走的轻盈(Lightness),让每个棋子都能尽量地高效。编程也一样要轻量(Lightness)。 阅读全文
在应用中一般普通的JavaPojo都是由Spring来管理的,所以使用autowire注解来进行注入不会产生问题,但是有两个东西是例外的,一个是Filter,一个是Servlet,这两样东西都是由Servlet容器来维护管理的,所以如果想和其他的Bean一样使用Autowire来注入的话,是需要做一些额外的功夫的。
对于Filter,Spring提供了DelegatingFilterProxy,所以本文主要讲述Servlet的解决。
1、比较直观但是不大优雅的做法是重写init()方法,在里面使用AutowireCapableBeanFactory来手工告诉Spring:我这个Servlet是需要这样的一个Bean的。具体写法:
public void init(ServletConfig servletConfig) throws ServletException {
ServletContext servletContext = servletConfig.getServletContext();
WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
AutowireCapableBeanFactory autowireCapableBeanFactory = webApplicationContext.getAutowireCapableBeanFactory();
autowireCapableBeanFactory.configureBean(this, BEAN_NAME);
}
其中,BEAN_NAME就是需要注入的Bean在spring中注册的名字.
这样写的主要问题是就是那个BEAN_NAME,这样写有点主动查找,而不是依赖注入的感觉。
2、创建一个类似于DelegatingFilterProxy那样的代理,通过代理根据配置来找到实际的Servlet,完成业务逻辑功能。
假定我们有一个Servlet名字叫UserServlet,需要注入一个UserManager,伪代码如下:
public class UserServlet extends HttpServlet {
@Autowired(required = true)
private UserManager userManager;
}
第一步:
public class DelegatingServletProxy extends GenericServlet {
private String targetBean;
private Servlet proxy;
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
proxy.service(req, res);
}
@Override
public void init() throws ServletException {
this.targetBean = getServletName();
getServletBean();
proxy.init(getServletConfig());
}
private void getServletBean() {
WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
this.proxy = (Servlet) wac.getBean(targetBean);
}
}
第二步:
配置web.xml文件,原来UserServlet的配置大致是这样的:
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>com.sample.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/userServlet</url-pattern>
</servlet-mapping>
现在修改为
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>com.sample.DelegatingServletProxy</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/userServlet</url-pattern>
</servlet-mapping>
注意,spring是根据Servlet的名字来查找被代理的Servlet的,所以,首先我们要在UserServlet类前面加上@Component,来告诉Srping:我也是一个Bean。如果名称和Web.xml里面定义的不一样的话,可以在这里指定Bean的名字,比如: @Component("userServlet")
在我的随笔 Extjs Tree + JSON + Struts2中我介绍了如何异步加载一个Extjs的树,但是很多网友留言说不能成功操作。现在我自己做了一个所有源代码的包,供大家下载。
有几点事项请大家注意
1、blogjava的文件上载要求单个文件不能超过4M,所以,我把web-inf目录下的所有jar文件删除了。
所有jar文件的列表是:
commons-beanutils-1.7.0.jar
commons-collections-3.2.jar
commons-digester-1.6.jar
commons-lang-2.3.jar
commons-logging-1.1.jar
dom4j-1.6.1.jar
ezmorph-1.0.4.jar
freemarker-2.3.8.jar
javassist-3.8.1.jar
json-lib-2.2.1-jdk15.jar
log4j-1.2.13.jar
ognl-2.6.11.jar
struts2-core-2.0.11.jar
xml-apis-1.0.b2.jar
xwork-2.0.4.jar
注意红色标记的那个jar文件是上次随笔中遗漏了的。这个文件是需要的。
2、blogjava要求上传文件不能是war文件,所以我把war文件改成了rar后缀。
文件的URL: war文件下载
struts2中conventions plugin的url取名规则:
假设有一个类:com.example.actions.HelloWorld,
Struts2会自动搜索所有实现了com.opensymphony.xwork2.Action接口或者在struts.xml中<constant name="struts.convention.package.locators" value="actions"/> 指定的包下的类。
现存HelloWorld只是一个POJO,但是他在actions包下,这样Struts2就认可这是一个Action.
那么URL会是什么呢?是hello-world,类似于:http://localhost:8080/<contextPath>/hello-world.action.
如果你不喜欢这样的自动分配的URL,那么可以在里面的方法使用@Action来改变
@Action("/helloWorld")
public void execute() throws Exception {
return "success";
}
目前新版本的Eclipse在启动应用服务器的时候有一个新的选项:Start the server in profiling mode。
我个人使用的是tomcat6.0
但是我在一开始点击这个按钮的时候,出现了错误提示信息: Could not launch in profiling mode because no profilers are configured.
经过一番搜索,发现要求安装TPTP(Test and Performance Tools Platform),下面我把我的安装步骤简单地列举如下:
1、下载,TPTP的最新版本是4.6.0,下载地址:http://www.eclipse.org/tptp/home/downloads/?ver=4.6.0#tptp-plugins. 这里包括两个部分:runtime和sdk,如果你只是进行profile的运行分析,不对TPTP进行任何扩展,那就只需要下载runtime。下载的时候可以下载相应平台的(比如:windows的)或者下载全部平台的压缩包。我选择的是下载runtime和SDK两个的所有平台的压缩包,
2、安装。安装TPTP是非常简单的,把下载的压缩包解压到Eclipse的安装目录下就行了
3、除了上面的runtime/SDK,还需要安装Agent Controller,第一步当然还是下载了,地址:http://www.eclipse.org/tptp/home/downloads/?ver=4.6.0#rac
同样的,这也有runtime和SDK两部分,选择和步骤1一样。
4、把下载的两个压缩也解压到Eclipse的安装目录下。
5、其他还有一些可选的步骤,比如:Native Logging/Generic Log Adapter之类的东西,我一概都没有下载安装。
6、重新启动Eclipse,点击start server in profiling mode, OK, 成功,出现了一个对话框,让你选择监控的类型,是要监控线程,还是内存,选择一个,然后浏览一下你的网站,就能得到一张列表了。
总的来说,整个过程并不复杂,非常顺利。
1. 数据库的表结构
CREATE TABLE `software` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `version` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`publish_time` datetime NOT NULL,
`software_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
);
2. java的class
---------------------------------------
Software.java
import java.util.LinkedHashSet;
import java.util.Set;
import javax.persistence.Entity;
@Entity
public class Software {
private Long id;
private String name;
private Set<Version> versions = new LinkedHashSet<Version>();
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@OneToMany(cascade = { CascadeType.ALL }, mappedBy="software")
@JoinColumn(name = "software_id")
@Fetch(FetchMode.SUBSELECT)
@OrderBy("id")
public Set<Version> getVersions() {
return version;
}
public void setVersions(Set<Version> Versions) {
this.versions = versions;
}
}
-----------------------------------------------------
Version.java
import java.util.Date;
import javax.persistence.Entity;
@Entity
public class Version{
private Long id;
private Date publishTime;
private Software software;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Date getPublishTime() {
return publishTime;
}
public void setPublishTime(Date publishTime) {
this.publishTime = publishTime;
}
@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
@JoinColumn(name = "software_id")
public Software getSoftware() {
return software;
}
public void setSoftware(Software software) {
this.software = software;
}
}
3. 测试代码
Software software = new Software();
software.setName("Windows");
Version version = new Version;
version.setPublishTime(new Date());
version.setSoftware(software);
software.getVersions().add(version);
software.save();
hibernate会自动生成两条insert语句,一条是software的insert语句,一条是version的insert语句。
同样,如果删除software的话,也会生成两条delete语句
前一段时间,使用了NetBeans的6.5版本,发现诸多不如意的地方,详见:http://www.blogjava.net/usherlight/archive/2009/08/07/247005.html
最近,看到NetBeans发布了新版本6.7,后来我又升级到了6.7.1. 在使用过程中,还是感到了不少不满意的地方。
1. 可选择的或者说内建支持的应用服务器种类偏少。没有Jetty
2. 应用的发布的运行还是一如上次的不顺利。
a. 启动、停止还是有问题,我使用的是jdk1.6+Tomcat6.0,点击服务器停止按钮,有时候并没有真正停止Tomcat,我只有运行Tomcat目录下的bat文件来停止Tomcat
b. 自动的部署有问题,我最后就是因为这个问题而放弃NetBeans的,我的应用修改了之后,总是无法正确部署。Tomcat启动后,进入首页,内容还是没有变化,经常需要先Clean,再Deploy。
不过,NetBeans也有优点,至少内建支持maven,就是一个比较方便的地方。
根据前面的4部分内容,我们已经了解了Tapestry的基本概念,掌握了配置、组件等内容。现在我们通过剖析Tapestry的入门示例来对Tapestry进行一个总体上认识。
1、web.xml
<web-app>
<display-name>app Tapestry 5 Application</display-name>
<context-param>
<!-- The only significant configuration for Tapestry 5, this informs Tapestry
of where to look for pages, components and mixins. -->
<param-name>tapestry.app-package</param-name>
<param-value>t5demo</param-value>
</context-param>
<filter>
<filter-name>app</filter-name>
<filter-class>org.apache.tapestry.TapestryFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>app</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
这就是一个最简单的Tapestry应用所需要配置的内容了。
a.context-param中的tapestry.app-package配置,这在第一部分说过:这是Tapestry要求配置的java package的名称,Tapestry相关内容都需要在这个package下面的pages, services, componets子package下。这里的配置是t5demo
b.TapestryFileter的配置。这个非常容易理解,几乎所有现在流行的web框架都需要一个类似的定义。
2、start.tml以及相应的java class,例子中就是t5demo.pages.Start.java
Start.java非常简单,只定义了一个get方法:
public class Start
{
public Date getCurrentTime()
{
return new Date();
}
}
相应的页面start.tml
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd">
<head>
<title>app Start Page</title>
</head>
<body>
<h1>app Start Page</h1>
<p> This is the start page for this application, a good place to start your modifications.
Just to prove this is live: </p>
<p> The current time is: ${currentTime}. </p>
<p>
[<t:pagelink t:page="Start">refresh</t:pagelink>]
</p>
</body>
</html>
首先要注意在html的tag中加入了Tapestry的命名空间。
第二、${currentTime}就是Tapestry的Tag了,这里就会调用对应class的getCurrentTime方法在页面上显示对应的值。
第三、<t:pagelink>定义一个到自己本身页面的链接,来完成刷新的任务。t:pagelink在本系列的第4部分介绍过。
3、需要的library:
commons-codec.jar
javassist.jar
log4j.jar
slf4j-api.jar
slf4j-log4j.jar
tapestry5-annotations-5.1.0.5.jar
tapestry-core-5.1.0.5.jar
tapestry-ioc-5.1.0.5.jar
4、再加上一个log4j.properties,这就是一个最简单的tapestry应用所需要的全部东西了。
怎么样,感觉还是挺简单的吧。
个人认为flex项目不没有能够迅速普及的原因是:
缺少IDE的支持。adobe做为一个商业公司对flex
builder进行收费当然无可厚非。但是我认为这确实在一定程度上阻碍了flex的发展。做一个对比,jdk和flex
sdk一样都免费了。但是我们有eclipse,
netbeans这样优秀的免费IDE来进行开发,而eclipse是ibm捐献出来的,netbeans是sun提供的。为什么adobe不能这样搞
呢,毕竟赢利途径不止是卖ide一条嘛。
好了,闲话说了一堆,现存转入重点,谈一下我们今天要介绍的内容:
JSF-FLEX项目的目的是为了让用户能够象创建JSF组件一样创建Flex组件。JSF-FLEX项目能够自动生成mxml,swc,swf等文件,
并能把这些组件的值通过JSON+JAVASCRIPT和传递给Managed
Beans。另外还提供渲染工具,能够把JSF-FLEX的组件与普通组件结合起来显示在同一个视图中。
http://code.google.com/p/jsf-flex/
发布谷歌 Chorme 浏览器之后的 9 个月是令人兴奋的。今天,全球超过 3
千万的用户经常性地在使用它。我们为那些活在网络中的人们设计了谷歌 Chrome
浏览器,帮助他们搜索信息、查收邮件、获知新闻、购物,或者与朋友保持联络。然而,浏览器工作在的各种操作系统却诞生于没有互联网的时代——这正是为什么
今天我们要宣布这个新的项目:谷歌 Chrome 操作系统,它是谷歌 Chrome 浏览器的自然延伸,也是我们重新思考操作系统之道的尝试。
谷歌 Chrome 操作系统是一个开放源代码的、精巧的操作系统,它最初会针对上网本。在今年晚些时候,我们将开放它的源代码。在 2010
年下半年,运行谷歌 Chrome
操作系统的上网本就将被带给我们的消费者。我们已经与合作伙伴讨论过这个项目,并将很快与开放源代码社区合作,所以选择现在与大家分享我们的愿景,以让大
家了解我们正在努力成就的目标。
快速、简便和安全是谷歌 Chrome
操作系统的核心特质。我们正在将该操作系统设计得更快速更精巧,数秒间即可启动并将您带入互联网世界。用户界面最小化以避免掩盖您的个人风格,而大多数的
用户体验则将发生于网络之上。就像我们为谷歌 Chrome 浏览器所做的一样,我们要回归本来,彻底地重新设计谷歌 Chrome
操作系统的底层安全架构。如此一来,用户就不必再应对病毒、恶意软件和安全更新。一切皆由系统搞定。
谷歌 chrome 操作系统可运行于 x86 和 ARM
芯片,并且,我们正与各类原始设备商合作,以便于明年为市场带来许多的上网本选择。谷歌 Chrome 运行于一个新的基于 Linux
核心的窗口系统,其软件架构十分简单。对应用开发者们来说,网络即是平台。所有的互联网应用都将自动运行,而新的各种应用可由您最喜欢的网络语言编写。当
然,这些应用不仅可以运行于谷歌 Chrome 操作系统,也可以运行于 Windows、Mac 和Linux
上的任何标准浏览器,从而令开发者拥有任何平台上的最大用户基数。
谷歌 Chrome 操作系统是一个全新的项目,与 Android 无关。Android
从设计之初就跨手机、机顶盒和上网本等多样终端而工作。谷歌 Chrome 操作系统则为那些花费绝大部分时间用于上网的用户而设计,与此同时,谷歌
Chrome 操作系统被设计用于装备从小型上网本到大型台式系统在内的各种计算机。谷歌 Chrome 操作系统和 Android
有一些领域重合,因为我们坚信选择驱动创新,并让包括谷歌在内的每一个人受益。
我们从用户那里听到了很多反馈,他们所传达的信息毋庸置疑——计算机应该更好。人们希望可以即刻获得电子邮件,而不是浪费时间等待计算机启动和浏
览器开启;人们希望他们的计算机总是像第一次买到时那样快速运行;人们希望他们的数据可以唾手可得,无论他们在哪里,也无需担心电脑丢失或者忘记备份文
件。更重要的是,人们不希望花几个小时在每一片新的硬件上以配置他们的计算机,或是不得不为不断的软件更新而烦心。当我们的用户拥有了更佳计算机体验的时
候,这些快乐的用户们就更愿意呆在互联网上,而谷歌即可从中受益。
我们仍有许多工作需要完成,并且为了成就这一愿景,我们显然需要来自开放源代码社区的很多帮助。如果您对这个项目感兴趣或者有其他问题,请查看常
见问题及回答。我们为即将到来的一切而兴奋不已,我们希望您的心情和我们一样。敬请在这个秋季期待更多的更新,并祝愿大家度过一个愉快的夏天。
另外也有人持有不同意见:
LinuxWorld发表分析文章称,有5大原因显示Chrome OS无法成功:
1,上网本所占份额很小
Google计划推出Chrome OS是基于对上网本需求强劲的预期,不过,虽然上网本很重要,但在PC销售中所占比重仍很小。
另外,Google可能还有个想法:对微软来说,他们需要靠出售操作系统获利,而对Google来说,可以免费贡献Chrome。然而,诸如Linux这样的免费操作系统已经存在多年,但Linux在操作系统市场仅占很小份额,约为1%。
Chrome OS主要还是依靠那些上网本用户,他们不需要特别的应用,因此能接受低价但又能提供相当功能的操作系统。
2,微软或许会拼命回击
想想,如果微软宣布2010年Windows 7上网本版将免费提供,那还有多少人会用Chrome OS。至少从理论上来说,任何目前Google能做的事情,微软都能做得更好。
如果微软变得疯狂,任何事情都可能发生。多年来,微软的敌人一直是它自己。像Chrome OS这样的外部刺激或许会帮助微软获得活力,就如免疫系统一样消灭入侵的外来威胁。
3,Google在云计算应用上缺少建树
目前,Google Docs算是Google推出的最好的云计算应用,其它的乏善可陈。而且,就Google Docs来说,仍有很多事情无法做到。如果偶尔使用,倒也不错,但作为完全替代的解决方案,无法满足要求。
4,Chrome并不是一个真正的操作系统
Google希望推出的Chrome OS对用户来说隐藏操作系统的影子,而直接提供方便易用的应用。但究竟有多少操作系统的功能会被牺牲掉?
而另外,如果Chrome越接近真正的操作系统,那它就更像Linux。而这又不是Google希望看到的。
5,兼容性问题
对硬件和软件兼容性问题的解决是微软能一直统治操作系统市场的原因之一。微软在通过推行标准解决兼容性问题的同时也成为垄断者。不过,消费者还是投了微软的票,因为他们不必再担心兼容性问题。
因此,Chrome OS对于那些可以说是一次性使用的上网本来说,或许有点用处,但对整个操作系统市场来说将是微不足道的一份子。
一段时间没有用笔记本上的Eclipse,今天给安装了一个新的3.5版(Galileo)的Eclipse,结果发现启动的时候,跳出一个对话框说是
JVM terminated. Exit Code=-1.
在网上找了一下,最后有效而且最简单的方法是:
删除eclipse目录下的eclipse.ini,然后Eclipse又能启动工作了。
原因应该是Eclipse.ini文件里的启动参数和我的笔记本不配合(个人猜测),删除后,使用默认配置,启动成功。
摘要: Tapestry最基本的组成部分:页面组件 阅读全文
摘要: Tapestry重要特性之一:页面的缓存以及页面间值的传递 阅读全文
摘要: Tapestry的基本配置和目录结构 阅读全文
Ben Gidley进行了一个关于Tapestry5.1.0.5的性能测试。原文见:http://blog.gidley.co.uk/2009/05/tapestry-load-testing-round-up.html
最后,他得出的结论是:
1、Tapestry的速度是比较快的。即使在一定的压力下Tapestry的反应时间也相当短。Tapestry并不总是最快的解决方案,但它对于我(译注:Gidley)已经足够快了。
2、Tapestry没有内存泄漏。我以前曾经听说过Tapestry会占用大量的内存,实际上,正好相反。它使用的内存比struts/jsp还要少。内存使用曲线相当的平坦。
3、Tapestry在表单应用中比struts要快。Tapestry在应用变得非常复杂的时候有一定的优势。这可能利益于其模块池技术。
4、Tapestry不轻易崩溃,即使崩溃,也会恢复。Tapestry在极大压力的情况下确实会相应变慢,但是它会暂停或者遇到瓶颈(译注:我怀疑是作者这里有笔误,从语气和上下文来看,感觉应该不是暂停和没有瓶颈),这的确是一个好事情。另外在压力减轻之后,Tapestry能够自动恢复。
5、更多的CPU并一定会提升性能。在一系列的测试中,性能与CPU的数量并不是线性增长。2个CPU确实比一个CPU的性能翻倍了,但是4个CPU并不比2个CPU的性能翻倍。因此,建议在多个双核CPU的虚拟机上运行,而不是少数的4核CPU上运行。
6、64位比32位要快。这一点很让我惊奇。不管在Solaris还是Linux上,运行在64位JVM中要比在32位JVM要快。
7、Linux要比Open Solaris X86要快。这一点同样让我惊奇。我本来以为性能应该是相似的。
最终的结论是:Tapestry即使是对于一个大并发量的Web应用来说也已经足够快了。如果你的应用有性能问题的话,那么问题应该出在你自己本身的代码上。
上述是原文的翻译。下面是一些评论:
Howard(应该是Tapestry的作者):Taptestry5和Struts相比,我认为差别应该是在反射的使用上(包括在java.bean.Introspector中大量的synchronization)。因此在Struts将查询参数的名称映射成JavaBean属性的时候,会比较耗时。而Tapestry5是不使用反射的,Tapestry在查询参数和JavaBean的属性之间使用一种“预编程”向量组件,也许这就是两者(Tapestry和Struts)的差别。当然,这只是猜想,如果要证实的话,是需要花费很多时间的。我认为OGNL的教训不是说反射很慢,而是在于一个关键代码上的序列存取对于性能的影响是相当大的。
最后一个小提示:我觉得在Tapestry5应用中如果把BeanModel从BeanModelSource中只提取一次,然后给Grid,BeanEditForm等等提供一个可以存取的方法,将会获得相当的性能提升。这样就不是需要每次都重建BeanModel,将减少操作的消耗。
jeverest:我也进行了Tapestry的性能测试,并且同意Tapestry5的性能比较以前版本的要快。
Tapestry5.1经过数个alpha,Beta版的非正式发布,今天终于在主页看到最终正式版5.1.0.5的发布。
这次的版本算得上是比较迅速了,从官方主页中可以看到,第一版5.1.0.1是2月24日发布了,短短3个月不到的时间,发了4个版本,动作不可谓不迅速。
5.1中具有以下几个新特性
1、Tapestry现在开始采用BlackBird作为JavaScript的调试工具
2、一个Ajax的事件请求现在可以返回一个MultiZoneUpdate实例来更新浏览器中的多个区
3、客户端数字的检验实现了国际化
4、相对于5.0.18有显著的性能提升,主要是页面的加载时间和页面的渲染时间大大缩短
5、Tapestry的IoC服务现在既可以是Advised也可以是Decorated
6、Tapestry的服务现在可以注入到Spring的Bean中
7、对于支持Gzip的客户端,Tapestry现在可以压缩返回包
8、有序的和Mapped的配置信息现在可以被重新赋值
9、属性表达式得到加强,现在可以调用有参数的方法,或者创建一个列表
10、IoC的贡献既可以是类(自动生成实例)也可以是具体的实例
11、现在提供了一个简单的可以重写内建服务的方法
这里面最让我感兴趣的还是性能的提升,不知道在展示大数据量的时候性能的提升到底有多少,有机会一定要测试一下。
MySql中设置了Replication后,平常的使用都一直没有问题。
今天,我在Sql Brower中用Sql命令插入了几条数据却没有被复制。
原因是这样的:我在Sql Browser中没有选择我需要数据更新的数据库,而且使用Mysql这个数据库作为当前数据库。
而在Sql中指定了我的数据库名称,这样,我的数据如我所愿地进行了更新。
但是,通过这种方式的操作好像无法被复制。
我思考了一下,觉得应该是Log记录的问题,MySql设置了数据库复制后,有一个Log会记录所有数据库的变更,另一个数据库会根据这个Log来进行同样的数据操作。这样就实行了数据的复制。
我感觉如果你没有使用use <数据库名>这个命令,而是使用其他的数据库作为当前数据库,那么Log的记录就缺失了,因此复制也将不会进行。
1. 安装路径中的目录尽量不要有带空格的, 比如:最好不要安装在programs file下,好像会导致找不到conf下的配置文件
2. 不知道为什么,我无法使用development.conf来启动Resin, 只有resin.conf是可用的。
1. TagLib的运用(Spring Security)
在web.xml中添加:
<servlet>
<servlet-name>JSPSupportServlet</servlet-name>
<servlet-class>org.apache.struts2.views.JspSupportServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
在页面的最上面添加<#assign security=JspTaglibs["http://www.springframework.org/security/tags"] />
使用的时候:
<@security.authorize ifAnyGranted="ROLE_USER,ROLE_ADMIN">
Hello
</@security.authorize>
注意中间用的是句号,而不再是冒号,我一开始在这里没注意,花了不少时间解决这个问题
2. Context Path的取得
在Google中搜了一下,有人提问题,但是没有得到解决,后来查资料才知道应该是这样写的:
${request.contextPath}
3. 字符串的比较
字符串不能直接比较大小,我原来两个日期字符串的比较就需要先转换成日期型
<#if dateString1?date("yyyy-MM-dd HH:mm:ss") < dateString2?date("yyyy-MM-dd HH:mm:ss")>
日期小
</#if>
4. <#if><#else>
在if比较时小于号可以直接使用,但是大于号不行,要写成
<#if a > b>
</#if>
最近把Tapestry和JSF都研究了一下,最后还是决定选择Tapestry。
最主要的原因还是从性能上的考虑。
Tapestry的5.1版的最主要改动就是想提升性能,而JSF似乎还没有这方面的行动。
而且从Tapestry5.1的版本发布情况来看,动作相当的迅速。预计Final版马上就要出来了。
据Lewis的说法,Tapestry5.0在页面内容比较少的时候,速度几乎和纯JSP页面一样快。只是在展示大量数据的会有一定的性能瓶颈。
所以,他推出5.1来解决这个问题。
而JSF的实现和组件库呈现一种百花齐放的状态,难免会有一些良莠不齐。
当然大家需要选择其中比较好的,但是选型本身就是一件非常头疼的事情。
我选择的是MyFaces+RichFaces,但是我查看了RichFaces的在线Demo后,对其展示速度不是很满意。
不知道是演示网站的问题还是RichFaces本身的问题。
也欢迎大家进行讨论。
不过Tapestry相对JSF而言的一个缺点就是文档不够丰富。毕竟JSF是标准啊。
1. 启动速度比我的MyEclipse7.0要快不少。
2. 部署还是有问题
a. 速度慢,
b. 有一个spring的配置文件没有自动更新
c. 启动和停止glassfish都非常慢
3. 没有内建支持Jetty
最近一段时间研究了一下Tapestry, 确实是一个非常优秀的web框架。到目前为止,我觉得与别的框架比较下来,Tapestry最独到的地方在于Taglib的设计。
根据我的观察,好像只有Tapestry实现了将Taglib嵌入到html控件这样的功能。或许这个说法不准确,不过最是想表达这样一个意思。使用了Tapestry标签的jsp页面是可以在DreamWeaver之类的页面编辑工具中完整地显示出来的。
不你struts或者jsf那样,使用s:或者h: 即:<s:text>...
而Tapestry是:<input t:type="">
当然,其事件驱动的思想也是与大多数的Web框架不同的,不过JSF在这一点上和它是非常类似的。关键是jsf是JEE的标准,而且得到了不少开源组织的
拥护,产生了myfaces之类的实现,而且有了大量诸如:fichfaces,icefaces,restfaces等等的components
package. 在这一点上是Tapesty无法比拟的。
Struts2的官方文档看似琳琅满目,但实际上并不完备,许多细节问题并没有深入涉及,部分内容甚至还有错误(可参见:http://www.blogjava.net/usherlight/archive/2008/12/30/249143.html)。这次主要是补充一下,使用了ZeroConfig插件情况下,如何返回Stream类型的配置。
在ZeroConfig+CodeBehind结合使用的情况下,Struts.xml中基本上已经空无一物了。配置基本上使用Annotation在Java的类中注释完成。
@Parent("default")
@Result(name="rawFile", value="inputStream", type=StreamResult.class, params={"contentType", "text/html", "inputName", "inputStream", "", "", "bufferSize", "1024"})
public class FileDownloadAction {
public String execute() throws Exception {
inputStream = new FileInputStream("c:\\temp\\file.txt");
}
private InputStream inputStream;
//... getter and setter
}
这里最重要的是Result里面params的写法,params后面的大括号中,奇数个字符串是key值,偶数个字符串是value值。
其中,inputName的值-inputStream要和Class中的InputStream的属性名一致,而Result中的value的值也要与之一致。
今天试了一下引用url作为<s:a>的href,结果页面根本不能正确显示,后台log里面出现了一大片的错误,大意是<s:param>的用法错误。但是我的写法是完全copy在线文档http://struts.apache.org/2.x/docs/a.html
具体内容就是下面这一部分:
<s:url id="testUrlId" namespace="/subscriber" action="customField" method="delete">
<s:param name="customFieldDefinition.id" value="${id}"/>
</s:url>
经过尝试发现实际上应该这样写: <s:param name="customFieldDefinition.id" value="id"/>
struts2的文档一直被大家所指责,看来这种指责不无道理啊
我使用的是annotation方式的hibernate配置。结果在启动Tomcat的时候报错:
Invocation of init method failed; nested exception is org.hibernate.HibernateException: cannot simultaneously fetch multiple bags
解决方法:
去除Annotation中的所有FetchType="EAGAR"
NetBeans从功能来上,是相当不错的。而Eclipse则有一些成也插件,败也插件的感觉,至少存在插件的版本依赖造成混乱的抱怨。
但是NetBeans的在使用上的体验与eclipse相比还是有较大的差距(个人感觉).
首先令我感到不爽的是,netbeans不能正常停止tomcat.需要我手动在任务管理器里终止进程.
其次,netbeans的部署好像有些问题,部署速度比较慢,deploying的进度条总在那里闪,需要较长的时间才部署完毕。而且重新部署的策略也让人疑惑,感觉不是有了更新才部署,而是定时部署,因为我过了一段时间我就发现deploying的进度条出现了。最让人不爽的是部署有问题。我更新了 applicationContext-security.xml,点击clean and build,结果系统运行不正确,我查了半天才发现build目录下的这个文件根本没有改变。
在视图的查看上,netbeans也没有eclipse方便灵活,在eclipse中我喜欢将源文件(package presentation)设置为hierarchical, 这样在package explorer里面,视图显得比较简洁。另外,eclipse可以设置代码窗口与文件窗口里的文件连动。这两个功能我在netbeans里还没有找到。
mysql安装调试完毕,正式投入运行后,马上进行了mysql备份任务的生成。结果第二天一看,并没有能够如愿地生成备份文件。
马上开始查找原因。首先在mysql administrator里立即运行备份任务,没有问题,备份文件很快就生成了。但备份任务就是不能正确执行。
在事件查看器里发现了mysql的错误日志,root@localhost(password: no)access denied, error number: 1045。
奇怪,root@localhost无法登录数据库?可是登录mysql administrator,并且在里面单独运行backup都是正常的啊。
冷静一下,在windows控制面板的计划任务里,找到数据库备份的计划任务(Mysql5.X的备份计划实际上是生成了一个windows的计划任务,执行其设置好的脚本),查看了一下脚本,并没有什么问题。
再仔细研究了一下,发现问题应该是在password:no上,相当于试图不提供密码而使用root@localhost进行备份,所以出错了。那么如何改正呢。
最后发现问题是在;mysql_user_connection.xml里。这个文件里包含了mysql登录用的别名信息。结果不知道是什么原因这个文件里相同的别名出现了两次,第一次的配置里密码为空,而第二次的配置是正确的。
<last_connection>2</last_connection>
<password_storage_type>3</password_storage_type>
<user_connection>
<connection_name></connection_name>
<username>root</username>
<hostname>localhost</hostname>
<port>3306</port>
<schema></schema>
<advanced_options/>
<storage_path></storage_path>
<notes></notes>
<connection_type>0</connection_type>
<storage_type>2</storage_type>
<password_storage_type>3</password_storage_type>
<password/>
</user_connection>
<user_connection>
<connection_name>proddb</connection_name>
<username>root</username>
<hostname>localhost</hostname>
<port>3306</port>
<schema>message</schema>
<advanced_options/>
<storage_path></storage_path>
<notes></notes>
<connection_type>0</connection_type>
<storage_type>2</storage_type>
<password_storage_type>3</password_storage_type>
<password/>
</user_connection>
<user_connection>
<connection_name>proddb</connection_name>
<username>root</username>
<hostname>localhost</hostname>
<port>3306</port>
<schema>message</schema>
<advanced_options/>
<storage_path></storage_path>
<notes></notes>
<connection_type>0</connection_type>
<storage_type>1</storage_type>
<password_storage_type>3</password_storage_type>
<password>9D203859E</password>
</user_connection>
而登录mysql administrator时,根据last_connection的值,使用的是proddb的第二个配置。所以可以正常登录,而且在里面执行脚本,备 份都没有问题,而windows执行计划任务时,通过别名proddb在mysql_user_connection.xml中查找,找到的是第一个,其 中没有密码信息,所以报错。
问题找到了,解决就容易了,删除mysql_user_connection.xml中的proddb的第一个配置。备份计划任务果然能够正确地执行了。
今天尝试使用了一下两款MSN的插件应用:Msn Shell和Msn Plus! Live。两者都能解决多账号登录的要求。
其中Msn Shell是国人产品而Msn Plus! Live是法国人开发的产品,但是其I18N做的还是不错,内置了许多语言。
我首先尝试了Msn Plus! Live,第一感还是不错的,安装过程很顺利。安装好了之后,马上就是相关的设置。
设置完毕后,就可以直接使用了。其中我比较感兴趣的是快捷文字输入。
但是我马上发现一个问题:不知道为什么Msn Plus! Live总是占用15%左右的CPU,这一点令我十分不爽。直接从电脑从喀嚓出去。
接下来安装Msn Shell。安装过程也是一样的顺利,功能明显比Msn Plus! Live要多一点。在联系人上面多了一条“最近联系人”。
还多了一个“天气预报”可以自由选择国内的城市。另外还可以设置屏蔽MSN里面的广告。
但也有一点不爽的是在下面多了一条MSN Shell的工具条,我并不需要,但无法取消。
最后结论当然是选择Msn Shell。
right click project -> property -> source -> source/binary format -> choose JDK 5 instead of JDK 1.4
最好的办法是在新建项目时,在下拉框选择J2EE 1.4后,不要根据NetBeans提示的勾选set source same as JDK,这样会自动将Source设置为JDK 5而不是JDK 1.4.
01. download SpringSide 3.0.4 and extract to a folder and extract to a folder-SpringSide304
02. cd SpringSide304, mvn compile
03. mvn clean install
04. cd examples\mini-web\bin
05. start nexus, \tools\nexus\nexus-webapp-1.1.0\bin\jsw\windows-x86-32\Nexus.bat
06. run copy-jar.bat, all the jar files will be generated in exmples\mini-web\webapp
07. slf4j: jul-to-slf4j-1.5.6.jar
08. dozer in sourceforge.net
09. copy all the java source code in directory: modules\core\src\main\java\
10. copy all the java source code in directory: examples\mini-web\src\main\java
11. copy all the resource file in directory:examples\mini-web\src\main\resources
12. copy all the file in examples\mini-web\webapp
13. create database and run script in directory:
examples\mini-web\src\main\sql\derby, if the database you use isn't
derby, should modify the script
14. modify the the datasource in ApplicationContext.xml, the database name, user name, user password
15. copy the database jdbc driver package(for example: mysql-connector-java-X.jar) to lib
16. mini-web.log is in tomcat\logs
17. click run button in Netbeans.
UltraEdit是一个功能非常强大的文本编辑工具。
但是,它不是免费的。
在网上搜索了阵,对比了不少工具,诸如EMacs、Crimson Editor等等。最后选定了PSPad
主要是因为:
1、界面操作与UltraEdit比较类似,其他很多工具与日常的相差比较大。
2、功能与UltraEdit相比,并没有什么差别,我目前发觉得比较大的差别就在于列模式的选择上。
3、最大的优点就是免费。
在网上搜索对比了一番,最后觉得还是EssentialPIM这个软件最适合我的需要。
我主要就是想找一个能够每天记录一下当天的工作日志,最好能有提醒功能(Schedule)
EssentialPIM完全能够胜任这个工作。
其主要功能有:
Schedule
To-do List
Notes
界面简单清晰。另外还有一个密码保护的功能。
非常适合我。
推荐一把。
地址:www.essentialpim.com
1.Tools to satisfy your calendar, contact management, to do list and notes needs.
2.Synchronization with Outlook, Windows Mobile devices, Palm, iPOD, Google Calendar.
3.Simple printout of any or all modules and quick export of your data into the most useful formats (iCal, vCard, HTML).
4.Strong data protection using Advanced Encryption Standard (AES) algorithm.
5.Intuitive interface in many languages including German, Italian, French and Spanish.
EssentialPIM有Pro版和Free版,功能自然是有差别了,但对于我来说,Free就完全够用了。
[WARNING] POM for 'org.hibernate:jtidy:pom:r8-20060801:runtime' is invalid. It will be ignored for artifact resolution. Reason: Parse error reading PO
M. Reason: TEXT must be immediately followed by END_TAG and not START_TAG (position: START_TAG seen ...<licenses>\n\t\t\t<license>... @12:13) for pro
ject org.hibernate:jtidy at F:\Document\Datafile\repository\org\hibernate\jtidy\r8-20060801\jtidy-r8-20060801.pom
把原来的maven的local repository里面org\hibernate\jtidy\r8-20060801\jtidy-r8-20060801.pom目录下的pom.xml文件中
<licenses>
<licenses>
<license>
<name>Java HTML Tidy License</name>
<url>http://svn.sourceforge.net/viewvc/*checkout*/jtidy/trunk/jtidy/LICENSE.txt?revision=95</url>
<distribution>repo</distribution>
</license>
</licenses>
</licenses>
改为:
<licenses>
<license>
<name>Java HTML Tidy License</name>
<url>http://svn.sourceforge.net/viewvc/*checkout*/jtidy/trunk/jtidy/LICENSE.txt?revision=95</url>
<distribution>repo</distribution>
</license>
</licenses>
也就是删除多余的<licenese>起止标签.
应该是central的repository里面的这个文件有问题。
在FreeMarker中可以使用Strust2的标签set调用Java类的方法来获取返回值
<@s.set name="val" value="@com.test.utils.Property@getInstance().get('Value')"/>
然后就可以使用以下的语句在FreeMarker中取得val的值
<#if (Request.appType)?default("") == "OK">
</#if>
使用方法:
var now = new Date();
now.format("m/dd/yy");
// Returns, e.g., 6/09/07
// Can also be used as a standalone function
dateFormat(now, "dddd, mmmm dS, yyyy, h:MM:ss TT");
// Saturday, June 9th, 2007, 5:46:21 PM
// You can use one of several named masks
now.format("isoDateTime");
// 2007-06-09T17:46:21
// ...Or add your own
dateFormat.masks.hammerTime = 'HH:MM! "Can\'t touch this!"';
now.format("hammerTime");
// 17:46! Can't touch this!
// When using the standalone dateFormat function,
// you can also provide the date as a string
dateFormat("Jun 9 2007", "fullDate");
// Saturday, June 9, 2007
// Note that if you don't include the mask argument,
// dateFormat.masks.default is used
now.format();
// Sat Jun 09 2007 17:46:21
// And if you don't include the date argument,
// the current date and time is used
dateFormat();
// Sat Jun 09 2007 17:46:22
// You can also skip the date argument (as long as your mask doesn't
// contain any numbers), in which case the current date/time is used
dateFormat("longTime");
// 5:46:22 PM EST
// And finally, you can convert local time to UTC time. Either pass in
// true as an additional argument (no argument skipping allowed in this case):
dateFormat(now, "longTime", true);
now.format("longTime", true);
// Both lines return, e.g., 10:46:21 PM UTC
// ...Or add the prefix "UTC:" to your mask.
now.format("UTC:h:MM:ss TT Z");
// 10:46:21 PM UTC
/*
* Date Format 1.2.2
* (c) 2007-2008 Steven Levithan <stevenlevithan.com>
* MIT license
* Includes enhancements by Scott Trenda <scott.trenda.net> and Kris Kowal <cixar.com/~kris.kowal/>
*
* Accepts a date, a mask, or a date and a mask.
* Returns a formatted version of the given date.
* The date defaults to the current date/time.
* The mask defaults to dateFormat.masks.default.
*/
var dateFormat = function () {
var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
timezoneClip = /[^-+\dA-Z]/g,
pad = function (val, len) {
val = String(val);
len = len || 2;
while (val.length < len) val = "0" + val;
return val;
};
// Regexes and supporting functions are cached through closure
return function (date, mask, utc) {
var dF = dateFormat;
// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
if (arguments.length == 1 && (typeof date == "string" || date instanceof String) && !/\d/.test(date)) {
mask = date;
date = undefined;
}
// Passing date through Date applies Date.parse, if necessary
date = date ? new Date(date) : new Date();
if (isNaN(date)) throw new SyntaxError("invalid date");
mask = String(dF.masks[mask] || mask || dF.masks["default"]);
// Allow setting the utc argument via the mask
if (mask.slice(0, 4) == "UTC:") {
mask = mask.slice(4);
utc = true;
}
var _ = utc ? "getUTC" : "get",
d = date[_ + "Date"](),
D = date[_ + "Day"](),
m = date[_ + "Month"](),
y = date[_ + "FullYear"](),
H = date[_ + "Hours"](),
M = date[_ + "Minutes"](),
s = date[_ + "Seconds"](),
L = date[_ + "Milliseconds"](),
o = utc ? 0 : date.getTimezoneOffset(),
flags = {
d: d,
dd: pad(d),
ddd: dF.i18n.dayNames[D],
dddd: dF.i18n.dayNames[D + 7],
m: m + 1,
mm: pad(m + 1),
mmm: dF.i18n.monthNames[m],
mmmm: dF.i18n.monthNames[m + 12],
yy: String(y).slice(2),
yyyy: y,
h: H % 12 || 12,
hh: pad(H % 12 || 12),
H: H,
HH: pad(H),
M: M,
MM: pad(M),
s: s,
ss: pad(s),
l: pad(L, 3),
L: pad(L > 99 ? Math.round(L / 10) : L),
t: H < 12 ? "a" : "p",
tt: H < 12 ? "am" : "pm",
T: H < 12 ? "A" : "P",
TT: H < 12 ? "AM" : "PM",
Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
};
return mask.replace(token, function ($0) {
return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
});
};
}();
// Some common format strings
dateFormat.masks = {
"default": "ddd mmm dd yyyy HH:MM:ss",
shortDate: "m/d/yy",
mediumDate: "mmm d, yyyy",
longDate: "mmmm d, yyyy",
fullDate: "dddd, mmmm d, yyyy",
shortTime: "h:MM TT",
mediumTime: "h:MM:ss TT",
longTime: "h:MM:ss TT Z",
isoDate: "yyyy-mm-dd",
isoTime: "HH:MM:ss",
isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
};
// Internationalization strings
dateFormat.i18n = {
dayNames: [
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
],
monthNames: [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
]
};
// For convenience
Date.prototype.format = function (mask, utc) {
return dateFormat(this, mask, utc);
};
补充说明:
| Mask |
Description |
d |
Day of the month as digits; no leading zero for single-digit days. |
dd |
Day of the month as digits; leading zero for single-digit days. |
ddd |
Day of the week as a three-letter abbreviation. |
dddd |
Day of the week as its full name. |
m |
Month as digits; no leading zero for single-digit months. |
mm |
Month as digits; leading zero for single-digit months. |
mmm |
Month as a three-letter abbreviation. |
mmmm |
Month as its full name. |
yy |
Year as last two digits; leading zero for years less than 10. |
yyyy |
Year represented by four digits. |
h |
Hours; no leading zero for single-digit hours (12-hour clock). |
hh |
Hours; leading zero for single-digit hours (12-hour clock). |
H |
Hours; no leading zero for single-digit hours (24-hour clock). |
HH |
Hours; leading zero for single-digit hours (24-hour clock). |
M |
Minutes; no leading zero for single-digit minutes.
Uppercase M unlike CF timeFormat's m to avoid conflict with months. |
MM |
Minutes; leading zero for single-digit minutes.
Uppercase MM unlike CF timeFormat's mm to avoid conflict with months. |
s |
Seconds; no leading zero for single-digit seconds. |
ss |
Seconds; leading zero for single-digit seconds. |
l or L |
Milliseconds. l gives 3 digits. L gives 2 digits. |
t |
Lowercase, single-character time marker string: a or p.
No equivalent in CF. |
tt |
Lowercase, two-character time marker string: am or pm.
No equivalent in CF. |
T |
Uppercase, single-character time marker string: A or P.
Uppercase T unlike CF's t to allow for user-specified casing. |
TT |
Uppercase, two-character time marker string: AM or PM.
Uppercase TT unlike CF's tt to allow for user-specified casing. |
Z |
US timezone abbreviation, e.g. EST or MDT. With non-US timezones or in the Opera browser, the GMT/UTC offset is returned, e.g. GMT-0500
No equivalent in CF. |
o |
GMT/UTC timezone offset, e.g. -0500 or +0230.
No equivalent in CF. |
S |
The date's ordinal suffix (st, nd, rd, or th). Works well with d.
No equivalent in CF. |
'…' or "…" |
Literal character sequence. Surrounding quotes are removed.
No equivalent in CF. |
UTC: |
Must
be the first four characters of the mask. Converts the date from local
time to UTC/GMT/Zulu time before applying the mask. The "UTC:" prefix
is removed.
No equivalent in CF. |
And here are the named masks provided by default (you can easily change these or add your own):
| Name |
Mask |
Example |
| default |
ddd mmm dd yyyy HH:MM:ss |
Sat Jun 09 2007 17:46:21 |
| shortDate |
m/d/yy |
6/9/07 |
| mediumDate |
mmm d, yyyy |
Jun 9, 2007 |
| longDate |
mmmm d, yyyy |
June 9, 2007 |
| fullDate |
dddd, mmmm d, yyyy |
Saturday, June 9, 2007 |
| shortTime |
h:MM TT |
5:46 PM |
| mediumTime |
h:MM:ss TT |
5:46:21 PM |
| longTime |
h:MM:ss TT Z |
5:46:21 PM EST |
| isoDate |
yyyy-mm-dd |
2007-06-09 |
| isoTime |
HH:MM:ss |
17:46:21 |
| isoDateTime |
yyyy-mm-dd'T'HH:MM:ss |
2007-06-09T17:46:21 |
| isoUtcDateTime |
UTC:yyyy-mm-dd'T'HH:MM:ss'Z' |
2007-06-09T22:46:21Z |
A couple issues:
- In the unlikely event that there is ambiguity in the meaning of your mask (e.g.,
m followed by mm,
with no separating characters), put a pair of empty quotes between your
metasequences. The quotes will be removed automatically.
- If you need to include literal quotes in your mask, the following rules apply:
- Unpaired quotes do not need special handling.
- To
include literal quotes inside masks which contain any other quote marks
of the same type, you need to enclose them with the alternative quote
type (i.e., double quotes for single quotes, and vice versa). E.g.,
date.format('h "o\'clock, y\'all!"')
returns "6 o'clock, y'all". This can get a little hairy, perhaps, but I
doubt people will really run into it that often. The previous example
can also be written as date.format("h") + "o'clock, y'all!".
通过上两篇文章的研究,
详见:
我的struts2项目性能调优三步曲:http://www.blogjava.net/usherlight/archive/2008/07/01/211869.html
我的struts2项目性能调优三步曲(续):http://www.blogjava.net/usherlight/archive/2008/07/12/214462.html
得出的结论是:影响Struts2性能的原因在于Ognl的Value Stack的性能不佳。那么如果解决呢:
* 我首先尝试使用JSF。
一开始选择JSF的原因主要是:
1、Stuts2自己提供了JSF的Plugin
2、JSF是Sun作为标准提出,而且已经通过的。从Google的趋势搜索上也可以看出,搜索JSF的人在增多。
3、JSF作为一种以组件为基础的Web Framework有其独到之处,其内建的和其他许多开源的组件使用起来相当方便、强大。当然,对于不同的应用来说也有不利之处(后面会提到),但是如果能够坚持长期使用,逐渐积累组件库的话,JSF是一个很好的选择。
4、JSF的文档(或者说是书籍)还是比较多的。
经过测试使用后,发现其性能与Struts2相比确实提升不少。但是后来遇到了一个问题,所以最后还是放弃了JSF。这个问题是关于JSF的DataTable的,JSF提供的DataTable其实使用起来很方便,可定制化程度也不错,只是刚好缺少了我所希望的功能(也可能是我不知道如何实现)。我的应用中的DataTable是一个动态的结果集,也就是说输出的列是不能预先确定的,而DataTable却要求先声明好所有的DataColumn,我不知道如何解决这个问题。所以最后放弃了JSF。
* 我的第二个选择是FreeMarker
选择FreeMarker的原因是:
1、FreeMarker是Struts2缺少的模板引擎,Struts2的标签大部分是使用FreeMarker的,使用FreeMarker的话,连Plugin都省去了。
2、FreeMarker相对比较轻量级、因为他本身只是一个模板引擎,与JSF这样一个大而全的WebFramework相对,轻巧多了。
3、FreeMarker的学习起来非常容易,只要把他网站上的Document过一遍,基本上就OK了。
4、FreeMarker虽然体积小,功能还是相当强的,I18N,Converter之类的东西基本都全了,至少我所需要的功能全有。
5、FreeMarker相当灵活,他不象JSF把底层的东西封装了以后,暴露出一些属性可以设置,如果你需要的属性不能设置,你就没有办法了。在FreeMarker你直接操作最底层的东西,拥有很大的灵活性。当然,牺牲了一些方便性,比如,要用FreeMarker生成一个下拉框,就需要较多的工作量了。
测试之后,使用FreeMarker的性能很不错,在大数据量操作的情况下,至少一个数量级的性能提升。
主要原因是freeMarker的值直接从action中取得的,所以避开了ognl的stack value.
* 我的最终结论,如果要在Struts2中,展示或者操作大量数据,强烈推荐使用FreeMarker。
原文链接:http://www.theserverside.com/news/thread.tss?thread_id=50360
Jt2.7已经发布了,Jt是一个针对Java应用快速实现的面向模式的框架。Jt已经在数个重要的大型系统被采用。Jt实现了很多个被熟知的模式,包括:Dao,GoF的设计模式以及一些J2EE的模式。
Jt2.7的部分部件的功能得到了增强,而且增加了一个Jt自动化向导。Jt向导一个在Jt框架上建起来的应用,能够自动生成应用的框架结构。Jt向导能够在设计模式(包括Jt消息、DAO,MVC,GoF)的基础上自动生成应用模块。目前Jt向导实现了与MVC Struts和DAO Hibernate的集成。DAO的映射文件,Struts的配置文件,视图(JSPs),Java类都能够使用Jt向导来自动生成。
具体的功能包括:
* 实现了J2EE设计模式,包括J2EE business delegate,J2EE Session Facade,J2EE Service Locator和J2EE Value Object。
* 通过实现Web Service适配器和代理集成Web Service。Jt messaging API极大地简化了web service的开发和部署。
* 与business process modeling(BPM)的集成。Jt框架提供了一个jBPM的适配器。jBPM是一个BPM技术的开源实现。Jt应用现在可以使用流程图来模块化,这是一个非常好的模块化业务流程的方法。
* 与MVC设计模式和Ajax的集成。统一化的Jt模块和适配器提供了Jt框架API和上述两种技术间的透明接口。业务逻辑可以由Jt框架模块或BPM业务流程来实现。
* 与Hibernate的集成。Jt适配器提供了与Hibernate的透明接口。
* 与JDBC的集成
* 通过实现命令模式,支持Log,排队机制和操作的回退。
* 与JavaMail的集成
* 与EJB的集成。EJB客户端可以透明地存取远程框架对象。比EJB开发要简单的多。另外还实现了J2EE Service Locator模式。
* 方便的定制应用。主要通过配置文件完成:对象的属性可以从资源文件中加载。
* 与JSP的集成
* 通过XML适配器、助手、和内建的bean/XML映射与XML API的集成
Jt的在线文档: http://jt.dev.java.net/servlets/ProjectDocumentList
其他的一些信息可以在这里找到:http://jt.dev.java.net
前一阵子使用了DHtmlx的Tree,视觉效果不错,功能也不弱。具体参见: http://dhtmlx.com
现在把Struts2结合DHtmlxTree的经验心得整理一下,发表出来:
一、Struts.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="demo" extends="struts-default">
<action name="menu" method="execute" class="demo.TreeMenuAction">
<result>/WEB-INF/menu.jsp</result>
</action>
</package>
</struts>
二、tree.jsp
<%@ taglib prefix="s" uri="/struts-tags"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path;
%>
<html>
<head>
<title>Main Page</title>
<!-- 注意此处的路径需要根据各自具体情况修改 -->
<link rel="STYLESHEET" type="text/css" href="styles/dhtmlxtree.css">
<script src="scripts/dhtmlxcommon.js"></script>
<script src="scripts/dhtmlxtree.js"></script>
</head>
<body onload="loadTree(); " style="padding: 0; margin: 0; overflow: hidden; height: 100%;">
<script>
String.prototype._dhx_trim = function(){
return this.replace(/ /g," ").replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g,"");
}
/* init tree */
var tree;
function loadTree(){
tree=new dhtmlXTreeObject("doctree_box","100%","100%",0);
tree.setImagePath("images/"); <!-- 注意此处的路径需要根据各自具体情况修改 -->
tree.setOnClickHandler(
function(id){ openPathDocs(id); }
);
tree.loadXML("<%=basePath%>/menu.do");
}
/* open path funtion */

function openPathDocs(id){
if (tree.getUserData(id, "thisurl") != null ){
window.frames.sampleframe.location.href = "<%=path%>/" + tree.getUserData(id, "thisurl") + ".do";
return;
}
}
function autoselectNode(){
tree.selectItem(node,true);
tree.openItem(node);
}
</script>
<table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td valign="top" width="276">
<div id="doctree_box" style="width: 274px; height: 100%;"></div>
</td>
<td width="10" background="images/grid.gif">
</td>
<td align="right">
<iframe id="sampleframe" name="sampleframe" width="100%" height="99%" frameborder="0" src="blank.html" style="border: 0px solid #cecece;"></iframe>
</td>
</tr>
</table>
</body>
</html>

上面的JavaScript基本上是从dhtmlx的例子中修改而来,理解起来并不复杂,只有
String.prototype._dhx_trim = function(){
return this.replace(/ /g," ").replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g,"");
}
这一段代码含义不明。
三、Action
package demo;
 public class TreeMenuAction {
private String menuString;
 public String execute() {
StringBuffer buf = new StringBuffer();
buf.append("<tree id=\"0\">");
buf.append(" <item text=\"Java\">");
buf.append(" <item text=\"Thinking in java\">");
buf.append(" <userdata name=\"thisurl\">java_tij.do</userdata>");
buf.append(" </item>");
buf.append(" <item text=\"Head first design pattern\">");
buf.append(" <userdata name=\"thisurl\">java_hfdp.do</userdata>");
buf.append(" </item>");
buf.append(" </item>");
buf.append(" <item text=\"Fiction\">");
buf.append(" <item text=\"Harry Porter\">");
buf.append(" <userdata name=\"thisurl\">fiction_hp.do</userdata>");
buf.append(" </item>");
buf.append(" <item text=\"Oliver Twist\">");
buf.append(" <userdata name=\"thisurl\">fiction_ot.do</userdata>");
buf.append(" </item>");
buf.append(" </item>");
buf.append("</tree>");
menuString = buf.toString();
return "success";
}
 public String getMenuString() {
return menuString;
}
 public void setMenuString(String menuString) {
this.menuString = menuString;
}
}

四、menu.jsp
<%@ page contentType="text/xml;charset=UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
< s:property value="menuXmlString" escape="false"/>
过程是这样的:首先在浏览器地址栏中输入:http://......./tree.jsp
展示tree.jsp,在load函数中调用menu.do
menu.do对应TreeMenuAction,返回menu.jsp,而menu.jsp只包含menuString的值,注意在menu.jsp中的escape="false"
最近,继续研究了Struts2性能的调优方法,总结了一下,得出新三步曲:
4. 使用FreeMarker的最新版本2.3.13,因为在版本2.3.11中,FreeMarker针对性能进行了改进,以下是FreeMarker2.3.11的release notes:
2.3.11
Date of release: 2007-12-04
This release contains several performance and usability improvements.
5. ognl2.7
所称ognl2.7相对于2.6在性能上有了“显著”的提升,于是下载了2.7以及2.7所需要的javassist-3.8.0.GA.jar
其实,经过上面2个步骤,我并没有发现应用的性能有显著的改善,可能我的页面中从ValueStack中的存取操作并不是特别多,也不是特别的复杂,所以,Ognl对我的影响并不明显。
6. 最后使用了JProfiler对Tomcat进行了监控,最后发现问题在自定义模板上,我将页面的自定义模板全部删除,果然页面的响应速度有了较大的提升。
前一段时间有反映说是一个使用了struts2的生产系统的页面显示速度太慢。登录后发现确实如此,于是进行了一番性能调优的研究和测试。
一,根据struts2官方的性能调优说明进行了一些参数的修改。
http://struts.apache.org/2.x/docs/performance-tuning.html
http://cwiki.apache.org/WW/performance-tuning.html
Turn off logging and devMode.(关闭logging和Devmode)
这个当然没问题,但是全部关闭logging不现实,我只是关闭了struts2相关package的logging
Do not use interceptors you do not need.
把struts.xml中不需要的interceptor统统删除
Use the correct HTTP headers (Cache-Control & Expires).
不确定应该如何修改
Copy the static content from the Struts 2 jar when using the Ajax theme (Dojo) or the Calendar tag.
关于这点,后面会提到
Create a freemarker.properties file in your WEB-INF/classes directory.
照做
Create the freemarker.properties file and add the following setting (or whatever value you deem fitting):
template_update_delay=60000
照做
Enable Freemarker template caching
As of Struts 2.0.10, setting the property struts.freemarker.templatesCache to true will enable the Struts internal caching of Freemarker templates. This property is set to false by default.
照做
进行上述修改后,发现页面打开的速度并没有明显的提高.
二,此时我已经基本锁定网页打开速度慢的原因与ajax(或者说是dojo)有关。因为dojo的js库大概有450K左右,先尝试使用gzip压缩javascript,减小传输量,看能否加快页面的加载速度
在Tomcat的server.xml的connector中添加如下配置,激活gzip功能
compression="on"
compressionMinSize="2048"
noCompressionUserAgents="gozilla, traviata"
compressableMimeType="text/html,text/xml,text/javascript,application/x-javascript,application/javascript"
进行上述修改后,发现页面打开的速度还是没有明显的提高.
三,经过上述两个实验,觉得应该是struts2所封闭的dojo的性能问题了。于是引入JQuery.
JQuery的js文件最小是55K, gzip后应该更小,页面的响应速度明显改善(一个数量级以上的提高),主要原因在于与服务器交互的处理上极大地提升了效率。而且页面处理代码更加简洁明了。
最后,我删除了所有的<s:head theme="ajax"/>和 <s:head/>(如果页面中加入<s:head />,那么在Struts2生成的html中后包含dojo.js),使用JQuery来完成所有的Ajax和javascript功能。
在利用网页展示查询结果,经常会遇到要求导出成Excel的需求。采用这种方法可以定制输出的格式和内容(还不支持合并单元格和公式),生成真正的Excel格式(不是csv)的Excel。
一、struts.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.i18n.encoding" value="UTF-8"/>
<package name="demo" extends="struts-default">
<action name="excel" method="execute" class="demo.ExcelAction">
<result name="excel" type="stream">
<param name="contentType">application/vnd.ms-excel</param> <!-- 注意这里的ContentType -->
<param name="inputName">excelStream</param> <!-- 这里需要和Action里的变量名一致 -->
<param name="contentDisposition">filename="standard.xls"</param>
<param name="bufferSize">1024</param>
</result>
</action>
</package>
</struts>
二、Struts2的 Action
package demo;
public class ExcelAction {
private InputStream excelStream; // 需要生成getter和setter
public String execute() throws Exception {
StringBuffer excelBuf = new StringBuffer();
excelBuf.append("BookName").append("\t").append("Year").append("\t").append("author").append("\n");
excelBuf.append("Thinking in Java").append("\t").append("2001").append("\t").append("Eckel").append("\n");
excelBuf.append("Spring in action").append("\t").append("2005").append("\t").append("Rod").append("\n");
String excelString = excelBuf.toString();
logger.debug("result excel String: " + excelString);
excelStream = new ByteArrayInputStream(excelString.getBytes(), 0, excelString.length());
return "excel";
}
// getter and setter
...
}
三、Jsp页面
<%@ taglib prefix="s" uri="/struts-tags"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<s:head />
</head>
<body>
<s:form action="" method="post">
<s:submit key="button.submit"/>
</s:form>
</body>
</html>
iBatis自己带了一个simple的数据库连接池,基本的功能都有。但是在处理部分数据库(比如mysql)的连接空闲时间太长(mysql是8小时)自动超时的时候,就比不上象c3p0这样的连接池软件了(c3p0能自动处理数据库连接被关闭的情况)。
我目前采用的方法是iBatis本身提供的一种算得上是取巧的办法,基本思想就是每隔一段时间往数据库发一条查询语句,这样使得数据库空闲时间不会太长,而使得其自动关闭。
方法是在SqlMapConfig.xml的dataSource进行如下配置:
<dataSource type="SIMPLE">
<property name="JDBC.Driver" value="${jdbc.driverClassName}"/>
<property name="JDBC.ConnectionURL" value="${jdbc.url}"/>
<property name="JDBC.Username" value="${jdbc.username}"/>
<property name="JDBC.Password" value="${jdbc.password}"/>
<property name="Pool.PingEnabled" value="true"/>
<property name="Pool.PingQuery" value="select 1"/>
<property name="Pool.PingConnectionsOlderThan" value="0"/>
<property name="Pool.PingConnectionsNotUsedFor" value="3600000"/>
</dataSource>
开始的3行是关于数据库连接信息的,不需要说明了。
Pool.PingEnabled:是用于设置开启是否允许检测连接状态
Pool.PingQuery:是用于检测连接的查询语名,当然是越简单越好
Pool.PingConnectionOlderThan:对持续连接时间超过设定值(毫秒)的连接进行检测,我将其设置为0(不进行此项检测),否则,iBatis在超过这个时间后,执行每个sql以前检测连接,对于性能可能会有一定的影响。
Pool.PingConnectionsNotUsedFor:对空闲超过设定值(毫秒)的连接进行检测,我设置为1小时(mysql缺省的关闭时间是8小时)
当然,还有一个办法是使用c3p0这样的连接池
但是需要自己写一部分代码,实现以下接口:
public interface DataSourceFactory {
public void initialize(Map map);
public DataSource getDataSource();
}
在table中,如果表格内容为空,那么显示的时候,表格的边框会少掉一块。在HTML的规范中,应该使用empty-cells:
show这个style来解决,但是IE要到IE8之后才支持这个属性。而FireFox2是已经支持了。
那么在IE7之前的版本中要解决这个问题,好像是需要使用border-collapse:collapse;这个style.
<table style="border-collapse:collapse">
首先,在web项目的页面根目录下建立目录template
然后创建目录simple和xhtml,以上的目录名是struts2缺省使用的,不同的主题使用相应的目录。然后再创建一个components目录,在这个目录下,创建一个property.ftl。 最后的目录结构如下:
template/simple/components/property.ftl
template/xhtml/components/property.ftl
然后在property.ftl中可以使用FreeMarker来定义新的模板(FreeMarker的具体语法可以查看FreeMarker的官方网站,相当的详细易懂):
<#include "/${parameters.templateDir}/${parameters.theme}/controlheader.ftl" />
<@s.if test="${parameters.value} == null || ${parameters.value} == '' "> </@s.if>
<@s.else><@s.property value="${parameters.value}" /></@s.else>
<#include "/${parameters.templateDir}/xhtml/controlfooter.ftl" />
以上是一个我自定义的模板,检测结果是否为空字符串,如果是空的话,就输出一个 这样在输出结果时表格的边框线就是完整的了。
定义好之后,在jsp页面中就可以这样使用了:
<s:component template="/components/property.ftl" theme="simple">
<s:param name="value" value="%{'bookName'}"/>
</s:component>
第一行中的目录名从自components开始,struts2会自动在template目录下去寻找,如是主题是simple, 就在simple目录下找。
另外,param的语法要注意一下,%{}里面需要加一对引号
对于jsp页面,为了防止页面被服务器缓存、始终返回同样的结果。
通常的做法是在客户端的url后面加上一个变化的参数,比如加一个当前时间。
我现在使用的方法是在jsp头部添加以下代码:
<%
request.setAttribute("decorator", "none");
response.setHeader("Cache-Control","no-cache"); //HTTP 1.1
response.setHeader("Pragma","no-cache"); //HTTP 1.0
response.setDateHeader ("Expires", 0); //prevents caching at the proxy server
%>
这样如果有多个调用此页面的链接就不需要一个一个全部添加参数了。
在数据库的设计中,字典项是经常使用的技巧。
比如在一个图书馆系统中,书籍表(Book)会有一个分类字段,这时候我们一般会单独建立一张分类表(Category),在书籍表只保存分类表的ID。
在用户界面上显示书籍明细的时候,会要求显示CategoryID在Category表中对应的名称。
这样通常的做法是把Book和Category两张表进行关联。
但在实际应用中,Category一般都是Cache在应用服务器端,再使用数据表的连接就不够高效。
我的做法是这样的:在iBatis中使用SqlMap从表中将数据取出,此时不使用数据表的连接。
package com.demo;
public class Book {
/* 省略了getter和setter方法 */
private int id;
private String name;
private int categoryId;
private String author;
}
package com.demo;
public class Category {
private static Map<Integer, Category> cacheMap;
/* 省略了这两个属性的getter和setter方法 */
private int id;
private String name;
public static Category getCategory(int id) {
init();
return cacheMap.get(id);
}
public static Map<Integer, Category> getCategoryMap() {
init();
return cacheMap();
}
private init() {
if ( cacheMap != null ) return;
// the code to load category from datebase
// 在这里为了演示的需要,使用以下代码
cacheMap = new HashMap<Integer, Category>();
Category category = new Category();
category.setId(1);
category.setName("Fiction");
cacheMap.put(1, category);
category = new Category();
category.setId(2);
category.setName("Cartoon");
}
}
package com.demo;
public class BookAction {
/* 省略了属性的getter和setter方法 */
Book book;
public String execute() {
book = new Book();
book.setId(1);
book.setName("Thinking in java");
book.setCategoryId(1);
bookList.add(book);
return SUCCESS;
}
}
JSP:
<%@ taglib prefix="s" uri="/struts-tags"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<s:head />
</head>
<body>
<table border="1">
<tr>
<td>
<s:text name="page.label.userName" />
</td>
<td>
<s:property value="book.name" />
</td>
</tr>
<tr>
<td>
<s:text name="page.label.category" />
</td>
<td>
<s:property value="@com.demo.Category@getCategory(book.categoryId).getName()"/></td>
</tr>
</body>
</html>
2008年2月24日,Flex3正式发布了。分为标准版和Professional版。
两者差别不大,标准版没有advanced datagrid和一些性能分析工具。(advanced datagrid)可是我期盼好久的功能了。
另外,价钱也是相当的不菲。Standard is $249 US, Professional costs $699.
此外,还可以选择从flex2升级上去:升级到Standard的话,99美元,升级到professional要299美元。
Tomcat在后台重起后,所有的session失效。如果客户端继续点击了一个菜单项,发出一个请求。会得到一个exception。
这时候,可以定义一个名为:sessionTimeout的global results
<result name="sessionTimeout">/WEB-INF/pages/session_timeout.jsp</result>
这样,所有Action的session timeout都会被定向到指定的页面
最近尝试用extjs来展示树状菜单。着实花了一番功夫。树状菜单的菜单项需要动态加载,而目前版本的extjs中只支持JSON格式的数据。查了一些资
料,决定使用struts2的json-plugin。首先按照例子做了一个,但是结果就是不成功,界面上只出来了一个js中生成的root节点,不能加
载从后台生成的数据。研究后发现是数据格式有问题。使用json-plugin生成的数据格式如下:
{"cls":"folder","id":10,"leaf":false,"children":[{"cls":"file","id":11,"leaf":true,"children":null,"text":"S600"},{"cls":"file","id":12,"leaf":true,"children":null,"text":"SLK200"}],"text":"Benz"}
而extjs需要的数据格式如下:
[{"cls":"folder","id":10,"leaf":false,"children":[{"cls":"file","id":11,"leaf":true,"children":null,"text":"S600"},{"cls":"file","id":12,"leaf":true,"children":null,"text":"SLK200"}],"text":"Benz"}]
区别很小,就只相差最外面的两个方括号。但是少了这两个方括号,在json中,含义迥然不同,前者表示一个对象,而后者表示一个数组。而extjs中
tree的dataloader需要的数据必须是一个数组。而这样的数据格式是json-plugin自动生成的,无法改变。所以,我最后放弃了json
-plugin,转而使用json-lib来解决这个问题。
1. 下载json-lib, http://json-lib.sourceforge.net/
2. lib目录下的jar文件清单:
commons-beanutils-1.7.0.jar
commons-collections-3.2.jar
commons-digester-1.6.jar
commons-lang-2.3.jar
commons-logging-1.1.jar
dom4j-1.6.1.jar
ezmorph-1.0.4.jar
freemarker-2.3.8.jar
javassist-3.8.1.jar
json-lib-2.2.1-jdk15.jar
log4j-1.2.13.jar
ognl-2.6.11.jar
struts2-core-2.0.11.jar
xml-apis-1.0.b2.jar
xwork-2.0.4.jar
首先配置web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
然后是struts.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true"/>
<constant name="struts.i18n.encoding" value="UTF-8"/>
<package name="person" extends="struts-default">
<action name="menus" method="execute" class="com.lab.MenuAction">
<result>/menu.jsp</result>
</action>
</package>
</struts>
3. 树的节点模型(省略了getter,setter)
public class Menu {
private int id;
private String text;
private boolean leaf;
private String cls;
private List<Menu> children;
}
4. action
package com.lab;
import java.util.ArrayList;
import java.util.List;
import net.sf.json.JSONArray;
public class MenuAction {
private String menuString;
private List<Menu> menus;
public String execute() {
menus = new ArrayList<Menu>();
Menu benz = new Menu();
benz.setText("Benz");
benz.setCls("folder");
benz.setLeaf(false);
benz.setId(10);
menus.add(benz);
List<Menu> benzList = new ArrayList<Menu>();
benz.setChildren(benzList);
Menu menu;
menu = new Menu();
menu.setText("S600");
menu.setCls("file");
menu.setLeaf(true);
menu.setId(11);
benzList.add(menu);
menu = new Menu();
menu.setText("SLK200");
menu.setCls("file");
menu.setLeaf(true);
menu.setId(12);
benzList.add(menu);
Menu bmw = new Menu();
bmw.setText("BMW");
bmw.setCls("folder");
bmw.setLeaf(false);
bmw.setId(20);
menus.add(bmw);
List<Menu> bmwList = new ArrayList<Menu>();
bmw.setChildren(bmwList);
menu = new Menu();
menu.setText("325i");
menu.setCls("file");
menu.setLeaf(true);
menu.setId(21);
bmwList.add(menu);
menu = new Menu();
menu.setText("X5");
menu.setCls("file");
menu.setLeaf(true);
menu.setId(22);
bmwList.add(menu);
JSONArray jsonObject = JSONArray.fromObject(menus);
try {
menuString = jsonObject.toString();
} catch (Exception e) {
menuString = "ss";
}
return "success";
}
public String getMenuString() {
return menuString;
}
public void setMenuString(String menuString) {
this.menuString = menuString;
}
}
5. menu.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<s:property value="menuString" escape="false"/>
6. html页面和js
我使用的就是extjs的example中的reorder.html和reorder.js,更改了reorder.js中treeloader的dataurl: menus.action
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<title>Reorder TreePanel</title>
<link rel="stylesheet" type="text/css" href="extjs/resources/css/ext-all.css" />
<!-- GC -->
<!-- LIBS -->
<script type="text/javascript" src="extjs/adapter/ext/ext-base.js"></script>
<!-- ENDLIBS -->
<script type="text/javascript" src="extjs/ext-all.js"></script>
<script type="text/javascript" src="reorder.js"></script>
<!-- Common Styles for the examples -->
<link rel="stylesheet" type="text/css" href="extjs/resources/css/example.css" />
</head>
<body>
<script type="text/javascript" src="../examples.js"></script><!-- EXAMPLES -->
<h1>Drag and Drop ordering in a TreePanel</h1>
<p>This example shows basic drag and drop node moving in a tree. In this implementation there are no restrictions and
anything can be dropped anywhere except appending to nodes marked "leaf" (the files). <br></p>
<p>Drag along the edge of the tree to trigger auto scrolling while performing a drag and drop.</p>
<p>In order to demonstrate drag and drop insertion points, sorting was <b>not</b> enabled.</p>
<p>The data for this tree is asynchronously loaded with a JSON TreeLoader.</p>
<p>The js is not minified so it is readable. See <a href="reorder.js">reorder.js</a>.</p>
<div id="tree-div" style="overflow:auto; height:300px;width:250px;border:1px solid #c3daf9;"></div>
</body>
</html>
js:
/*
* Ext JS Library 2.0.1
* Copyright(c) 2006-2008, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
Ext.onReady(function(){
// shorthand
var Tree = Ext.tree;
var tree = new Tree.TreePanel({
el:'tree-div',
autoScroll:true,
animate:true,
enableDD:true,
containerScroll: true,
loader: new Tree.TreeLoader({
dataUrl:'http://localhost:8080/lab/menus.action'
})
});
// set the root node
var root = new Tree.AsyncTreeNode({
text: 'Ext JS',
draggable:false,
id:'source'
});
tree.setRootNode(root);
// render the tree
tree.render();
root.expand();
});
我已经上传了完整的War文件(包含所有源代码),见: Extjs Tree + JSON + Struts2 的所有示例源代码和war文件下载
|