Home - shuzheng/etmvc GitHub Wiki

使用教程

如今的Java Web开发对于需求来说已经变得过于复杂。当今众多Java领域的Web开发框架不仅使用复杂,而且并没有很好的遵循Don’t Repeat Yourself(DRY)原则。

一、什么是etmvc?

etmvc是一套轻量级简易高效的WEB开发框架,严格遵循MVC的思想。et一词源于1982年斯皮尔伯格执导的一部温馨科幻片《E.T.》(外星人),意思就是来自外星人的,不受束缚的MVC,开发者可以快乐地做WEB开发,而不受传统的烦杂折磨。

二、etmvc框架定位

我们给这个框架的定位如下:

  • 简易:代码要简单,开发要容易。约定优于配置,再也没有XML的配置之苦。
  • 性能:在满足功能的前提下尽量地提高性能。
  • 实用:没有太多花哨的东西,一切从实用的角度考虑。

三、授权协议

etmvc框架采用LGPL授权。

四、etmvc框架的组成

etmvc框架包括mvc和一个可选的orm实现,可选的orm实现是一个ActiveRecord框架,独立于mvc,可以在非WEB的应用程序中使用。

五、etmvc框架的安装

  1. 获取最新的框架:从本站获取最新的etmvc框架。
  2. 建立WEB项目,将下载的压缩文件解压至项目的/WEB-INF/lib目录中。
  3. 配置数据库,在/WEB-INF/classes目录中建立数据库连接配置文件activerecord.properties, 配置示例:
domain_base_class=com.et.ar.ActiveRecordBase  

com.et.ar.ActiveRecordBase.driver_class=com.mysql.jdbc.Driver  
com.et.ar.ActiveRecordBase.url=jdbc:mysql://localhost/mydb  
com.et.ar.ActiveRecordBase.username=root  
com.et.ar.ActiveRecordBase.password=soft123456  
com.et.ar.ActiveRecordBase.pool_size=2 
  1. 配置/WEB-INF/web.xml,添加一个过滤器,配置示例:
<filter>
    <filter-name>etmvc</filter-name>
    <filter-class>com.et.mvc.DispatcherFilter</filter-class>
    <init-param>
        <param-name>controllerBasePackage</param-name>
        <param-value>controllers</param-value>
    </init-param>
    <init-param>
        <param-name>viewBasePath</param-name>
        <param-value>/views</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>etmvc</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
  1. 建立保存视图模板的目录/views。

六、etmvc框架的基本概念

  1. controller:控制器是属于请求范围的,用于处理请求,创建或者准备响应。每次请求都会创建一个控制器实例,控制器的类名必须以Controller结尾,一般整个应用程序会创建一个控制器的基类
  2. ApplicationController,然后具体的其它控制器再继承之。
  3. action:每个URL操作将映射到一个action上,一个action是一个控制器的方法,一个控制器可以管理彼此相关的多个action。一个控制器中标准的action命名参考: * index: 默认的动作 * show:显示动作 * create:新建动作 * save:保存动作 * edit:修改动作 * update:更新动作 * destroy:删除动作
  4. model:模型,是一个数据实体,将对应到具体的数据表中,这种映射关系是通过ActiveRecord实现的。所以数据表中的字段名就是模型对象中的属性名,不再需要用XML配置描述了。
  5. view:视图,etmvc支持多种视图,甚至一个action多视图,最常用的视图是JspView,在AJAX应用中是JsonView,下载处理二进制数据时是BinaryView,等等。

我们利用etmvc来建立一个Hello,World的WEB应用程序。

一、首先,建立新的WEB项目,引入et-mvc.jar和paranamer-1.3.jar,配置web.xml,加入一个过滤器,如下所示:

<filter>
    <filter-name>etmvc</filter-name>
    <filter-class>com.et.mvc.DispatcherFilter</filter-class>
    <init-param>
        <param-name>controllerBasePackage</param-name>
        <param-value>controllers</param-value>
    </init-param>
    <init-param>
        <param-name>viewBasePath</param-name>
        <param-value>/views</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>etmvc</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

我们看到,过滤器com.et.mvc.DispatcherFilter目前只有二个参数,controllerBasePackage指的是控制器的包名,viewBasePath指的是视图模板的存放目录。

二、接下来,我们开始编写控制器HelloController,一般我们会编写控制器基类ApplicationController,我们的HelloController会继承它。注意到,控制器的包名是controllers,这就是前面配置中的controllerBasePackage配置值。

package controllers;

import com.et.mvc.Controller;

public class ApplicationController extends Controller{

}
package controllers;

import com.et.mvc.TextView;

public class HelloController extends ApplicationController{
    public TextView say(){
        return new TextView("hello,world");
    }
}

三、至些,我们的Hello,World程序编写完毕,部署后在浏览器地址栏输入http://localhost:8080/helloworld/hello/say,将会输出hello,world字样。

etmvc遵循“约定优于配置”的原则,通过文件的命名及存放位置来代替显式的配置,避免编写烦杂的XML配置文件。

etmvc的配置只有一处,即在web.xml中配置一个filter,如下所示:

<filter>
    <filter-name>etmvc</filter-name>
    <filter-class>com.et.mvc.DispatcherFilter</filter-class>
    <init-param>
        <param-name>controllerBasePackage</param-name>
        <param-value>controllers</param-value>
    </init-param>
    <init-param>
        <param-name>viewBasePath</param-name>
        <param-value>/views</param-value>
    </init-param>
    <init-param>
        <param-name>plugin</param-name>
        <param-value>plugin.OcrServer</param-value>
    </init-param>
    </filter>
<filter-mapping>
    <filter-name>etmvc</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

其中,filter的初始参数有三个:controllerBasePackage, viewBasePath, plugin,说明如下:

1 controllerBasePackage是控制器的基包名称,如controllers,所有的控制器类必须在controllers包中,或者在controllers的子包中。控制器类必须以Controller结尾,必须继承Controller,比如有如下的控制器类:

package controllers;

public class ArticleController extends ApplicationController{
    public View showImage(int id) throws Exception{
        //...
    }

    public View download(int id) throws Exception{
       //...
    }
    
    public void create(){

    }

}

控制器包名是controllers,控制器类名是ArticleController,有showImage等Action方法。

2 viewBasePath是存放视图模板的位置,如下所示: 视图模板的目录结构有一定的规则,在[viewBasePath]目录下是控制器名称(小写),再往下是对应每个Action方法的视图文件。如ArticleController控制器中的方法create对应到/article/create.jsp视图文件,即执行控制器的create方法后,etmvc根据执行的结果找到对应的视图进行渲染。

