乐者为王

Do one thing, and do it well.

实现图书的增删查改(CRUD)

图书的MySQL数据库创建脚本:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE books (
    id int NOT NULL AUTO_INCREMENT,
    title varchar(255) NOT NULL,
    description text,
    image_url varchar(255),
    price decimal(8, 2) NOT NULL,
    author varchar(255) NOT NULL,
    isbn varchar(255) NOT NULL,
    publisher varchar(255) NOT NULL,
    user_id int NOT NULL,
    PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf-8;

图书实体类文件Book.java主要代码:

1
2
3
4
5
6
7
8
9
10
public class Book {
    private Long id;
    private String title;
    private String description;
    private String image_url;
    private double price;
    private String author;
    private String isbn;
    private String publisher;
    private User user;

在User类中添加集合变量books:

1
2
public class User {
    private Set<Book> books = new HashSet<Book>();

在User.hbm.xml中配置和图书的一对多关联:

1
2
3
4
5
<!-- Bidirectional one-to-many association to Book -->
<set name="books" inverse="true">
    <key column="user_id" not-null="true" />
    <one-to-many class="com.codemany.book.model.Book" />
</set>

图书的表映射文件Book.hbm.xml代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<hibernate-mapping>
    <class name="com.codemany.book.model.Book" table="books">
        <id name="id">
            <generator class="increment" />
        </id>

        <property name="title" not-null="true" />
        <property name="description" />
        <property name="image_url" />
        <property name="price" not-null="true" />
        <property name="author" not-null="true" />
        <property name="isbn" not-null="true" />
        <property name="publisher" not-null="true" />

        <many-to-one name="user" column="user_id" not-null="true" />
    </class>
</hibernate-mapping>

BookService.java中的代码相对简单,因为没什么复杂的业务逻辑,只是负责把Action和Dao这两层连接起来。

BookDao.java中的代码主要实现图书的读取,更新和删除:

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
package com.codemany.book.dao;

import java.util.List;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;

import com.codemany.book.model.Book;

public class BookDao {
    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public List<Book> getBookList() {
        Session session = sessionFactory.openSession();
        try {
            Query query = session.createQuery("from Book b");
            List<Book> bookList = query.list();
            return bookList;
        } finally {
            session.close();
        }
    }

    public Book getBook(Long bookId) {
        Session session = sessionFactory.openSession();
        try {
            Query query = session.createQuery("from Book b where b.id = :id");
            query.setLong("id", bookId);
            query.setMaxResults(1);
            return (Book)query.uniqueResult();
        } finally {
            session.close();
        }
    }

    public void saveOrUpdateBook(Book book) {
        Session session = sessionFactory.openSession();
        Transaction ts = null;
        try {
            ts = session.beginTransaction();
            session.saveOrUpdate(book);
            ts.commit();
        } finally {
            session.close();
        }
    }

    public void deleteBook(Long bookId) {
        Session session = sessionFactory.openSession();
        Transaction ts = null;
        try {
            ts = session.beginTransaction();
            Book book = (Book)session.get(Book.class, bookId);
            session.delete(book);
            ts.commit();
        } finally {
            session.close();
        }
    }
}

BookAction.java负责协调图书应用程序间的运转。接收访问请求,与模型进行交互,将合适的视图展示给用户:

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
package com.codemany.book.action;

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 {
        bookList = bookService.getBookList();
        return "list";
    }

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

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

    public String saveOrUpdate() throws Exception {
        User user = (User)ActionContext.getContext().getSession().get("user");
        book.setUser(user);
        bookService.saveOrUpdateBook(book);
        return SUCCESS;
    }

    public String delete() throws Exception {
        bookService.deleteBook(bookId);
        return SUCCESS;
    }

在applicationContext.xml中添加Book的相关配置,如下面代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="mappingResources">
        <list>
            <value>com/codemany/book/model/Book.hbm.xml</value>
        </list>
    </property>
</bean>

<!-- book -->
<bean id="bookDao" class="com.codemany.book.dao.BookDao">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

<bean id="bookService" class="com.codemany.book.service.BookService">
    <property name="bookDao" ref="bookDao" />
</bean>

<bean id="bookAction" class="com.codemany.book.action.BookAction" scope="prototype">
    <property name="bookService" ref="bookService" />
</bean>

在Struts 2配置文件struts.xml中include文件book.xml。以下是book.xml文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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="book" extends="struts-default">
        <action name="*Book" method="{1}" class="bookAction">
            <result name="list">/books/list.jsp</result>
            <result name="show">/books/show.jsp</result>
            <result name="input">/books/form.jsp</result>
            <result type="redirectAction">listBook</result>
        </action>
    </package>
</struts>

视图文件show.jsp作为显示单本图书的信息,代码很简单,就不放出来了,只把list.jsp和form.jsp的代码列出来。

list.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
32
33
34
35
<body>
    <h1>Listing books</h1>

    <table>
        <tr>
            <th>Title</th>
            <th>Description</th>
            <th>Image url</th>
            <th>Price</th>
            <th>Author</th>
            <th>ISBN</th>
            <th>Publisher</th>
            <th>Action</th>
        </tr>

        <s:iterator value="bookList">
        <tr>
            <td><s:property value="title" /></td>
            <td><s:property value="description" /></td>
            <td><s:property value="image_url" /></td>
            <td><s:property value="price" /></td>
            <td><s:property value="author" /></td>
            <td><s:property value="isbn" /></td>
            <td><s:property value="publisher" /></td>
            <td>
                <s:a href="showBook.action?bookId=%{id}">Show</s:a>
                <s:a href="inputBook.action?bookId=%{id}">Edit</s:a>
                <s:a href="deleteBook.action?bookId=%{id}" method="delete">Destroy</s:a>
            </td>
        </tr>
        </s:iterator>
    </table>

    <s:a action="inputBook">Add</s:a>
</body>

当用户新建或者更新图书时,视图form.jsp就上场了。因为被两个逻辑使用,显示的标题等信息也不相同,所以需要判断图书的id是否存在,如果不存在的话即是新建业务,否则就是更新操作。

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
<body>
    <s:if test="book.id != null">
        <h1>Editing book</h1>
    </s:if>
    <s:else>
        <h1>Add book</h1>
    </s:else>

    <s:form action="saveOrUpdateBook" method="post">
        <div class="field">
            <s:hidden name="book.id" value="%{book.id}" />
        </div>
        <div class="field">
            <s:textfield name="book.title" label="Title" />
        </div>
        <div class="field">
            <s:textarea name="book.description" label="Description" rows="5" cols="25" />
        </div>
        <div class="field">
            <s:textfield name="book.image_url" label="Image URL" />
        </div>
        <div class="field">
            <s:textfield name="book.price" label="Price" />
        </div>
        <div class="field">
            <s:textfield name="book.author" label="Author" />
        </div>
        <div class="field">
            <s:textfield name="book.isbn" label="ISBN" />
        </div>
        <div class="field">
            <s:textfield name="book.publisher" label="Publisher" />
        </div>
        <div class="actions">
            <s:if test="book.id != null">
            <s:submit value="Update" />
            </s:if>
            <s:else>
            <s:submit value="Save" />
            </s:else>
      </div>
    </s:form>

    <s:if test="book.id != null">
        <s:a href="showBook.action?bookId=%{book.id}">Show</s:a> |
    </s:if>
    <s:a action="listBook">Back</s:a>
</body>

最后还要完成图书模型的服务端校验。Struts 2的校验文件有两种格式:ActionName-validation.xml和ActionName-alias-validation.xml。第一种会对该Action中的每个方法进行校验,不符合只对saveOrUpdate校验的要求。在BookAction.java同目录下创建BookAction-saveOrUpdateBook-validation.xml文件:

1
2
3
4
5
6
7
8
<validators>
    <field name="book">
        <field-validator type="visitor">
            <param name="appendPrefix">true</param>
            <message />
        </field-validator>
    </field>
</validators>

当然,还要在Book.java所在的位置创建Book-validation.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<validators>
    <field name="title">
        <field-validator type="required">
            <message>Price is required</message>
        </field-validator>
    </field>

    <field name="price">
        <field-validator type="required">
            <message>Price is required</message>
        </field-validator>
        <field-validator type="double">
            <param name="minInclusive">0.01</param>
            <message>Price should be at least 0.01</message>
        </field-validator>
    </field>

看着BookAction-saveOrUpdateBook-validation.xml这么长的文件名是不是有点无语,没关系,BookAction-validation.xml配置文件还是可以用的,只要在BookAction.java中那些不需要进行校验的方法上添加@SkipValidation;也可以在action配置中启用validation.excludeMethods参数:

1
2
3
4
<action name="*Book" method="{1}" class="bookAction">
    <interceptor-ref name="defaultStack">
        <param name="validation.excludeMethods">list,show,input,delete</param>
    </interceptor-ref>

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

Rails中通过Checkbox实现批量删除

在Rails生成的控制器模版中,包含的destroy方法只能处理单个对象,而批量删除要求能够同时处理多个对象,这需要自定义一个批量操作action。批量删除的效果图如下:

table-with-checkboxes

每一行记录的第一列设置成Checkbox,用于标记此行是否被选中。表下方放置一个全选Checkbox,表示全部选中或全部反选。全选和反选的JavaScript代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
function toggle_checkall(field_name, state) {
  var checkboxes = document.getElementsByTagName('input');
  var count = checkboxes.length;
  for (var i = 0; i < count; i++) {
    if (checkboxes[i].type == "checkbox"
        && checkboxes[i].name == field_name + "_ids[]") {
      checkboxes[i].checked = state;
    }
  }
}
</script>

在routes.rb中配置批量删除action的映射:

1
2
3
resources :departs do
  delete 'destroy_multiple', :on => collection
end

在index.html.erb中添加代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%= form_tag destroy_multiple_departs_path, method: :delete do %>
<%= submit_tag "删除选中" %>
<table>
  <thead>
    <tr>
      <th><input type="checkbox" onclick="toggle_checkall('depart', this.checked);" /></th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><%= check_box_tag "depart_ids[]", depart.id %></td>
    </tr>
    <tr>
      <td><%= check_box_tag "depart_ids[]", depart.id %></td>
    </tr>
  </tbody>
</table>
<% end %>

在控制器中添加批量删除实现代码:

1
2
3
4
5
6
7
8
def destroy_multiple
  Depart.destroy(params[:depart_ids]) unless params[:depart_ids].blank?

  respond_to do |format|
    format.html { redirect_to departs }
    format.json { head :no_content }
  end
end

如何从数组中抽取随机不重复的数字

今天浏览订阅的博客时发现了一个巧妙的从数组中抽取随机不重复数字的算法。譬如,在100个不重复的数字中选择10个不相同的数字,通过这个算法就不需要修改数组长度和删除数组元素等。具体算法如下:

1
2
3
4
5
6
7
8
9
10
11
int[] numbers = new int[100];
int[] selected = new int[10];

for (int i = 0, n = numbers.length; i < selected.length; i++) {
    int idx = (int)(Math.random() * n);    // 随机产生一个从0 - (n-1)的数字
    selected[i] = numbers[idx];
    numbers[idx] = numbers[n - 1];
    n--;    // 减1,从而在下次循环时产生的随机的numbers数组下标的范围从0 - (n-1)-1,
            // 保证了上一步中已经赋值给数组中其它数的numbers[n-1]不会在下次循环中给
            // 取得,确保了产生的数组selected中的数为不重复的。
}

表格内容对齐规则(left、right、center)

  1. 通常情况下内容是左对齐;
  2. 列中的所有数字是整数并且指的是相同事物,或者使用同一个常见单位(公斤,厘米,人数,等等)时,右对齐是一个正确的对齐方式(Table 1);
  3. 如果数字使用相同单位,但混合了整数和小数,通过小数点对齐它们;
  4. 如果数字没有使用同一个常见单位(例如:一个国家的比较中,每个国家都有一个单独的列,每一行包含一个不同的参数——面积,人口,人均收入和预期寿命),左对齐是正确的选择(Table 2);
  5. 居中数字永远都是错的;
  6. 常常会出现列中的一些单元格是空的:例如信息无效或不适用。不管你如何去表示,用dash,n.a.,[?],或其它,也不要让单元格空着,在列中设置符号居中来强调这一事实(Table 2的最后单元格)。

table-sample

使用jQuery显示隐藏侧边栏

需要在记账应用使用侧边栏开关功能,在网上找了段代码修改修改,可惜没有找到漂亮的小图标,只好使用>>和<<符号来表示,太丑陋了!

把jquery.toggle.sidebar.js的代码贴上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
jQuery(function() {
  var sidebar = $.cookie("sidebar");
  if (sidebar == "hide") {
    $('#sidebar').hide();
    $(".toggle").text(">>");
  }

  $(".toggle").click(function() {
    if ($('#sidebar').is(':visible')) {
      $(this).text(">>");
      $('#sidebar').hide();
      $.cookie("sidebar", "hide", { expires: 7 });  // expires in 7 days
    } else {
      $(this).text("<<");
      $('#sidebar').show();
      $.removeCookie("sidebar");
    };
    return false;
  });
});

因为jQuery本身不支持Cookie操作,需要使用官方的插件,可以在 http://plugins.jquery.com/project/Cookiehttps://github.com/carhartl/jquery-cookie 下载。

如何显示Hibernate的SQL参数值-P6Spy

英文原文:http://www.mkyong.com/hibernate/how-to-display-hibernate-sql-parameter-values-solution/

问题

有许多开发者问及Hibernate的SQL参数值问题。如何显示Hibernate传递给数据库的SQL参数值?Hibernate只会把所有的参数值显示为问号(?)。使用show_sql属性,Hibernate会显示所有生成的SQL语句,但不显示SQL参数值。

例如:

1
2
Hibernate: insert into mkyong.stock_transaction (CHANGE, CLOSE, DATE, OPEN, STOCK_ID, VOLUME)
values (?, ?, ?, ?, ?, ?)

有没有办法记录或显示确切的Hibernate的SQL参数值呢?

解决方案-P6Spy

好吧,如果有问题那么肯定就会有答案~

P6Spy是一个可以在SQL语句和参数值被发送到数据库之前把它们都记录下来的实用库。P6Spy是免费的,它拦截并记录所有数据库SQL语句到一个日志文件中,它适用于任何使用JDBC驱动的应用程序。

1. 下载P6Spy库

获取p6spy-install.jar,你可以从以下地址下载:

  1. P6Spy官方网站
  2. P6Spy在Sourceforge.net

2. 解压

解压p6spy-install.jar文件,查找p6spy.jar和spy.properties。

3. 添加库依赖

把p6spy.jar添加到项目库依赖中。

4. 修改P6Spy属性文件

修改数据库配置文件。你需要把现有的JDBC驱动替换为P6Spy的JDBC驱动——com.p6spy.engine.spy.P6SpyDriver。

原来的是MySQL的JDBC驱动——com.mysql.jdbc.Driver:

1
2
3
4
5
6
7
8
9
<session-factory>
    <property name="hibernate.bytecode.use_reflection_optimizer">false</property>
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/mkyong</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.connection.password">password</property>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="show_sql">true</property>
</session-factory>

把它改成P6Spy的JDBC驱动——com.p6spy.engine.spy.P6SpyDriver:

1
2
3
4
5
6
7
8
9
<session-factory>
    <property name="hibernate.bytecode.use_reflection_optimizer">false</property>
    <property name="hibernate.connection.driver_class">com.p6spy.engine.spy.P6SpyDriver</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/mkyong</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.connection.password">password</property>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="show_sql">true</property>
</session-factory>

5. 修改P6Spy属性文件

修改P6Spy属性文件——spy.properties。

用现有的MySQL的JDBC驱动替换“real driver”:

1
2
3
4
5
6
realdriver=com.mysql.jdbc.Driver

#specifies another driver to use
realdriver2=
#specifies a third driver to use
realdriver3=

修改logfile属性中的日志文件位置,所有的SQL语句将会被记录到这个文件里。

Windows

1
logfile     = c:/spy.log

*nix

1
logfile     = /srv/log/spy.log

6. 把spy.properties复制到项目的classpath

把spy.properties复制到项目根目录,确保项目可以定位到spy.properties,否则它会提示spy.properties文件没有找到。

7. 完成

运行应用程序并做一些数据库事务,你会注意到所有从应用程序发送到数据库的SQL语句将被记录到你在spy.properties中指定的一个文件中。

示例日志文件如下:

1
2
3
4
insert into mkyong.stock_transaction (CHANGE, CLOSE, DATE, OPEN, STOCK_ID, VOLUME)
values (?, ?, ?, ?, ?, ?)|
insert into mkyong.stock_transaction (CHANGE, CLOSE, DATE, OPEN, STOCK_ID, VOLUME)
values (10.0, 1.1, '2009-12-30', 1.2, 11, 1000000)

总结

坦率地说,P6Spy在减少开发者的调试时间方面非常有用。只要你的项目是使用JDBC驱动来连接,P6Spy就能够为你记录所有的SQL语句和参数值。

对于Maven用户

你可以在pom.xml中添加以下内容来下载P6Spy依赖:

1
2
3
4
5
<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>1.3</version>
</dependency>

不过下载下来的包中并没有spy.properties文件,你必须自己创建它。或者你可以下载这里的——spy.properties模板。

参考资料

  1. P6Spy configuration

实现用户注册功能

已经两个星期没有更新代码了,这次来给加上用户注册的功能。

先在User.java中添加confirmPassword字段。然后在UserDao.java中添加保存用户的代码:

1
2
3
4
5
6
7
8
9
10
11
public void addUser(User user) {
    Session session = sessionFactory.openSession();
    Transaction ts = null;
    try {
        ts = session.beginTransaction();
        session.save(user);
        ts.commit();
    } finally {
        session.close();
    }
}

在UserService.java中添加以下代码:

1
2
3
4
5
6
7
public void addUser(User user) throws UserAlreadyExistsException {
    User u = getUser(user.getUsername());
    if (u != null) {
        throw new UserAlreadyExistsException();
    }
    userDao.addUser(user);
}

其中UserAlreadyExistsException.java代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.codemany.account.service;

public class UserAlreadyExistsException extends Exception {
    private static final long serialVersionUID = -7518737598462419360L;

    public UserAlreadyExistsException() {
        super();
    }

    public UserAlreadyExistsException(String message) {
        super(message);
    }

    public UserAlreadyExistsException(String message, Throwable cause) {
        super(message, cause);
    }

    public UserAlreadyExistsException(Throwable cause) {
        super(cause);
    }
}

在applicationContext.xml中加上一个bean:

1
2
3
<bean id="registerAction" class="com.codemany.account.action.RegisterAction" scope="prototype">
    <property name="userService" ref="userService" />
</bean>

然后在account.xml中添加register动作,并修改login动作相应的页面:

1
2
3
4
5
6
7
8
9
<action name="login" class="loginAction">
    <result name="success">/index.jsp</result>
    <result name="input">/login.jsp</result>
</action>

<action name="register" class="registerAction">
    <result name="success">/index.jsp</result>
    <result name="input">/register.jsp</result>
</action>

建立RegisterAction.java类:

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.action;

import com.codemany.account.model.User;
import com.codemany.account.service.UserAlreadyExistsException;
import com.codemany.account.service.UserService;

import com.opensymphony.xwork2.ActionSupport;

public class RegisterAction extends ActionSupport {
    private static final long serialVersionUID = 7021982816578678150L;

    private User user;

    private UserService userService;

    public String execute() throws Exception {
        try {
            userService.addUser(user);
        } catch (UserAlreadyExistsException e) {
            return INPUT;
        }
        return SUCCESS;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

新建校验配置文件。先是对应RegisterAction类的RegisterAction-validation.xml配置:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE validators PUBLIC
    "-//OpenSymphony Group//XWork Validator 1.0.2//EN"
    "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">

<validators>
    <field name="user">
        <field-validator type="visitor">
            <param name="appendPrefix">true</param>
            <message />
        </field-validator>
    </field>
</validators>

这里的校验使用了visitor类型,它是用于检测action里的复合属性的。这里的复合属性是User对象,所以要在User.java同一个目录下建立校验配置文件User-validation.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
<!DOCTYPE validators PUBLIC
    "-//OpenSymphony Group//XWork Validator 1.0.2//EN"
    "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">

<validators>
    <field name="username">
        <field-validator type="requiredstring">
            <message>Username is required</message>
        </field-validator>
        <field-validator type="stringlength">
            <param name="minLength">2</param>
            <param name="maxLength">20</param>
            <message>Username must be between ${minLength} and ${maxLength} characters</message>
        </field-validator>
    </field>

    <field name="password">
        <field-validator type="requiredstring">
            <message>Password is required</message>
        </field-validator>
        <field-validator type="stringlength">
            <param name="minLength">6</param>
            <param name="maxLength">50</param>
            <message>Password must be between ${minLength} and ${maxLength} characters</message>
        </field-validator>
    </field>

    <field name="confirmPassword">
        <field-validator type="fieldexpression">
            <param name="expression">confirmPassword.equals(password)</param>
            <message>Password does not match the confirm password</message>
        </field-validator>
    </field>
</validators>

修改index.jsp,将登录表单移到单独的login.jsp页面中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>

<%@ taglib uri="/struts-tags" prefix="s" %>

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

<body>
    <s:if test="#session.logined">
        Welcome, you have logined. <a href="<%= request.getContextPath() %>/logout.action">Logout</a>
    </s:if>
    <s:else>
        <a href="<%= request.getContextPath() %>/login!input.action">Login</a> |
        <a href="<%= request.getContextPath() %>/register!input.action">Register</a>
    </s:else>
</body>
</html>

这里是登录页面login.jsp,首页index.jsp中的登录表单就是被移到这个文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>

<%@ taglib uri="/struts-tags" prefix="s" %>

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Login</title>
</head>

<body>
    <p>User: test/test</p>
    <s:form action="login">
        <s:textfield key="username" label="Username" />
        <s:password key="password" label="Password" />
        <s:submit value="Login" />
    </s:form>
</body>
</html>

注册页面register.jsp,负责生成注册时的表单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>

<%@ taglib uri="/struts-tags" prefix="s" %>

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Registration</title>
</head>

<body>
    <s:form action="register">
        <s:textfield key="user.username" label="Username" />
        <s:password key="user.password" label="Password" />
        <s:password key="user.confirmPassword" label="Password Confirmation" />
        <s:submit value="Register" />
    </s:form>
</body>
</html>

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

使用Struts 2 Validation框架校验登录

Struts 2有三种校验方式:

  1. 使用Annotation进行校验。
  2. 使用XML配置校验。
  3. 使用覆盖validate方法校验。

使用XML配置时validation.xml的命名规则和放置路径:

1
<ActionClassName>-validation.xml

<ActionClassName>就是要验证的Action类的名字,将此文件放在与Action类文件相同的目录下即可。如果该Action类在struts.xml配置中有多个action实例,那么对应某个action实例的校验文件命名规则如下:

1
<ActionClassName>-<aliasName>-validation.xml

例如:UserAction-login-validation.xml。注意,这里的<aliasName>并不是方法名,而是struts.xml中配置的action实例的名字。

既然用框架来校验了,那么在LoginAction.java中判断用户名和密码是否为空的代码就可以删除了。还要记得加上username和passowrd的getter方法,不然校验器(validator)会因为取不到field而报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String execute() throws Exception {
    User u = userService.getUser(username);
    if (u == null || !password.equals(u.getPassword())) {
        return INPUT;
    }

    ActionContext.getContext().getSession().put("logined", true);
    return SUCCESS;
}

public String getUsername() {
    return username;
}

public String getPassword() {
    return password;
}

同时在LoginAction.java相同目录下创建LoginAction-validation.xml,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE validators PUBLIC
    "-//OpenSymphony Group//XWork Validator 1.0.2//EN"
    "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">

<validators>
    <field name="username">
        <field-validator type="requiredstring">
            <message>Username is required</message>
        </field-validator>
    </field>
    <field name="password">
        <field-validator type="requiredstring">
            <message>Password is required</message>
        </field-validator>
    </field>
</validators>

message提供了校验出错的信息,在其中可以使用${}来引用被校验的对象。如果未出现异常,则转入用户请求的处理方法;出现异常则转入input所指定的视图。所以,对于校验的action实例,必须要在配置文件中为其指定input视图。

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

Struts 2 + Spring 3 + Hibernate 4登录实例

在pom.xml中添加Spring的依赖包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>3.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>3.1.2.RELEASE</version>
</dependency>
<!-- 注意:没有这个plugin包会导致Spring无法注入bean到Struts 2的Action中 -->
<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-spring-plugin</artifactId>
    <version>2.3.4.1</version>
</dependency>

配置web.xml文件,加上Spring的监听器:

1
2
3
4
5
6
7
8
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

删除hibernate.cfg.xml,添加applicationContext.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
36
37
38
39
40
41
<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- DriverManagerDataSource在每个连接请求时都新建一个connection。
         与DBCP的BasicDataSource不同,DriverManagerDataSource提供的连接没有进行池管理 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jbookshelf" />
        <property name="username" value="root" />
        <property name="password" value="123456" />
    </bean>

    <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.connection.pool_size">1</prop>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                <prop key="hibernate.cache.provider_class">org.hibernate.cache.internal.NoCacheProvider</prop>
                <prop key="show_sql">true</prop>
            </props>
        </property>
        <property name="mappingResources">
            <list>
                <value>com/codemany/account/model/User.hbm.xml</value>
            </list>
        </property>
    </bean>

    <bean id="userDao" class="com.codemany.account.dao.UserDao">
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>

    <!-- Struts 2的action是有状态的,必须显式设置scope为prototype,这样每次请求过来都会创建新的action -->
    <bean id="loginAction" class="com.codemany.account.action.LoginAction" scope="prototype">
        <property name="userDao" ref="userDao" />
    </bean>
</beans>

删除HibernateUtil.java,修改UserDao.java为以下内容:

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
package com.codemany.account.dao;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;

import com.codemany.account.model.User;

public class UserDao {
    // Spring 3集成Hibernate 4不再需要HibernateDaoSupport或HibernateTemplate了,
    // 直接使用原生API即可。
    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public User getUser(String username) {
        Session session = sessionFactory.openSession();
        try {
            Query query = session.createQuery("from User u where u.username = ?");
            query.setString(0, username);
            query.setMaxResults(1);
            return (User)query.uniqueResult();
        } finally {
            session.close();
        }
    }
}

在struts.xml中将action元素中login的class属性改为applicationContext.xml配置文件中的loginAction:

1
2
3
4
<action name="login" class="loginAction">
    <result name="success">/index.jsp</result>
    <result name="input">/index.jsp</result>
</action>

修改LoginAction.java的代码:

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
package com.codemany.account.action;

import com.codemany.account.dao.UserDao;
import com.codemany.account.model.User;

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

public class LoginAction extends ActionSupport {
    private static final long serialVersionUID = -389833745243649130L;

    private String username;
    private String password;

    private UserDao userDao;

    public String execute() throws Exception {
        if (username == null || username.length() == 0
                || password == null || password.length() == 0) {
            return INPUT;
        }

        User u = userDao.getUser(username);
        if (u == null || !password.equals(u.getPassword())) {
            return INPUT;
        }

        ActionContext.getContext().getSession().put("logined", true);
        return SUCCESS;
    }

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

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

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

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

Struts 2 + Hibernate 4登录实例

使用以下代码创建数据库:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE DATABASE IF NOT EXISTS jbookshelf;

USE jbookshelf;

DROP TABLE IF EXISTS users;

CREATE TABLE users (
    id int NOT NULL AUTO_INCREMENT,
    username varchar(20) NOT NULL,
    password varchar(50) NOT NULL,
    PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf-8;

在pom.xml中添加Hibernate的依赖配置:

1
2
3
4
5
6
7
8
9
10
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>4.1.6.Final</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.21</version>
</dependency>

创建User.java类:

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
package com.codemany.account.model;

public class User {
    private Long id;

    private String username;
    private String password;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

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

    public String getPassword() {
        return password;
    }

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

在同一目录下创建User.hbm.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="com.codemany.account.model">
    <class name="User" table="users">
        <id name="id">
            <generator class="increment" />
        </id>

        <property name="username" length="20" />
        <property name="password" length="50" />
    </class>
</hibernate-mapping>

在resources目录下创建hibernate.cfg.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
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <!-- MySQL connection settings -->
        <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="connection.url">jdbc:mysql://localhost:3306/jbookshelf</property>
        <property name="connection.username">yourname</property>
        <property name="connection.password">yourpassword</property>

        <!-- JDBC connection pool (use the built-in) -->
        <property name="connection.pool_size">1</property>

        <!-- SQL dialect -->
        <property name="dialect">org.hibernate.dialect.MySQLDialect</property>

        <!-- Disable the second-level cache  -->
        <property name="cache.provider_class">org.hibernate.cache.internal.NoCacheProvider</property>

        <!-- Echo all executed SQL to stdout -->
        <property name="show_sql">true</property>

        <mapping resource="com/codemany/account/model/User.hbm.xml" />
    </session-factory>
</hibernate-configuration>

建立管理Session的HibernateUtil.java类:

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
package com.codemany.account.dao.hibernate;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class HibernateUtil {
    private static final SessionFactory sessionFactory;

    static {
        try {
            // Create the SessionFactory from hibernate.cfg.xml
            Configuration configuration = new Configuration();
            configuration.configure();
            Properties props = configuration.getProperties();

            ServiceRegistry serviceRegistry =
                    new ServiceRegistryBuilder().applySettings(props).buildServiceRegistry();

            sessionFactory = configuration.buildSessionFactory(serviceRegistry);
        } catch (Throwable ex) {
            // Make sure you log the exception, as it might be swallowed
            System.err.println("Initial SessionFactory creation failed." + ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static Session getSession() {
        return sessionFactory.openSession();
    }
}

在dao包创建数据访问对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.codemany.account.dao;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.Transaction;

import com.codemany.account.model.User;

public class UserDao {
    public static User getUser(String username) {
        Session session = HibernateUtil.getSession();
        try {
            Query query = session.createQuery("from User u where u.username = ?");
            query.setMaxResults(1);
            query.setString(0, username);
            return (User)query.uniqueResult();
        } finally {
            session.close();
        }
    }
}

现在可以访问数据库了,所以要修改LoginAction.java的execute方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String execute() throws Exception {
    if (username == null || username.length() == 0
            || password == null || password.length() == 0) {
        return INPUT;
    }

    User u = UserDao.getUser(username);
    if (u == null) {
        return INPUT;
    }
    if (username.equals(u.getUsername()) && password.equals(u.getPassword())) {
        ActionContext.getContext().getSession().put("logined", true);
        return SUCCESS;
    }

    return INPUT;
}

如果显示jta-1.0.1b.jar缺失的错误,可以在pom.xml中添加以下语句:

1
2
3
4
5
6
<repositories>
    <repository>
        <id>java.net</id>
        <url>http://download.java.net/maven/2/</url>
    </repository>
</repositories>

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