個人覺得這題應該是easy,不過不重要。今天在710往永寧站的車上想說來試一下在手機瀏覽器leetcode用Go寫一題練習看看。感想是自己Go程式還是寫的不夠多,感覺還沒出來。
1 | /** |
個人覺得這題應該是easy,不過不重要。今天在710往永寧站的車上想說來試一下在手機瀏覽器leetcode用Go寫一題練習看看。感想是自己Go程式還是寫的不夠多,感覺還沒出來。
1 | /** |
想直接看程式碼hello-javaagent Project、hello-attach Project的話,直接點連結即可。
hello-javaagent Project講用-javaagent
的方式使用java instrumentation的hello world project。這是個multi module maven project。有兩個module一個是client一個是agent部分。另外,還有一個run.xml的ant script裡面有兩個task用來執行client,一個task有啟動agent一個沒有,可以感覺一下差異。
hello-attach project則是使用attach的方式,有三個project,client subproject是用spring boot寫的client,attach subproject是用來attach agent jar到target jvm,最後agent subproject是agent的本體。
Instrumentation讓我們可以在既有(compiled)方法插入額外的bytecode,而達到收集使用中資料來進一步分析使用。instrumentation底層是透過JVM Tool Interface
(JVMTI
)來達成,JVMTI是JVM暴露出來的extension point。
在Java 5的時候讓我們可以在啟動的時候使用-javaagent
的方式,指定agent。在Java 6的時候則更近一步提供了attach的方式,讓我們可以針對已啟動的JVM process執行agent的工作。
入口class我們要提供premain()
這個方法,需要兩個參數
1 public static void premain(String agentArgs, Instrumentation inst)
第一個參數是啟動agent時,可以由命令列給予agent的參數,第二個則是JVM會提供agent class使用的Instrumentation的入口。
註冊transformer
取得被load到JVM的所有class
針對已經loaded的classes重新在instrumentation機制由agent調整bytecode
也可以用maven build file去產生,我選擇自己寫,然後在maven build file設定使用自己寫的MANIFEST.MF。
1 | Premain-Class: main.AgentMain |
maven build file相關設定方式見Maven Assembly Plugin。
因為有ASM的依賴,所以打包jar的時候要連同依賴的library一起包進去才行。以前都用copy reference,改用assembly plugin做看看。
1 | <plugin> |
hello project的目的是在我們在意的類別裡在意的method被呼叫前和呼叫後在Console印個log下來。所以我們要做的事情是寫一個ClassFileTransformer
,在這裡使用ASM處理bytecode。然後在premain()
裡把我們寫的transformer加進去。
1 | public class MyClassTransformer implements ClassFileTransformer { |
這裡我們在意的類別是app.MyTest這個類別,所以在transformer裡遇到不在乎的class就把該class的bytecode直接回傳;如果遇到MyTest類別,就透過ASM來處理一下。
1 | public class MyClassVisitor extends ClassVisitor { |
跟書上範例不一樣的是我不想看main,所以遇到constructor和main的話就把原本的methodVisitor丟回去不處理,反之則回傳我們新寫的MyMethodVisitor
的instance回去。
1 | public class MyMethodVisitor extends AdviceAdapter { |
因為只是要在呼叫前後加一些bytecode進去,就可以利用asm-commons這包裡的AdviceAdapter
來幫忙,AdviceAdapter
提供了method enter/exit的方法給我們實作就可以達成目的。
這裡就只是很簡單的作出System.out.println(str)這樣的bytecode在原本method bytecode前後。
transformer都寫完就是在agent main裡面加進去
1 | public static void premain(String agentArgs, Instrumentation inst) { |
跟書上的例子一樣
1 | package app; |
執行方式就是在java指令加上-javaagent your_agent_jar,或參考run.xml裡的run-with-javaagent這個target的內容。
1 | <property name="AGENT_JAR" value="agent/target/agent-0.0.1-jar-with-dependencies.jar" /> |
執行結果如下
1 | D:\lab\java-basic\instrumentation\hello-javaagent>ant -f run.xml run-with-javaagent |
入口class我們要提供agentmain()
這個方法,需要兩個參數
1 public static void agentmain(String agentArgs, Instrumentation inst)
第一個參數是啟動agent時,可以由命令列給予agent的參數,第二個則是JVM會提供agent class使用的Instrumentation的入口。
就把原本的Premain-Class換成Agent-Class
1 | Agent-Class: main.AgentMain |
agentmain
內容,將transform加到instrumentation內之外,還要透過retransformClasses()
把目標類別重新transform他們的bytecode。
1 | public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException { |
後面利用ASM Core API作的事情就跟之前專案類似,就不貼了。
使用attach的方式需要target JVM的process id,可以透過jps查詢或者這裡我使用一個簡單的Spring Boot程式當作target,啟動Spring Boot Application預設會把process id吐到Console裡。
1 | public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { |
loadAgent()
的路徑是target virtual machine的檔案系統裡的agent jar file路徑。見JavaDoc
用Spring Boot做一個簡單的hello api當作測試的client。可以觀察到agent執行時,agent相關程式印到Console的log會在client執行的Console印出來。
attach agent
1 | D:\lab\java-basic\instrumentation\hello-attach\attach>java -jar target\attach-0.0.1.jar 17828 D:\lab\java-basic\instrumentation\hello-attach\agent\targe |
Srping boot console after attach agent
1 | 2020-09-14 13:15:00.687 INFO 17828 --- [ restartedMain] com.example.client.ClientApplication : Started ClientApplication in 2.632 seconds (JVM running for 3.352) |
呼叫hello api之後看到server log會印出我們agent添加的程式碼執行的log
1 | 2020-09-14 13:15:00.687 INFO 17828 --- [ restartedMain] com.example.client.ClientApplication : Started ClientApplication in 2.632 seconds (JVM running for 3.352) |
再提一次這篇用到的類別大多數在他們source的javadoc都有寫到
This is NOT part of any supported API. If you write code that depends on this, you do so at your own risk. This code and its internal interfaces are subject to change or deletion without notice.
Java沒有使用flex這些工具產生lexer或bison產生parser,選擇自己用java打造lexer和parser。
com.sun.tools.javac.parser.Scanner
這就是Java的Tokenizer。以下的例子是修改自dive into jvm bytecode一書,修改的原因是因為我練習時使用Java 14跟作者用的應該是不同版本,因此就跟javadoc警語一樣,使用者風險請自行負責。
1 | package compile; |
程式執行結果如下
1 | int |
以下的例子是使用JavaCompiler
寫個的程式parse簡單的java source code,走訪AST取得所有的variable。
1 | package compile; |
因為@SuppressWarnings("deprecation")
的礙眼,參考SimpleJavaFileObject
自己實作一個JavaFileObject
而做了第二個例子。因為很簡單,我們無法使用SimpleJavaFileObject
的建構子。這個類別的建構子是protected,而且看起來沒有static factory method可以簡單的方式產生物件,和使用建構子的類別的方法看起來大多是private method,加上JavaFileObject
好像沒很複雜,應該可以做一個簡單版本來測試看看parse tree。
1 | package compile; |
忽略把User.java的JCTree
印出來,程式執行結果如下
1 | CLASS |
對已經存在的類別加入新的attribute
作法是透過TreeMaker#VerDef()
來產生新的variable的定義,然後透過JCClassDecl
的defs#prepend()
加到類別的Class Tree上。
以下是使用TreeMaker#VerDef()
產生serialVersionUID
的定義和初始化。
1 | private JCTree.JCVariableDecl genSerialVersionUIDVariable() { |
以下是把VariableTree加到ClassTree的寫法
1 | jcClassDecl.defs = jcClassDecl.defs.prepend(genSerialVersionUIDVariable()); |
這個VariableTree對應的java程式碼如下
1 | private static final long serialVersionUID = 1L; |
lombok除了JSR 269之外的黑科技。簡單來說lombok是透過Java 6推出的JSR 269 (Pluggable Annotation Processing API)和修改AST這兩個東西,讓我們寫程式更輕鬆。
JCTree
就是讓我們可以處理Java AST的API。兩件事要先講,首先根據JCTree
的JavaDoc寫類別名稱看到JC是javac的縮寫。接著,這些API是internal使用,有可能改變或刪除,如果要用後果自行負責。
This is NOT part of any supported API. If you write code that depends on this, you do so at your own risk. This code and its internal interfaces are subject to change or deletion without notice.
如何透過JCTree API增加一個呼叫default constructor產生物件?
透過TreeMaker#NewClass()
1 | public JCNewClass NewClass(JCExpression encl, |
在JCTree
可以看到JCNewClass
類別。
1 | /** |
下面是一個簡單的例子,透過呼叫StringBuilder
的default constructor產生一個StringBuilder
,後續使用StringBuilder#append(str: String)
。
1 | JCTree.JCExpression varType = query("java", "lang", "StringBuilder"); |
對應產生的Java code就是
1 | StringBuilder result = new StringBuilder(); |
舉例來說,我們希望在AST裡加上如下列的內容
1 | Runnable r = new Runnable() { |
就會使用到TreeMaker#NewClass()
的第五個參數。
1 | JCTree.JCExpression runnableType = query("java", "lang", "Runnable"); |
很久沒寫東西,其實我不知道面試問這個的意義。是要考倒求職者彰顯自己的能力?還是甚麼目的?
真的面試到Java那麼熟的碼農,大多數台灣企業做的東西用的上這些知識又有幾間? 公司又能花多少錢養這種工程師,但大多數時間只需要寫CRUD,稍微進階點的東西要做當官的就龜縮? XD
Java 101教我們的Method Overloading機制允許我們在一個Java class裡可以有多個相同method name的methods,但argument list必須不一樣。
也就是說如果只有return type不一樣就不行,試著寫下列程式在compile(通常IDE就會給你紅色波浪)的時候就會發生錯誤。
1 | public class TestMethodOverload { |
在Terminal編譯的話會看到下列的錯誤
1 | PS D:\tmp> javac .\TestMethodOverload.java |
問題來了,如果同一個類別裡,有兩個同名的方法,argument list一樣,只有回傳型態不一樣,在JVM是合法的嗎?
答案是【是合法的】,JVM是允許有這樣的情況,method overloading這樣的規定是Java語言的規範,所以是java compiler會抱怨你這樣寫,但是如果你能產生這種情況的bytecode class,JVM是不在乎的。
證明下面這個例子,用BCEL產生一個簡單的Java Class,HelloJVM類別裡面有兩個test1()只有return type的差異,一個回int一個回double。然後順便弄個main method讓我們可已執行一下這兩個test1()。
1 | final String ClassName = "HelloJVM"; |
完整程式碼,接下來是來執行一下產生的HelloJVM class,順便用javap看一下。
1 | D:\lab\java-basic\java-basic>java HelloJVM |
Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.
1 | $ hexo new "My New Post" |
More info: Writing
1 | $ hexo server |
More info: Server
1 | $ hexo generate |
More info: Generating
1 | $ hexo deploy |
More info: Deployment