3 plugin是插件的配置,一般情况下无须用到,所以不用配置该项,关于插件的使用留到后面的章节再作介绍。

我们举个简单的例子说明一下从浏览器发送请求到服务器处理完请求返回信息给浏览器的过程。

1、浏览器发出http://localhost:8084/myweb/user/list这个请求,服务器将从这个URL分析出如下信息:

myweb:上下文路径信息

User:控制器信息

list:动作信息

2、服务器根据这个信息查找控制器UserController中的list方法,并执行之。

3、服务器将查找名称是list.jsp的视图并将处理结果传递到视图,完成渲染过程。

整个处理过程简单来说就是这样。

每个请求都会创建新的控制器实例,控制器的类名必须以Controller结尾,必须继承Controller类,比如ApplicationController, HelloController等,控制器的Action方法允许继承,我们一般都会创建一个根控制器,然后让其他控制器统一继承这个根控制器。

每个控制器允许有多个Action操作,这些操作将映射到相应的URL上。比如有如下的控制器:

public class UserController extends ApplicationController{
    public void create(){
    }

    public View save(User user) throws Exception{
    }
    
    public void login(String name, String password) throws Exception{
    }

    public void logout() throws Exception{
    }
}

将相应的URL 映射到控制器的Action方法上:

URL Action方法
/user/create create
/user/save save
/user/login login
/user/logout logout

至此,我们看到编写控制器处理WEB请求就是这样简单。

控制器的Action方法接受不同的参数,这些参数将自动绑定到Request的参数,方法可以返回不同的类型,比如void, String, JsonView, BinaryView等,etmvc将据此确定处理后以何种视图返回。

下面我们以一个用户登录的例子来说明控制器的一般用法:

1、建立控制器,如下所示:

package controllers;

public class UserController extends ApplicationController{
    public void login(){

    }

    public String handleLogin(String username, String password) throws Exception{
        return "你输入的用户:" + username + "密码:" + password;
    }
}

我们定义了二个Action方法,一个是login,该方法返回值是void,系统默认将寻找/views/user/login.jsp的视图进行显示,另一个是handleLogin,该方法将简单地将用户登录信息显示出来。

2、我们来建立login.jsp视图:

<form action="<c:url value="/user/handleLogin"/>" method="POST">
    <p>用户名:<input type="text" name="username"></p>
    <p>密码:<input type="password" name="password"></p>
    <p><input type="submit" value="提交"></p>
</form>

我们看到FORM中的action的URL指向,这个URL将映射到我们控制器中的handleLogin方法,而该方法将返回String类型,etmvc将其解释了文本视图,所以将会在浏览器上显示登录的信息。

当请求到达时,etmvc将创建控制器对象,控制器对象会查找与“被请求的action”同名的public实例方法。如此看来,控制器的Action方法是允许被继承的。如果你希望某些方法不被作为action调用,可以将其声明为protected或者private。比如有如下的控制器:

public class BlogController extends ApplicationController{
    public String show(){
        return "show method";
    }

    protected String create(){
        return "create method";
    }
}

当访问/blog/show时将输入框“show method” ,而访问/blog/create时将有“The requested resource (/test1/blog/create) is not available”的信息。

Action方法允许使用控制器环境提供的一些对象:

  • request
  • response
  • session
  • servletContext
  • controllerPath
  • controllerName
  • actionPath
  • flash
  • exception 他们的作用应该不言自明,其中flash对象的使用方法我们将分出一个主题专门作介绍。

我们来探讨etmvc如何使用视图,前面关于“Action方法”的介绍中我们提到,每个请求将会映射到一个Action方法。etmvc将根据Action方法的返回类型来决定使用何种视图,大体有以下三种:

  1. 返回void时将使用JSP视图。
  2. 返回String时将字符串直接输出至浏览器。
  3. 返回View或其子类时将使用对应的视图。

下面我们来分别说明,如有如下的action方法:

public class UserController extends ApplicationController{
    public void test1(){
        request.setAttribute("hello", "hello,test1");
    }
}

action方法“test1”的返回类型是void,这时etmvc将其解释为JSP视图,将会查找/views/user/test1.jsp的文件,文件内容:

<body>
    <h1>Hello World!</h1>
    <p>${hello}</p>
</body>

我们来编写返回类型是String的action方法:

public class UserController extends ApplicationController{
    public String test2(){
        return "hello,test2";
    }
}

这时会将返回字符串“hello,test2”直接输出至浏览器。

如果返回类型是View或其子类型,则etmvc会使用其定义的视图,如下所示:

public class UserController extends ApplicationController{
    public JspView test3(){
        JspView view = new JspView();
        view.setAttribute("hello", "hello,test3");
        return view;
    }

    public JspView test4(){
        JspView view = new JspView("/common/other.jsp");
        view.setAttribute("hello", "hello,test4");
        return view;
    }
}

上面例子中test3使用默认的JSP视图位置和目录/user/test3.jsp,而test4使用指定的视图位置和目录/common/other.jsp。

我们现来看个JsonView的例子,JsonView能够处理多种数据结构,能够将其正确地转换成客户端需要的JSON串,这在AJAX 的开发中非常有用,同EXTJS整合时也会很容易,如下所示:

public class UserController extends ApplicationController{
    public JsonView test5(){
        Map<String,Object> result = new HashMap<String,Object>();
        result.put("success", true);
        result.put("msg", "hello,test5");
        JsonView view = new JsonView(result);
        view.setContentType("text/html;charset=utf-8");//允许指定ContentType
        return view;
    }
}

上面 例子运行结果将向浏览器输出{"msg":"hello,test5","success":true}。

我们来总结一下,etmvc目前支持的视图包括:

  • JspView
  • TextView
  • FreeMarkerView
  • BinaryView
  • JsonView

etmvc内置了多种常用的视图,每种视图都有对应的renderer对象,视图对象中通过加上@ViewRendererClass注解将renderer对象关联起来。比如JspView有相应的JspViewRenderer,JsonView有对应的JsonViewRenderer。

如果要使用自定义视图,则可以扩展视图,扩展视图是很简单的事情,我们需要做二件事情:一是定义视图对象,二是定义renderer对象。

假如我们要来定义一个JavaScriptView视图,用于向页面输出并执行一段JavaScript代码。首先定义视图对象,视图对象建议继承View对象:

