Play1 直接调用 Action 方法,不作 302 跳转
用过 PlayFramework 的同学们应该都知道,Action 方法间的调用是进行的 302 重定向操作。
简单例子说明一下,当基于下面的 r1, r2 路由配置时,如果 Application.f1() 方法中调用了 f2() 方法,实际运作是 f1() 在调用 f2() 时,会先反向出 f2() 方法对应的路由 GET /r2,然后向 /r2 发出的一个 302 跳转.
上面也算是绕个弯形成了对 f2() 方法的调用,这也是非常合理,在 Action 中很容易理解的。
为什么说会反向出 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 的两个方法,应用如下:
再来看一下调用关系
在方法调用栈上 f1() 和 f2() 形成了毗邻的关系,并且也不再是 302 跳转,而是 200 OK 了,所以地址栏中的 /r1 不会变成 r2,但仍然是只有 f2() 的输出结果,响应数据与以前还是一样的。
也就是我们在 catch(RenderJson rj) 中并没有捕捉到异常,难道会是别的异常,不会,还是在我之前就有人把这一异常偷偷的截了去,需再度研究下。再次打开 RenderJson 类,它有个 apply(Request, Response) 方法,可以探索下这个方法是什么时候被调用的,打个断点追踪:
这时看到了 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,结果是
最后,形成一个工具类方法 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,必要时多留意
补充一下,如果是通过新线程来调用其他的 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's Blog
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
简单例子说明一下,当基于下面的 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我们可以用 curl 在命令行下对 f1() 调用 f2() 方法具体验证下 从 /r1 到 /r2 的 302 跳转:
No route able to invoke action Application.f4 with arguments {} was found.
yanbin@localhost ~> curl -i http://localhost:9000/r1因为 302 跳转关系,所以 f1() 和 f2() 方法间不存在实际的方法调用关系,也就是在 f2() 的调用栈上不存在 f1() 方法。
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: http://localhost:9000/r2
Content-Length: 0
那我们怎么才知道达到 f1() 对 f2() 的直接调用,而不是反向出路由来进行 302 转向呢?
针对如下的代码
1package controllers;
2
3public class Application extends BaseController {
4
5 public static void f1() {
6 f2();
7 f3();
8 renderJSON(f2 和 f3 的结果组合);
9 }
10
11 public static void f2() {
12 renderJSON("from f2");
13 }
14
15 public static void f3() {
16 renderJSON("from f3");
17 }
18}在 routes 中有 f1, f2, f3 三个方法相应的路由配置,它们可以被单独访问。想像一个这样的需求,当访问 /r1 时,想要把 f2() 和 f3() 的响应结果组合起来作为新的响应。
这是一个我们项目中现实的需求,现在我们清楚了,f1() 在进行 f2() 调用式就重定向到了对就的 /r2 路由,得到 "from f2" 的响应,f2() 之后的代码是多余的。我们怎么样才能避免这种默认行为呢,并且还能够取得 f2(), f3() 的结果数据。
我们还应该知道 Play1 中 Action 在 renderXxx() 时,其实是抛出的一个异常,对于 renderJSON() 是
1protected static void renderJSON(String jsonString) {
2 throw new RenderJson(jsonString);
3}各种 Result 就是个异常,继承自 FastRuntimeException。所以对于 renderJSON() 我们也试图 catch(RenderJson rj) 异常来获得 Action 方法的渲染数据。还必须利用 ControllerInstrumentation 的两个方法来避免 302 跳转,理想中的代码是这样的
我们可以使用 ControllerInstrumentation 的两个方法,应用如下:
1public static void f1() {
2 ControllerInstrumentation.initActionCall();
3 String json = null;
4 try{
5 f2();
6 }catch(RenderJson rj){
7 try {
8 Field jsonField = RenderJson.class.getDeclaredField("json");
9 jsonField.setAccessible(true);
10 json = (String)jsonField.get(rj);
11 } catch (Exception e) {
12 }
13 }
14 ControllerInstrumentation.stopActionCall();
15 renderJSON("New Json from f1: " + json);
16}再来看一下调用关系
在方法调用栈上 f1() 和 f2() 形成了毗邻的关系,并且也不再是 302 跳转,而是 200 OK 了,所以地址栏中的 /r1 不会变成 r2,但仍然是只有 f2() 的输出结果,响应数据与以前还是一样的。也就是我们在 catch(RenderJson rj) 中并没有捕捉到异常,难道会是别的异常,不会,还是在我之前就有人把这一异常偷偷的截了去,需再度研究下。再次打开 RenderJson 类,它有个 apply(Request, Response) 方法,可以探索下这个方法是什么时候被调用的,打个断点追踪:
这时看到了 ActionInvoker.invoke(Request, Response) 时拦截了 Result 异常,并且还没把该异常吐出来,调用了 Result 的 apply 方法直接就输出到 response 中去了,并且结束本次请求的处理。怎么样才能避免 Result 异常被 ActionInvoker 拦截掉呢,要用 ActionInvoker.invokeControllerMethod(actionMethod) 来调用 Action 方法来避免 Result 被 ActionInvoker 先拦截掉。
最后的 f1 方法这么写
1public static void f1() {
2 ControllerInstrumentation.initActionCall();
3 String json = null;
4 try{
5 Method f2Method = Application.class.getDeclaredMethod("f2");
6 ActionInvoker.invokeControllerMethod(f2Method);
7 }
8 catch(InvocationTargetException ite){
9 if(ite.getTargetException() instanceof RenderJson){
10 RenderJson renderJson = (RenderJson)ite.getTargetException();
11 try {
12 Field jsonField = RenderJson.class.getDeclaredField("json");
13 jsonField.setAccessible(true);
14 json = (String)jsonField.get(renderJson);
15 } catch (Exception e) {
16 }
17 }
18 }
19 catch(Exception ex){
20 ex.printStackTrace();
21 }finally{
22 ControllerInstrumentation.stopActionCall();
23 }
24 renderJSON("New Json from f1: " + json);
25}用 ActionInvoker.invokeControllerMethod() 调用 Action 方法时要捕获的是 InvocationTargetException 异常,它的 target 是 RenderJson 异常,再取得 json 数据。这时候 f2() 在得到调用时就不会触发 RenderJson 的 apply 方法,只有 f1() 会触发该方法。如果没有先执行 ControllerInstrumentation.initActionCall() 的话,它的 target 就是个 Redirect 的结果异常。
现在我们来浏览 http://localhost:9000/r1,结果是
最后,形成一个工具类方法 directActionCall 1public static void f1() {
2 String json = directActionCall(Application.class, "f2");
3 renderJSON("New json from f1: " +json);
4}
5
6public static String directActionCall(Class<? extends Controller> controllerClass, String methodName, Object...parameters){
7 String json = null;
8 Class[] parameterClasses = new Class[parameters.length];
9 for(int i=0; i<parameters.length; i++){
10 parameterClasses[i] = parameters[i].getClass();
11 }
12 ControllerInstrumentation.initActionCall();
13 try{
14 Method actionMethod = controllerClass.getDeclaredMethod(methodName, parameterClasses);
15 ActionInvoker.invokeControllerMethod(actionMethod);
16 }
17 catch(InvocationTargetException ite){
18 if(ite.getTargetException() instanceof RenderJson){
19 RenderJson renderJson = (RenderJson)ite.getTargetException();
20 try {
21 Field jsonField = RenderJson.class.getDeclaredField("json");
22 jsonField.setAccessible(true);
23 json = (String)jsonField.get(renderJson);
24 } catch (Exception e) {
25 }
26 }
27 }
28 catch(Exception ex){
29 }finally{
30 ControllerInstrumentation.stopActionCall();
31 }
32 return json;
33}总结一下,
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 带过去,像这样:
1final Request currentRequest = Request.current();
2
3callables.add(new Callable<String>(){
4 public String call() throws Exception {
5 return directActionCall(currentRequest, Application.class, "foo", params);
6 }
7});然后在 directActionCall() 方法中第一行加上
1Request.current.set(curentRequest);在还要注意一点就是在 Controller 中 Controller.params 和 Controller.request.params 的数据可能不一样的,我碰过前者为 null,前者是有值的。
接着补充:我们把对 Action 方法的调用关在
ControllerInstrumentation.initActionCall()
ControllerInstrumentation.stopActionCall()
中,虽然上面两个方法是静态的,但它们不会影响到其他线程默认行为,因为它们是在 ThreadLocal 上作的标记,原代码是这样的
1/**
2 * Runtime part needed by the instrumentation
3 */
4public static class ControllerInstrumentation {
5
6 public static boolean isActionCallAllowed() {
7 return allow.get();
8 }
9
10 public static void initActionCall() {
11 allow.set(true);
12 }
13
14 public static void stopActionCall() {
15 allow.set(false);
16 }
17 static ThreadLocal<Boolean> allow = new ThreadLocal<Boolean>();
18}[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。