Play1 直接调用 Action 方法,不作 302 跳转

用过 PlayFramework 的同学们应该都知道,Action 方法间的调用是进行的  302 重定向操作。

简单例子说明一下,当基于下面的 r1, r2 路由配置时,如果 Application.f1() 方法中调用了 f2() 方法,实际运作是 f1() 在调用 f2() 时,会先反向出 f2() 方法对应的路由  GET /r2,然后向 /r2 发出的一个 302 跳转.

上面也算是绕个弯形成了对 f2() 方法的调用,这也是非常合理,在 Action 中很容易理解的。

GET     /r1                                                                     Application.f1
GET     /r2                                                                    Application.f2
GET     /r3                                                                    Application.f3

为什么说会反向出 f2() 方法对应的路由,可以反证一下。

例如说在 f1() 中调用了一个 public static void f4() 方法,但是 f4() 并未出现在 routes 配置中,也就是 f4() 没有对应的路由配置,我们将会看到这样一个异常

No route found
No route able to invoke action Application.f4 with arguments {} was found.

我们可以用 curl 在命令行下对 f1() 调用 f2() 方法具体验证下 从 /r1 到  /r2 的 302 跳转:

yanbin@localhost ~> curl -i http://localhost:9000/r1
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: http://localhost:9000/r2
Content-Length: 0

因为 302 跳转关系,所以 f1() 和  f2() 方法间不存在实际的方法调用关系,也就是在  f2() 的调用栈上不存在 f1() 方法。

那我们怎么才知道达到 f1() 对 f2() 的直接调用,而不是反向出路由来进行 302  转向呢?

针对如下的代码

在 routes 中有 f1, f2, f3 三个方法相应的路由配置,它们可以被单独访问。想像一个这样的需求,当访问 /r1 时,想要把 f2() 和 f3() 的响应结果组合起来作为新的响应。

这是一个我们项目中现实的需求,现在我们清楚了,f1() 在进行 f2() 调用式就重定向到了对就的  /r2 路由,得到 "from f2" 的响应,f2() 之后的代码是多余的。我们怎么样才能避免这种默认行为呢,并且还能够取得  f2(), f3() 的结果数据。

我们还应该知道 Play1 中 Action 在 renderXxx() 时,其实是抛出的一个异常,对于 renderJSON() 是

各种 Result 就是个异常,继承自 FastRuntimeException。所以对于 renderJSON() 我们也试图 catch(RenderJson rj) 异常来获得 Action 方法的渲染数据。还必须利用 ControllerInstrumentation 的两个方法来避免 302 跳转,理想中的代码是这样的

我们可以使用 ControllerInstrumentation 的两个方法,应用如下:

再来看一下调用关系

play_action_call_2

在方法调用栈上 f1() 和 f2() 形成了毗邻的关系,并且也不再是 302 跳转,而是 200 OK 了,所以地址栏中的 /r1 不会变成 r2,但仍然是只有 f2() 的输出结果,响应数据与以前还是一样的。

也就是我们在 catch(RenderJson rj) 中并没有捕捉到异常,难道会是别的异常,不会,还是在我之前就有人把这一异常偷偷的截了去,需再度研究下。再次打开 RenderJson 类,它有个 apply(Request, Response) 方法,可以探索下这个方法是什么时候被调用的,打个断点追踪:

play_action_call_5

这时看到了 ActionInvoker.invoke(Request, Response) 时拦截了 Result 异常,并且还没把该异常吐出来,调用了 Result 的 apply 方法直接就输出到 response 中去了,并且结束本次请求的处理。

怎么样才能避免 Result 异常被 ActionInvoker 拦截掉呢,要用 ActionInvoker.invokeControllerMethod(actionMethod) 来调用 Action 方法来避免 Result 被 ActionInvoker  先拦截掉

最后的 f1 方法这么写

用 ActionInvoker.invokeControllerMethod() 调用 Action 方法时要捕获的是 InvocationTargetException 异常,它的 target 是 RenderJson 异常,再取得 json 数据。这时候 f2() 在得到调用时就不会触发 RenderJson 的 apply 方法,只有 f1() 会触发该方法。如果没有先执行 ControllerInstrumentation.initActionCall() 的话,它的 target 就是个 Redirect 的结果异常。

现在我们来浏览 http://localhost:9000/r1,结果是

play_action_call_6

最后,形成一个工具类方法 directActionCall

总结一下,

1. ControllerInstrumentation.initActionCall() 宣告后面的 Action 方法不要进行 302 跳转,否则即使是用 ActionInvoker.invokeControllerMethod 调用 Action 方法,始终捕获到的是 Redirect 异常,而不是具体的 RenderJson, RenderHtml, RenderText 等具体的结果类型异常。
2. ActionInvoker.invokeControllerMethod 告诉它自己不要去中途拦截掉 Result 异常,放马出去

这个 http://stackoverflow.com/questions/3899670/how-can-i-influence-the-redirect-behavior-in-a-play-controller 告诉了我可以 initActionCall,必要时多留意 play.classloading.enhancers.ControllersEnhancer 类的实现及功能。

补充一下,如果是通过新线程来调用其他的 Action 方法,还能把当前的 Request 带过去,像这样:

然后在 directActionCall() 方法中第一行加上

在还要注意一点就是在 Controller 中 Controller.params 和 Controller.request.params 的数据可能不一样的,我碰过前者为 null,前者是有值的。

接着补充:我们把对 Action 方法的调用关在

ControllerInstrumentation.initActionCall()
ControllerInstrumentation.stopActionCall()

中,虽然上面两个方法是静态的,但它们不会影响到其他线程默认行为,因为它们是在 ThreadLocal 上作的标记,原代码是这样的

本文链接 https://yanbin.blog/playframework-1-invoke-action-directly-no-302/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

2 Comments
Inline Feedbacks
View all comments
zhanghaikun
zhanghaikun
6 years ago

可以直接在被调用方法上加@Util。