playframework拦截器和热加载 源码浅析
继上一篇 playframework拦截器,这次来看看play怎么样实现拦截行为,同时看看play magic—— hot swap的实现原理。(本文基于play1.2版本)
要想实现http拦截行为,需要在拿到request信息后在路由分发到具体实现类之前做点文章,我们把目光锁定到PlayHandler的messageReceived方法下,发现最终会执行这行代码:
Invoker.invoke(new NettyInvocation(request, response, ctx, nettyRequest, messageEvent));
NettyInvocation类实现了Runable接口,它是带有所有用户请求信息的一个线程。
跟进到invoke方法 :
/** * Run the code in a new thread took from a thread pool. * @param invocation The code to run * @return The future object, to know when the task is completed */ public static Future<?> invoke(final Invocation invocation) { Monitor monitor = MonitorFactory.getMonitor("Invoker queue size", "elmts."); monitor.add(executor.getQueue().size()); invocation.waitInQueue = MonitorFactory.start("Waiting for execution"); return executor.submit(invocation); }上面的方法把带有完整信息的线程加入到executor线程池,线程池会决定是否立即执行该线程。
当执行submit(invocation)时会回调NettyInvocation的run方法:
@Override public void run() { try { if (Logger.isTraceEnabled()) { Logger.trace("run: begin"); } super.run(); } catch (Exception e) { serve500(e, ctx, nettyRequest); } if (Logger.isTraceEnabled()) { Logger.trace("run: end"); } }
进入 super.run(),看注释就知道,高潮来了:
/** * It's time to execute. */ public void run() { if (waitInQueue != null) { waitInQueue.stop(); } try { preInit(); if (init()) { before(); execute(); after(); onSuccess(); } } catch (Suspend e) { suspend(e); after(); } catch (Throwable e) { onException(e); } finally { _finally(); } } }preInit()清理当前的线程池。
before() 和 after() 为自定义的classLoader和加载plugin做准备和善后工作
追踪execute()方法,发现最终会执行ActionInvoker的invoke方法
public static void invoke(Http.Request request, Http.Response response) { Monitor monitor = null; try { resolve(request, response); Method actionMethod = request.invokedMethod; // 1. Prepare request params Scope.Params.current().__mergeWith(request.routeArgs); // add parameters from the URI query string String encoding = Http.Request.current().encoding; Scope.Params.current()._mergeWith(UrlEncodedParser.parseQueryString(new ByteArrayInputStream(request.querystring.getBytes(encoding)))); // 2. 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("routeArgs").set(null, Scope.RouteArgs.current()); Controller.class.getDeclaredField("validation").set(null, Validation.current()); } ControllerInstrumentation.stopActionCall(); Play.pluginCollection.beforeActionInvocation(actionMethod); // Monitoring monitor = MonitorFactory.start(request.action + "()"); // 3. Invoke the action try { // @Before handleBefores(request); // Action Result actionResult = null; String cacheKey = null; // Check the cache (only for GET or HEAD) if ((request.method.equals("GET") || request.method.equals("HEAD")) && actionMethod.isAnnotationPresent(CacheFor.class)) { cacheKey = actionMethod.getAnnotation(CacheFor.class).id(); if ("".equals(cacheKey)) { cacheKey = "urlcache:" + request.url + request.querystring; } actionResult = (Result) play.cache.Cache.get(cacheKey); } if (actionResult == null) { ControllerInstrumentation.initActionCall(); try { inferResult(invokeControllerMethod(actionMethod)); } catch(Result result) { actionResult = result; // Cache it if needed if (cacheKey != null) { play.cache.Cache.set(cacheKey, actionResult, actionMethod.getAnnotation(CacheFor.class).value()); } } catch (InvocationTargetException ex) { // It's a Result ? (expected) if (ex.getTargetException() instanceof Result) { actionResult = (Result) ex.getTargetException(); // Cache it if needed if (cacheKey != null) { play.cache.Cache.set(cacheKey, actionResult, actionMethod.getAnnotation(CacheFor.class).value()); } } else { // @Catch Object[] args = new Object[]{ex.getTargetException()}; List<Method> catches = Java.findAllAnnotatedMethods(Controller.getControllerClass(), Catch.class); Collections.sort(catches, new Comparator<Method>() { public int compare(Method m1, Method m2) { Catch catch1 = m1.getAnnotation(Catch.class); Catch catch2 = m2.getAnnotation(Catch.class); return catch1.priority() - catch2.priority(); } }); ControllerInstrumentation.stopActionCall(); for (Method mCatch : catches) { Class[] exceptions = mCatch.getAnnotation(Catch.class).value(); if (exceptions.length == 0) { exceptions = new Class[]{Exception.class}; } for (Class exception : exceptions) { if (exception.isInstance(args[0])) { mCatch.setAccessible(true); inferResult(invokeControllerMethod(mCatch, args)); break; } } } throw ex; } } } // @After handleAfters(request); monitor.stop(); monitor = null; // OK, re-throw the original action result if (actionResult != null) { throw actionResult; } throw new NoResult(); } catch (IllegalAccessException ex) { throw ex; } catch (IllegalArgumentException ex) { throw ex; } catch (InvocationTargetException ex) { // It's a Result ? (expected) if (ex.getTargetException() instanceof Result) { throw (Result) ex.getTargetException(); } // Re-throw 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); } } catch (Result result) { Play.pluginCollection.onActionInvocationResult(result); // OK there is a result to apply // Save session & flash scope now Scope.Session.current().save(); Scope.Flash.current().save(); result.apply(request, response); Play.pluginCollection.afterActionInvocation(); // @Finally handleFinallies(request, null); } catch (PlayException e) { handleFinallies(request, e); throw e; } catch (Throwable e) { handleFinallies(request, e); throw new UnexpectedException(e); } finally { if (monitor != null) { monitor.stop(); } } }第37、74、105、152行分别定义了play的各种拦截顺序和行为,方法体中定义了自己具体的拦截规则。
private static void handleBefores(Http.Request request) throws Exception { 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(); } }); ControllerInstrumentation.stopActionCall(); for (Method before : befores) { String[] unless = before.getAnnotation(Before.class).unless(); String[] only = before.getAnnotation(Before.class).only(); boolean skip = false; for (String un : only) { if (!un.contains(".")) { un = before.getDeclaringClass().getName().substring(12).replace("$", "") + "." + un; } if (un.equals(request.action)) { skip = false; break; } else { skip = true; } } for (String un : unless) { if (!un.contains(".")) { un = before.getDeclaringClass().getName().substring(12).replace("$", "") + "." + un; } if (un.equals(request.action)) { skip = true; break; } } if (!skip) { before.setAccessible(true); inferResult(invokeControllerMethod(before)); } } }
第56行会真正的执行用户请求。在这里我们可以非常清晰的看到 所有httpRequest过来后都会先检查是否有@Before注解的方法需要优先执行。用户请求执行完毕后,又会检查是否有@After 和@Finally 注解的方法需要执行。当用户请求执行抛出异常后这里会catch住,同时检查是否有@Catch注解的方法需要执行,这就是play的filter chain。
然后我们再来谈谈 play的热部署机制
所谓的热加载是指无需重启JVM就可以加载修改过的类,更新运行时的class行为。
play做到了只需刷新页面就可以实现play的热加载
因为load class是在JVM 里面的native方法执行的,要想执行热加载有两条路:
1.修改JVM源码。这个就像给自己挖个大坑,随时都存在性能隐患,各种专家强烈不建议,play也没走这条路。
2.实现自己的classLoader,并且创建对象的行为,指定为用自定义的classLoader加载的class,play就是这么干的。
在上面的run()方法中 执行 execute()前有一个init()方法
/** * Init the call (especially usefull in DEV mode to detect changes) */ public boolean init() { Thread.currentThread().setContextClassLoader(Play.classloader); Play.detectChanges(); if (!Play.started) { if (Play.mode == Mode.PROD) { throw new UnexpectedException("Application is not started"); } Play.start(); } InvocationContext.current.set(getInvocationContext()); return true; }我们可以发现
Play.detectChanges()
里面有这么一句
classloader.detectChanges();这就是play自己的classLoader了
/** * Detect Java changes */ public void detectChanges() { // Now check for file modification List<ApplicationClass> modifieds = new ArrayList<ApplicationClass>(); for (ApplicationClass applicationClass : Play.classes.all()) { if (applicationClass.timestamp < applicationClass.javaFile.lastModified()) { applicationClass.refresh(); modifieds.add(applicationClass); } } Set<ApplicationClass> modifiedWithDependencies = new HashSet<ApplicationClass>(); modifiedWithDependencies.addAll(modifieds); if (modifieds.size() > 0) { modifiedWithDependencies.addAll(Play.pluginCollection.onClassesChange(modifieds)); } List<ClassDefinition> newDefinitions = new ArrayList<ClassDefinition>(); boolean dirtySig = false; for (ApplicationClass applicationClass : modifiedWithDependencies) { if (applicationClass.compile() == null) { Play.classes.classes.remove(applicationClass.name); currentState = new ApplicationClassloaderState();//show others that we have changed.. } else { int sigChecksum = applicationClass.sigChecksum; applicationClass.enhance(); if (sigChecksum != applicationClass.sigChecksum) { dirtySig = true; } BytecodeCache.cacheBytecode(applicationClass.enhancedByteCode, applicationClass.name, applicationClass.javaSource); newDefinitions.add(new ClassDefinition(applicationClass.javaClass, applicationClass.enhancedByteCode)); currentState = new ApplicationClassloaderState();//show others that we have changed.. } } if (newDefinitions.size() > 0) { Cache.clear(); if (HotswapAgent.enabled) { try { HotswapAgent.reload(newDefinitions.toArray(new ClassDefinition[newDefinitions.size()])); } catch (Throwable e) { throw new RuntimeException("Need reload"); } } else { throw new RuntimeException("Need reload"); } } // Check signature (variable name & annotations aware !) if (dirtySig) { throw new RuntimeException("Signature change !"); } // Now check if there is new classes or removed classes int hash = computePathHash(); if (hash != this.pathHash) { // Remove class for deleted files !! for (ApplicationClass applicationClass : Play.classes.all()) { if (!applicationClass.javaFile.exists()) { Play.classes.classes.remove(applicationClass.name); currentState = new ApplicationClassloaderState();//show others that we have changed.. } if (applicationClass.name.contains("$")) { Play.classes.classes.remove(applicationClass.name); currentState = new ApplicationClassloaderState();//show others that we have changed.. // Ok we have to remove all classes from the same file ... VirtualFile vf = applicationClass.javaFile; for (ApplicationClass ac : Play.classes.all()) { if (ac.javaFile.equals(vf)) { Play.classes.classes.remove(ac.name); } } } } throw new RuntimeException("Path has changed"); } }
这个方法会遍历所有发生改变的class然后重现加载之,因为是放在用户请求的过程中,所以我们的直观感受就是刷新页面就热加载了一切,这就是play的 hotswap magic!
关于自定义classLoader的思路给个传送门
http://www.ibm.com/developerworks/cn/java/j-lo-hotdeploy/index.html?ca=drs-