Play! Framework 学习笔记(二):ActionInvoker源码分析
>点我返回上一篇:Play! Framework 学习笔记(一):初识Play <
再往下看,我被雷到了= =# 第一遍看没反应过来,因为我见识较浅,也从来没这么写过代码,之前也没看过这样写的。就是这句:
throw new RenderTemplate(template, templateBinding.data);
这里用RenderTemplate的构造方法new了一个RenderTemplate对象,
然后.......抛出去了。
看看这里抛出去的是什么,先进去看看RenderTemplate类的实现:
public class RenderTemplate extends Result {
再看看Result
public abstract class Result extends RuntimeException {
原来RenderTemplate是个RuntimeException
http://java.sun.com/docs/books/tutorial/essential/exceptions/runtime.html
提到RuntimeException,我们最熟悉的可能就是NullPointerException,由于程序编写疏漏,造成访问空指针时,就会抛出此异常。
我们写个最简单的代码就是这样
public class TestString {
public static void main(String[] args){
testNull(null);
}
public static void testNull(String a){
a.charAt(0);
}
}
main方法里对testNull传了个null,但是charAt方法需要String obj,此时却是null,触发NullPointerException退出程序。
在看看console的信息:
Exception in thread "main" java.lang.NullPointerException
at com.langtest.TestString.testNull(TestString.java:9)
at com.langtest.TestString.main(TestString.java:6)
main 线程引发空指针异常,程序到main后也没对此异常处理的逻辑,导致程序退出,并在控制台打印出错信息。
throw之后的异常对象沿着方法栈往回扔(即调用此方法的方法),如果一直没有截获异常,则一直扔到栈底。
既然是个异常,下一步则是抛向上级调用者,往下走,我们找这个“不是异常的异常”是在何处被截获的。。。 (对比JAVA官网那篇对运行时异常小心翼翼的陈述,这种做法简直有点#_#,要么是因为我太菜,不能理解这么用的高明之处吧)
Debug F6后,程序转至play.mvc.ActionInvoker的invoke方法中的catch语句
Result actionResult = null;
ControllerInstrumentation.initActionCall();
try {
Java.invokeStatic(actionMethod, getActionMethodArgs(actionMethod));
} catch (InvocationTargetException ex) {
// It's a Result ? (expected)
if (ex.getTargetException() instanceof Result) {
actionResult = (Result) ex.getTargetException();
} else {
// @Catch
Object[] args = new Object[]{ex.getTargetException()};
List<Method> catches = Java.findAllAnnotatedMethods(Controller.getControllerClass(), Catch.class);
Collections.sort(catches, new Comparator<Method>() {
try... catch块中:
try块:用JAVA的反射机制invoke静态方法,这里其实就是invoke了我们在控制器中写的index方法。
自编最土最俗的方式理解反射帮助同学理解反射(可能不一定准确):
反射说:“我是个万能的对象生成器和方法调用器,只要你给我类名,方法名以及相应的参数,我就能帮你new出对象或者调用相应的方法”
不过任何事情都是双刃剑,反射的能力过于强大,可以任意的生成和操作对象,所以就赋予程序员Do evil的可能.
而JAVA本身的设计是个尽量安全的语言,没有指针运算,虚拟机帮着整理内存,异常机制让做出健壮性的程序变得简化不少,所以我觉得反射机制对于JAVA的总体设计思想来说,还是不小的challenge。
此处的invokeStatic方法的两个参数,用debug工具的watch功能可以看到
actionMethod就是通过反射被invoke的方法。
catch块:拦截InvocationTargetException,这个exception是当通过反射的方式invoke的方法throw异常时,反射机制会触发这个异常,并将上一级throw出的异常存为这个异常的taget变量。
本例的过程是这样的,Play框架通过反射的方式invoke 控制器中的index方法(Application.index()),然后进入render(),在render方法里调用renderTemplate方法,在此方法将RenderTemplate这个异常(再次汗)抛出,反射机制发现有异常抛出,随后抛出InvocationTargetException异常,并将RenderTemplate存入InvocationTargetException的target变量..Play在使用反射invoke方法处catch了此异常,然后把target引用的RenderTemplate取出,则得到了render完成的模板。
无论何时,我们都得有追本溯源的精神,下面是几个疑问:
①为什么框架这么设计
②如果可以,自己想一个替代的实现方式,对比此方式看看为何要用这么怪的设计
ActionInvoker源码分析
既然现在Debug走到ActionInvoker,不妨看看这个类:
由类上的注释:
这个类是根据Http request Invoke相应的action。
这个类没有成员变量和函数,只有三个共有的静态方法,这三个方法分别是(用附加注释的方法解释):
public class ActionInvoker {
//响应请求的主函数,其实ActionInvoker这个类主要用途就是放置这个方法,因此这个类也同样也不具备面向对象特性的类,这个类注重的是响应HTTP请求的逻辑
public static void invoke(Http.Request request, Http.Response response) {
//通过传入的action(ie:Application.index),得到对应的method,以便反射时invoke使用
public static Object[] getActionMethod(String fullAction) {
//从method中取出方法的参数,这两个get方法都是为反射调用服务的。
public static Object[] getActionMethodArgs(Method method) throws Exception {
}
可见invoke是Play框架的运行的核心控件(说是核心是因为web框架的主要职责就是完成处理HTTP请求的过程)
为了了解Play的核心运行机制,我们断开debug线程,在invoke方法设上断点,重新跑Debug
进入方法,传入该方法的两个参数是由上一层调用者HttpHandler的内部类MinaInvocation的execute方法传入的。由于HttpHandler里做的工作比ActionInvoker更加基础(Mina应用服务器下的http协议处理及session管理),我们到后面再研究。
public static void invoke(Http.Request request, Http.Response response) {
Monitor monitor = null;
try {
if (!Play.started) {
return;
}
Http.Request.current.set(request);
Http.Response.current.set(response);
Scope.Params.current.set(new Scope.Params());
Scope.RenderArgs.current.set(new Scope.RenderArgs());
Scope.Session.current.set(Scope.Session.restore());
Scope.Flash.current.set(Scope.Flash.restore());
......
}
先new一个Monitor ,用来监控
然后判断Play是否启动
随后的是一系列xxx.xxx.current.set方法:
这里的current变量都是ThreadLocal
public static ThreadLocal<Request> current = new ThreadLocal<Request>();
@
//对于Java开发,ThreadLocal是必须要了解的概念。
//ThreadLocal虽然是个对象,但是ThreadLocal的set方法存的东西并不是放在ThreadLocal对象里
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Many applications will have no need for
* this functionality, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current threads' copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//由上可见,set方法首先取得当前的Thread对象,然后取得该线程的ThreadLocalMap ,如果map不为空,则写入map,以当前的ThreadLocal对象为key,将传入的value存入map。
//这里也只是个引子,没概念的可能很难理解清楚,毕竟ThreadLocal也不是我这么三言两语能说清的,建议同学多谷哥一下,多看多用多体会。
将Request,Response以及Scope的引用放入当前线程后,实际上是完成了线程的初始化过程。
// 1. Route and resolve format if not already done
if (request.action == null) {
for (PlayPlugin plugin : Play.plugins) {
plugin.routeRequest(request);
}
Router.route(request);
}
request.resolveFormat();
Router.route(request); 根据请求的URL找到router中相应的action,并将action的名字赋值给request.action.
request.resolveFormat();此时request中format为html,如果request中format为null,则根据http头来取得相应的format
往下走:
// 2. Find the action method
Method actionMethod = null;
try {
Object[] ca = getActionMethod(request.action);
actionMethod = (Method) ca[1];
request.controller = ((Class<?>) ca[0]).getName().substring(12);
request.actionMethod = actionMethod.getName();
request.action = request.controller + "." + request.actionMethod;
request.invokedMethod = actionMethod;
} catch (ActionNotFoundException e) {
throw new NotFound(String.format("%s action not found", e.getAction()));
}
声明一个Method变量,供后面反射Invoke.
getActionMethod(request.action) 前面提到过了,通过request.action这个String得到存有application.index()方法相应Method对象的obj数组
得到Method对象(ca[1],ca[0]存放的是对应controllers.Application的Class对象)后,将request对象中与Action相关的成员变量赋值
此处:request.controller值为Application,request.actionMethod值为index,后面两个变量,一个是照前两个拼出来的action,另一个传入的是Method对象
继续:下面的代码为合并action用到的参数:
// 3. Prepare request params
Scope.Params.current().__mergeWith(request.routeArgs);
// add parameters from the URI query string
Scope.Params.current()._mergeWith(UrlEncodedParser.parseQueryString(new ByteArrayInputStream(request.querystring.getBytes("utf-8"))));
Lang.resolvefrom(request);
routeArgs是在route中附加的http参数
/**
* Additinal HTTP params extracted from route
*/
public Map<String, String> routeArgs;
除此之外还将QueryString中的参数也合并进来
后面的Lang.resolvefrom(request)没仔细看实现,看Lang的包名中与i18n有关,这部分等以后专门看国际化的实现单独写吧(继续欠账)
下面的代码,又看到雷人的片段了...
// 4. Easy debugging ...
if (Play.mode == Play.Mode.DEV) {
Controller.class.getDeclaredField("params").set(null, Scope.Params.current());
Controller.class.getDeclaredField("request").set(null, Http.Request.current());
Controller.class.getDeclaredField("response").set(null, Http.Response.current());
Controller.class.getDeclaredField("session").set(null, Scope.Session.current());
Controller.class.getDeclaredField("flash").set(null, Scope.Flash.current());
Controller.class.getDeclaredField("renderArgs").set(null, Scope.RenderArgs.current());
Controller.class.getDeclaredField("validation").set(null, Java.invokeStatic(Validation.class, "current"));
}
!!!!!!
Controller.class.getDeclaredField("xxx").set(null,xxx);
这里Play用反射的方式将Controller中受保护的静态变量强行赋值!!!
而且还是自己设置了变量的访问权限后又自己暴力**赋值......
不过作者在注释里加了句// 4. Easy debugging ...,可能是对此做法无奈的解释吧。
让我不禁又想起那句"Java中的关键字只是给编译器看的"...
不过仔细想想,这可能也是开源项目的特色之一,如果在企业里写这种代码直接破坏框架,不知道老板的脸会怎么黑- -|||..
这里废话这么多,是因为我们学生在校时往往喜欢比较另类的代码,所以学弟学妹需要醒目一下:
仔细想想
我们初涉编程是不是很佩服算出i=3,k=(i++)+(++i)+(i++)的同学
我们是不是佩服过能看懂16层嵌套并且变量名没任何意义的if else
我们是不是对于能写出令人头晕的指针运算的同学无比崇敬过。
自己玩玩可以,但是以后工作,一份代码的生命期有可能是伴随着企业的生命期。
写一些另类代码有很多恶果,最明显的是难以维护。
至于类似修改私有变量的危险行为,虽然用起来比较cool,但是对于个人学习来说,不提倡,尽量不要给自己灌输用非正常途径解决问题的思想。
不过这里,没说play不好的意思,只是对于咱们来说,学生阶段对语言的理解力掌控力还太差,需要不断深入学习,假以时日,能有技术方面的批判性思维,能把这些java特性用的恰到好处当然是好事。
又跑题了,回到主题....这部分是判断play的模式(play有两种运行模式DEV和实际运行模式,在config里文件配置切换),在开发模式下,直接将request,response和scope等赋值给Cotroller类相应的静态变量
可能便于实际invoke控制器时访问这些值.
#遍历各个PlugIn看在action invoke前做些动作
ControllerInstrumentation.stopActionCall();
for (PlayPlugin plugin : Play.plugins) {
plugin.beforeActionInvocation(actionMethod);
}
ControllerInstrumentation这个类的作用是对allow这个标志位进行操作,allow是个ThreadLocal<Boolean>,对其set值则将其引用存入当前Thread内,换句话说,其实是对Thread做了标记
public static class ControllerInstrumentation {
public static boolean isActionCallAllowed() {
return allow.get();
}
public static void initActionCall() {
allow.set(true);
}
public static void stopActionCall() {
allow.set(false);
}
static ThreadLocal<Boolean> allow = new ThreadLocal<Boolean>();
}
beforeActionInvocation方法则是在action前Plugin做的事情,这里我看了一下都是空的实现.
#打开monitor
// Monitoring
monitor = MonitorFactory.start(request.action + "()");
#找到标记@Before Annotation的方法,并先于action invoke执行
// 5. Invoke the action
try {
// @Before
List<Method> befores = Java.findAllAnnotatedMethods(Controller.getControllerClass(), Before.class);
Collections.sort(befores, new Comparator<Method>() {
public int compare(Method m1, Method m2) {
Before before1 = m1.getAnnotation(Before.class);
Before before2 = m2.getAnnotation(Before.class);
return before1.priority() - before2.priority();
}
});
......
Controller.getControllerClass()方法返回class controllers.Application
Java.findAllAnnotatedMethods()找到所有带有@Before Annotation的方法
再根据各个方法的优先级,来对befores中的Method排序
此处实现比较器用了匿名内部类,按Before的priority进行排序
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Before {
/**
* Does not intercept these actions
*/
String[] unless() default {};
/**
* Interceptor priority (0 is high priority)
*/
int priority() default 0;
}
Annotation Before除了成员变量priority外,还有一个String数组变量unless,存放的是action的名字,表示不拦截这些action。
看看这部分的实现
ControllerInstrumentation.stopActionCall();
//遍历包含Before Annotation的方法
for (Method before : befores) {
//取出当前Before action的unless数组
String[] unless = before.getAnnotation(Before.class).unless();
//设置标志位
boolean skip = false;
//遍历unless数组
for (String un : unless) {
if (!un.contains(".")) {
un = before.getDeclaringClass().getName().substring(12) + "." + un;
}
//如果unless与当前被调用的action名字相同,标志位skip设为true,退出循环
if (un.equals(request.action)) {
skip = true;
break;
}
}
//如果skip为false,调用before方法
if (!skip) {
//加个保护,判断被调用方法是否为静态,因为下面用到得是invokeStatic..
if (Modifier.isStatic(before.getModifiers())) {
before.setAccessible(true);
Java.invokeStatic(before, getActionMethodArgs(before));
}
}
}
通过Before拦截器后,再往下就是我们前面看到的实际执行Action的地方:
//声明一个Result变量用来保存方法调用的结构
Result actionResult = null;
//与之前stopActionCall()相反,这里调用initActionCall()将allow设为true,意思是允许此线程invoke方法
ControllerInstrumentation.initActionCall();
try {
//invoke action
Java.invokeStatic(actionMethod, getActionMethodArgs(actionMethod));
} catch (InvocationTargetException ex) {
// It's a Result ? (expected)
if (ex.getTargetException() instanceof Result) {
//得到调用action后返回的Result
actionResult = (Result) ex.getTargetException();
//else部分本例未涉及,先跳过不管
} else {
.....
执行完action,下面的代码部分是After拦截器,和Before基本一致,不赘述。
随后将monitor关闭
之后...继续将返回结果往上扔。
// Ok, rethrow the original action result
if (actionResult != null) {
throw actionResult;
}
throw new NoResult();
catch (InvocationTargetException ex) {
// It's a Result ? (expected)
if (ex.getTargetException() instanceof Result) {
throw (Result) ex.getTargetException();
}
// Rethrow the enclosed exception
if (ex.getTargetException() instanceof PlayException) {
throw (PlayException) ex.getTargetException();
}
StackTraceElement element = PlayException.getInterestingStrackTraceElement(ex.getTargetException());
if (element != null) {
throw new JavaExecutionException(Play.classes.getApplicationClass(element.getClassName()), element.getLineNumber(), ex.getTargetException());
}
throw new JavaExecutionException(Http.Request.current().action, ex);
}
一直扔到invoke方法的第一个try..catch块
public static void invoke(Http.Request request, Http.Response response) {
Monitor monitor = null;
try {
.......
}catch (Result result) {
//遍历执行plugin的onActionInvocationResult()方法,对结果进行处理
for (PlayPlugin plugin : Play.plugins) {
plugin.onActionInvocationResult(result);
}
// Ok there is a result to apply
// Save session & flash scope now
Scope.Session.current().save();
Scope.Flash.current().save();
//相应结果的apply方法,此处result实际是RenderTemplate对象,它的apply方法最终的HTML输出
result.apply(request, response);
//这里可见Plugin的功能是非常灵活的,因为几乎在action生命期的每阶段都出现,其实到后面可以发现PlugIn几乎随处可见,否则怎么能叫做框架的插件呢= =#
for (PlayPlugin plugin : Play.plugins) {
plugin.afterActionInvocation();
}
最后看看RenderTemplate类
public class RenderTemplate extends Result {
private Template template;
private String content;
Map<String,Object> args;
public RenderTemplate(Template template, Map<String,Object> args) {
this.template = template;
this.args = args;
this.content = template.render(args);
}
//apply方法是在invoke方法截获Result后,确认其是需要的返回结果后,调用的结果最终执行代码
public void apply(Request request, Response response) {
try {
setContentTypeIfNotSet(response, MimeTypes.getContentType(template.name, "text/plain"));
response.out.write(content.getBytes("utf-8"));
} catch(Exception e) {
throw new UnexpectedException(e);
}
}
}
执行完结果代码后,来到Invoke方法的结尾处,仍处于catch块,即找到@final的方法并执行。
// @Finally
//这个if判断不知道有什么意义,前面在get action的时候,就是找Application(Controller Class)的action方法,此处怎么会得到null呢,等以后理解加深再解释吧。
if (Controller.getControllerClass() != null) {
try {
List<Method> allFinally = Java.findAllAnnotatedMethods(Controller.getControllerClass(), Finally.class);
//后面略,与@before和@after同
}
Controller.getControllerClass()这个方法,涉及到Play的classloader,大概看了一下,比较复杂,等以后专门研究。
不过其中发现一些比较核心的与play热加载功能相关的代码,如下:
byte[] bc = BytecodeCache.getBytecode(name, applicationClass.javaSource);
if (bc != null) {
applicationClass.enhancedByteCode = bc;
applicationClass.javaClass = defineClass(applicationClass.name, applicationClass.enhancedByteCode, 0, applicationClass.enhancedByteCode.length, protectionDomain);
resolveClass(applicationClass.javaClass);
Logger.trace("%sms to load class %s from cache", System.currentTimeMillis() - start, name);
return applicationClass.javaClass;
}
这里大概能看出,play可以直接通过读java源代码来动态的生成java class.这应该与Play修改代码不需编译就能运行有关。
小结:到此处,从两个层次学习了Play框架的中处理和响应请求的模块。
最里面一层是Controller层,就是Application,这里放置的是Request最终invoke的action
往外一层是ActionInvoker,负责通过Http Request来判断需要调用的action,并执行调用,此外,还对action起拦截器作用分别在action的生命期的几个阶段Before,After和Finally阶段进行拦截并执行有相应Annotation的方法。除了上述两个作用,ActionInvoker还负责执行PlugIn。可以看出ActionInvoker的职责是控制action。
由此容易想到,ActionInvoker外面应该还有一层,负责实际获取客户端的HTTP Request,并转给ActionInvoker,是的,这个类就是HttpHandler,在下一篇我会详细分析。
画图表示从客户端的Request进入Play到Response返回并跳出Play的过程
呼,写了半天今天就写了这么短个小节,这么写下去不知写到猴年马月了,~~~~(>_<)~~~~
下一篇会重点分析HttpHandler源码,从而更加深刻理解此流程,这篇就到此为止。