Java探針參考:Java探針技術在應用安全領域的新突破java
最近面試阿里,面試官先是問我類加載的流程,而後問了個問題,可否在加載類的時候,對字節碼進行修改linux
我懵逼了,答曰不知道,面試官說能夠的,使用Java探針技術,可以實現git
我查了一下關於探針技術的知識:github
2. 基於javaAgent和Java字節碼注入技術的java探針工具技術原理web
圖0-0:動態代理功能實現說明面試
咱們利用javaAgent和ASM字節碼技術開發java探針工具,實現原理以下:spring
jdk1.5之後引入了javaAgent技術,javaAgent是運行方法以前的攔截器。咱們利用javaAgent和ASM字節碼技術,在JVM加載class二進制文件的時候,利用ASM動態的修改加載的class文件,在監控的方法先後添加計時器功能,用於計算監控方法耗時,同時將方法耗時及內部調用狀況放入處理器,處理器利用棧先進後出的特色對方法調用前後順序作處理,當一個請求處理結束後,將耗時方法軌跡和入參map輸出到文件中,而後根據map中相應參數或耗時方法軌跡中的關鍵代碼區分出咱們要抓取的耗時業務。最後將相應耗時軌跡文件取下來,轉化爲xml格式並進行解析,經過瀏覽器將代碼分層結構展現出來,方便耗時分析,如圖0-1所示。windows
圖0-1:java探針工具原理圖瀏覽器
Java探針工具功能點:安全
一、支持方法執行耗時範圍抓取設置,根據耗時範圍抓取系統運行時出如今設置耗時範圍的代碼運行軌跡。
二、支持抓取特定的代碼配置,方便對配置的特定方法進行抓取,過濾出關係的代碼執行耗時狀況。
三、支持APP層入口方法過濾,配置入口運行前的方法進行監控,至關於監控特有的方法耗時,進行方法專題分析。
四、支持入口方法參數輸出功能,方便跟蹤耗時高的時候對應的入參數。
五、提供WEB頁面展現接口耗時展現、代碼調用關係圖展現、方法耗時百分比展現、可疑方法凸顯功能。
下面看個例子:
第一篇:
JavaAgent 是JDK 1.5 之後引入的,也能夠叫作Java代理。
JavaAgent 是運行在 main方法以前的攔截器,它內定的方法名叫 premain ,也就是說先執行 premain 方法而後再執行 main 方法。
那麼如何實現一個 JavaAgent 呢?很簡單,只須要增長 premain 方法便可。
看下面的代碼和代碼中的註釋說明:
先寫一個premain方法:
package agent; import java.lang.instrument.Instrumentation; public class pre_MyProgram { /** * 該方法在main方法以前運行,與main方法運行在同一個JVM中 * 並被同一個System ClassLoader裝載 * 被統一的安全策略(security policy)和上下文(context)管理 * * @param agentOps * @param inst * @author SHANHY * @create 2016年3月30日 */ public static void premain(String agentOps,Instrumentation inst){ System.out.println("====premain 方法執行"); System.out.println(agentOps); } /** * 若是不存在 premain(String agentOps, Instrumentation inst) * 則會執行 premain(String agentOps) * * @param agentOps * @author SHANHY * @create 2016年3月30日 */ public static void premain(String agentOps){ System.out.println("====premain方法執行2===="); System.out.println(agentOps); } public static void main(String[] args) { // TODO Auto-generated method stub } }
寫完這個類後,咱們還須要作一步配置工做。
在 src 目錄下添加 META-INF/MANIFEST.MF 文件,內容按以下定義:
Manifest-Version: 1.0
Premain-Class: agent.pre_MyProgram
Can-Redefine-Classes: true
要特別注意,一共是四行,第四行是空行,還有就是冒號後面的一個空格,以下截圖:
而後咱們打包代碼爲 pre_MyProgram.jar
注意打包的時候選擇咱們本身定義的 MANIFEST.MF ,這是導出步驟:
(1)
(2) 注意選擇pre的MF文件
接着咱們在建立一個帶有main方法的主程序工程,截圖以下:
這時候別忘了:
main函數也有MF文件:別寫錯了,否則導出報錯:No main manifest attribute(說明MF文件寫錯了)
Manifest-Version: 1.0
Main-Class: alibaba.MyProgram
按一樣的方法導出main的jar包命名爲:MyProgram.jar
以下:
選擇它的MF文件:
如何執行 MyProgram.jar ?咱們經過 -javaagent 參數來指定咱們的Java代理包,值得一說的是 -javaagent 這個參數的個數是不限的,若是指定了多個,則會按指定的前後執行,執行完各個 agent 後,纔會執行主程序的 main 方法。
命令以下:
C:\WINDOWS\system32>java -javaagent:C:\Users\z003fe9c\Desktop\tessdata\agent\pre
_MyProgram.jar=Hello1 -javaagent:C:\Users\z003fe9c\Desktop\tessdata\agent\pre_My
Program.jar=Hello2 -jar C:\Users\z003fe9c\Desktop\tessdata\agent\MyProgram.jar
輸出結果:
====premain 方法執行 Hello1 ====premain 方法執行 Hello2 =========main方法執行====
特別提醒:
(1)若是你把 -javaagent 放在 -jar 後面,則不會生效。也就是說,放在主程序後面的 agent 是無效的。
好比執行:
java -javaagent:G:\myagent.jar=Hello1 -javaagent:G:\myagent.jar=Hello2 -jar myapp.jar -javaagent:G:\myagent.jar=Hello3
(2)若是main函數忘了選擇MF文件或是MF文件選擇的不對,就會報錯:
只會有前個生效,第三個是無效的。
命令中的Hello1爲咱們傳遞給 premain 方法的字符串參數。
至此,咱們會使用 javaagent 了,可是單單看這樣運行的效果,好像沒有什麼實際意義嘛。
咱們能夠用 javaagent 作什麼呢?下篇文章咱們來介紹如何在項目中應用 javaagent。
最後說一下,還有一種,在main方法執行後再執行代理的方法,由於不經常使用,並且主程序須要配置 Agent-Class,因此不經常使用,若是須要自行了解下 agentmain(String agentArgs, Instrumentation inst) 方法。
第二篇:
今後處開始,到最後,是我直接複製了其餘人員的,由於我本身的一直沒有調試出來,不過思路清楚了:
第二篇能夠直接看別人的 JavaAgent 應用(spring-loaded 熱部署),如下的能夠忽略掉:
上一篇文章簡單介紹了 javaagent ,想了解的能夠移步 「JavaAgent」
本文重點說一下,JavaAgent 能給咱們帶來什麼?
JDK5中只能經過命令行參數在啓動JVM時指定javaagent參數來設置代理類,而JDK6中已經不只限於在啓動JVM時經過配置參數來設置代理類,JDK6中經過 Java Tool API 中的 attach 方式,咱們也能夠很方便地在運行過程當中動態地設置加載代理類,以達到 instrumentation 的目的。
Instrumentation 的最大做用,就是類定義動態改變和操做。
最簡單的一個例子,計算某個方法執行須要的時間,不修改源代碼的方式,使用Instrumentation 代理來實現這個功能,給力的說,這種方式至關於在JVM級別作了AOP支持,這樣咱們能夠在不修改應用程序的基礎上就作到了AOP,是否是顯得略吊。
接着上一篇文章的2個工程,分別添加下面的類。
MyTransformer.java 添加到 MyAgent 工程中。
package com.shanhy.demo.agent; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod; /** * 檢測方法的執行時間 * * @author 單紅宇(365384722) * @myblog http://blog.csdn.net/catoop/ * @create 2016年3月30日 */ public class MyTransformer implements ClassFileTransformer { final static String prefix = "\nlong startTime = System.currentTimeMillis();\n"; final static String postfix = "\nlong endTime = System.currentTimeMillis();\n"; // 被處理的方法列表 final static Map<String, List<String>> methodMap = new HashMap<String, List<String>>(); public MyTransformer() { add("com.shanhy.demo.TimeTest.sayHello"); add("com.shanhy.demo.TimeTest.sayHello2"); } private void add(String methodString) { String className = methodString.substring(0, methodString.lastIndexOf(".")); String methodName = methodString.substring(methodString.lastIndexOf(".") + 1); List<String> list = methodMap.get(className); if (list == null) { list = new ArrayList<String>(); methodMap.put(className, list); } list.add(methodName); } @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { className = className.replace("/", "."); if (methodMap.containsKey(className)) {// 判斷加載的class的包路徑是否是須要監控的類 CtClass ctclass = null; try { ctclass = ClassPool.getDefault().get(className);// 使用全稱,用於取得字節碼類<使用javassist> for (String methodName : methodMap.get(className)) { String outputStr = "\nSystem.out.println(\"this method " + methodName + " cost:\" +(endTime - startTime) +\"ms.\");"; CtMethod ctmethod = ctclass.getDeclaredMethod(methodName);// 獲得這方法實例 String newMethodName = methodName + "$old";// 新定義一個方法叫作好比sayHello$old ctmethod.setName(newMethodName);// 將原來的方法名字修改 // 建立新的方法,複製原來的方法,名字爲原來的名字 CtMethod newMethod = CtNewMethod.copy(ctmethod, methodName, ctclass, null); // 構建新的方法體 StringBuilder bodyStr = new StringBuilder(); bodyStr.append("{"); bodyStr.append(prefix); bodyStr.append(newMethodName + "($$);\n");// 調用原有代碼,相似於method();($$)表示全部的參數 bodyStr.append(postfix); bodyStr.append(outputStr); bodyStr.append("}"); newMethod.setBody(bodyStr.toString());// 替換新方法 ctclass.addMethod(newMethod);// 增長新方法 } return ctclass.toBytecode(); } catch (Exception e) { System.out.println(e.getMessage()); e.printStackTrace(); } } return null; } }
TimeTest.java 添加到 MyProgram 工程中。
package com.shanhy.demo; /** * 被測試類 * * @author 單紅宇(365384722) * @myblog http://blog.csdn.net/catoop/ * @create 2016年3月30日 */ public class TimeTest { public static void main(String[] args) { sayHello(); sayHello2("hello world222222222"); } public static void sayHello() { try { Thread.sleep(2000); System.out.println("hello world!!"); } catch (InterruptedException e) { e.printStackTrace(); } } public static void sayHello2(String hello) { try { Thread.sleep(1000); System.out.println(hello); } catch (InterruptedException e) { e.printStackTrace(); } } }
修改MyAgent.java 的 permain 方法,以下:
public static void premain(String agentOps, Instrumentation inst) { System.out.println("=========premain方法執行========"); System.out.println(agentOps); // 添加Transformer inst.addTransformer(new MyTransformer()); }
修改MANIFEST.MF內容,增長 Boot-Class-Path 以下:
Manifest-Version: 1.0 Premain-Class: com.shanhy.demo.agent.MyAgent Can-Redefine-Classes: true Boot-Class-Path: javassist-3.18.1-GA.jar
對2個工程分別打包爲 myagent.jar 和 myapp.jar 而後將 javassist-3.18.1-GA.jar 和 myagent.jar 放在一塊兒。
最後執行命令測試,結果以下:
G:\>java -javaagent:G:\myagent.jar=Hello1 -jar myapp.jar =========premain方法執行======== Hello1 hello world!! this method sayHello cost:2000ms. hello world222222222 this method sayHello2 cost:1000ms.
在項目開發中咱們能夠把一些重要但又可能會變動的邏輯封裝到某個 logic.jar 中,當咱們須要隨時更新實現邏輯的時候,能夠在不重啓服務的狀況下讓修改後的 logic.jar 被從新加載生效。
spring-loaded是一個開源項目,項目地址:https://github.com/spring-projects/spring-loaded
使用方法:
在啓動主程序以前指定參數
在啓動主程序以前指定參數
-javaagent:C:/springloaded-1.2.5.RELEASE.jar -noverify
若是你想讓 Tomat 下面的應用自動熱部署,只須要在 catalina.sh 中添加:
set JAVA_OPTS=-javaagent:springloaded-1.2.5.RELEASE.jar -noverify
這樣就完成了 spring-loaded 的安裝,它可以自動檢測Tomcat 下部署的webapps ,在不重啓Tomcat的狀況下,實現應用的熱部署。
經過使用 -noverify 參數,關閉 Java 字節碼的校驗功能。
使用參數 -Dspringloaded=verbose;explain;watchJars=tools.jar 指定監視的jar (verbose;explain; 非必須),多個jar用「冒號」分隔,如 watchJars=tools.jar:utils.jar:commons.jar
固然,它也有一些小缺限:
1. 目前官方提供的1.2.4 版本在linux上能夠很好的運行,但在windows還存在bug,官網已經有人提出:https://github.com/spring-projects/spring-loaded/issues/145
2. 對於一些第三方框架的註解的修改,不能自動加載,好比:spring mvc的@RequestMapping
3. log4j的配置文件的修改不能即時生效。