关于分享

分享好处多多

通过做一些分享活动,一方面可以扩大自己的影响力,让更多的人知道你。 另外一方面,可以加深自己对知识的理解,在交流中纠正一些错误的理解。 还可以提高自己的表达能力。可以说,除了需要花时间准备之外,分享对自己来说,几乎是百利而无一害的。

参与度与氛围

自己做分享,总是希望有更多的人来参与,并从中营造一个愉悦的氛围, 互惠互利,从中可以学习到更多。但是,经常遇到的事实是,参与的童鞋常常不愿意发言。 一方面是可能是自己不擅长,更多看成是培训,而不是交流活动,害怕犯错。还有就是, 他们没有事先做一些准备,对内容理解不够快,所以也没什么特别的疑问。 我认为,提出一些低级问题,并不是什么丢人的事情,可能是分享人没有照顾到你(经常是一类人), 如果你不提出来的话,或许分享人会自认为大家都很明白。总之,参与一些分享活动,能全身心 投入,收获会更大。

敢于分享

很多童鞋都害怕分享,总是担心自己水平不够,准备不够充分,所以一拖再拖。 以我的经验,分享的内容你刚刚够得着就可以了,因为你准备的过程中,会对自己掌握的内容进行 筛选,收集更多的资料,然后进行浓缩总结,到最后你把它分享给其他人,整个过程下来,就是一次很好的 锻炼机会。不要太担心做不好,即使有些地方自己说不清,但或许经过众人的讨论,理解得就更好了。 还有就是,什么都要计划,给自己的分享计划加个合理的期限吧,少许的压力能让你做得更好。

节奏与时间

每次分享的时间不要过长,我就犯过这样的错误,到后面很多人都开始走神了。 还有就是,主题要比较单一,免得铺开太大,不好收缩,影响时间和效果。 时间比较长的话,中间可以适当休息一下。

想不到的收获

最近做了一次关于oracle执行计划的分享活动,说实话,我对这个主题不是很熟悉。 但是我觉得这东西很有用,也非常适合团队里的童鞋,所以花了几个星期的时间收集资料, 最后做了这么一个分享活动。经自己讲解了一遍,自己对这些知识加深了理解。更让我想不到的事情就是, 有其他项目组的童鞋遇到了相关的问题,跑来跟我讨论(哥俨然是专家啦)。正因为这样,我才有机会遇到更多现实的问题, 从而促进我对这些东西的理解,如果没有这次分享,有些东西可能就没法用在实践上了。 我想,很多高手也是这么炼成的。:)

javacore入门介绍

什么是javacore

  • javacore是java应用程序在某个时间点的线程转储文件,通常也称为Thead Dump
  • 记录了整个JVM的运行情况(线程, 垃圾回收, JVM运行参数, 内存地址等信息)
  • 用来诊断程序问题,其中比较典型的包括线程阻塞, CPU使用率过高, JVM Crash, 堆内存不足和类装载等问题

文件命名方式

javacore文件通常以txt方式结尾,名称格式主要是以javacore开头, 加上日期,产生的时间,当时的线程编号,如javacore.20100719.003424.299228.txt(Unix)

javacore获取方式

  • 发送中断signal
    AIX和Linux: SIGQUIT, kill -3 PID
    Windows: Ctrl+Break, DrAdmin in WAS(未验证过)
  • 在java的执行代码中使用JavaDump()方法(未验证过)
    com.ibm.jvm.Dump.javaDump()方法促使JVM dump
    发布ProblemDiagnosticsLabToolkit应用包,通过可视化页面直接生成相关文件
  • 系统在异常时自动throw(程序不一定退出)
    严重的本地调用出错
    内存不足(例如 OutOfMemory)

javacore基本内容

javacore基本内容

  • 操作系统相关信息JDK版本信息
    关注jdk的版本(1.5前后差别比较大),32位和64位在内存要求上也有区别
  • JVM的启动参数信息
    关注一些特殊的虚拟机参数如xms,xmx,xss,java.ext.dirs等,特别是内存分配要和线程数匹配,避免xms太小(频繁gc和扩展)
  • JVM堆内存信息,已装载入的类信息
    JIT Core Cache和JIT Data Cache是JIT编译器使用的,用于提高运行效率。一般关注Object(Reversed)(堆内存分配)和Class(用于类加载)的内存使用率
  • dump产生原因与时间
    用于追溯当时的现状
  • GC记录
    关注Full GC(Global GC)情况,通常伴随比较多的GC线程
  • 所有Thread执行情况(包括应用程序内部执行线程,容器线程,垃圾回收线程,定时线程,线程池线程,页面请求转发线程等多种线程信息)

