[Appfuse 源代码分析]

前面《Appfuse & tapestry 小记》中介绍了Appfuse的基本使用和一些小实例,感觉不过瘾~使用这个第一次让我感觉到“轻量”的J2EE框架,的确有一种爱不释手的感觉~所以就索性另写一篇《Appfuse 源代码分析》把这个“轻量级”的强大框架介绍给大家~少说废话,说来就来~


[Appfuse 源代码分析]

以下我们会以标准的ssh框架来说分析,因为这个骨架基本覆盖了现在最主流的j2EE技术(包括Spring2(Acegi)/Struts2/Hibernate3/Sitemesh/Velocity/XFire/DWR等),下面是建立骨架的命令:
mvn archetype:create -DarchetypeGroupId=org.appfuse.archetypes -DarchetypeArtifactId=appfuse-basic-struts -DremoteRepositories=http://static.appfuse.org/releases -DarchetypeVersion=2.0.2 -DgroupId=com.mycompany.app -DartifactId=myproject

按照《Appfuse & tapestry 小记》中介绍的安装步骤安装好源码后,你可以先尝试一下Appfuse的大致功能。Appfuse提供给我们一个最初始的框架,包括用户登录、信息管理、角色管理和一个简单的文件上传功能,以便我们可以更自由的扩展它,当然我建议你在这之前先全面阅读一遍它的代码,这样以后的工作才能更顺手哦~ 下面让我们开始分析代码:

由于我更倾向于用更符合人们接受和思考的顺序记录方式来剖析这个框架,所以下面我拟从配置文件开始,然后进入MVC模式的层次内部,中间穿插介绍事务和安全控制的内容,最后重点分析一些核心代码,希望能有更好的讲解效果~

1. pom.xml & web.xml

