乐者为王

Do one thing, and do it well.

敏捷开发走下坡路了吗?

英文原文:http://thatextramile.be/blog/2008/11/agile-development-going-downhill

James Shore(非常精彩的书《The Art Of Agile Development》的作者)在他的博客上写了一篇很有趣的帖子:The Decline And Fall Of Agile。你绝对应该读读它。

我想我同意James的观点。在过去的几年里,我已经听到很多的人说他们在做敏捷开发,实际上,他们几乎都不是。引用James的话:

But guess which part people adopt? That's right--Sprints and Scrums. Rapid cycles, but none of the good stuff that makes rapid cycles sustainable.

不幸的是,这是非常真实的。现在许多团队都在进行短迭代工作,很多团队也在做每日例会,或者Scrum,或者站立会议。但是,有多少人事实上致力于使敏捷开发成功的技术实践和原则呢?老实说,我一个也没见过。

我是真正的敏捷开发的铁杆迷,但即使是在我现在的工作中,我最近的两个团队也没有完全正确地实现它。我们的研究结果虽然还不错,但我认为我们仍然可以做得更好。我逐渐尝试引入更多的原则和实践,但它确实需要一些时间。但是,对敏捷开发的所有这些误解,许多人(开发者、项目经理、总经理等)都没有真正的帮助。在我当前的工作中,这些误解是非常小的,并没有真正的不良影响。以前在客户那里,我确实注意到这些误解是如何导致异常低效的情况。这实际上很可悲,因为管理者迟早可能会对敏捷方法持怀疑态度。如果这导致人们放弃一些技术实践和原则,对这个事业来说将会是一个很大的损失。

我觉得有更多的理由去读James的那本出色的书。如果我可以合法地迫使人们去阅读这本书,我会的:)

扫描ISBN条码实现藏书管理

每个程序猿家里都有一堆技术书籍,偶也不例外,因此想写个Android应用来管理自己的藏书以及想买的书籍。在网上找到marshal的识别图书ISBN号并输出查询结果的示例完善图书查询原型,增加收藏夹功能两篇文章,写的非常不错,还提供源代码。下载代码研究后发现已基本具备了想要的功能,决定在它的基础上做些修改供自己使用。

把原来uses-sdk的minSdkVersion改成了9,增加android:targetSdkVersion="17"。然后使用手机测试程序时发现,在连接网络时后台会抛出android.os.NetworkOnMainThreadException,并且应用崩溃打不开。通过查阅相关资料了解到,自从Android 2.3之后,系统增加了一个类StrictMode。这个类对网络的访问方式进行了一定的改变。官方文档给出了这个类设置的目的:

StrictMode is a developer tool which detects things you might be doing by accident and brings them to your attention so you can fix them.

StrictMode is most commonly used to catch accidental disk or network access on the application's main thread, where UI operations are received and animations take place. Keeping disk and network operations off the main thread makes for much smoother, more responsive applications. By keeping your application's main thread responsive, you also prevent ANR dialogs from being shown to users.

Note that even though an Android device's disk is often on flash memory, many devices run a filesystem on top of that memory with very limited concurrency. It's often the case that almost all disk accesses are fast, but may in individual cases be dramatically slower when certain I/O is happening in the background from other processes. If possible, it's best to assume that such things are not fast.

因为marshal把访问网络的代码直接放到UI线程中,造成和主线程的首要工作——UI交互——相矛盾。解决这类问题很容易,把访问网络的代码放到AsyncTask中就行了。

接着发现豆瓣API查询返回的是500错误,在浏览器上访问却又正常,后来给HttpClient加上Agent头就没问题了,不知道是不是期间豆瓣的API在实现上作了改变。

1
2
3
HttpClient client = new DefaultHttpClient();
String agent = System.getProperty("http.agent");
client.getParams().setParameter(CoreProtocolPNames.USER_AGENT, agent);

解析豆瓣XML查询结果的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
XmlPullParser parser = factory.newPullParser();
parser.setInput(inputStream, "utf-8");

Book book = new Book();
book.setIsbn(getIntent().getExtras().getString("ISBN"));