线程状态分类

线程状态分类

整个图包括三部分,左边是线程列表,选择某个线程后,中间会展示该线程与其他线程的等待依赖关系,右边会显示线程的详细堆栈信息。 通过在线程列表中对Name,State,Stack几列进行排序,可以对每类线程数量、是否有明显的瓶颈有个大概的印象,便于接下来的具体分析。

  • 死锁 Deadlock
    多个线程竞争多个资源,出现相互等待资源又不释放资源的情况,这种情况在现实中并不多见。
  • 执行中 Runnable
    通常这类线程堆栈真处于发送数据,数值计算,类型转换等操作。
  • 等待资源 Waiting on condition
    等待资源,如果堆栈信息明确是应用代码,则证明该线程正在等待资源,一般是大量读取某资源,且该资源采用了资源锁的情况下, 线程进入等待状态,等待资源的读取。又或者,正在等待其他线程的执行等
  • 等待监控器检查资源 Waiting on monitor
  • 暂停 Suspended
  • 对象等待中 Object.wait()
  • 阻塞 Blocked
    线程阻塞,是指当前线程执行过程中,所需要的资源长时间等待却一直未能获取到,被容器的线程管理器标识为阻塞状态, 可以理解为等待资源超时的线程。这种情况在was的日志中,一般可以看到CPU饥渴,或者某线程已执行了XX秒的信息
  • 停止 Parked

常见的线程分类

Thread开头 一般是应用自己起的后台线程,或者自己搞的线程池(没有重命名的时候),如果是独立进程大多业务都跑在这里
WebContainer was容器线程,was的话业务一般都在这里,受容器线程数量控制
Timer Timer定时任务,一些连接池等用于监控池内对象
Quartz Quartz用于定时任务的线程,不受容器控制
Finalizer 用于实现finalize特殊方法的线程
GC Slave GC线程,频繁gc的时候会出现不少
Main 主线程
JIT 用于实现JIT的线程

相关工具与资料

策略分析

  • 数百K的纯文本,最好借助工具,例如jca分析工具
  • 采集连续的多个时间点的javacore,方便对比(手动)
    一般的线程执行都会非常快,如果出现某个资源的阻塞,在短时间内的两个javacore,该线程的堆栈会变化不大。 或多次javacore的线程都集中在等待某些资源。
  • 了解app的性质,基本处理流程
  • app相关的处理能力(以前的数据,用于对比)
  • 问题出现时,多了解周边情况(cpu,io,外围),记录现状
  • 对thread状态进行分类,业务分布情况,资源等待情况(细化)
  • 如有必要,获取heapdump分析(oom)

javacore和heapdump的区别

javacore经常和heapdump联系在一起,有时候也一起出现。这里简单对两个文件进行对比,希望大家有个印象:

heapdump javacore
文件类型 二进制 文本
分析工具 Heap Analyzer jca
文件内容 内存映射 对象占用信息 CPU执行信息,线程堆栈
作用 分析内存泄漏 大对象 分析进程挂死,响应速度慢,大对象
影响 对系统影响大,生成文件大,较少使用 对系统影响小,生成文件小,比较方便

相关工具与资料

  • javacore-tutorial.pdf 出自IBM的文档
  • jca javacore分析工具
  • heap analyzer heapdump分析工具

reflection in java

什么是反射

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法; 对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用 对象方法的功能成为java语言的反射机制。

典型应用

  • spring生成Bean对象
  • struts2使用配置进行对象校验
  • hibernate,ibatis进行数据库对象映射
  • xml与java对象的相互转化
  • springmvc进行方法参数注入
  • more……

理解类型

原型类型vs包装类型(XXX.TYPE)

Void -> void -> V
Byte -> byte -> B
Character -> char -> C
Short -> short -> S
Integer -> int -> I
Long -> long -> J
Float -> float -> F
Double -> double -> D
Boolean -> boolean -> Z

类与接口

  • 接口(Interface)是一种特殊的类
  • 注解(Annotation)是一种特殊的接口
  • 枚举(Enum)是一种特殊的类
class java.lang.String //类
class java.util.AbstractList //抽象类
interface java.lang.Runnable //接口

数组类型