@ViewRendererClass(JavaScriptViewRenderer.class)
public class JavaScriptView extends View{
    private String js;

    public JavaScriptView(String js){
        this.js = js;
    }

    public String getJs() {
        return js;
    }

    public void setJs(String js) {
        this.js = js;
    }
}

我们看到,视图对象仅是一个定义了需要的数据载体,其渲染输出是通过renderer对象完成的。下面看来一下renderer对象的定义:

public class JavaScriptViewRenderer extends AbstractViewRenderer<JavaScriptView>{
    public void renderView(JavaScriptView view, ViewContext context) throws Exception{
        PrintWriter out = context.getResponse().getWriter();
        out.print("<script>"+view.getJs()+"</script>");
        out.close();
    }
}

renderer对象扩展了AbstractViewRenderer ,视图的渲染就是通过renderView方法实现的。

好了,自定义的JavaScriptView扩展完成了,我们来看看怎样使用:

public class TestController extends ApplicationController{
    public View jstest(){
        JavaScriptView view = new JavaScriptView("alert(&apos;abc&apos;);");
        return view;
    }
}

我们在控制器中定义一个Action方法,只要将该方法的返回类型设为JavaScriptView就行了。

我们以一个用户注册的例子来说明模型绑定问题。首先,建立一个用户注册表单:

<h1>用户注册</h1>
<form action="<c:url value="/user/save"/>" method="POST">
    <p>名称:<input type="text" name="name"></p>
    <p>密码:<input type="password" name="password"></p>
    <p>确认密码:<input type="password" name="confirmPassword"></p>
    <p>邮箱:<input type="text" name="email"></p>
    <p>电话:<input type="text" name="phone"></p>
    <p><input type="submit" value="提交"> <input type="reset" value="重置"></p>
</form>

现在编写控制器用以处理表单:

public class UserController extends ApplicationController{
    /**
     * 用户注册页面
     */
    public void register(){
    }
    
    public String save(){
        String name = request.getParameter("name");
        String password = request.getParameter("password");
        String confirmPassword = request.getParameter("confirmPassword");
        String email = request.getParameter("email");
        String phone = request.getParameter("phone");
        if (!password.equals(confirmPassword)){
            return "确认密码不对。";
        }
        String info = "name:"+name+"<br/>"
                + "password:"+password+"<br/>"
                + "email:"+email+"<br/>"
                + "phone:"+phone;
        return info;
    }
}

我们可以通过控制器环境提供的request对象获取表单数据,进而进行处理。

但我们有更好的处理方法,我们建立一个User对象来描述表单,这个相当于struts中的ActionForm,

public class User {
    private String name;
    private String password;
    private String email;
    private String phone;
    private String confirmPassword;
    //get set...
}

现在,我们改写控制器中的save方法:

public class UserController extends ApplicationController{
    /**
     * 用户注册页面
     */
    public void register(){
    }
    
    public String save(User user){
        if (!user.getPassword().equals(user.getConfirmPassword())){
            
        }
        String info = "name:"+user.getName()+"<br/>"
                + "password:"+user.getPassword()+"<br/>"
                + "email:"+user.getEmail()+"<br/>"
                + "phone:"+user.getPhone();
        return info;
    }
}

我们看到etmvc自动将表单数据绑定到Action方法的参数中。我们把定义的User称作模型,etmvc 会根据Action方法的参数自动将请求参数绑定进来,Action方法的参数类型和顺序可以是任意的,对于复杂的对象类型,可以用@Bind注解说明绑定的前缀,如@Bind(prefix="user")User user,这时页面表单项的名称就不能是name,password,而应是user.name, user.password。

etmvc处理大部分常用的数据类型绑定,如果想自已处理数据转换绑定,可以实现DataBinder接口,如:

public class DateBinder implements DataBinder{
    public Object bind(BindingContext ctx) throws Exception{
        //...
        return null;
    }
}

然后进行注册:

DataBinders.register(java.util.Date.class, new DateBinder());

这时候就可以在Action方法中使用java.util.Date参数类型了。

etmvc中访问数据可以使用JDBC,HIBERNATE等,鉴于JDBC的烦琐和HIBERNATE的复杂,我们同时提供了一个ORM的简易实现版本ActiveRecord。在大多数中小型WEB系统中,使用ActiveRecord就足够了。

1、使用前须将et-ar.jar, asm.jar, cglib.jar等包引入项目,然后进行配置activerecord.properties:

domain_base_class=com.et.ar.ActiveRecordBase

com.et.ar.ActiveRecordBase.driver_class=com.mysql.jdbc.Driver
com.et.ar.ActiveRecordBase.url=jdbc:mysql://localhost/mydb
com.et.ar.ActiveRecordBase.username=root
com.et.ar.ActiveRecordBase.password=soft123456
com.et.ar.ActiveRecordBase.pool_size=2

在上面配置中我们配置了MYSQL数据库连接,配置文件activerecord.properties放在CLASSPATH能找到的地方就好。 2、我们来建立一张数据表:

create table users(
    id int primary key auto_increment,
    name varchar(10) default null,
    addr varchar(50) default null,
    email varchar(50) default null,
    remark varchar(50) default null
)

然后建立对应的域对象:

@Table(name="users")
public class User extends ActiveRecordBase{
    @Id private Integer id;
    @Column private String name;
    @Column private String addr;
    @Column private String email;
    @Column private String remark;
    //get,set...
}

我们的域模型对象继承自ActiveRecordBase,到些,ORM就建立完成了,我们看到,不需要复杂的配置文件,仅用几个简单的注解就完成了。

3、基本的CRUD操作

增加记录:

User user = new User();
user.setName("name1");
user.setAddr("addr1");
user.setEmail("[email protected]");
user.save();

修改记录:

User user = User.find(User.class, 3);
user.setRemark("user remark");
user.save();

删除记录:

User user = User.find(User.class, 3);
user.destroy();

查询记录:

List<User> users = User.findAll(User.class);
for(User user: users){
    System.out.println(user.getName());
}

条件查询:

List<User> users = User.findAll(User.class, "addr like ?", new Object[]{"%百花路%"});
for(User user: users){
    System.out.println(user.getName());
}

我们看到,借助ActiveRecord,操作数据是如此容易。

我们来编写一个用户资料管理的小程序,旨在说明etmvc的基本用法。需具备的基础:

  • 理解基本控制器
  • ActiveRecord操作基础 现在建立一个测试用的表结构:
create table users(
id int primary key auto_increment,
name varchar(10) default null,
addr varchar(50) default null,
email varchar(50) default null,
remark varchar(50) default null
)

