Expert Beginner

Learning by doing

0%

個人覺得這題應該是easy,不過不重要。今天在710往永寧站的車上想說來試一下在手機瀏覽器leetcode用Go寫一題練習看看。感想是自己Go程式還是寫的不夠多,感覺還沒出來。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func inorder(root *TreeNode, res *[]int) {
if root == nil {
return
}
inorder(root.Left, res)
*res = append(*res, root.Val)
inorder(root.Right, res)
}

func inorderTraversal(root *TreeNode) []int {
var res []int
inorder(root, &res)
return res
}

想直接看程式碼hello-javaagent Projecthello-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的本體。

Java Instrumentation

Instrumentation讓我們可以在既有(compiled)方法插入額外的bytecode,而達到收集使用中資料來進一步分析使用。instrumentation底層是透過JVM Tool Interface(JVMTI)來達成,JVMTI是JVM暴露出來的extension point。

在Java 5的時候讓我們可以在啟動的時候使用-javaagent的方式,指定agent。在Java 6的時候則更近一步提供了attach的方式,讓我們可以針對已啟動的JVM process執行agent的工作。

  • 啟動JVM時啟動代理 (hello-javaagent project)
  • 針對已啟動JVM啟動代理 (hello-attach project)

hello-javaagent

入口

入口class我們要提供premain()這個方法,需要兩個參數

1
public static void premain(String agentArgs, Instrumentation inst)

第一個參數是啟動agent時,可以由命令列給予agent的參數,第二個則是JVM會提供agent class使用的Instrumentation的入口。

Instrumentation Interface

  • void addTransformer(ClassFileTransformer transformer, boolean canRetransform)

註冊transformer

  • Class[] getAllLoadedClasses()

取得被load到JVM的所有class

  • void retransformClasses(Class<?>… classes) throws UnmodifiableClassException

針對已經loaded的classes重新在instrumentation機制由agent調整bytecode

Configuration

MANIFEST.MF

也可以用maven build file去產生,我選擇自己寫,然後在maven build file設定使用自己寫的MANIFEST.MF。

1
2
3
Premain-Class: main.AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true

maven build file相關設定方式見Maven Assembly Plugin。

Maven Assembly Plugin

因為有ASM的依賴,所以打包jar的時候要連同依賴的library一起包進去才行。以前都用copy reference,改用assembly plugin做看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>${maven-assembly-plugin.version}</version>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>

Using ASM for manipulate bytecode

hello project的目的是在我們在意的類別裡在意的method被呼叫前和呼叫後在Console印個log下來。所以我們要做的事情是寫一個ClassFileTransformer,在這裡使用ASM處理bytecode。然後在premain()裡把我們寫的transformer加進去。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if(!"app/MyTest".equals(className)) {
return classfileBuffer;
}
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new MyClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
return classWriter.toByteArray();
}
}

這裡我們在意的類別是app.MyTest這個類別,所以在transformer裡遇到不在乎的class就把該class的bytecode直接回傳;如果遇到MyTest類別,就透過ASM來處理一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyClassVisitor extends ClassVisitor {
public static final String NAME_CONSTRUCTOR = "<init>";
public static final String NAME_MAIN = "main";
public static final Set WhiteList = Collections.unmodifiableSet(Set.of(NAME_CONSTRUCTOR, NAME_MAIN));

public MyClassVisitor(ClassVisitor classVisitor) {
super(ASM8, classVisitor);
}

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if(WhiteList.contains(name)) {
return methodVisitor;
}
return new MyMethodVisitor(ASM8, methodVisitor, access, name, descriptor);
}
}

跟書上範例不一樣的是我不想看main,所以遇到constructor和main的話就把原本的methodVisitor丟回去不處理,反之則回傳我們新寫的MyMethodVisitor的instance回去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyMethodVisitor extends AdviceAdapter {
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}

@Override
protected void onMethodEnter() {
super.onMethodEnter();
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn(">>> enter " + this.getName());
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}