int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
    switch (eventType) {
    case XmlPullParser.START_TAG:
        if ("link".equals(parser.getName())
                && "image".equals(parser.getAttributeValue(null, "rel"))) {
            book.setImageUrl(parser.getAttributeValue(null, "href"));
            eventType = parser.next();
        } else if ("summary".equals(parser.getName())) {
            book.setSummary(parser.nextText());
        } else if ("attribute".equals(parser.getName())) {
            String name = parser.getAttributeValue(null, "name");
            if ("title".equals(name)) {
                book.setTitle(parser.nextText());
            } else if ("author".equals(name)) {
                book.setAuthor(parser.nextText());
            } else if ("publisher".equals(name)) {
                book.setPublisher(parser.nextText());
            }
        }
        break;
    case XmlPullParser.END_TAG:
        break;
    }
    eventType = parser.next();
}

然后,然后就是结果页面不显示图书信息。想要找到原因,肿么办?看来要调试WebView了!http://developer.android.com/guide/webapps/debugging.html 告诉了我们如何调试。

首先在WebView上设置setWebChromeClient方法:

1
2
3
4
5
6
7
webView.setWebChromeClient(new WebChromeClient() {
    public void onConsoleMessage(String message,
            int lineNumber, String sourceID) {
        Log.d(TAG, message + " -- From line "
                + lineNumber + " of " + sourceID);
    }
});

然后在JavaScript脚本中使用以下方法就可以在logcat中看到调试信息了。

1
2
3
4
console.log(String)
console.info(String)
console.warn(String)
console.error(String)

重新运行程序,果然在logcat中看到报如下错误:

1
Uncaught TypeError: Object [object Object] has no method 'xxx'

搜索后在Stack Overflow找到了问题的答案(Stack Overflow真的非常不错,问题的回答都非常详尽)。http://developer.android.com/guide/webapps/webview.html#BindingJavaScript 是官方文档的解释。

解决方法就是在要被JavaScript调用的方法上加@JavascriptInterface注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Book {

    @JavascriptInterface
    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        if (this.author != null) {
            this.author += ", " + author;
        } else {
            this.author = author;
        }
    }

代码下载:https://github.com/dohkoos/Bookly

绘制圆角的正确方法

英文原文:http://usabilitypost.com/2009/01/26/the-proper-way-to-draw-rounded-corners/

在网络上,我已经注意到当人们在他们的设计中实现圆角时犯了很多同样的错误。因为某些原因,当有另外一个圆角在它里面的时候,这个圆角会带来一个问题—无论是有一个围绕周围的边界,还是另一个圆角形状坐落在一个圆角形状内。

这里是我要说的:

bad-corner

我看到了很多这种类型的圆角。你可以看到圆角的半径和内圆角的是相同的。这是错误的,因为如果你保持相同半径,两个拐角之间的空间量不会自始至终相等。

下面是相等间距看起来的样子:

good-corner

这是正确的做法。有什么不同?内角的半径被减小了。好吧,但你怎么知道半径应该有多大?这很简单—如果你把外面的圆角想象成一个圆形,你可以看到它的圆心在哪里。

corner-center

把这个圆心同样地作为内角的圆心,你就会得到内角的半径。使用这种方法将确保两个形状之间的完美契合。

good-corner-ruler

现在你知道了吧?这是做多个圆角彼此包含的正确办法。当然,不需要你使用精确的圆心—有时为了设计良好还需要你移动一点点。但请不要仅仅把相同的圆角向内移动—这是绝对错误的。

使用SiteMesh做网页布局

SiteMesh是一个基于GoF的Decorator模式,用于页面布局的框架。能帮助我们在由大量页面构成的项目中创建一致的页面布局和外观。

这里我们将会把它整合到JBookShelf里去。要和Struts 2整合,先在pom.xml添加以下插件,该插件会将依赖的SiteMesh也一并包含到项目中。

1
2
3
4
5
<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-sitemesh-plugin</artifactId>
    <version>2.3.12</version>
</dependency>

将web.xml配置中原来的

1
2
3
4
5
6
7
8
9
10
11
<filter>
    <filter-name>struts2</filter-name>
    <filter-class>
        org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
    </filter-class>
</filter>

<filter-mapping>
    <filter-name>struts2</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

改成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<filter>
    <filter-name>struts-prepare</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareFilter</filter-class>
</filter>

<filter>
    <filter-name>sitemesh</filter-name>
    <filter-class>com.opensymphony.sitemesh.webapp.SiteMeshFilter</filter-class>
</filter>