接下来建立对应的模型对象:

@Table(name="users")
public class User extends ActiveRecordBase{
    @Id private Integer id;
    @Column
    @NotEmpty(message="用户名称必须填写")
    private String name;
    @Column private String addr;
    @Column
    @Email(message="邮箱格式不对")
    private String email;
    @Column private String remark;
    //get,set...
}

注意到我们对name, email进行了有效性约束。

现在,我们来编写控制器代码,包括了用户管理的CRUD操作:

public class UserController extends ApplicationController{
    /**
     * 显示用户列表页面
     */
    public void index() throws Exception{
        List<User> users = User.findAll(User.class);
        request.setAttribute("users", users);
    }

    /**
     * 新建用户页面
     */
    public void create(){
    }

    /**
     * 保存新增用户
     */
    public JspView save(User user) throws Exception{
        if (user.save()){
            redirect("index");  //重定向到列表页面
            return null;
        }
        else{
            return new JspView("create.jsp", "user", user);
        }
    }
    
    /**
     * 修改指定的用户资料页面
     */
    public void edit(int id) throws Exception{
        User user = User.find(User.class, id);
        request.setAttribute("user", user);
    }
    
    /**
     * 更新指定的用户资料
     */
    public JspView update(int id) throws Exception{
        User user = User.find(User.class, id);
        user = User.updateModel(user, request.getParameterMap());
        if (user.save()){
            redirect("index");  //重定向到列表页面
            return null;
        }
        else{
            return new JspView("edit.jsp", "user", user);
        }
    }
    
    public void destroy(int id) throws Exception{
        User user = User.find(User.class, id);
        user.destroy();
        redirect("index");  //重定向到列表页面
    }
}

最后,是JSP视图,先来看用户列表的视图index.jsp:

<h1>用户列表</h1>
<table border="1">
    <tr>
        <th>名称</th>
        <th>地址</th>
        <th>邮箱</th>
        <th>备注</th>
        <th style="width:80px;">操作</th>
    </tr>
    <c:forEach items="${users}" var="user">
        <tr>
            <td>${user.name}</td>
            <td>${user.addr}</td>
            <td>${user.email}</td>
            <td>${user.remark}</td>
            <td>
                <a href="<c:url value="/user/edit/${user.id}"/>">修改</a>
                <a href="<c:url value="/user/destroy/${user.id}"/>">删除</a>
            </td>
        </tr>
    </c:forEach>
</table>
<a href="<c:url value="/user/create"/>">增加</a>

新建用户资料页面create.jsp:

<h1>新增用户资料</h1>
<form action="<c:url value="/user/save"/>" method="POST">
    <jsp:include page="form.jsp"/>
    <input type="submit" value="提交">
    <input type="reset" value="重置">
</form>

修改用户资料页面edit.jsp:

<h1>修改用户资料</h1>
<form action="<c:url value="/user/update/${user.id}"/>" method="POST">
    <jsp:include page="form.jsp"/>
    <input type="submit" value="提交">
    <input type="reset" value="重置">
</form>

新增和修改页面共用了一个表单form.jsp:

<div>
    <c:if test="${fn:length(user.errors)>0}">
        <ul>
            <c:forEach items="${user.errors}" var="err">
                <li style="color:red;">${err}</li>
            </c:forEach>
        </ul>
    </c:if>
</div>
<table>
    <tr>
        <th>名称</th>
        <td><input type="text" name="name" value="${user.name}"></td>
    </tr>
    <tr>
        <th>地址</th>
        <td><input type="text" name="addr" value="${user.addr}"></td>
    </tr>
    <tr>
        <th>邮箱</th>
        <td><input type="text" name="email" value="${user.email}"></td>
    </tr>
    <tr>
        <th>备注</th>
        <td><input type="text" name="remark" value="${user.remark}"></td>
    </tr>
</table>

至此,程序编写完毕。

我们先来看一下ActiveRecord(下简称AR)的基本配置:

domain_base_class=com.et.ar.ActiveRecordBase

com.et.ar.ActiveRecordBase.driver_class=com.mysql.jdbc.Driver
com.et.ar.ActiveRecordBase.url=jdbc:mysql://localhost/mydb
com.et.ar.ActiveRecordBase.username=root
com.et.ar.ActiveRecordBase.password=soft123456
com.et.ar.ActiveRecordBase.pool_size=2

其中的配置项domain_base_class是我们域模型对象的基类,我们在定义模型类时必须让其继承ActiveRecordBase,AR将根据此找到对应的数据库连接。

如果我们想同时使用多个数据库,这时我们可以先定义二个基类:

public class Base1 extends ActiveRecordBase{
}
public class Base2 extends ActiveRecordBase{
}

然后进行配置:

domain_base_class=models.Base1 models.Base2

models.Base1.driver_class=com.mysql.jdbc.Driver
models.Base1.url=jdbc:mysql://localhost/mydb1
models.Base1.username=root
models.Base1.password=soft123456
models.Base1.pool_size=2

models.Base2.driver_class=com.mysql.jdbc.Driver
models.Base2.url=jdbc:mysql://localhost/mydb2
models.Base2.username=root
models.Base2.password=soft123456
models.Base2.pool_size=2

我们只要让我们的模型类继承Base1或Base2,就能正确使用对应的数据库连接。如果那一天又要改回去连接一个数据库了,只要改一下这个activerecord.properties属性文件就OK了。

AR中同时访问多个数据库时是不是很简单。

我们在ActiveRecord(下简称AR)中提供一对一、一对多、多对一等关联,分述如下:

1、一对多:一对多关联是指一个类(比如Author)拥有另一个类(比如Book)的多个实例,用@HashMany注解描述:

@Table(name="authors")
public class Author extends ActiveRecordBase{
    @Id private Integer id;
    @Column private String name;

    @HasMany(foreignKey="authorId", dependent=DependentType.DELETE, order="id")
    private List<Book> books;
    //get,set...
}
@Table(name="books")
public class Book extends ActiveRecordBase{
    @Id private Integer id;
    @Column private Integer authorId;
    @Column private String name;

    @BelongsTo(foreignKey="authorId")
    private Author author;
    //get,set...
}

@HasMany有几个属性:

foreignKey指定多方的外键,必须指定。 dependent指定在删除主表时做何操作,可选。有DELETE,DESTROY,NULLIFY,DELETE是简单的删除从表记录;DESTROY是再以级联的方式销毁从表对应的对象,如果这种级联关系有二级以上,则使用DESTROY会比较合适;NULLIFY是将从表的外键置为NULL值而并不删除。 order指定获取从表对应的记录时的排序字段,可选。