既然是Maven管理J2EE项目,首先当然是看看pom.xml和web.xml这两个文件了,关于pom.xml就不多做解释,如果有疑问可以看看之前的文章《Maven2 小记》,一般来说我们需要修改pom.xml尾部的datasource的username和password这两个地方,就可以开始安装,当然如何你想为Appfuse加入一些其他的插件或者扩展库可以在这里控制,我们着重分析一下web.xml(主要介绍filter部分) ...(参考http://blog.csdn.net/halenabc/archive/2005/10/19/509555.aspx)
... ...
    <filter>
        <filter-name>cacheFilter</filter-name>
        <filter-class>com.opensymphony.oscache.web.filter.CacheFilter</filter-class>
    </filter>
    <filter>
        <filter-name>clickstreamFilter</filter-name>
        <filter-class>com.opensymphony.clickstream.ClickstreamFilter</filter-class>
    </filter>
    <!-- 用于区别爬虫和正常的用户流量 -->
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>exportFilter</filter-name>
        <filter-class>org.displaytag.filter.ResponseOverrideFilter</filter-class>
    </filter>
    <!-- 用于表格展示/排列/分页等 -->
    <filter>
        <filter-name>gzipFilter</filter-name>
        <filter-class>net.sf.ehcache.constructs.web.filter.GzipFilter</filter-class>
    </filter>
    <!-- 缓存静态文件,如css/html/js -->
    <!--<filter>
        <filter-name>lazyLoadingFilter</filter-name>
        <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
    </filter>-->
    <!-- Use "org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter" if you're using JPA -->
    <filter>
        <filter-name>localeFilter</filter-name>
        <filter-class>com.appfuse.app.webapp.filter.LocaleFilter</filter-class>
    </filter>
    <!-- 分析获取prefererd locale信息 -->
    <filter>
        <filter-name>rewriteFilter</filter-name>
        <filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
        <init-param>
            <param-name>logLevel</param-name>
            <param-value>log4j</param-value>
        </init-param>
    </filter>
    <!-- Java服务器内部的urlrewrite -->
    <filter>
        <filter-name>securityFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>springSecurityFilterChain</param-value>
        </init-param>
    </filter>
    <!-- 整合Spring Security框架 -->
    <filter>
        <filter-name>sitemesh</filter-name>
        <filter-class>com.opensymphony.module.sitemesh.filter.PageFilter</filter-class>
    </filter>
    <filter>
        <filter-name>staticFilter</filter-name>
        <filter-class>com.appfuse.app.webapp.filter.StaticFilter</filter-class>
        <init-param>
            <param-name>includes</param-name>
            <param-value>/scripts/dojo/*,/dwr/*</param-value>
        </init-param>
    </filter>
    <!-- 允许dojo和dwr的action使用.html作为后缀 -->
    <filter>
        <filter-name>struts-cleanup</filter-name>
        <filter-class>org.apache.struts2.dispatcher.ActionContextCleanUp</filter-class>
    </filter>
    <!-- 管理ActionContext提供与其他框架整合的方案,如sitemesh -->
    <filter>
        <filter-name>struts</filter-name>
        <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
    </filter>
... ...
顺便提一下,appfuse的filter都是通过扩展org.springframework.web.filter.OncePerRequestFilter类得来。
另外附上一个请求在Struts2框架中的处理的主要步骤,不是很清楚的朋友可以看一下:
<1> 客户端初始化一个指向Servlet容器(例如Tomcat)的请求
<2> 这个请求经过一系列的过滤器(Filter)(这些过滤器中有一个叫做ActionContextCleanUp的可选过滤器,这个过滤器对于Struts2和其他框架的集成很有帮助,例如:SiteMesh Plugin)
<3> 接着FilterDispatcher被调用,FilterDispatcher询问ActionMapper来决定这个请是否需要调用某个Action
<4> 如果ActionMapper决定需要调用某个Action,FilterDispatcher把请求的处理交给ActionProxy
<5> ActionProxy通过Configuration Manager询问框架的配置文件,找到需要调用的Action类
<6> ActionProxy创建一个ActionInvocation的实例。
<7> ActionInvocation实例使用命名模式来调用,在调用Action的过程前后,涉及到相关拦截器(Intercepter)的调用。
<8> 一旦Action执行完毕,ActionInvocation负责根据struts.xml中的配置找到对应的返回结果。返回结果通常是(但不总是,也可能是另外的一个Action链)一个需要被表示的JSP或者FreeMarker的模版。在表示的过程中可以使用Struts2 框架中继承的标签。在这个过程中需要涉及到ActionMapper,在上述过程中所有的对象(Action,Results,Interceptors,等)都是通过ObjectFactory来创建的。



2.Startup

这里顺便介绍一下mvn jetty:run的执行过程,从中也可以看到服务器在初始化阶段所进行的一些工作:
compile> 预编译步骤
aspectj:compile
native2ascii:native2ascii
resources:resources
compiler:compile
prepare-data> 准备数据
aspectj:compile
native2ascii:native2ascii
resources:resources
hibernate3:hbm2dll
start-server> 读取配置文件创建规则
jetty:run
确定src目录和web.xml位置并读取以下配置文件(附说明):
/WEB-INF/applicationContext-struts.xml
>>>定义了主要的bean
/WEB-INF/applicationContext.xml
/WEB-INF/decorators.xml
>>>sitemesh的配置文件
/WEB-INF/dwr.xml
/WEB-INF/menu-config.xml
>>>velocity的菜单(权限)配置
/WEB-INF/resin-web.xml
/WEB-INF/security.xml
>>>spring(acegi)security配置
/WEB-INF/sitemesh.xml
/WEB-INF/urlrewrite.xml
>>>重写规则
/WEB-INF/web.xml
/WEB-INF/xfire-servlet.xml
>>>使用xfire做webservice
init-runtime> 最后加载资源/启动监听器
>>>详细如下(参考web.xml中的contextConfigLocation规则):
> classpath:/applicationContext-resources.xml (*.properties/dataSource)
> classpath:/applicationContext-dao.xml (sessionFactory/transactionManager/*Dao)
> classpath:/applicationContext-service.xml (AOP/Transaction/Mail*/velocityEngine/passwordEncoder/*Manager[WebService])
> classpath*:/applicationContext.xml (non-exists)
> /WEB-INF/applicationContext*.xml (loaded)
> /WEB-INF/xfire-servlet.xml (xfire-configs)
> /WEB-INF/security.xml (global-security-configs)
> classpath:/struts.xml (interceptors/actions/admin-actions)
> ... (其他的配置在下面介绍)
>>>END
顺便复习一下servlet的生命周期概念:)
[servlet生命周期:加载实例化>初始化init()>请求处理service()>服务终止destroy()]

3.View-Layer

毫无疑问,Appfuse还是按照MVC规范来设计的,这里我首先想要介绍的是View层,因为我认为对于一个网络系统来说,人们最先看到的就是界面,那么从这里入手应该会比较“人性化”,当然也是因为这部分相对比较简单,循序渐进嘛~
Appfuse使用到的技术包括jstl/sitemesh/velocity/struts/appfuse可以参考(/common/taglibs.jsp)
其中sitemesh用于构造站点界面结构,velocity主要用于一些公用板块,如:菜单、发邮件等
相关的配置文件如下:
/WEB-INF/sitemesh.xml
/WEB-INF/decorators.xml
classpath:/accountCreated.vm
classpath:/cssHorizontalMenu.vm
classpath:/cssVerticalMenu.vm
classpath:/velocity.properties
分析一下相关目录/文件,以下是列表:
/admin/         - 管理页面
/common/        - 公用页面(页头、尾/taglibs)
/decorators/    - sitemesh模板
/images/        - 图片
/scripts/       - js
/styles/        - css
/template/      - freemark模版
/WEB-INF/pages/ - appfuse页面
大家可以参照看一下,由于时间问题,这里没有办法逐个介绍用法了,希望以后有时间补充一下。

4.Model-Layer

抓紧时间我们来看看整个系统的核心部分把,Appfuse使用Hibernate+JPA作为持久层解决方案,更加简洁清晰。
相关的配置文件如下:
classpath:/applicationContext-resources.xml
classpath:/applicationContext-dao.xml
classpath:/applicationContext-service.xml
classpath:/hibernate.cfg.xml
关于JPA可以看看com.appfuse.app.model下面的model实现。
然后DAO包都在com.appfuse.app.dao.hibernate下,使用Spring的HibernateDaoSupport简化代码。
Dao代码本身没有什么值得说的地方,这里主要讲讲appfuse使用spring aop对事务的配置 (值得注意的是这里与传统的通过定义transactionInterceptor的层的区别,个人认为这种方式更加优雅,也更加符合松耦合的设计原则):
看到classpath:/applicationContext-service.xml的xsi:schemaLocation多了如下两行
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
看"AOP: Configuration and Aspects"部分注释
... ...
    <!-- =================================================================== -->
    <!-- AOP: Configuration and Aspects                                      -->
    <!-- =================================================================== -->
    <aop:config>
        <aop:advisor id="userManagerTx" advice-ref="userManagerTxAdvice" pointcut="execution(* *..service.UserManager.*(..))" order="0"/>      
        <aop:advisor id="userManagerSecurity" advice-ref="userSecurityAdvice" pointcut="execution(* *..service.UserManager.saveUser(..))" order="1"/>
        <aop:advisor id="managerTx" advice-ref="txAdvice" pointcut="execution(* *..service.*Manager.*(..))" order="2"/>
    </aop:config>
    <!-- 配置了三个Transaction规则 -->
    <!-- Enable @Transactional support -->
    <tx:annotation-driven/>
    <!-- 用于开启了事务行为(参考com.appfuse.app.dao.UserDao中使用),这里需要注意的是:Spring团队的建议是你只在具体的类上使用 @Transactional 注解, 而不要注解在接口上。你当然可以在接口(或接口方法)上使用 @Transactional 注解, 但是这只有在你使用基于接口的代理时它才会生效。因为注解是 不能继承 的, 这就意味着如果你正在使用基于类的代理时,事务的设置将不能被基于类的代理所识别,而且对象也不会被事务代理所包装 (这是很糟糕的)。 -->
    <!-- Enable @AspectJ support -->
    <aop:aspectj-autoproxy/>
    <!-- 在启用@AspectJ支持的情况下,在ApplicationContext中定义的任意带有一个@Aspect切面(拥有@Aspect注解)的bean都将被Spring自动识别并用于配置在Spring AOP里 -->
    <!-- Enable @Configured support -->
    <aop:spring-configured/>
    <!-- 只要设置 @Configurable 注解中的autowire属性就可以让Spring来自动装配了: @Configurable(autowire=Autowire.BY_TYPE) 或者 @Configurable(autowire=Autowire.BY_NAME,这样就可以按类型或者按名字自动装配了 -->
    <tx:advice id="txAdvice">
        <tx:attributes>
            <!-- Read-only commented out to make things easier for end-users -->
            <!-- http://issues.appfuse.org/browse/APF-556 -->
            <!--tx:method name="get*" read-only="true"/-->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
    <!-- 给所有方法加入通知 -->
    <tx:advice id="userManagerTxAdvice">
        <tx:attributes>
            <tx:method name="save*" rollback-for="UserExistsException"/>
        </tx:attributes>
    </tx:advice>
    <!-- 单独定义save方法,捕获UserExistsException异常 -->
    <bean id="userSecurityAdvice" class="com.appfuse.app.service.UserSecurityAdvice"/>
    <!-- 单独对service.UserManager.saveUser做限定,只允许administrators修改,改完重新setAuthentication -->
... ...
(参考http://www.redsaga.com/spring_ref/2.0/html/aop.html)
着重介绍一下com.appfuse.app.service.UserSecurityAdvice这个类
... ...
    public void before(Method method, Object[] args, Object target) throws Throwable {
        // 获得spring-seurity框架的上下文
        SecurityContext ctx = SecurityContextHolder.getContext();
        // 获得当前的authenticated规则
        if (ctx.getAuthentication() != null) {
            Authentication auth = ctx.getAuthentication();
            boolean administrator = false;
            GrantedAuthority[] roles = auth.getAuthorities();
            for (GrantedAuthority role1 : roles) {
                if (role1.getAuthority().equals(Constants.ADMIN_ROLE)) {
                    administrator = true;
                    break;
                }
            }
            // 如果参数args[0]是null或者不是User对象,抛出异常
            User user = (User) args[0];
            // Makes trust decisions based on whether the passed Authentication is an instance of a defined class.
            AuthenticationTrustResolver resolver = new AuthenticationTrustResolverImpl();
            // allow new users to signup - this is OK b/c Signup doesn't allow setting of roles
            boolean signupUser = resolver.isAnonymous(auth);
            // 如果是匿名用户则跳过
            if (!signupUser) {
                User currentUser = getCurrentUser(auth);
                // 除了administrator用户,用户没有权限修改其他用户的信息
                if (user.getId() != null && !user.getId().equals(currentUser.getId()) && !administrator) {
                    log.warn("Access Denied: '" + currentUser.getUsername() + "' tried to modify '" + user.getUsername() + "'!");
                    throw new AccessDeniedException(ACCESS_DENIED);
                // 如果你不是administrator用户,又要编辑自己的信息,先要检查你的角色是否符合当前的authenticated规则
                } else if (user.getId() != null && user.getId().equals(currentUser.getId()) && !administrator) {
                    // get the list of roles the user is trying add
                    Set<String> userRoles = new HashSet<String>();
                    if (user.getRoles() != null) {
                        for (Object o : user.getRoles()) {
                            Role role = (Role) o;
                            userRoles.add(role.getName());
                        }
                    }
                    // get the list of roles the user currently has
                    Set<String> authorizedRoles = new HashSet<String>();
                    for (GrantedAuthority role : roles) {
                        authorizedRoles.add(role.getAuthority());
                    }
                    // if they don't match - access denied
                    // regular users aren't allowed to change their roles
                    if (!CollectionUtils.isEqualCollection(userRoles, authorizedRoles)) {
                        log.warn("Access Denied: '" + currentUser.getUsername() + "' tried to change their role(s)!");
                        throw new AccessDeniedException(ACCESS_DENIED);
                    }
                }
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("Registering new user '" + user.getUsername() + "'");
                }
            }
        }
    }
... ...
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target)
    throws Throwable {
        User user = (User) args[0];
        // 如果用户存在则判断是否是当前用户,若是则把SecurityContext设置回来
        if (user.getVersion() != null) {
            // reset the authentication object if current user
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            AuthenticationTrustResolver resolver = new AuthenticationTrustResolverImpl();
            // allow new users to signup - this is OK b/c Signup doesn't allow setting of roles
            boolean signupUser = resolver.isAnonymous(auth);
            // 如果是匿名用户则跳过
            if (auth != null && !signupUser) {
                User currentUser = getCurrentUser(auth);
                // 如果你编辑过自己的信息,则需要得到最新的权限信息,设置到spring-seurity的上下文中去
                if (currentUser.getId().equals(user.getId())) {
                    auth = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            }
        }
    }
... ...
(参考http://www.blogjava.net/DoubleJ/archive/2008/03/04/183796.html)

5.Control-Layer

关于控制层我们放在最后,因为这里可能是最需要添加代码的部分了,和后面的章节可能联系的比较紧。Appfuse使用的是Struts2来作为Action控制器,Struts2我不多说了,实际上更接近于Webwork系统~ 使用起来应该说比老的Struts还是有一定进步的,代码更少,也更多的使用注释器来控制逻辑。
先看看一些关于Action的配置applicationContext-struts.xml:
里面定义了struts.xml需要的bean,最重要的是 com.appfuse.app.webapp.interceptor.UserRoleAuthorizationInterceptor这个是系统 z最主要的权限控制拦截器,appfuse的初始角色有两个:ROLE_ADMIN, ROLE_USER,有关于此的配置文件有/WEB-INF/applicationContext-struts.xml /WEB-INF/menu-config.xml /WEB-INF/security.xml。
所有的Action类都是继承com.opensymphony.xwork2.ActionSupport,拦截器继承com.opensymphony.xwork2.interceptor.Interceptor,都是Struts2的“标配”。
这里涉及到关于Security控制我们重点介绍一下>
A.总体(url)安全控制
最新的Appfuse使用了Spring(Acegi)Security来集中控制整个系统的总体安全策略,更加优雅。看看security.xml的注释(定义见前面的web.xml配置文件,这里主要分析appfuse如何通过识别url,来粗粒度的控制系统安全):
... ...
    <http auto-config="true" lowercase-comparisons="false">
        <!--intercept-url pattern="/images/*" filters="none"/>
        <intercept-url pattern="/styles/*" filters="none"/>
        <intercept-url pattern="/scripts/*" filters="none"/-->
        <intercept-url pattern="/admin/*" access="ROLE_ADMIN"/>
        <intercept-url pattern="/passwordHint.html*" access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER"/>
        <intercept-url pattern="/signup.html*" access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER"/>
        <intercept-url pattern="/a4j.res/*.html*" access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER"/>
        <!-- APF-737, OK to remove line below if you're not using JSF -->
        <intercept-url pattern="/**/*.html*" access="ROLE_ADMIN,ROLE_USER"/>
        <form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?error=true" login-processing-url="/j_security_check"/>
        <remember-me user-service-ref="userDao" key="e37f4b31-0c45-11dd-bd0b-0800200c9a66"/>
    </http>
    <!-- 配置所有需要拦截过滤的url,登录表单的提交地址和remember-me功能 -->
    <authentication-provider user-service-ref="userDao">
        <password-encoder ref="passwordEncoder"/>
    </authentication-provider>
    <!-- 默认使用daoAuthenticationProvider,指明这个 -->
    <!-- Override the default password-encoder (SHA) by uncommenting the following and changing the class -->
    <!-- <bean id="passwordEncoder" class="org.springframework.security.providers.encoding.ShaPasswordEncoder"/> -->
    <!-- 默认使用SHA加密,如果要修改可以在这里设置,Acegi提供了三种加密器,如下:
    PlaintextPasswordEncoder—默认,不加密,返回明文.
    ShaPasswordEncoder—哈希算法(SHA)加密
    Md5PasswordEncoder—消息摘要(MD5)加密
    -->
    <global-method-security>
        <protect-pointcut expression="execution(* *..service.UserManager.getUsers(..))" access="ROLE_ADMIN"/>
        <protect-pointcut expression="execution(* *..service.UserManager.removeUser(..))" access="ROLE_ADMIN"/>
    </global-method-security>
    <!-- 设置更细粒度的方法执行权限 -->
... ...
注意:我们可以取出userDao的实现类(com.appfuse.app.dao.hibernate.UserDaoHibernate)观察代码。可以看到,它实现了org.springframework.security.userdetails.UserDetailsService接口,填充了loadUserByUsername方法,返回一个UserDetails对象,抛出一个 UsernameNotFoundException,这样子daoAuthenticationProvider就可以使用这个pojo来做自己的安全验证了。
B.用户角色控制
在com.appfuse.app.webapp.interceptor.UserRoleAuthorizationInterceptor里面控制,没有权限则返回403。
这是系统里面唯一一个自己定义的拦截器,至于为什么不在上面的Spring(Acegi)Security里面一起配置掉,我想可能是appfuse的设计者想让我们多一点选择吧:)


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/shagoo/archive/2009/04/23/4103937.aspx

你可能感兴趣的:(spring,xml,Web,struts,Appfuse)