@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn(">>> exit " + this.getName());
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}

因為只是要在呼叫前後加一些bytecode進去,就可以利用asm-commons這包裡的AdviceAdapter來幫忙,AdviceAdapter提供了method enter/exit的方法給我們實作就可以達成目的。

這裡就只是很簡單的作出System.out.println(str)這樣的bytecode在原本method bytecode前後。

transformer都寫完就是在agent main裡面加進去

1
2
3
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new MyClassTransformer(), true);
}

Cleint

跟書上的例子一樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package app;

public class MyTest {
public static void main(String[] args) {
new MyTest().foo();
}

public void foo() {
bar1();
bar2();
}

private void bar1() {
System.out.println("running bar1...");
}

private void bar2() {
System.out.println("running bar2...");
}
}

Execute it!!!

執行方式就是在java指令加上-javaagent your_agent_jar,或參考run.xml裡的run-with-javaagent這個target的內容。

1
2
3
4
5
6
7
<property name="AGENT_JAR" value="agent/target/agent-0.0.1-jar-with-dependencies.jar" />

<target name="run-with-javaagent">
<java jar="client/target/client-0.0.1.jar" fork="true">
<jvmarg value="-javaagent:${AGENT_JAR}" />
</java>
</target>

執行結果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
D:\lab\java-basic\instrumentation\hello-javaagent>ant -f run.xml run-with-javaagent
Buildfile: D:\lab\java-basic\instrumentation\hello-javaagent\run.xml

run-with-javaagent:
[java] >>> enter foo
[java] >>> enter bar1
[java] running bar1...
[java] >>> exit bar1
[java] >>> enter bar2
[java] running bar2...
[java] >>> exit bar2
[java] >>> exit foo

BUILD SUCCESSFUL
Total time: 0 seconds

hello-attach

入口

入口class我們要提供agentmain()這個方法,需要兩個參數

1
public static void agentmain(String agentArgs, Instrumentation inst)

第一個參數是啟動agent時,可以由命令列給予agent的參數,第二個則是JVM會提供agent class使用的Instrumentation的入口。

Configuration

就把原本的Premain-Class換成Agent-Class

1
2
3
Agent-Class: main.AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true

Using ASM for manipulate bytecode

agentmain內容,將transform加到instrumentation內之外,還要透過retransformClasses()把目標類別重新transform他們的bytecode。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
System.out.println("Agent Running...");
inst.addTransformer(new MyClassTransformer(), true);
Class[] classes = inst.getAllLoadedClasses();
for(Class clz : classes) {
//System.out.println(clz.getName());
if(clz.getName().equals("com.example.client.controller.HelloController")) {
System.out.println("Reloading: " + clz.getName());
inst.retransformClasses(clz);
break;
}
}
}

後面利用ASM Core API作的事情就跟之前專案類似,就不貼了。

Attach

使用attach的方式需要target JVM的process id,可以透過jps查詢或者這裡我使用一個簡單的Spring Boot程式當作target,啟動Spring Boot Application預設會把process id吐到Console裡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
System.out.println("Attach agent to pid");
String pid = args[0];
String agent = args[1];
System.out.println("pid: " + args[0]);
System.out.println("agent: " + agent);
VirtualMachine vm = VirtualMachine.attach(pid);
try {
vm.loadAgent(agent);
System.out.println("Done.");
} finally {
vm.detach();
}
}

loadAgent()的路徑是target virtual machine的檔案系統裡的agent jar file路徑。見JavaDoc

Cleint

用Spring Boot做一個簡單的hello api當作測試的client。可以觀察到agent執行時,agent相關程式印到Console的log會在client執行的Console印出來。

Execute it!!!

attach agent

1
2
3
4
5
6
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
t\agent-0.0.1-jar-with-dependencies.jar
Attach agent to pid
pid: 17828
agent: D:\lab\java-basic\instrumentation\hello-attach\agent\target\agent-0.0.1-jar-with-dependencies.jar
Done.

Srping boot console after attach agent