<filter>
    <filter-name>struts-execute</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsExecuteFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>struts-prepare</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
    <filter-name>sitemesh</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
    <filter-name>struts-execute</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

这里要注意过滤器的位置。SiteMesh过滤器必须在StrutsPrepareFilter之后和StrutsExecuteFilter之前,否则在SiteMesh的修饰器页面中将访问不到ActionContext。这是因为Struts 2的所有值是保存在Stack Context或者ValueStack中的。默认情况下,某个过滤器一旦访问了该Stack Context或ValueStack,里面对应的值会被清洗掉。如果先使用Struts 2的StrutsPrepareAndExecuteFilter来过滤用户请求,则SiteMesh的过滤器将无法取得Stack Context或者ValueStack中的数据。为了解决这个问题,Struts 2提供了StrutsPrepareFilter和StrutsExecuteFilter类(在2.1.3版本前是ActionContextCleanUp和FilterDispatcher),通过它们协同来确保SiteMesh正常工作。

在WEB-INF下添加decorators.xml文档:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<decorators defaultdir="/layouts">
    <excludes>
        <pattern>/stylesheets/*</pattern>
        <pattern>/javascripts/*</pattern>
        <pattern>/images/*</pattern>
    </excludes>

    <decorator name="application" page="application.jsp">
        <pattern>/*</pattern>
    </decorator>
</decorators>

stylesheets、javascripts、images目录下的内容是不需要被修饰的,可以把它们放到execludes块中排除掉。

新建/layouts/application.jsp模版页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<%@ taglib uri="/struts-tags" prefix="s" %>
<%@ taglib uri="http://www.opensymphony.com/sitemesh/decorator" prefix="decorator" %>

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title><decorator:title default="JBookShelf" /></title>
    <decorator:head />
</head>

<body>
    <div>
        <s:if test="#session.user_session_key != null">
        <s:a action="listBook">All Books</s:a>
        Welcome, you have logined.
        <s:a action="logout">Logout</s:a>
        </s:if>
        <s:else>
        <s:a action="login!input">Login</s:a> |
        <s:a action="register!input">Register</s:a>
        </s:else>
    </div>
    <hr />
    <div>Navigation</div>
    <hr />
    <decorator:body />
    <hr />
    <div>Footer</div>
</body>
</html>

代码下载:https://github.com/dohkoos/JBookShelf

使用ichartjs画2D条形图

在Rails中使用Open Flash Chart II中使用了OFC2来画2D条形图。现在Flash快要不行了,因为有了更好的HTML5。好的程序猿也要紧随潮流,使用新的技术来改进和增强他们的代码。这次就尝试使用HTML5图形库来替换OFC2。比较已有的一些图形库,最后选定国产的ichartjs

实现2D条形图真的很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script type="text/javascript" src="ichart.1.1.min.js"></script>
<script type="text/javascript">
$(function() {
  var data = [
    {name: 'IE', value: 35.75, color: '#a5c2d5'},
    {name: 'Chrome', value: 29.84, color: '#cbab4f'},
    {name: 'Firefox', value: 24.88, color: '#76a871'},
    {name: 'Safari',value: 6.77, color: '#9f7961'},
    {name: 'Opera',value:2.02, color: '#a56f8f'},
    {name: 'Other',value: 0.73, color: '#6f83a5'}
  ];

  new iChart.Bar2D({
    render: 'canvasDiv',
    data: data,
    title: 'Top 5 Browsers from 1 to 29 Feb 2012',
    showpercent: true,
    decimalsnum: 2,
    width: 800,
    height: 400,
    coordinate: {
      scale: [{
        position: 'bottom',
        start_scale: 0,
        end_scale: 40,
        scale_space: 8,
        listeners: {
          parseText: function(t, x, y) {
            return {text: t + "%"}
          }
        }
      }]
    }
  }).draw();
});
</script>

<div id="canvasDiv"></div>

逻辑题-老师的生日

小明和小强都是某老师的学生,老师的生日是M月N日,他们都知道老师的生日是以下十组中的一个。老师把M告诉了小明,把N告诉了小强,然后问他们是否知道自己的生日。小明说“如果我不知道小强也不知道”,小强说“本来我不知道,你说了这话我就知道了”,小明说“那我也知道了 ”。问老师的生日是以下的哪个?

3/4, 3/5, 3/8, 6/4, 6/7, 9/1, 9/5, 12/1, 12/2, 12/8

按M排:

1
2
3
4
3/4, 3/5, 3/8
6/4, 6/7
9/1, 9/5
12/1, 12/2, 12/8

按N排:

1
2
3
4
5
6
9/1, 12/1
12/2
3/4, 6/4
3/5, 9/5
6/7
3/8, 12/8

条件一:小明说“如果我不知道小强也不知道”。分析:因为M都是重复的,所以小明一定不知道。小明知道M以后肯定小强不知道,说明N肯定也是重复,可以剔除12/2和6/7这两个日期。另外,还可以排队N为2与7所对应的月份,因为当老师生日的M为6或12时,小强是有可能知道的,与已知条件相违背。结果:3/4, 3/5, 3/8, 9/1, 9/5。

按M排:

1
2
3/4, 3/5, 3/8
9/1, 9/5

按N排:

1
2
3
4
3/4
3/5, 9/5
3/8
9/1

条件二:小强说“本来我不知道,你说了这话我就知道了”。分析:根据上面的结果和条件二可以知道,现在N一定不能是重复的,可以把3/5和9/5排除。结果:3/4, 3/8, 9/1。

按M排:

1
2
3/4, 3/8
9/1

按N排:

1
2
3
3/4
3/8
9/1

条件三:小明说“那我也知道了”。分析:综合条件二的结果和条件三可以判定现在M应该是单一的,所以只能是9。结果:9/1。

老师的生日为9/1。

.htaccess实例详解

又一次迁移博客服务器。因为是迁移,就没有采取安装的方式,而是把以前的程序拷贝到新服务器上。

打开首页的时候没问题,访问具体文章时就出错了:

1
2
Not Found
The requested URL /sample-article.html was not found on this server.

试着去后台重新做设置,然后就发现在根目录下多了个.htaccess文件,内容如下:

1
2
3
4
5
6
7
8
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

以前也看过这个文件,总是模模糊糊不太明白。今天研究的时候发现一下子就清清楚楚了(这大概就是佛家所说的顿悟吧,不过它的发生应该还是建立在渐修的基础上)。

下面来说说这些内容的理解:

第1行就是打开重写引擎。

第2行是设置URI前缀。

1
2
3
4
5
6
7
8
9
10
Syntax: RewriteRule Pattern Substitution [flags]

- (dash)
 A dash indicates that no substitution should be performed (the existing path
 is passed through untouched). This is used when a flag (see below) needs to
 be applied without changing the path.

last|L
 Stop the rewriting process immediately and don't apply any more rules.
 Especially note caveats for per-directory and .htaccess context.

第3行是完全匹配index.php的URI,但不做替换,并且匹配成功后就停止执行重写过程。

第6行就是把所有的访问请求重写,指给index.php,然后停止执行重写过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Syntax: RewriteCond TestString CondPattern

REQUEST_FILENAME
 The full local filesystem path to the file or script matching the request,
 if this has already been determined by the server at the time REQUEST_FILENAME
 is referenced. Otherwise, such as when used in virtual host context, the same
 value as REQUEST_URI.

You can prefix the pattern string with a '!' character (exclamation mark) to
specify a non-matching pattern.

'-d' (is directory)
 Treats the TestString as a pathname and tests whether or not it exists, and
 is a directory.

'-f' (is regular file)
 Treats the TestString as a pathname and tests whether or not it exists, and
 is a regular file.

第4和第5行是不去测试访问URI是否是目录或文件。其实有了第6行这两行基本就是摆设了。

最后再唠叨一句,有问题直接查官方文档应该是最好的选择。官方文档链接:Apache Module mod_rewrite

逻辑题-100盏电灯

房间里有100盏电灯,编号为1,2,3...100,每盏灯上有一个按钮,初始时灯全都是关的。

编好号的100位同学由房间外依次走进去,将自己编号的倍数的灯的按钮全部按一次,例如第一位同学把编号是1的倍数的灯的按钮按一下(此时100盏灯全亮),第二位同学把编号是2的倍数的灯的按钮按一下(此时只有50盏灯亮着,50盏被这个人按灭了)...第100位同学把编号是100的倍数的灯(即编号为100的灯)的按钮按一下,请问依次走完后,还有多少盏灯亮着?

解题思路:

被按过奇数次的灯亮着,偶数次的灯关了。因为每个同学会把自己编号的倍数的灯全部按一次,所以:

1
2
3
4
5
6
7
8
9
1号灯会被1号同学按下;
2号灯会被1,2号同学按下;
3号灯会被1,3号同学按下;
4号灯会被1,2,4号同学按下;
5号灯会被1,5号同学按下;
6号灯会被1,2,3,6号同学按下;
7号灯会被1,7号同学按下;
...
依此类推

从上面可以总结出n号灯会被以它所有约数为编号的同学按下。

这样,问题就变成了求每盏灯的编号有多少个约数的数学问题。如果约数个数为奇数,灯亮着;反之,灯关着。

Ruby代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Light state: ON/OFF = true/false
lamps = Array.new(100, false)

1.upto(lamps.length) do |lno|
  1.upto(lno) do |fno|
    lamps[lno - 1] = !lamps[lno - 1] if lno % fno == 0
  end
end

lamps.each_index do |i|
  print "Light #{i + 1} is "
  puts lamps[i] ? "ON" : "OFF"
end

最后,我们会发现编号1,4,9,16,25...100的灯是亮着的,其它都是灭的。即编号值可以开出整数平方根的灯是亮的。

我们真的需要接口命名规范吗?

英文原文:http://www.adam-bien.com/roller/abien/entry/in_the_age_of_dryness

译者:感觉这篇文章讲的不错,也不算太长,就翻译了过来。

在过去的项目/审查中我发现了如下的接口及其实现的命名规范:

1
2
3
4
5
IService service = new Service();

Service service = new ServiceImpl();

ServiceIF service = new Service();

每一条都含有多余信息。无论是强调接口或其实现,相关信息都已经包含在代码中,所以都是多余的。这种命名规范仅当你真的不知道如何命名实现的时候才需要,作为去思考一个名称的替代,它更容易依靠现有的模板。

实现名称的缺乏可能也是作为它的非充分责任的一个指标(an indicator for it's unsufficient responsibilities)。因此,如果你找不到一个适当的实现名称,考虑移除接口直接暴露实现可能是个好主意——没有奇怪的规范。实际上JDK中没有这样的规范,接口及其实现也经常称为不同(are often even called differently):

1
2
3
4
5
6
7
Runnable r = new Thread();

RootPaneContainer c = new JFrame();

TableModel t = new DefaultTableModel();

Remote remote = new UnicastRemoteObject();

接口的目的是抽象或从几个实现解耦。一个单一的实现并不需要通过接口来解耦。也有一些例外的规则,如需要使用动态代理等。

通过单个的,有着奇怪命名规范的实现(implementation)来实现(realize)一个接口不应该被认为是一般的最佳实践……

使用Interceptor禁止用户访问未授权的图书信息

现在的JBookShelf有两个问题:

  1. 未登录的用户可以访问图书信息;
  2. 登录后的用户可以访问其他用户的图书信息。

第一个问题可以使用Struts 2的Interceptor来解决:

  1. 创建一个实现了Interceptor接口的类;
  2. 在struts.xml配置中定义这个拦截器;
  3. 在struts.xml中定义一个使用了上面拦截器的拦截栈;
  4. 在struts.xml中定义一个全局转向配置。

实现自己的拦截器有点要注意的是,拦截器必须是无状态的,不要使用在API提供的ActionInvocation之外的任何东西。要求拦截器是无状态的原因是Struts 2不能保证为每一个请求或者action创建一个实例,所以如果拦截器带有状态,会引发并发问题。

创建AuthorizationInterceptor类,继承类AbstractInterceptor。为什么继承它呢?而不是直接实现接口Interceptor。这是因为AbstractInterceptor已经实现了Interceptor接口,并且实现了接口中的init和destroy方法。而在这个拦截器中,我们并不需要使用这两个方法。下面上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.codemany.account.interceptor;

import java.util.Collections;
import java.util.Set;

import com.codemany.account.model.User;

import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import com.opensymphony.xwork2.util.TextParseUtil;

public class AuthorizationInterceptor extends AbstractInterceptor {
    private static final long serialVersionUID = -5140884040684756043L;

    protected Set<String> skipActions = Collections.emptySet();

    public void setSkipActions(String skipActions) {
        this.skipActions = TextParseUtil.commaDelimitedStringToSet(skipActions);
    }

    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        User user = (User)invocation.getInvocationContext()
                .getSession().get(User.SESSION_KEY);

        boolean isLogined = user != null;
        String action = invocation.getProxy().getActionName();
        // 如果用户未登录,并且访问的是需要登录权限的Action,就跳转到全局转向配置login上
        if (isLogined || skipActions.contains(action)) {
            return invocation.invoke();
        } else {
            return Action.LOGIN;
        }
    }
}

这里的skipActions目的是为了跳过一些不需要拦截的Action。因为默认情况下,拦截器会拦截Action中的所有的方法。像login,register这类Action是任何用户在任何状态下都可以访问的,所以不需要拦截,这里就可以将这些Action放到skipActions中来跳过拦截。

实现了拦截类后还要在struts.xml进行配置使它起作用,以下是struts.xml的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
    "http://struts.apache.org/dtds/struts-2.3.dtd">

<struts>
    <constant name="struts.enable.DynamicMethodInvocation" value="true" />
    <constant name="struts.devMode" value="true" />

    <package name="default" extends="struts-default">
        <interceptors>
            <interceptor name="authorization"
                class="com.codemany.account.interceptor.AuthorizationInterceptor" />

            <interceptor-stack name="authorizationStack">
                <interceptor-ref name="authorization">
                    <param name="skipActions">login, logout, register</param>
                </interceptor-ref>
                <interceptor-ref name="defaultStack" />
            </interceptor-stack>
        </interceptors>

        <default-interceptor-ref name="authorizationStack" />

        <!-- 全局转向配置 -->
        <!-- 还记得拦截器里面的return Action.LOGIN这句吧,当程序执行完这一行后,-->
        <!-- 就会到struts.xml文件的global-results中找name为login的全局转向配置。-->
        <global-results>
            <result name="login" type="redirectAction">login!input</result>
        </global-results>
    </package>

    <include file="account.xml" />
    <include file="book.xml" />
</struts>

然后将account.xml和book.xml中package继承的父包改成struts.xml配置中的default包:

1
2
3
<package name="account" extends="default">

<package name="book" extends="default">

第二个问题的解决就是修改BookAction.java的代码,不再从数据库中读取Book数据,而是从当前登录用户的books属性中查找Book信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package com.codemany.book.action;

import java.util.ArrayList;
import java.util.List;

import com.codemany.account.model.User;
import com.codemany.book.model.Book;
import com.codemany.book.service.BookService;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;

public class BookAction extends ActionSupport {
    private static final long serialVersionUID = 2538923417705852774L;

    private Long bookId;
    private Book book;
    private List<Book> bookList;

    private BookService bookService;

    public String list() throws Exception {
        if (bookList == null) {
            bookList = new ArrayList<Book>();
        }
        bookList.addAll(getCurrentUser().getBooks());
        return "list";
    }

    public String show() throws Exception {
        book = getCurrentUser().getBook(bookId);
        return "show";
    }

    public String input() throws Exception {
        if (bookId != null) {
            book = getCurrentUser().getBook(bookId);
        }
        return INPUT;
    }

    public String saveOrUpdate() throws Exception {
        book.setUser(getCurrentUser());
        bookService.saveOrUpdateBook(book);
        return SUCCESS;
    }

    public String delete() throws Exception {
        Book book = getCurrentUser().getBook(bookId);
        if (book != null) {
            bookService.deleteBook(bookId);
            getCurrentUser().getBooks().remove(book);
        }
        return SUCCESS;
    }

    public List<Book> getBookList() {
        return bookList;
    }

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    public void setBookId(Long bookId) {
        this.bookId = bookId;
    }

    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }

    private User getCurrentUser() {
        return (User)ActionContext.getContext()
                .getSession().get(User.SESSION_KEY);
    }
}

上面的代码会产生异常,提示取出的Book数据为空,所以要在User.hbm.xml中set标签后添加属性lazy=“false”,这样Hibernate从数据库中读取User数据时会连带取出对应的Book数据:

1
<set name="books" inverse="true" lazy="false">

下面是BookAction类中用到的User.getBook(bookId)代码:

1
2
3
4
5
6
7
8
9
10
public class User {

    public Book getBook(Long bookId) {
        for (Book book : books) {
            if (bookId != null && bookId.equals(book.getId())) {
                return book;
            }
        }
        return null;
    }

代码下载:https://github.com/dohkoos/JBookShelf