2、一对一:一对一是一对多的特例,使用@HasOne描述,如:

@Table(name="authors")
public class Author extends ActiveRecordBase{
    @Id private Integer id;
    @Column private String name;
    
    @HasOne(foreignKey="authorId")
    private Book book;
    //get,set...
}

@HasOne注解的其他用法同@HasMany。

3、多对一:是指一个类属于另一个类,比如上面的Book类,使用@BelongsTo注解描述多对一关系。

好了,定义了模型对象之间的关联之后,我们的编码简单了很多,比如我们要访问某个作者所拥有的图书,可以这样写:

Author author = Author.find(Author.class, 1);
List<Book> books = author.getBooks();
for(Book book: books){
    System.out.println(book.getName());
}

AR会维护对象之间的关联,在级联保存、删除及更新操作中都能保证在一个事务中,要么全部成功,要么全部失败。

比如我们想删除某个作者及其拥有的图书记录,可以这样写:

Author author = Author.find(Author.class, 1);
author.destroy();

建立一个新的Author对象及其对应的Book对象集合,并同时保存进数据库,可以这样写:

Author author = new Author();
author.setName("author1");
List<Book> books = new ArrayList<Book>();
for(int i=0; i<3; i++){
    Book book = new Book();
    book.setName("book" + i);
    books.add(book);
}
author.setBooks(books);
author.save();

我们看到,AR帮我们做了大部分工作,她会记住对象之间的关联并试图维护这种关系。

当然,实际情况很复杂,但AR能够完成大多数的工作。

et-mvc上传文件是对Commons-fileupload组件的封装,所以使用时需要引入commons-fileupload.jar, commons-io.jar, commons-logging.jar三个包。

上传文件的第一步就是象下面一样创建一个multipart/form-data表单:

<form action="<c:url value="/upload/doUpload"/>" method="POST" enctype="multipart/form-data">
    <input type="file" name="file">
    <input type="submit" value="提交">
</form>

然后编写控制器,定义上传的Action方法:

public class UploadController extends ApplicationController{
    public String doUpload(MultipartFile file) throws Exception{
        file.transferTo(new File("e:/temp/" + file.getOriginalFilename()));
        return file.getOriginalFilename()+":"+file.getSize();
    }
}

需要下载文件时,可以使用BinaryView,如下所示:

public BinaryView download() throws Exception{
    BinaryView view = BinaryView.loadFromFile("e:/temp/arrow.gif");
    view.setContentType("image/gif");
    //view.setContentDisposition("attachment"); //下载
    return view;
}

上传下载就介绍这些,看起来应该是比较简单的。

分页是每个WEB应用程序必不可少的一环,数据量一大,就需要分页。分页需要前后台的配合才能完成,extjs的分页是由分页工具栏PagingToolbar完成的,而etmvc中的分页由ActiveRecord完成。

我们以显示用户资料为例子来看分页是怎么做的,建立一个分页的UserGrid,我们选择继承Ext.grid.GridPanel,这样形成的Component是可以复用的。来看看代码:

UserGrid = Ext.extend(Ext.grid.GridPanel, {
    initComponent:function(){
        var ds = new Ext.data.JsonStore({
            url:'user/getUsers',
            fields:['id','name','addr','email','remark'],
            root:'users',
            id:'id',
            totalProperty:'total'
        });
        ds.load({
            params:{start:0,limit:20}
        });
        Ext.apply(this, {
            title:'用户资料管理',
            store:ds,
            columns:[
                {header:'名称',dataIndex:'name',width:100},
                {header:'地址',dataIndex:'addr',width:200},
                {header:'邮箱',dataIndex:'email',width:200},
                {header:'备注',dataIndex:'remark',width:300}
            ],
            bbar:new Ext.PagingToolbar({
                store:ds,
                pageSize:20
            })
        });
        UserGrid.superclass.initComponent.call(this);
    }
});

我们看到,分页时的JsonStore比不分页时增加了三个属性:root, id, totalProperty。

现在来看看后台的控制器代码:

public class UserController extends ApplicationController{
    public JsonView getUsers(int start, int limit) throws Exception{
        long total = User.count(User.class, null, null);
        List<User> users = User.findAll(User.class, null, null, "id", limit, start);

        Map<String,Object> result = new HashMap<String,Object>();
        result.put("total", total);
        result.put("users", users);
        return new JsonView(result);
    }
}

我们看到,Action方法接受二个参数:start和limit,由ActiveRecord完成分页查询操作,然后返回JsonView,由etmvc将处理好的JSON串输出至客户端。

分页操作实际上并不复杂。

struts2有拦截器,非常强大,spring mvc也有拦截器机制,但没有环绕的拦截处理机制。

etmvc也有过滤器,etmvc的过滤器类似于ROR,分为前置过滤器,后置过滤器和环绕过滤器。过滤器能够处理所有像安全、日志等的横切关注点。

我们先来看看应用得最多的前置过滤器,前置过滤器是控制器类中的一个方法,方法要求返回一个boolean类型,如果返回false,将停止后续的所有操作,否则继续后面的操作。来举个用户登录认证的例子,要求用户必须通过登录才能访问系统,使用前置过滤器是非常合适的,比如jpetstore中访问订单时要求用户必须登录后才能访问,否则重定向到登录页面让用户登录:

@BeforeFilter(execute="auth")
public class OrderController extends ApplicationController{
    protected boolean auth() throws Exception{
        if (session.getAttribute("sessionAccount") == null){
            String url = request.getServletPath();
            String query = request.getQueryString();
            if (query != null){
                url += "?" + query;
            }
            redirect("account/signon?signonForwardAction=" + url);
            return false;
        }
        return true;
    }
}

我们通过在控制器上使用@BeforeFilter注解增加了一个前置过滤器,过滤器将执行auth方法,检查用户是否登录,是否能够合法访问订单。

如果想对所有的控制器加上过滤器,则无须在每个控制器类上加上注解,只须在控制器基类ApplicationController中加注解就好。也就是说,过滤器具有继承特性。

过滤器可以有only或except选项,以确定对那些Action方法有效,比如:

@BeforeFilters({
    @BeforeFilter(execute="checkAdminPrivilege",except={"showImage","download","addComment"}),
    @BeforeFilter(execute="checkUserPrivilege",only={"addComment"})
})
public class ArticleController extends ApplicationController{
    /**
     * 显示文章附件图片
     */
    public View showImage(int id) throws Exception{
    }