1
2
3
4
5
6
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)
Agent Running...
Reloading: com.example.client.controller.HelloController
<init>
hello
Modify hello()

呼叫hello api之後看到server log會印出我們agent添加的程式碼執行的log

1
2
3
4
5
6
7
8
9
10
11
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)
Agent Running...
Reloading: com.example.client.controller.HelloController
<init>
hello
Modify hello()
2020-09-14 13:16:52.757 INFO 17828 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-09-14 13:16:52.757 INFO 17828 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-09-14 13:16:52.770 INFO 17828 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 13 ms
>>> enter hello
>>> exit hello

再提一次這篇用到的類別大多數在他們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。

Scanner

com.sun.tools.javac.parser.Scanner這就是Java的Tokenizer。以下的例子是修改自dive into jvm bytecode一書,修改的原因是因為我練習時使用Java 14跟作者用的應該是不同版本,因此就跟javadoc警語一樣,使用者風險請自行負責。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package compile;

import com.sun.tools.javac.parser.Scanner;
import com.sun.tools.javac.parser.ScannerFactory;
import com.sun.tools.javac.parser.Tokens;
import com.sun.tools.javac.util.Context;

/**
* from dive into jvm bytecode
*/
public class ParseTest {
public static void main(String[] args) {
ScannerFactory scannerFactory = ScannerFactory.instance(new Context());
Scanner scanner = scannerFactory.newScanner("int k = i + j;", false);

scanner.nextToken();
Tokens.Token token = scanner.token();
while(token.kind != Tokens.TokenKind.EOF) {
if(token.kind == Tokens.TokenKind.IDENTIFIER) {
System.out.print(token.name() + " - ");
}
System.out.println(token.kind);
scanner.nextToken();
token = scanner.token();;
}
}
}

程式執行結果如下

1
2
3
4
5
6
7
int
k - token.identifier
=
i - token.identifier
+
j - token.identifier
';'

JavaCompiler

以下的例子是使用JavaCompiler寫個的程式parse簡單的java source code,走訪AST取得所有的variable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package compile;

import com.sun.source.tree.Tree;
import com.sun.tools.javac.main.JavaCompiler;
import com.sun.tools.javac.main.Option;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Options;

public class JavaCompilerTest {
public static void main(String[] args) {
Context context = new Context();
Options.instance(context).put(Option.ENCODING, "UTF-8");

JavaCompiler compiler = new JavaCompiler(context);
compiler.genEndPos = true;
compiler.keepComments = true;

@SuppressWarnings("deprecation") JCTree.JCCompilationUnit compilationUnit = compiler.parse("src/main/java/client/User.java");

compilationUnit.defs.stream()
.forEach(jcTree -> {
System.out.println(jcTree.toString());
System.out.println(jcTree.getKind());
listVariable(jcTree);
});
}

private static void listVariable(JCTree tree) {
tree.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl classDecl) {
classDecl.defs.stream()
.filter(it -> it.getKind().equals(Tree.Kind.VARIABLE))
.map(it -> (JCTree.JCVariableDecl) it)
.forEach(it -> {
System.out.println(it.getName() + ": " + it.getKind() + ": " + it.getType());
});
}
});
}
}

因為@SuppressWarnings("deprecation")的礙眼,參考SimpleJavaFileObject自己實作一個JavaFileObject而做了第二個例子。因為很簡單,我們無法使用SimpleJavaFileObject的建構子。這個類別的建構子是protected,而且看起來沒有static factory method可以簡單的方式產生物件,和使用建構子的類別的方法看起來大多是private method,加上JavaFileObject好像沒很複雜,應該可以做一個簡單版本來測試看看parse tree。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package compile;