//基本类型数组
int[].class -> [I
int[][].class -> [[I

//普通类型数组
String[].class -> [Ljava.lang.String;
Integer[][].class -> [[Ljava.lang.Integer;

理解类型判断方法

//是否某个类型的实例
i instanceof Integer
Integer.class.isInstance(i)
Person.class.isInstance(new SuperTeacher(""))

//是否数组
x.getClass().isArray()

//是否父子类/接口关系
Number.class.isAssignableFrom(Integer.class)

//是否原型类型
i.getClass().isPrimitive()
Integer.class.isPrimitive()
Integer.TYPE.isPrimitive()

//是否注解
Logger.class.isAnnotation()

//是否枚举
Status.class.isEnum()

//是否接口
List.class.isInterface()

理解修饰符

  • 手段:Class#getModifiers()和Modifier工具类
  • 适用于Class,Method,Field,Constructor

示例代码如下:

int mod = Teacher.class.getModifiers();
assertTrue(Modifier.isPublic(mod));

常见的修饰符判断方法有:

//可见性
isPublic,isProtected,isPrivate
//类型属性相关
isInterface,isAbstract,isFinal,isStatic
//特殊作用
isSynchronized,isNative,isTransient,isVolatile

理解类型关系

//类->父类(单继承,只有一个)
Class#getSuperclass()

//类、接口->实现接口(多继承,可能有多个)
Class#getInterfaces()

//类->内部类(可能有多个)
Class#getDeclaredClasses()

//内部类->外部类(只有一个)
Class#getEnclosingClass()

相关练习

  • Test:输出某个类实现的所有接口
  • Test:输出某个类实现的继承结构
  • Test:能够获取到之类的信息么?

理解对象生成与调用

对象生成

可以采用无参数构造方法或有参数方式

//需要存在无参构造方法
String s = "com.xxx.Student";
Class t = Class.forName(s);
Teacher teacher = (Teacher)t.newInstance();

//直接调用构造方法
Constructor[] cons = SuperTeacher.class.getConstructors();
Object obj = cons[0].newInstance("hello");

获取成员

成员一般指Field,Method,Constructor等信息

//属性
Field[] fs = Teacher.class.getDeclaredFields();
Field[] fs = Teacher.class.getFields();//只有公有的

//方法
Medhod[] ms = Teacher.class.getDeclaredMethods();
Medhod[] ms = Teacher.class.getdMethods();//只有公有的

//构造方法
Constructor[] cs = Teacher.class.getDeclaredConstructors();
Constructor[] cs = Teacher.class.getConstructors();//只有公有的

获取成员可以通过参数直接定位,以属性和方法为例:

SuperTeacher.class.getField("name");

SuperTeacher.class.getMethod("setBounds", Integer.TYPE);//参数是int
SuperTeacher.class.getMethod("setBounds", Integer.class);//参数是Integer

相关练习

  • Test:找到某个类所有的非private的普通方法

调用方式

拿到成员之后,就可以进行调用了。像Field需要使用get/set方法, Method使用invoke方法,Constructor使用newInstance方法。如:

Field#set(obj);
Field#get(obj);

Method#invoke(obj, args);

有时候需要绕过可见性进行调用,需要通过setAccessible方法进行处理,如

Field#setAccessible(true);// if not set, fail

对于参数数组的话,通过args并不能区分是数组类型还是不定参数类型(都是通过数组进行传递)。 如果需要继续区别,应该对Method进行检测,检测方式是Method#isVarArgs

处理返回值

对于方法调用,可以对返回值进行处理。即使返回为void或数组类型也是可以识别的。如:

Class<?> returnType = method.getReturnType();
assertEquals(void.class, returnType);
assertTrue(returnType.isArray());

如果是数组的话,由于返回值是Object类型,需要通过Array工具类进行处理(见java api文档)

其他:关于内部类调用的秘密

普通内部类

public class A{
    private List stus;
    public int test() { return new B().test(); }
    class B{
        public int test() { return stus.size(); }
    }
}

关于上面的内部类,为什么B能够调用到A的stus属性,通过观察生成的字节码,得到的实际结果是:

public class A{
    private List stus;
    static List access$0(A a) { return a.stus; }
    public int test() { return new A$B(this).test(); }
}

class A$B{
    A this$0;
    A$B(A a) { this$0 = a; }
    public int test() { return A.access$0(this$0),size(); }
}

我们可以发现:

  1. A对B类需要访问的每一个私有属性生成对应的一个静态访问方法,B的访问就是通过这个私有方法的
  2. 生成B类实例的时候把A类实例引用传入并进行保存,所以每一个B类实例都有一个A类实例关联
  3. 通过反射可以看到新方法access$0和新内部变量this$0

静态内部类

public class A{
    private static List stus;
    public int test() { return new B().test(); }
    static class B{
        public int test() { return stus.size(); }
    }
}

对于静态内部类,情况就比较简单,得到的实际结果是:

public class A{
    private static List stus;
    static List access$0() { return stus; }
    public int test() { return new A$B().test(); }
}

class A$B{
    A$B() {}
    public int test() { return A.access$0(),size(); }
}

我们可以发现:

  1. A对B类需要访问的静态私有属性生成一个静态访问方法,B的访问就是通过这个私有方法的
  2. 由于是static方法,所以不需要保存实例的引用
  3. 通过反射可以看到新方法access$0

所以,如果可以的话,优先使用静态内部类(特别是没有使用到内部普通变量的情况)。

理解泛型

java的泛型是擦除式,运行时没有泛型的概念.所以下面的代码会编译通过但执行报类型转换错误:

List<String> strs = new Array<String>();
add2List(strs);
String str = strs.get(0);

//add2List的实现
void add2List(List lt){
    lt.add(1);
}

通过javap -c Test.class可以看到类似下面的指令:

// String str = strs.get(0);
7: invokeinterface #31, 2; //interfaceMethod java/util/List.get:(l)Ljava/lang/Object;
12: checkcast #37; //class java/lang/String

// lt.add(1);
2: invokestatic #41; //Method java/lang/Integer.valueOf:(l)Ljava/lang/Integer;
5: invokeinterface #47, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

很明显,在checkcast的时候会出错。

虽然说运行时不关注是否泛型,但是还是有办法可以找到一些泛型相关信息的。和泛型类型的类图如下面所示: java泛型类型结构

下面以Method为例进行描述(关于field之类的就不描述了):

public T method2(List<T> a, List<String> b, List<?> c, float[] f) throws Exception

分析入口

Method#getGenericParameterTypes()
Method#getGenericReturnType()
Method#getGenericExceptionTypes()

通过上面一些方法可以得到下面的类型关系:

T -> TypeVariable
List<String> -> ParameterizedType
List<?> -> ParameterizedType
float[] -> GenericArrayType
Exception -> Class

ParameterizedType描述的是一种多级泛型的关系,可以继续通过getActualTypeArguments继续进行处理,可以得到

List<String> -> Class
List<?> -> WildcardType
List<T> -> TypeVariable

处理Type是比较麻烦的事情,每种具体类型都有自己特殊的处理方法(见图所示),但平时这个东西很少用到。

New in JDK7

jdk7以前有4种方法调用指令:

invokestatic -> 静态方法调用
invokespecial -> 特殊方法(如构造方法)调用
invokevirtual -> 普通方法调用
invokeinterface -> 接口方法调用

在jdk7中实现了新的规范jsr292,增加了新的字节码指令invokedynamic,通过下面的选项可以开启。

XX:+UnlockExperimentalVMOptions
XX:+EnableInvokeDynamic
XX:+EnableMethodHandles

该指令只需要制定方法名称,只要求引用非空对象而已,但javac还没法生成这样的指令。调用方式如下:

invokedynamic #10; //DynamicMethod java/lang/Object.lessThan:(Ljava/lang/Object;)

但是可以有相关的api可以模拟出这种指令调用方式,如下(看上去和普通反射调用没什么明显不同):

MethodType type = MethodType.methodType(int.class, int.class, int.class);
MutableCallSite callSite = new MutableCallSite(type);
MethodHandle invoker = callSite.dynamicInvoker();

MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mhMax = lookup.findStatic(Math.class, "max", type);

callSite.setTarget(mhMax);
invoker.invokeWithArguments(3,5);

这种调用方式有几个关键的概念:

method handle 方法句柄,是callsite的目标
bootstrap method 引导方法
CallSite 调用点
method type 方法类型

这个指令可以增强很多基于JVM的动态语言的方法调用性能,所以潜力很大,更多具体信息,可以参考下面的一些链接:

  • http://www.infoq.com/cn/news/2011/01/invokedynamic
  • http://icyfenix.iteye.com/blog/1392441
  • http://book.51cto.com/art/201205/339215.htm
  • http://www.from0to1.net/%E5%86%8D%E6%8E%A2-invokedynamic/

小毛的jforum2源码分析(旧)

这个文章大约是08年的时候写的了,jforum是java中还算比较有名的开源论坛系统,我上一家公司拿它来做二次开发。jforum2是内部自己实现的mvc,到了jforum3的时候全部重写了,都使用开源框架来做,现在不知发展到什么情况了。所以,这里特指jforum2.

正文

怎么才算好的源码分析呢?当然我这个肯定不算。我想大概分为几个层面吧,写写注释那算最基本的了, 写写要点思路和难点,算是还不错拉,再难的就是跳出源码举一反三,形成自己的一套思路吧。好好努力吧。

这次针对的是jforum2.1.8,大概jforum团队已经没心情理这个版本了,都冲着jforum3去了。选择这个版本, 主要是因为jforum在java论坛类应用中算是佼佼者了,很多人都拿这个来做二次开发,而jforum3使用的是另外一套架构了, 而且还没完全release,所以斟酌一下,还是选择这个经典的版本。

关于jforum的介绍网上已经很多了,这里也简单抄录一段:JForum 是一个功能强大 ,易于管理的论坛。 它的设计完全遵从MVC设计模式,能够在任何Servlet容器与EJB服务器上运行。而且可以轻松的定制与扩展JForum论坛。 上面这段简述还是中肯的。另外,jforum是模仿phpbb写的,使用的是classic-blue风格,但不能自己选择风格,要的话只能自己修改了。 再说几句,说jforum比较优秀是因为java开源的论坛系列精品少,而且jforum的bug也真的不少,不信试试就知道了。 不过作为一个成型的组件,功能强大并且适合二次开发,还是应该列入考虑范围的。

不管怎样,jforum是个不错的学习范本,至少让你觉得写个山寨框架不是什么难事, 而事实也的确是这样的。重要的一点是,不要轻易拿出来害人就是了:)这里先列举出可能一些分析点:

● web.xml
● 初始化流程
● 处理请求流程(mvc)
● 文件监控
● 缓存实现
● 数据库访问实现
● 权限控制

首先了解一个web应用,首要的就是知道处理流程。首先来看看入口web.xml,里边的内容还是挺清晰的, 可以看到里边有个监听器ForumSessionListener,*.page的过滤器ClickstreamFilter,还有2个*.page的处理器, 其中InstallServlet是安装相关的,JForum则是前端处理器。

基本上整个流程就是

client request -> ForumSessionListener -> ClickstreamFilter -> JForum -> server response.

ForumSessionListener实现了HttpSessionListener接口,但是只是对session destory做了处理, 在这个过程中,保存session的历史记录到DB,并清除用户信息和相关的security信息。 ClickstreamFilter实现了Filter接口,主要的任务就交给BotChecker了,是用来检测client是不是一个robot来的。 主要的工作还是在JForum上面,不过先来看看jforum是怎么检测robot的?

BotChecker只有一个静态工具方法isBot,首先是检测是否请求robot.txt(这是标准的robot协议文件), 接下去判断User-Agent头部,最后是判断remotehost。而已知的robot都是写在文件clickstream-jforum.xml 里边的(包括agent和host),并通过ConfigLoader加载进来的(SAX方式)。

可以看到JForum和InstallServlet都继承了JForumBaseServlet这个HttpServlet,而JForumBaseServlet包括2个重要的方法init和startApplication。众所周知,init是servlet初始化时调用的方法,JForumBaseServlet里边的init方法的流程是:

调用父类的init(正常情况这是必须调用的)  -> 配置log4j -> startSystemglobals(加载全局参数配置SystemGlobals.properties -> 加载数据库配置database.driver.config(如mysql就是WEB-INF/config/database/mysql/mysql.properties)  -> 加载自定义配置(默认的是jforum-custom.conf)) -> 配置缓存引擎 -> 配置freemarker模板引擎 -> 加载模块配置modulesMapping.properties -> 加载url映射配置urlPattern.properties -> 加载I18n配置(languages/*) -> 加载页面映射配置(templatesMapping.properties) -> 加载BBcode配置bb_config.xml -> 结束

jforum实现了自己的mvc,整个mvc的脉络就是

client request -> 解析url(urlPattern.properties),获取module/action/param -> 通过module获取相应的module class,并通过action识别并调用相应的方法(modulesMapping.properties) -> 使用dao完成业务逻辑 -> 调用template进行渲染(templatesMapping.properties)

其实整个mvc和struts没什么两样的,具体的流程以后再提。

JForumBaseServlet里边的startApplication方法的流程是:

加载通用sql文件sql.queries.driver(就是/database/generic/generic_queries.sql) -> 加载特定sql文件(如mysql就是/database/mysql/mysql.sql) -> 加载Quartz定时任务配置 -> 加载登录验证器(验证方式) -> 加载Dao实现方式 -> 加载文件修改监听器 -> 加载查询索引管理器 -> 加载定时统计任务

jforum实现了自己的orm,当然不是hibernate那种,是类似ibatis的那种sql mapping,并提供了多套的sql文件来实现数据库无关的特性, 整个流程也是比较清晰的:

加载数据库配置 -> 加载sql mapping file -> 设置DAO实现 -> 通过named sql找到对应的sql(在*.sql里边对应着) -> 运行出数据

继续重点。JForum的init流程如下:

JForumBaseServlet.init -> JForumBaseServlet.startApplication -> 启动数据库 -> 预加载一些数据到缓存中(ForumRepository[Categories,Forums,同时在线最大人数,最后登录用户,注册用户数等等],用户等级,表情数据,屏蔽列表) -> 结束

上面简单提到了Jforum处理请求的过程,现在在来看看这个过程,就是service方法,这次采用代码概要的方式展示:

 // 初始化JForumExecutionContext
JForumExecutionContext ex = JForumExecutionContext.get();
// 包装request和response
request = new WebRequestContext(req);
response = new WebResponseContext(res);
// 检查数据库状态
this.checkDatabaseStatus();
// 创建JForumContext并设置到JForumExecutionContext中去
.......
JForumExecutionContext.set(ex);
// 刷新session    	   
utils.refreshSession();
// 加载用户权限    	  
SecurityRepository.load(SessionFacade.getUserSession().getUserId());
// 预加载模板需要的上下文
utils.prepareTemplateContext(context, forumContext);
// 从request中解析module name
String module = request.getModule();
// module name  -> module class
String moduleClass = module != null ? ModulesRepository.getModuleClass(module) : null;
// 判断是否在ban list里边
......
boolean shouldBan = this.shouldBan(request.getRemoteAddr());
// 主角出场
out = this.processCommand(out, request, response, encoding, context, moduleClass);
// 扫尾工作,例如db的rollback
this.handleFinally(out, forumContext, response);

processCommand会调用Command的process方法:

// 获取一个module实例(继承了Command)
Command c = this.retrieveCommand(moduleClass);
// 进入process
Template template = c.process(request, response, context);
// 这里开始是process方法
//获取action
String action = this.request.getAction();
//如果不是ignore的,就调用这个action
if (!this.ignoreAction) {this.getClass().getMethod(action, NO_ARGS_CLASS).invoke(this, NO_ARGS_OBJECT);}
//如果是转发的,就把TemplateName清空
if (JForumExecutionContext.getRedirectTo() != null) {this.setTemplateName(TemplateKeys.EMPTY);}
//不是转发且attribute里边存在template,则设置为templateName
else if (request.getAttribute("template") != null) {this.setTemplateName((String)request.getAttribute("template"));}
//是否coustomContent?例如下载,验证码子类的不需要页面的操作
if (JForumExecutionContext.isCustomContent()) {return null;}
//返回一个template
return JForumExecutionContext.templateConfig().getTemplate(
                new StringBuffer(SystemGlobals.getValue(ConfigKeys.TEMPLATE_DIR)).
                append('/').append(this.templateName).toString());
        }
// 从process出来,回到processCommand
// 设置content type
response.setContentType(contentType);
//生成页面并flush
if (!JForumExecutionContext.isCustomContent()) {
				out = new BufferedWriter(new OutputStreamWriter(response.getOutputStream(), encoding));
				template.process(JForumExecutionContext.getTemplateContext(), out);
				out.flush();
			}
		}

这是一般的流程,就像上面提到的customContent,就是要自己处理了,可以参考CaptchaAction.generate().

这样的话,如果我们要增加一些action进行二次开发的话,大体的流程就是,增加一个继承了Command的类, 例如叫ExampleAction,定义一个方法,例如叫test(),在urlPattern.properties中定义一个映射, 例如为example.test.1 = forum_id,再在modulesMapping.properties中定义module class的映射, 如example = ExampleAction,最后我们在templatesMapping.properties定义个模板的映射, 如:example.test = example_test.htm。现在假设我们的请求url是/example/test/1,再来看看test里边的一些方法:

this.request.getIntParameter("forum_id"))  //获取参数,得到1
this.context.put("obj", obj); //把结果写入context,这样可以在template中获取到
this.setTemplateName("example.test");//设置template的名字

这样的简单流程应该还比较好理解吧?

另外,还可以看出,jforum使用了自己的一套映射机制,这是通过urlPattern.properties来定义的 (参考上面JForumBaseServlet的init流程),这是在JForumBaseServlet的loadConfigStuff方法的第一行实现的, 并加载到UrlPatternCollection中去,如下所示:

Properties p = new Properties();
fis = new FileInputStream(SystemGlobals.getValue(ConfigKeys.CONFIG_DIR) + "/urlPattern.properties");
p.load(fis);

for (Iterator iter = p.entrySet().iterator(); iter.hasNext(); ) {
   Map.Entry entry = (Map.Entry) iter.next();
   UrlPatternCollection.addPattern((String)entry.getKey(), (String)entry.getValue());
}

可以知道这里的key和value都是String来的

UrlPatternCollection.patternsMap.put(name, new UrlPattern(name, value));

但在addPattern方法里边其实是生成一个UrlPattern作为value,如何构造一个UrlPattern可以看看代码, 举例来说把,对于example.hello.2=a,b,这样会生成一个UrlPattern,里边的内容是name为example.hello.2,value为a,b. 而size和vars是用a,b解析出来的,用来表示一共有多少个参数,参数名组成的数组。 所以UrlPattern存储的就是一个url格式的定义,而放在UrlPatternCollection里边的一系列的url映射格式是在请求的url解析的时候用到的。

现在再分析一下jforum怎么使用这个UrlPatternCollection的?按照我们不严格的思路,应该是service中处理url, 获取.page前面的一部分,如/example/hello/2/1,用/做一下split,获取module name,action name, 把最后的作为参数,用module,action,参数个数组成一个key(example.hello.2),通过UrlPatternCollection找到对应的UrlPattern, 通过里边的格式对应(vars里边的参数名和url的参数值)就可以把参数添加到request的parameters里边去。 实际的情况也差不多就这个样。在说到jforum中的service方法的时候,简单提到过request和response是经过包装的:

request = new WebRequestContext(req);
response = new WebResponseContext(res);

WebResponseContext只是简单的delegate给HttpServletResponse(这样做的好处是全部方法都限制在ResponseContext中), 而WebRequestContext是继承了HttpServletRequestWrapper并实现了RequestContext接口。 所以WebRequestContext是一个HttpRequest,但是通过RequestContext接口实现了一些特定的方法就是了, 例如getModule/getAction,而这个解析url的过程是在构建WebRequestContext对象的过程中实现的。 可以看看WebResponseContext的构造方法,这里就不详细说了。注意的是,所有的parameters最后都保存到query(一个私有的map)里边去的。 还有就是上面说到的jforum的特定url映射机制,这是通过WebRequestContext的parseFriendlyURL方法实现的, 原理就和上面提到的那样,也不详说了。

到这里,基本上整个处理流程就差不多了。现在来说说jforum里边的文件修改监听器(JForumBaseServer的startApplication流程), 如果你在使用jforum的过程中,修改了某些文件如*.sql,jforum就会重新加载修改后的配置。 我原来以为是用quartz框架来实现的,后来才知道是用jdk的TimerTask类来实现的。 请看ConfigLoader的listenForChanges方法:

FileMonitor.getInstance().addFileChangeListener(new QueriesFileListener(),
				SystemGlobals.getValue(ConfigKeys.SQL_QUERIES_GENERIC), fileChangesDelay);

这里给各个部分分一下责任,FileMonitor是大管家,负责管理所有的文件监听器;FileChangeListener是一个监听器接口, 只有一个方法,就是fileChanged(String filename),意思就是对某个filename的修改作出怎样的反应。 使用的方法也很简单,就是实现一个FileChangeListener,并和监控的文件名,检查间隔作为参数传入就可以生效了。 FileMonitor里边的实现原理就是,通过一个map(timerEntries)来保存(文件名/timertask), 每次加入一个监听器的时候,会根据文件名先移出原来的文件监听器(缺点是只能能对一个文件添加一个监听器), 然后构建一个TimerTask并加入到timerEntries中去。关于TimerTask的具体用法,可以参考api。

作为一个论坛,应用层缓存这样的东西似乎必不可少,jforum也提供了缓存配置(上面也提到一些)。 jforum提供了数种缓存实现(JForumBaseServlet的init流程),分别是DefaultCacheEngine(简单的内存实现), JBossCacheEngine,EhCacheEngine。,请看ConfigLoader的startCacheEngine方法, 流程大概就是得到cacheEngine的实现配置(SystemGlobals.properties中配置cache.engine.implementation), 然后产生CacheEngine的实例,调用它的init方法进行初始化,然后找到所有的可缓存类(实现了Cacheable接口,并在SystemGlobals.properties中配置cacheable.objects),最后把cacheEngine注入进去获得cache的能力。 虽然jforum自己实现了许多这样的注入(除了cacheEngine,还有db,dao等等), 虽然达到了一定的的目的,可是怎么说还是到处充满了Singleton的实现(参考spring2.5文档3.9. 粘合代码和可怕的singleton), 为了寻求更好的组织方式(例如使用ioc来管理对象,使用成熟的orm来隔离数据库)和获得更多的用户群(选择更广泛使用的框架帮助), 大概才会萌发jforum3的想法吧。

顺便提一下jforum的Dao实现方式(参考JForumBaseServlet的startApplication流程), 参考ConfigLoader的loadDaoImplementation方法,原理就是通过配置dao.driver(在特定的数据库配置里边如mysql.properties):

获取到DataAccessDriver的实现 -> 初始化DataAccessDriver -> 获取到所有的Dao实现。

可以这么理解,实现一个DataAccessDriver就获得一整套Dao的实现方式,对于dao里边的实现方法,给个范例:

//例行公事
PreparedStatement p = null;
ResultSet rs = null;
//获得connect,并执行named sql
p = JForumExecutionContext.getConnection().prepareStatement(SystemGlobals.getSql("GroupModel.selectById"));
p.setInt(1, groupId);
rs = p.executeQuery();
Group g = new Group();
//循环resultset进行处理
if (rs.next()) {g = this.getGroup(rs);}

整个实现很直白,就是一个jdbc实现方式来的。对于如何获取connection,查看JForumExecutionContext的getConnection(),可以注意到这么一句:

c = DBConnection.getImplementation().getConnection();

也是比较清晰的,另外可以知道的是,在每次请求的过程中,connection只会获取一次, 并在第一次获取到以后放到ThreadLocal里边去,这样在每个线程中保留一份数据(正确理解TheradLocal ), 在请求请求结束以后才释放connection(service流程中的handleFinally方法)。

JForumExecutionContext,如字面意,就是请求执行的上下文,例如上面提到的数据库连接, 还有ForumContext(放着和request,response相关的信息),context(freemarker的上下文变量), redirectTo(转发地址),contentType(响应内容格式),isCustomContent(不使用默认渲染,上面有提到), enableRollback(db是否会滚)。

jforum是可以配置权限的,可控制的权限类型放在SecurityConstants里边, 对应的配置界面是根据permissions.xml生成的(参考GroupAction的permissions)。而每个用户的权限(PermissionControl)是通过SecurityRepository来管理的

最用形成的权限系统是role(权限)-group(用户组,可以多级)-用户这样的结构图。

如何判断权限? 对于一个用户来说,为了获取用户的权限(PermissionControl),流程是这样的(详细看SecurityRepository的load方法):

获取用户信息 -> 获取用户的所有groupid并组成一个用逗号隔开的字符串groupids  -> 根据groupids获取所有的name/role_value -> 组装成RoleValueCollection -> 生成RoleCollection -> 最后生成PermissionControl

判断权限是使用SecurityRepository的canAccess(int userId, String roleName, String value)方法:

根据userid获取PermissionControl-> 如果value参数为空的话,就判断是否拥有该roleName(通过内部的RoleCollection对象的keys),就是是否含有该权限 -> 如果value参数不为空的话,除了需要含有该权限,还要拥有相应的rolevalue(通过内部的RoleCollection对象的values)。参数中的value指数可以为论坛分类id,论坛id之类,随业务而定。

总体上jforum还算清晰,大部分的业务代码没有细看(那些Command类),有兴趣可以对照着写, 大体分为三个包(admin是管理,jforum是公共页面,install是安装页面)。

既然说到验证,就顺便要说说jforum的sso验证机制 官方文档:

http://www.jforum.net/doc/SSO
http://www.jforum.net/doc/ImplementSSO
http://www.jforum.net/doc/SSOcookies
http://www.jforum.net/doc/SSOremote

有上面这些文档基本可以自己实现一个,主要就是实现net.jforum.sso接口就是了。

在Jforum的service方法里边有段(service流程中的刷新session):

ControllerUtils utils = new ControllerUtils()
utils.refreshSession();//重点

里边提到,在没有usersession的情况下,如果配置的验证类型是sso(authentication.type):

调用checkSSO(UserSession userSession)的方法 -> 生成SSO实例(使用sso.implementation来配置) -> 调用authenticateUser(RequestContext request)返回username -> 假如取不到的username,就设为匿名 -> 否则,如果不存在该用户(utils.userExists(username)则注册一个(utils.register(password, email)) -> 假如已经存在,则让用户登录(configureUserSession(userSession, utils.getUser()))

当已经存在usersession的时候,并且验证方式是sso的时候,就是验证是否有效(sso.isSessionValid(userSession, request))。 所以,整个过程和官方文档提到的流程是一样的,如果要实现自己的sso,这是实现SSO接口, 使用authenticateUser来验证不存在usersession的情况,并返回username or null, 而使用isSessionValid来判断一个已经存在的usersession是否有效。 参考上面几个连接文档,实现和已有系统的sso集成,还是比较清晰明了的。