    /**
     * 下载文章附件
     */
    public View download(int id) throws Exception{
    }

    /**
     * 增加评论
     */
    public void addComment(int articleId, String content) throws Exception{
    }
    
    public void create(){
    }

    public void edit(int id) throws Exception{
    }

    public void save(String title, String content, MultipartFile file, String source, Integer flag) throws Exception{
    }

    public void update(int id, String title, String content, MultipartFile file, String source, Integer flag) throws Exception{
    }

    public void destroy(int id) throws Exception{
    }

    public void destroyAttach(int id) throws Exception{
    }
}

在文章管理的控制器中加入了二个前置过滤器,以检查管理员和普通用户的操作权限:

  • 管理员权限检查除"showImage","download","addComment"方法外将全部检查
  • 普通用户权限检查只对"addComment"方法进行检查 可以对控制器加上多个过滤器,形成一个过滤器链。

前面介绍的是前置过滤器,这是用得最多的一种,后置过滤器用法相同。

关于环绕过滤器我们将分专门章节进行介绍。

etmvc中ActiveRecord(下称AR)在使用上可以独立使用,其数据库的连接信息通过activerecord.properties进行配置。 AR提供一个简单的连接池实现,如果需要使用更高效的连接池,则可以利用spring来进行配置。AR集成spring分二步进行:

1、配置spring的连接池

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost/mydb"/>
    <property name="username" value="root"/>
    <property name="password" value="soft123456"/>
</bean>

2、配置AR的连接工厂

<bean id="ds1" class="com.et.ar.ConnectionFactoryBean">
    <property name="domainBaseClass" value="com.et.ar.ActiveRecordBase"/>
    <property name="adapterClass" value="com.et.ar.adapters.MySqlAdapter"/>
    <property name="dataSource" ref="dataSource"/>
</bean>

这样就完成了切换数据源的操作,下面再给出一个使用多数据库的配置实例

<bean id="dataSource1"
   class="org.springframework.jdbc.datasource.DriverManagerDataSource"
   p:driverClassName="com.microsoft.sqlserver.jdbc.SQLServerDriver"
   p:url="jdbc:sqlserver://192.168.10.21:1433;databaseName=smqxt;user=data1;password=data1qaz"
   p:username="data1"
   p:password="data1qaz" />
<bean id="dataSource2"
   class="org.springframework.jdbc.datasource.DriverManagerDataSource"
   p:driverClassName="com.mysql.jdbc.Driver"
   p:url="jdbc:mysql://localhost/mydb"
   p:username="root"
   p:password="soft123456" />
   
<bean id="connection1" 
   class="com.et.ar.ConnectionFactoryBean"
   p:domainBaseClass="com.et.ar.ActiveRecordBase"
   p:dataSource-ref="dataSource1" />
<bean id="connection2" 
   class="com.et.ar.ConnectionFactoryBean"
   p:domainBaseClass="javaapplication1.User"
   p:dataSource-ref="dataSource2" />

ActiveRecord中模型对象调用的每个方法如save, destroy, create, update等都是原子的,都受事务保护。

如果需要保证多个数据操作在一个事务中,则需要使用事务控制,如下所示:

try {
	ActiveRecordBase.beginTransaction();
	//do something
	ActiveRecordBase.commit();
	
} catch (Exception ex) {
	ActiveRecordBase.rollback();
}

如上所示,ActiveRecord中的事务是通过beginTransaction, commit, rollback等方法进行控制的。

etmvc中支持前置过滤器,后置过滤器和环绕过滤器,前面介绍过前置过滤器了,请参阅《etmvc的过滤器基础》。

环绕过滤器是功能最强的一类过滤器,允许拦截action方法的执行前和执行后,这实际上就是一种AOP。所以通过环绕过滤器,我们可以在action方法执行前和执行后处理一些逻辑,甚至中断action的执行,可以用它做日志处理、异常处理等。

etmvc中创建一个环绕过滤器同前置过滤器有些不同,前置过滤器只要简单在控制器中编写一个方法就行了,环绕过滤器必须是单独的一个类,这个类要求实现AroundHandler接口,或者继承AbstractAroundHandler。我们先来看个简单的环绕过滤器例子:

public class TestAroundFilter implements AroundHandler{
	
	@Override
	public boolean before(Controller controller) throws Exception {
		System.out.println("begin invoke:" + controller.getActionName());
		return true;
	}

	@Override
	public boolean after(Controller controller) throws Exception {
		System.out.println("after invoke:" + controller.getActionName());
		return true;
	}

}

其中before和after分别是在action方法执行之前和之后执行,如果返回true则继续后续代码执行,如果返回false则中断后续代码执行,所以如果before返回false将中止action方法的执行。

如此,利用环绕过滤器,我们完全能够控制action方法之前和之后的逻辑,只要在before和after中编写处理逻辑的代码就行了。如若要记录日志,只要在before中记录action开始执行的时间,在after中记录action执行完成的时间,就能够清楚执行那个action,什么时间开始执行,什么时间结束执行,执行了多长时间等。

好了,我们来看看这个环绕过滤器怎样安装到控制器上,看下面的示例:

@AroundFilter(execute=TestAroundFilter.class)
public class HelloController extends ApplicationController{
	public String say() throws Exception{
		return "hello,world";
	}
}

用@AroundFilter注解就能将环绕过滤器安到控制器上,注意到这里的execute是类,而前置过滤器和后置过滤器是方法名称。如果需要安多个环绕过滤器,用@AroundFilters就好了。

在上面例子中,我们执行http://localhost:8080/xxx/hello/say时,将在TOMCAT控制台输出:

begin invoke:say

after invoke:say

etmvc中的ActiveRecord将数据表中的字段映射成模型类的字段,相应的将数据表中的字段类型映射成模型类的字段类型。

在多数情况下,ActiveRecord能够自动处理从JDBC类型到Java Object类型的映射,此种映射如下表所示:

JDBC 类型 Java Object 类型
CHAR String
VARCHAR String
LONGVARCHAR String
NUMERIC java.math.BigDecimal
DECIMAL java.math.BigDecimal
BIT Boolean
TINYINT Integer
SMALLINT Integer
INTEGER Integer
BIGINT Long
REAL Float
FLOAT Double
DOUBLE Double
BINARY byte.md
VARBINARY byte.md
LONGVARBINARY byte.md
DATE java.sql.Date
TIME java.sql.Time
TIMESTAMP java.sql.Timestamp

在类型的映射上可能存在一些需要转换的,比如在MSSQL数据库中定义了一个datetime的字段类型,而在模型类中定义了java.sql.Date类型。按照上表中的映射关系,将无法直接从datetime映射成java.sql.Date,所以需要作些转换。

我们需要为上面的这种转换编写一个转换器,转换器必须实现com.et.ar.Converter接口:

public class DateConverter implements Converter {
	@Override
	public Object convert(Object value) {
		if (value == null) {
			return null;
		}
		String s = value.toString().substring(0, 10);	//取yyyy-mm-dd
		return java.sql.Date.valueOf(s);
	}
}

这个转换器将对象value转换成java.sql.Date类型的对象,在上面的转换实现中,我们仅是简单地取前十个字符串,然后调用valueOf进行转换。

最后将这个转换器进行注册登记:

static{
	ConvertUtil.register(new DateConverter(), java.sql.Date.class);
}

好了,ActiveRecord在处理到需要映射成java.sql.Date类型的字段时会调用我们自定义的转换器进行处理。

etmvc中ActiveRecord模型对象拥有很多的操作方法,其中有一类称为回调方法,在ActiveRecord模型对象的生命周期内,回调给予你更多的、更灵活控制能力。回调方法就象一个钩子,它允许在模型对象操作数据的前后执行一段逻辑,这实际就是ActiveRecord模型对象的AOP编程。

ActiveRecord模型对象支持的回调方法有:

回调方法 执行时机
beforeCreate 对象创建前执行
afterCreate 对象创建后执行
beforeUpdate 对象更新前执行
afterUpdate 对象更新后执行
beforeSave 对象保存前执行
afterSave 对象保存后执行
beforeDestroy 对象删除前执行
afterDestroy 对象删除后执行

回调方法签名:public void callbackMethodName() throws ActiveRecordException

我们举个回调方法应用的典型例子,在用户资料管理中,用户的信息除基本的信息外,还包括照片,而照片以文件的形式被保存在某个地方。为保证数据的完整性,在用户资料删除时必须同时删除其关联的照片文件。

我们来看一下用户的模型类定义:

@Table(name="users")
public class User extends ActiveRecordBase{
	@Id private Integer id;
	@Column private String name;
	@Column private String phone;
	@Column private String filename;
	
	public void afterDestroy() throws ActiveRecordException{
		String path = getImagePath();
		File file = new File(path);
		if (file.exists() && file.isFile()){
			file.delete();
		}
	}
	
	public String getImagePath(){
		return "d:/temp/" + id + "_" + filename;	//获取照片文件存放路径
	}
	
	//get,set...
}

我们重载了afterDestroy,告诉ActiveRecord框架,在记录删除后将相关的照片文件删除。现在来看调用代码:

User user = User.find(User.class, 1);
user.destroy();

我们无须在调用时编写删除照片文件的代码,仅仅将User对象删除就好,User对象会做相应的回调,执行相关的逻辑。

另外,在ActiveRecord模型对象执行回调方法时是有事务保证的,所以一旦照片文件删除失败,整个对象将执行回滚操作。如此,对于模型对象的调用者来说,这将变得更清晰。

etmvc框架拥有一套插件体系结构,如果需要扩展某些功能,就可以通过插件来完成。etmvc框架在初始化时会扫描安装进来的插件,如果发现有插件就进行加载。

举个例子,在WEB应用程序中如若需要一个后台服务进程处理一些事情,则使用插件提供的机制可能很合适,又如,在需要整个WEB应用程序加载之前执行一些代码时,也可以利用插件来完成,因为插件的启动执行是在etmvc框架初始化时完成的。

编写插件分二个步骤:

1、编写插件实现代码,要求实现com.et.mvc.PlugIn接口,如:

public class RouteLoader implements PlugIn{

	@Override
	public void destroy() {
	}

	@Override
	public void init(PlugInContext ctx) {
		Route route = new Route("blog/$year/$month/$day", DefaultRouteHandler.class);
		route.setController("blog");
		route.setAction("show");
		RouteTable.addRoute(0, route);
	}

}

其中init方法做一些初始化的工作,destroy作一些销毁的工作。

2、注册插件,在web.xml中增加一个plugin的参数:

<filter>
	<filter-name>etmvc</filter-name>
	<filter-class>com.et.mvc.DispatcherFilter</filter-class>
	<init-param>
		<param-name>controllerBasePackage</param-name>
		<param-value>controllers</param-value>
	</init-param>
	<init-param>
		<param-name>viewBasePath</param-name>
		<param-value>/views</param-value>
	</init-param>
	<init-param>
		<param-name>plugin</param-name>
		<param-value>utils.RouteLoader</param-value>
	</init-param>
</filter>
<filter-mapping>
	<filter-name>etmvc</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

plugin参数值为插件实现类的名称,如果有多个插件,则以“,”分开。

3、重启WEB容器,这样就能正确加载插件了。

etmvc框架使用路由技术实现把URL映射到控制器类中的action方法上,典型的http://localhost:8080/xxx/user/show将映射到UserController类中的show方法上,实际上这个规则是允许改变的。etmvc框架将允许你自定义自已的匹配规则来映射你的控制器类及其行为,这就需要定义路由。

一个路由的定义由一些占位符组成,占位符由美元符后面跟着字母组成,如“$controller/$action/$id”,这是框架采用的默认路由。根据这个路由,下面的这些例子将被匹配:

URL CONTROLLER ACTION ID
/user UserController index
/user/show UserController show
/blog/show/123 BlogController show 123

如果没匹配到$action,则将默认使用index方法。

定义一个新的路由时,必须实例化Route,如下面的这个例子:

Route route = new Route("blog/$year/$month/$day", DefaultRouteHandler.class);
route.setController("blog");
route.setAction("show");
RouteTable.addRoute(0, route);

其中我们定义了嵌入式变量$year,$month,$day,这个路由规划将能够映射到BlogController类中的方法:

public String show(int year, int month, int day) {
	return year + "-" + month + "-" + day;
}

嵌入式变量将自动映射成方法的参数。

可以定义多个路由规则,匹配是顺序进行的,也将是在路由表中从第一个规则开始进行匹配,找到就按照这个路由查找控制器类和方法。

在上面的例子中,这些URL将会有如下的映射:

URL CONTROLLER ACTION
/blog/2009/07/10 BlogController show
/user/list UserController list
/product/show ProductController show

利用路由技术可以提供非常优雅的URL,一看URL就知道是那个控制器类和方法在处理。

最后有一点需要注意的是:定义一个路由后必须将它加入路由表中,并且确保在应用程序启动时是可用的。

etmvc框架中可以使用环绕过滤来处理异常,在WEB应用程序中如果需要处理全局的异常,比如我们可能需要拦截全局的异常然后集中处理,这时可以使用环绕过滤器。

下面给出一个参考的处理方法:

定义异常过滤器:

public class ExceptionFilter implements AroundHandler{

	@Override
	public boolean after(Controller controller) throws Exception {
		Exception ex = controller.getException();
		if (ex != null){
			controller.getSession().setAttribute("error", ex);
			controller.getResponse().sendRedirect("/myweb/application/error");
			return false;
		}
		return true;
	}

	@Override
	public boolean before(Controller controller) throws Exception {
		return true;
	}

}

异常过滤器中检测到有异常发生,则重定向到全局的错误页面,为了方便,我们将错误页面显示的方法写在了ApplicationController类中,当然也可以专门写个处理异常的控制器:

@AroundFilter(execute=ExceptionFilter.class)
public class ApplicationController extends Controller{
	public void error() throws Exception{
		Exception ex = (Exception)session.getAttribute("error");
		session.setAttribute("error", null);
		ex.printStackTrace();
		request.setAttribute("error", ex);
	}
}

最后需要一个页面(/views/applicaion/error.jsp)来显示异常信息:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<p>error message</p>
	<p>${error }</p>
</body>
</html>

在下面的myweb例子中执行http://localhost:8080/myweb/test/test1时成功执行,显示一个字符串,执行http://localhost:8080/myweb/test/test2时出现异常,重定向至错误页面。

etmvc框架在处理国际化上分两个部分,第一是在视图处理上,第二是在ActiveRecord(下简称AR)的数据验证信息处理上。

对于第一部分,可以使用目前成熟的技术,如JSTL等来处理I18N问题,所以在etmvc中并不对此重复制作轮子,etmvc中对国际化的处理主要集中在AR的数据验证信息处理上。AR的数据验证是通过验证注解应用在数据模型的字段上,而验证信息是通过注解中的message属性说明的。来看一个例子:

@Table(name="users")
public class User extends ActiveRecordBase{
	@Id private Integer id;
	@Column 
	@Length(min=0,max=10,message="数据长度非法,最多不超过10。")
	private String name;
	@Column private String pass;
	@Column private String addr;
	@Column private String email;
}

在对用户名称进行验证时,采用了@Length注解,我们设定最大长度为10,所以一旦超过这个长度,将获得由message指定的错误信息。

现在来测试一下:

User user = new User();
user.setName("1234567890123456");
user.validate();
for(String err : user.getErrors()) {
	System.out.println(err);
}

运行时将获得“数据长度非法,最多不超过10。”的错误信息。

这种错误信息的设置方法采用了硬编码的方式,无法解决国际化问题。为了解决国际化问题,我们需要做以下的改造:

准备资源文件

解决I18N问题就需要制作资源文件,对于上面的例子,我们制作了res.properties, res_zh_CN.properties文件。

其中res.properties的内容如下:

user.name.length=user name length is invalid,the max length is {max}

在资源文件中可以通过{max},{min}等引用注解中的属性值。

设置数据模型中的message

数据模型中的验证信息message不能再用硬编码了,应该用资源文件中的key代替,前面的User类修改为:

@Table(name="users")
public class User extends ActiveRecordBase{
	@Id private Integer id;
	@Column 
	@Length(min=0,max=10,message="user.name.length")
	private String name;
	@Column private String pass;
	@Column private String addr;
	@Column private String email;
}

注意比较一下message的不同。

在AR中设置验证资源

调用ActiveRecordBase中的setValidatorResource方法来指定验证信息的获取是从那个资源文件中来的,如:

ResourceBundle resource = ResourceBundle.getBundle("pp.res");
ActiveRecordBase.setValidatorResource(resource);

User user = new User();
user.setName("1234567890123456");
user.validate();
for(String err : user.getErrors()) {
	System.out.println(err);
}

运行后,将显示错误信息“用户名称长度不对,最大不超过10.“。

如此,我们将拥有国际化的能力了。

使用etmvc时必须在web.xml中配置一个Filter,其filter-class是com.et.mvc.DispatcherFilter。如果想集成spring,则必须改成com.et.mvc.SpringDispatcherFilter,看一下集成spring的web.xml配置范例:

<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
	<filter-name>etmvc</filter-name>
	<filter-class>com.et.mvc.SpringDispatcherFilter</filter-class>
	<init-param>
		<param-name>controllerBasePackage</param-name>
		<param-value>controllers</param-value>
	</init-param>
	<init-param>
		<param-name>viewBasePath</param-name>
		<param-value>/views</param-value>
	</init-param>
</filter>
<filter-mapping>
	<filter-name>etmvc</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

下面,我们以用户管理为例子来说明需注意的步骤,首先,编写UserService类:

public class UserService {
	public String say(){
		return "say from service";
	}
	
	public List<User> getUsers() throws Exception{
		return User.findAll(User.class);
	}
}

我们提供二个方法,一个简单返回一个字符串,一个通过ActiveRecord返回一个用户资料集合。

再来看看控制器中的写法:

public class UserController extends ApplicationController{
	private UserService userService;
	
	public String say(){
		return userService.say();
	}
	
	public void show() throws Exception{
		List<User> users = userService.getUsers();
		request.setAttribute("users", users);
	}

	public UserService getUserService() {
		return userService;
	}

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

控制器类UserController提供二个action方法,say简单向浏览器输出一个问候信息,show返回一个用户资料页面:

<table border="1">
	<thead>
		<tr>
			<th>用户名称</th>
			<th>地址</th>
		</tr>
	</thead>
	<tbody>
		<c:forEach items="${users }" var="user">
			<tr>
				<td>${user.name }</td>
				<td>${user.address }</td>
			</tr>
		</c:forEach>
	</tbody>
</table>

控制器中userService由spring注入,来看看spring配置:

<bean class="controllers.UserController" p:userService-ref="userService" scope="prototype" />
<bean id="userService" class="services.UserService" />

至此,程序编写完毕,可以部署运行,试一试执行http://localhost:8080/myweb/user/say和http://localhost:8080/myweb/user/show,将看到问候信息和用户资料列表。

⚠️ **GitHub.com Fallback** ⚠️