import javax.lang.model.element.Modifier;
import javax.lang.model.element.NestingKind;
import javax.tools.JavaFileObject;
import java.io.*;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class MySimpleJavaFileObject implements JavaFileObject {
private URI uri;
private Kind kind;

public MySimpleJavaFileObject(URI uri) {
Objects.requireNonNull(uri);
if (uri.getPath() == null) {
throw new IllegalArgumentException("URI must have a path: " + uri);
}
if (!uri.getPath().toLowerCase().endsWith(Kind.SOURCE.extension)) {
throw new UnsupportedOperationException("Only support SOURCE kind");
}
this.uri = uri;
this.kind = Kind.SOURCE;
}

@Override
public Kind getKind() {
return this.kind;
}

@Override
public boolean isNameCompatible(String simpleName, Kind kind) {
String baseName = simpleName + kind.extension;
return kind.equals(getKind())
&& (baseName.equals(toUri().getPath())
|| toUri().getPath().endsWith("/" + baseName));
}

@Override
public NestingKind getNestingKind() {
return null;
}

@Override
public Modifier getAccessLevel() {
return null;
}

@Override
public URI toUri() {
return uri;
}

@Override
public String getName() {
return toUri().getPath();
}

@Override
public InputStream openInputStream() throws IOException {
throw new UnsupportedOperationException();
}

@Override
public OutputStream openOutputStream() throws IOException {
throw new UnsupportedOperationException();
}

@Override
public Reader openReader(boolean ignoreEncodingErrors) throws IOException {
CharSequence charContent = getCharContent(ignoreEncodingErrors);
if (charContent == null)
throw new UnsupportedOperationException();
if (charContent instanceof CharBuffer) {
CharBuffer buffer = (CharBuffer)charContent;
if (buffer.hasArray())
return new CharArrayReader(buffer.array());
}
return new StringReader(charContent.toString());
}

@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
Path path = Paths.get(this.uri);

Stream<String> lines = Files.lines(path);
String data = lines.collect(Collectors.joining("\n"));
lines.close();
return data;
}

@Override
public Writer openWriter() throws IOException {
return new OutputStreamWriter(openOutputStream());
}

@Override
public long getLastModified() {
return 0;
}

@Override
public boolean delete() {
return false;
}
}
```

接著就可以寫第二個測試,跟第一個只有差一行就不貼全部程式碼

``` java
JCTree.JCCompilationUnit compilationUnit = compiler.parse(new MySimpleJavaFileObject(URI.create("file:///D:/lab/learning-jvm/src/main/java/client/User.java")));

忽略把User.java的JCTree印出來,程式執行結果如下

1
2
3
CLASS
id: VARIABLE: Long
name: VARIABLE: String

Question

對已經存在的類別加入新的attribute

Answer

作法是透過TreeMaker#VerDef()來產生新的variable的定義,然後透過JCClassDecldefs#prepend()加到類別的Class Tree上。

以下是使用TreeMaker#VerDef()產生serialVersionUID的定義和初始化。

1
2
3
4
5
6
7
private JCTree.JCVariableDecl genSerialVersionUIDVariable() {
JCTree.JCModifiers modifiers = treeMaker.Modifiers(PRIVATE + STATIC + FINAL);
Name id = names.fromString("serialVersionUID");
JCTree.JCExpression varType = treeMaker.Type(new Type.JCPrimitiveType(TypeTag.LONG, null));
JCTree.JCExpression init = treeMaker.Literal(1L);
return treeMaker.VarDef(modifiers, id, varType, init);
}

以下是把VariableTree加到ClassTree的寫法

1
jcClassDecl.defs = jcClassDecl.defs.prepend(genSerialVersionUIDVariable());

這個VariableTree對應的java程式碼如下

1
private static final long serialVersionUID = 1L;

Discussion

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.

Question

如何透過JCTree API增加一個呼叫default constructor產生物件?

Answer

透過TreeMaker#NewClass()

1
2
3
4
5
6
7
8
public JCNewClass NewClass(JCExpression encl,
List<JCExpression> typeargs,
JCExpression clazz,
List<JCExpression> args,
JCClassDecl def)
{
return SpeculativeNewClass(encl, typeargs, clazz, args, def, false);
}

JCTree可以看到JCNewClass類別。

