[Java] Appfuse 最佳实践

前段时间刚写了《Catalyst Tutorial 最佳实践》,现在又手痒,给大家奉献这篇《Appfuse 最佳实践》,目的主要是趁这段相对比较空闲的时间,多写一些有用的教程,一方面在网上也看到过很多关于 Appfuse 的教程,但是总觉得写的不够系统,看起来不够过瘾~所以这次石头特意通过一个完整的“员工管理系统”的实例来比较系统的介绍一下这个框架的开发技巧,希望大家喜欢~

[Appfuse Best Tutorial]

首先,按照《Appfuse & tapestry 小记》中的第3节(开发笔记)中建立好`Employee`表并用appfuse工具把代码生成好了。
附带DDL:
CREATE TABLE `Employee` (
  `id` bigint(20) NOT NULL auto_increment,
  `code` varchar(10) NOT NULL,
  `dept` varchar(50) NOT NULL,
  `name` varchar(20) NOT NULL,
  `status` varchar(10) NOT NULL,
  `telephone` varchar(20) default NULL,
  `title` varchar(50) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
然后启动jetty,就可以看到首页的“登录”菜单旁边多出来一个“Employee List”的菜单项。
接下来我们做一些界面上的修改(在ApplicationResources_zh.properties添加):
... ...
# -- add by james --
webapp.name=员工管理系统
webapp.tagline=我们以一个员工管理系统来作为开发Appfuse的入门实例.
company.name=员工管理系统
company.url=http://localhost:8080
... ...
# -- Employee-START copied from ApplicationResources.properties
employee.id=Id
employee.code=Code
employee.dept=部门
employee.name=姓名
employee.status=目前状态
employee.telephone=电话
employee.title=职位

employee.added=新员工添加成功。
employee.updated=员工信息更新成功。
employee.deleted=员工信息删除成功。

# -- employee list page --
employeeList.title=员工管理
employeeList.heading=员工列表
employeeList.employee=员工
employeeList.employees=员工

# -- employee detail page --
employeeDetail.title=员工详细信息
employeeDetail.heading=员工详细信息
# -- Employee-END
... ...
然后为菜单赋权,即在menu-config.xml的EmployeeMenu加上roles="ROLE_ADMIN,ROLE_USER",允许用户添加/修改,重启后你就可以看到菜单和界面变成中文了,登录之后你可以试着在“员工管理”板块下做一些简单的CRUD操作。
以下是appfuse:gen所产生/改动的代码,请参考:
resources/struts.xml
resources/ApplicationResources.properties
webapp/WEB-INF/applicationContext.xml
webapp/WEB-INF/menu-config.xml
webapp/common/menu.jsp
webapp/pages/employeeList.jsp
webapp/pages/employeeForm.jsp
java/com/appfuse/app/model/Employee.java
java/com/appfuse/app/model/Employee-validation.xml
java/com/appfuse/app/dao/EmployeeDao.java
java/com/appfuse/app/dao/hibernate/EmployeeDaoHibernate.java
java/com/appfuse/app/service/EmployeeManager.java
java/com/appfuse/app/service/impl/EmployeeManagerImpl.java
java/com/appfuse/app/webapp/action/EmployeeAction.java
java/com/appfuse/app/webapp/action/EmployeeAction-validation.java
这里遇到两个问题需要注意:
a> 输入中文的时候保存数据不正常。
解决方法:这种问题一般都是数据库字段字符集问题,我建议你先修改mysql的配置文件my.ini的default-character-set=utf8,然后再重新建表,检查一下新表的字段字符集是否都为utf8_general_ci,如果是的话这个问题应该就能迎刃而解。
b> 重启jetty的时候修改过的数据会被覆盖回去。
解决方法:重新设置pom.xml里面关于hibernate3-maven-plugin的配置,删除executions命令(把pom.xml的154行到161行注释掉)。同时把969行的<dbunit.operation.type>CLEAN_INSERT</dbunit.operation.type>改成<dbunit.operation.type>NONE</dbunit.operation.type>。
接下来我们就可以开始做一些更深入的设计和编码。

>>> 前期系统设计
光是一张Employee表当然没有办法架设出一个比较完整的公司员工结构,于是我又添加了`Status`,`Dept`和`Title`分别用于存储员工状态、部门信息和职位头衔,DDL如下:
CREATE TABLE `Status` (
  `id` int(11) NOT NULL auto_increment,
  `status` varchar(10) NOT NULL,
  `description` text NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `Status` SET `status`='在职', `description`='工作中';
INSERT INTO `Status` SET `status`='入职', `description`='等待入职';
INSERT INTO `Status` SET `status`='离职', `description`='离开公司';
==========
CREATE TABLE `Dept` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(50) NOT NULL,
  `description` text NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `Dept` SET `name`='董事', `description`='决策';
INSERT INTO `Dept` SET `name`='人事', `description`='招聘';
INSERT INTO `Dept` SET `name`='财务', `description`='算账';
INSERT INTO `Dept` SET `name`='开发', `description`='产品';
INSERT INTO `Dept` SET `name`='市场', `description`='宣传';
==========
CREATE TABLE `Title` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(50) NOT NULL,
  `description` text NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `Title` SET `name`='经理', `description`='决策管理';
INSERT INTO `Title` SET `name`='主管', `description`='管理员工';
INSERT INTO `Title` SET `name`='员工', `description`='日常工作';
然后生成model并为`Dept`建立基本代码结构(关于appfuse命令,参考http://static.appfuse.org/plugins/appfuse-maven-plugin/plugin-info.html):
#mvn appfuse:gen-model
#mvn appfuse:gen -Dentity=Dept
#mvn appfuse:gen -Dentity=Title
#mvn appfuse:gen-core -Dentity=Status
这里我们生成了“Employee List”和“Title List”两个完整模块以及Status的Dao和Manager核心类。这里值得注意的是,由于我们只需要Status的数据结构,不需要Action和页面代码,所以这里我们使用“mvn appfuse:gen-core -Dentity=Status”命令指定只生成“核心代码”,执行过程中有报错,不过没关系,代码还是正确生成的。
然后我们要做的就是和上面提到的“Employee List”类似的设置(但是DeptMenu最好加上roles="ROLE_ADMIN"只允许admin用户添加/修改,这样比较不容易出问题),重启服务,看到界面已经变了,多出了一个菜单“部门管理”,你会注意到这个菜单跑到第二行去了,不是很美观,于是我首先考虑能不能把一些没用的菜单去掉~ 按照主流的设计风格,我们很自然的会想要把“退出”这个菜单移到右上方的位置。于是打开menu-config.xml,删除“<Menu name="Logout" title="user.logout" page="/logout.jsp" roles="ROLE_ADMIN,ROLE_USER"/>”这行,以及menu.jsp的“<menu:displayMenu name="Logout"/>”这行,然后在header.jsp相应位置加上如下代码:
... ...
    <security:authorize ifAnyGranted="ROLE_ADMIN,ROLE_USER">
    &nbsp;<a href="<c:url value='/logout.jsp'/>"><fmt:message key="user.logout"/></a>
    </security:authorize>
... ...
由于“退出”选项只对登录用户才有意义,所以我这里使用了SpringSecurity的authorize标签来限定用户。这里顺便提一下这个比较常用的SpringSecurity标签的用法(参考http://static.springframework.org/spring-security/site/reference/html/authorization-common.html):
*ifAllGranted: 满足所有角色。
*ifAnyGranted: 满足任意一个角色。
*ifNotGranted: 所有角色都不被允许。
到这里该系统最主要的系统前期设计工作已经完成,基本代码也生成好了,接下来我们从一些细节地方进行讨论。

>>> 基本功能设计
代码Appfuse已经帮我们生成了,真是省去了我们不少“造轮子”的时间,但是仔细看看,还是有一些不合理的地方,我们到“员工管理”打开“添加”页面,我们看到“部门”这里还是一个输入框,这显然不合理,接下来我们要把这里变成一个下拉菜单并关联刚才添加的“部门管理”的数据。
applicationContext-struts.xml:
... ...
    <bean id="employeeAction" class="com.appfuse.app.webapp.action.EmployeeAction" scope="prototype">
        <property name="employeeManager" ref="employeeManager"/>
        <property name="deptManager" ref="deptManager"/>
    </bean>
... ...
struts.xml
... ...
        <!--EmployeeAction-START-->
        <action name="employees" class="employeeAction" method="list">
            <result>/WEB-INF/pages/employeeList.jsp</result>
        </action>

        <action name="editEmployee" class="employeeAction" method="edit">
            <result>/WEB-INF/pages/employeeForm.jsp</result>
            <result name="error">/WEB-INF/pages/employeeList.jsp</result>
        </action>

        <action name="saveEmployee" class="employeeAction" method="save">
            <result name="input">/WEB-INF/pages/employeeForm.jsp</result>
            <result name="cancel" type="redirect-action">employees</result>
            <result name="delete" type="redirect-action">employees</result>
            <result name="success" type="redirect-action">employees</result>
        </action>
        <!--EmployeeAction-END-->
... ...
EmployeeAction.java:
... ... /** * Add for selecting department */ private DeptManager deptManager; private List<Dept> deptList; public void setDeptManager(DeptManager deptManager) { this.deptManager = deptManager; } public List<Dept> getDeptList() { return deptList; } ... ... public String edit() { if (id != null) { employee = employeeManager.get(id); } else { employee = new Employee(); } // get department list deptList = deptManager.getAll(); return SUCCESS; } ... ... public String save() throws Exception { ... ... if (!isNew) { // get department list deptList = deptManager.getAll(); // stay input page return INPUT; } else { return SUCCESS; } } ... ...
employeeForm.jsp:
<<< remove 1 line
    <s:textfield key="employee.dept" required="true" maxlength="50" cssClass="text medium"/>
>>> change to
    <s:select key="employee.dept" headerKey="" headerValue="Select Dept"
        list="deptList"
        listKey="id"
        listValue="name"
        value="employee.dept"
        required="true"
    />
<<< 7 lines
做完这些修改后会发现,员工编辑页面“部门”这一栏已经不再是随便输入的文本框,而是下拉菜单了,这样子不仅从系统操作安全性方面提高了很多,而且也使整个系统的各个数据结构可以更好的结合起来。接下来,我们用同样的方法加入“职位管理”这个模块,同样的代码修改过后,于是员工编辑页面的“职位”这个选项也变成关联的下拉菜单了。
我们保存一下,功能正常,但是返回“员工列表”的时候发现了一个不好的事情,那就是编辑过的“部门”和“职位”栏都变成数字了,这是怎么回事呢,很明显我们刚才使用的方法是有问题的~ 我们只是从界面的角度把`Dept`和`Title`这两张表的内容结合到“员工管理”去,但是实际上从数据层面这几张表并没有真正的“关联”起来~ 于是我们对`Employee`表作如下调整:
CREATE TABLE `employee` (
  `id` bigint(20) NOT NULL auto_increment,
  `code` varchar(10) NOT NULL,
  `dept_id` int(11) NOT NULL,
  `name` varchar(20) NOT NULL,
  `status` varchar(10) NOT NULL,
  `telephone` varchar(20) default NULL,
  `title_id` int(11) NOT NULL,
  PRIMARY KEY  (`id`)                                                                
) ENGINE=InnoDB DEFAULT CHARSET=utf8
我们看到原varchar型的dept和title字段分别变成了int类型的dept_id和title_id(建议这里命名遵循一般的Hibernate的设计原则),准备作为外键关联`Dept`和`Title`表(如果是大范围的字段修改我们也可以使用mvn appfuse:gen-model来重新生成model类,但是像这种小范围的修改,我还是建议大家手动来修改一下对应的model类)。
然后我们修改com.appfuse.app.model.Employee类:
... ...
<<< remove 2 lines
    private String dept;
    private String title;
>>> change to
    private Dept dept;
    private Title Title;
<<< 2 lines
... ...
    @ManyToOne
    @JoinColumn(name = "title_id")
    public Title getTitle() {
        return this.title;
    }
   
    public void setTitle(Title title) {
        this.title = title;
    }

    @ManyToOne
    @JoinColumn(name = "dept_id")
    public Dept getDept() {
        return this.dept;
    }
   
    public void setDept(Dept dept) {
        this.dept = dept;
    }
... ...
可以看到我们为Employee的新model加入了@ManyToOne映射(注意appfuse新版本使用的是JPA的设置规范,个人认为这比写映射文件更简洁和直观),接着我们修改employeeList.jsp:
<<< 2 line2
    <display:column property="dept" sortable="true" titleKey="employee.dept"/>
    <display:column property="title" sortable="true" titleKey="employee.title"/>
>>> change to
    <display:column property="dept.name" sortable="true" titleKey="employee.dept"/>
    <display:column property="title.name" sortable="true" titleKey="employee.title"/>
<<< 2 lines
... ...
然后我们重启一下服务,重新进入“员工列表”页面,我们发现原先的数字不见了,取代的是关联的职位名称,酷~ 到这里大家应该也可以体会我开始的时候为什么说appfuse是“第一次让我感觉到‘轻量’的J2EE框架”了吧,代码改动可以说是“前所未有”的小了~
* 这里大家可能会遇到以下问题:org.hibernate.ObjectNotFoundException: No row with the given identifier exists
有两张表,table1和table2. 产生此问题的原因就是table1里做了关联<one-to-one>或者<many-to-one unique="true">(特殊的多对一映射,实际就是一对一)来关联table2.当hibernate查找的时候,table2里的数据没有与table1相匹配的,这样就会报这个错(简单来说就是数据的问题)。
别忘了还有employeeForm.jsp:
... ...
    <s:select name="employee.dept.id" key="employee.dept" headerKey="" headerValue="Select Dept" cssStyle="width:120px"
        list="deptList"
        listKey="id"
        listValue="name"
        value="employee.dept.id"
        required="true"
    />
    <s:select name="employee.title.id" key="employee.title" headerKey="" headerValue="Select Title" cssStyle="width:120px"
        list="titleList"
        listKey="id"
        listValue="name"
        value="employee.title.id"
        required="true"
    />
    <s:radio key="employee.status"
        list="statusList"
        listKey="status"
        listValue="status"
        value="employee.status"
        required="true"
    />
... ...
这里看到"employee.dept"和"employee.title"的select控件的默认值我们已经设成"employee.dept.id"和"employee.title.id",这样才可以在编辑页面载入正确的默认值,至于"employee.status"我们做的修改是把listKey="id"变成listKey="status"直接记录status的值到`Employee`表的status字段中去,并没有关联`Status`表,其实对于一些比较固定的小配置表我们完全可以选择这种方案(减少表关联操作),适合的才是最好的嘛,呵呵~
* 另外,大家如果要查看Hibernate生成的sql语句,可以在log2j.xml里面打开如下注释即可:
... ...
    <!--logger name="org.hibernate.SQL">
        <level value="DEBUG"/>
    </logger-->
... ...

>>> 高级功能设计
到这里我们的“员工管理系统”的功能已经基本完整了,但是如果要变成一个真正的企业管理工具,还有很多的工作要做,由于篇幅限制,我们在这个部分只介绍一下分页功能的实现吧,其他更高级的用法就留给大家自由发挥了:)
实际上由于Appfuse使用displayTag作为表格展示的工具,所以也使我们省去了不少界面和编码方面的工作,所以我们接下来就来介绍一下displayTag的常用功能,然后分析一下优缺点,最后我们就大数量分页进行一下研究。
1> 常用功能
分页功能:如果想对代码分页,只需在display:table标签中添加一项pagesize="每页显示行数",为了测试我们在employeeList.jsp里面把分页数设小一点<display:table name="employees" class="table" requestURI="" id="employeeList" export="true" pagesize="5">,这样应该就可以看到分页的links,至于显示样式,我们可以在styles/displaytag.css修改。
排序功能:可以为table设置默认排序列(defaultsort),以及排序方式(defaultorder:"ascending"or"descending"),以及排序范围(sort:"page"or"list"),另外,需要排序的列只要为该column加上sortable="true"就好了。
导出数据:默认有CSV,Excel,XML,PDF这几种输出方式,appfuse默认就设置了可以看看代码。
其他功能:displayTag还有很多功能,例如计算总数等,可以参考:http://displaytag.sourceforge.net/1.2/displaytag/tagreference.html
2> 优缺点分析
优点:很明显用displayTag帮我们节省了很多重复“造轮子”的时间,而且看起来功能也算强大。
缺点:分页样式不够灵活(links),似乎只能打出所有页数。另外,如果你打出取数据的sql语句就可以发现,displayTag默认是一次把所有的数据取出来,然后再排序的,如果数据量比较大的时候会有性能问题。
3> 大数据分页
上面分析到了在默认情况下,displayTag的分页是很低效的,因此我们必须像一个方法来,关于这点displayTag推荐两种解决方案,一种是使用partialList="true"和size="resultSize"这两个标签来解决,但实际上这个方案是从内存分页的基础上改过来的,打出来的sql实际上没有变化,所以我们使用第二种方式,那就是实现PaginatedList接口。
PageList.java:
... ...
import java.util.List;
import org.displaytag.pagination.PaginatedList;
import org.displaytag.properties.SortOrderEnum;

/**
 * 实现分页列表
 */
public class PageList implements PaginatedList {

    /**
     * 每页的列表
     */
    private List list;

    /**
     * 当前页码
     */
    private int pageNumber = 1;

    /**
     * 每页记录数 page size
     */
    private int objectsPerPage = 15;

    /**
     * 总记录数
     */
    private int fullListSize = 0;

    private String sortCriterion;

    private SortOrderEnum sortDirection;

    private String searchId;

    public List getList() {
        return list;
    }

    public void setList(List list) {
        this.list = list;
    }

    public int getPageNumber() {
        return pageNumber;
    }

    public void setPageNumber(int pageNumber) {
        this.pageNumber = pageNumber;
    }

    public int getObjectsPerPage() {
        return objectsPerPage;
    }

    public void setObjectsPerPage(int objectsPerPage) {
        this.objectsPerPage = objectsPerPage;
    }

    public int getFullListSize() {
        return fullListSize;
    }

    public void setFullListSize(int fullListSize) {
        this.fullListSize = fullListSize;
    }

    public String getSortCriterion() {
        return sortCriterion;
    }

    public void setSortCriterion(String sortCriterion) {
        this.sortCriterion = sortCriterion;
    }

    public SortOrderEnum getSortDirection() {
        return sortDirection;
    }

    public void setSortDirection(SortOrderEnum sortDirection) {
        this.sortDirection = sortDirection;
    }

    public String getSearchId() {
        return searchId;
    }

    public void setSearchId(String searchId) {
        this.searchId = searchId;
    }

}
以上就是PaginatedList的实现。
GenericDao.java:
... ...
    /**
     * Find a list of records by using a named query
     * @param where can be a sql like : "t.id > 2" (t is a reference for current table)
     * @param offset limit start
     * @param length limit end
     * @return a list of the records found
     */
    List<T> getListForPage(String where, final int offset, final int length);
   
    /**
     * Generic method to get total count (mysql)
     * @param where can be a sql like : "t.id > 2" (t is a reference for current table)
     * @return total count
     */
    int getTotalCount(String where);
... ...
以上为GenericDao添加两个方法,准备在GenericDaoHibernate中实现,getListForPage取得分页列表,getTotalCount取得数据总数。
GenericDaoHibernate.java:
... ...
    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public List<T> getListForPage(String where, final int offset, final int length) {
        String whereSql = (where != null && where.length() > 0) ? " where " + where : where;
        final String hql = "from " + this.persistentClass.getName() + " t " + whereSql;
        List list = getHibernateTemplate().executeFind(new HibernateCallback() {
            public Object doInHibernate(Session session)
                    throws HibernateQueryException, SQLException {
                Query query = session.createQuery(hql);
                query.setFirstResult(offset);
                query.setMaxResults(length);
                List list = query.list();
                return list;
            }
        });
        return list;
    }
   
    /**
     * {@inheritDoc}
     */
    public int getTotalCount(String where) {
        String whereSql = (where != null && where.length() > 0) ? " where " + where : where;
        final String hql = "select count(t) from " + this.persistentClass.getName() + " t " + whereSql;
        Object result = getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session)
                    throws HibernateQueryException, SQLException {
                Query query = session.createQuery(hql);
                return query.uniqueResult();
            }
        });
        return Integer.parseInt(result.toString());
    }
... ...
以上为getListForPage和getTotalCount两个方法的实现,注意的一点是我们这里使用hibernateTemplate的回调函数来传参给hibernate的sessionFactory进行处理,参考这种方法可以自己编写需要的sql,让程序更加灵活。
EmployeeDao.java:
... ...
    /**
     * Fetch paging employee list
     * @param offset limit start
     * @param length limit end
     * @return a list of the records found
     */
    List getPageList (int start, int length);
   
    /**
     * Fetch paging employee total count
     * @return total count
     */
    int getPageCount ();
... ...
EmployeeDaoHibernate.java:
... ...
    public List getPageList(int page, int size) {
        return this.getListForPage("", (page-1)*size, size);
    }
   
    public int getPageCount() {
        return this.getTotalCount("");
    }
... ...
以上使用GenericDao中定义的两个方法很方便的取得employee分页信息。
applicationContext-struts.xml:
... ...
    <bean id="employeeAction" class="com.appfuse.app.webapp.action.EmployeeAction" scope="prototype">
        <property name="employeeManager" ref="employeeManager"/>
        <property name="employeeDao" ref="employeeDao"/>
        <property name="deptManager" ref="deptManager"/>
        <property name="titleManager" ref="titleManager"/>
        <property name="statusManager" ref="statusManager"/>
    </bean>
... ...
然后把employeeDao通过Spring Ioc注入到EmployeeAction中使用。
EmployeeAction.java:
... ...
    private EmployeeDao employeeDao;

    public void setEmployeeDao(EmployeeDao employeeDao) {
        this.employeeDao = employeeDao;
    }
... ...
    /**
     * Add for paging
     */
    private static int PAGE_SIZE = 5;
    private PageList employeePageList;
   
    public PageList getEmployeePageList() {
        return employeePageList;
    }
   
    public String list() {
        //employees = employeeManager.getAll();

        // 获取当前页数,displaytag通过参数"page"传递这个值
        int pageNumber;
        if (getRequest().getParameter("page") != null
                && !"".equals(getRequest().getParameter("page"))) {
            pageNumber = Integer.parseInt(getRequest().getParameter("page"));
        } else {
            pageNumber = 1;
        }
       
        PageList pageList = new PageList();
        List pageResults = employeeDao.getPageList(pageNumber, PAGE_SIZE);
        int pageTotalCount = employeeDao.getPageCount();
       
        // 设置当前页数
        pageList.setPageNumber(pageNumber);
        // 设置当前页列表
        pageList.setList(pageResults);
        // 设置page size
        pageList.setObjectsPerPage(PAGE_SIZE);
        // 设置总页数
        pageList.setFullListSize(pageTotalCount);
       
        employeePageList = pageList;

    return SUCCESS;
    }
... ...
以上使用getPageList和getPageCount两个方法把取得的分页信息赋给pageList,然后传给显示层的displayTag组件进行渲染展示。
employeeList.jsp:
... ...
<display:table name="employeePageList" class="table" requestURI="" id="employeeList" export="true"
    pagesize="5" defaultsort="2" defaultorder="descending" sort="list"
    partialList="true" size="8">
... ...
我们这里把name换成employeePageList,但是要注意这么做了之后,原先的排序等功能就不再起作用的,而必须通过程序实现,所以这里可以先把sortable="true"去掉。
到这里我们这次的代码修改算是结束了,我们成功的实现了PaginatedList接口,并通过自己编写的sql来取得分页信息,应该说通过这次修改diplayTag的性能已经“脱胎换骨”,可以适用于大数量的结果查询了。

>>> 教程总结
到这里我们使用appfuse框架只花了很少的时间,编写了很少的代码,就已经搭建了一个完整的包含权限管理的员工管理系统,我们可以使用admin用户建立新的普通管理帐户来分配给指定的人员来使用这个系统,普通管理帐户只有“员工管理”模块的权限,而admin则还有“部门管理”和“职位管理”的权限,整个系统层次清晰,功能完整;另外,基于Spring Security的框架还让该系统可以无缝集成到一些主流的SSO系统集群和基于LDAP的企业工具中去,真是非常棒一个解决方案~

>>> 回顾展望
实际上appfuse还有很多功能没有介绍完,比如xfire做webservice(访问http://localhost:8080/services可见),dwr的使用以及发送邮件等,但是由于篇幅问题,这里只介绍到这里了,有空的话我会抽时间把这些部分内容补充一下,如果朋友有什么疑问或者建议,欢迎与我联系交流~ 待续~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值