1
2
3
4
5
/**
* A new(...) operation.
*/
public static class JCNewClass extends JCPolyExpression implements NewClassTree {

下面是一個簡單的例子,透過呼叫StringBuilder的default constructor產生一個StringBuilder,後續使用StringBuilder#append(str: String)

1
2
3
4
5
6
7
8
9
10
11
12
JCTree.JCExpression varType = query("java", "lang", "StringBuilder");

Name result = names.fromString("result");
JCTree.JCModifiers resultModifiers = treeMaker.Modifiers(Flags.PARAMETER);

JCTree.JCNewClass stringBuilderClass = treeMaker.NewClass(null,
com.sun.tools.javac.util.List.nil(),
varType,
com.sun.tools.javac.util.List.nil(),
null);

JCTree.JCVariableDecl resultIdent = treeMaker.VarDef(resultModifiers, result, varType, stringBuilderClass);

對應產生的Java code就是

1
StringBuilder result = new StringBuilder();

Discussion

Anonymous class: 以Runnable為例

舉例來說,我們希望在AST裡加上如下列的內容

1
2
3
4
5
Runnable r = new Runnable() {
public void run() {

}
};

就會使用到TreeMaker#NewClass()的第五個參數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
JCTree.JCExpression runnableType = query("java", "lang", "Runnable");

Name runnable = names.fromString("r");
JCTree.JCModifiers rModifiers = treeMaker.Modifiers(Flags.PARAMETER);
ListBuffer<JCTree.JCStatement> runStatements = new ListBuffer<>();
JCTree.JCBlock rBody = treeMaker.Block(0, runStatements.toList());

JCTree.JCMethodDecl runMethod = treeMaker.MethodDef(
treeMaker.Modifiers(PUBLIC),
names.fromString("run"),
treeMaker.Type(new Type.JCVoidType()),
com.sun.tools.javac.util.List.nil(),
com.sun.tools.javac.util.List.nil(),
com.sun.tools.javac.util.List.nil(),
rBody,
null
);

List<JCTree> defs = List.of(runMethod);

JCTree.JCClassDecl classDecl = treeMaker.AnonymousClassDef(treeMaker.Modifiers(PARAMETER),
defs);

JCTree.JCNewClass rClass = treeMaker.NewClass(null,
com.sun.tools.javac.util.List.nil(),
runnableType,
com.sun.tools.javac.util.List.nil(),
classDecl);

JCTree.JCVariableDecl rIdent = treeMaker.VarDef(rModifiers, runnable, runnableType, rClass);

很久沒寫東西,其實我不知道面試問這個的意義。是要考倒求職者彰顯自己的能力?還是甚麼目的?
真的面試到Java那麼熟的碼農,大多數台灣企業做的東西用的上這些知識又有幾間? 公司又能花多少錢養這種工程師,但大多數時間只需要寫CRUD,稍微進階點的東西要做當官的就龜縮? XD

Quick Start

Method Overloading

Java 101教我們的Method Overloading機制允許我們在一個Java class裡可以有多個相同method name的methods,但argument list必須不一樣。

也就是說如果只有return type不一樣就不行,試著寫下列程式在compile(通常IDE就會給你紅色波浪)的時候就會發生錯誤。

1
2
3
4
5
6
7
8
9
public class TestMethodOverload {
public void test() {

}

public int test() {
return 0;
}
}

在Terminal編譯的話會看到下列的錯誤

1
2
3
4
5
PS D:\tmp> javac .\TestMethodOverload.java
.\TestMethodOverload.java:6: error: method test() is already defined in class TestMethodOverload
public int test() {
^
1 error

Extra Story

問題來了,如果同一個類別裡,有兩個同名的方法,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
final String ClassName = "HelloJVM";
ClassGen classGen = new ClassGen(ClassName,
"java.lang.Object",
"<generated>",
Constants.ACC_PUBLIC | Constants.ACC_SUPER,
null);
ConstantPoolGen constantPoolGen = classGen.getConstantPool();
InstructionList instructionList = new InstructionList();
InstructionFactory instructionFactory = new InstructionFactory(classGen);

MethodGen test1MethodGen = new MethodGen(Constants.ACC_PUBLIC,
Type.INT,
Type.NO_ARGS,
null,
"test1",
ClassName,
instructionList,
constantPoolGen);
instructionList.append(new PUSH(constantPoolGen, 5));
instructionList.append(InstructionFactory.createReturn(Type.INT));

test1MethodGen.setMaxLocals();
test1MethodGen.setMaxStack();
classGen.addMethod(test1MethodGen.getMethod());
instructionList.dispose();

MethodGen test2MethodGen = new MethodGen(Constants.ACC_PUBLIC,
Type.DOUBLE,
Type.NO_ARGS,
null,
"test1",
ClassName,
instructionList,
constantPoolGen);
instructionList.append(new PUSH(constantPoolGen, 10.112));
instructionList.append(InstructionFactory.createReturn(Type.DOUBLE));

test2MethodGen.setMaxLocals();
test2MethodGen.setMaxStack();
classGen.addMethod(test2MethodGen.getMethod());
instructionList.dispose();

MethodGen mainMethodGen = new MethodGen(Constants.ACC_PUBLIC | Constants.ACC_STATIC,
Type.VOID,
new Type[] {
new ArrayType(Type.STRING, 1)
},
new String[] {"argv"},
"main",
ClassName,
instructionList,
constantPoolGen);
ObjectType p_stream = new ObjectType("java.io.PrintStream");

instructionList.append(instructionFactory.createNew(ClassName));
instructionList.append(InstructionFactory.createDup(1));
instructionList.append(instructionFactory.createInvoke(ClassName,
"<init>",
Type.VOID,
new Type[] {},
(short) Constants.INVOKESPECIAL));
instructionList.append(InstructionFactory.createStore(Type.OBJECT, 1));
instructionList.append(instructionFactory.createFieldAccess("java.lang.System",
"out",
p_stream,
(short) Constants.GETSTATIC));
instructionList.append(InstructionFactory.createLoad(Type.OBJECT, 1));
instructionList.append(instructionFactory.createInvoke(ClassName,
"test1",
Type.INT,
Type.NO_ARGS,
(short) Constants.INVOKEVIRTUAL));
instructionList.append(instructionFactory.createInvoke("java.io.PrintStream",
"println",
Type.VOID,
new Type[] {Type.INT},
(short) Constants.INVOKEVIRTUAL));
instructionList.append(instructionFactory.createFieldAccess("java.lang.System",
"out",
p_stream,
(short) Constants.GETSTATIC));
instructionList.append(InstructionFactory.createLoad(Type.OBJECT, 1));
instructionList.append(instructionFactory.createInvoke(ClassName,
"test1",
Type.DOUBLE,
Type.NO_ARGS,
(short) Constants.INVOKEVIRTUAL));
instructionList.append(instructionFactory.createInvoke("java.io.PrintStream",
"println",
Type.VOID,
new Type[] {Type.DOUBLE},
(short) Constants.INVOKEVIRTUAL));

instructionList.append(InstructionFactory.createReturn(Type.VOID));

mainMethodGen.setMaxStack();
mainMethodGen.setMaxLocals();
classGen.addMethod(mainMethodGen.getMethod());

instructionList.dispose();

classGen.addEmptyConstructor(Constants.ACC_PUBLIC);

try {
classGen.getJavaClass().dump("HelloJVM.class");
} catch(IOException e) {
e.printStackTrace();
}

完整程式碼,接下來是來執行一下產生的HelloJVM class,順便用javap看一下。

1
2
3
4
5
6
7
8
9
10
11
12
D:\lab\java-basic\java-basic>java HelloJVM
5
10.112

D:\lab\java-basic\java-basic>javap HelloJVM.class
Compiled from "<generated>"
public class HelloJVM {
public int test1();
public double test1();
public static void main(java.lang.String[]);
public 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.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment