Skip to content

บทที่ 18 — Reflection + Annotation Processing (เวทมนตร์ของ Spring/Hibernate)

← บทที่ 17: JDBC | สารบัญ | บทที่ 19: Performance + Profiling →

📓 โซนขั้นสูง—ข้ามได้ (บท 11-19) บทนี้อธิบาย "เวทมนตร์" เบื้องหลัง Spring/Hibernate/Lombok ระดับลึก มือใหม่ข้ามไปก่อนได้ ใช้ของพวกนี้ได้โดยไม่ต้องรู้กลไกภายใน ค่อยกลับมาเปิดตอนอยากเข้าใจว่าเฟรมเวิร์กทำงานยังไง หรือต้องเขียนเครื่องมือ/library ของตัวเอง

ศัพท์ย่อที่จะเจอ:

ศัพท์คำแปล/อธิบาย
Reflectionรีเฟลกชัน — ความสามารถส่อง/แก้โครงสร้างคลาสของตัวเองขณะรัน
runtimeรันไทม์ — ช่วงที่โปรแกรมกำลังรันอยู่
dynamic proxyไดนามิกพร็อกซี — วัตถุตัวแทนที่ Java สร้างขึ้นตอนรัน เพื่อดักจับ (intercept) การเรียกฟังก์ชัน (method) แล้วทำงานพิเศษก่อน/หลัง เช่น บันทึก log หรือเปิด transaction
DIDependency Injection — การฉีด object ที่ต้องใช้เข้ามาให้ แทนที่จะสร้างเอง
ORMObject-Relational Mapping — แปลง object ↔ ตารางฐานข้อมูล
annotation processorตัวประมวลผล annotation ตอน compile เพื่อสร้างโค้ดให้ (เช่นที่ Lombok ใช้)
JMHJava Microbenchmark Harness — เครื่องมือวัดความเร็วโค้ดอย่างแม่นยำ (ดูบทที่ 19)
bytecodeโค้ดกลางที่ JVM อ่าน

เคยสงสัยไหมว่า:

  • ทำไม Spring แค่ใส่ @Autowired แล้ว field ก็มี value? (ยังไม่เคยใช้ Spring ก็ข้ามคำถามนี้ได้ ค่อยกลับมาตอนเรียน Spring จบ)
  • ทำไม Jackson แปลง JSON เป็น object ได้โดยไม่ต้องสอน?
  • ทำไม @Entity ของ JPA ทำให้ class กลายเป็น table?
  • Lombok เพิ่ม getter/setter อัตโนมัติยังไง?
  • @SpringBootApplication ทำงานยังไง?

คำตอบทั้งหมดอยู่ที่ 2 เทคโนโลยี:

  1. Reflection — ตรวจ + ใช้ class ตอน runtime (ขณะโปรแกรมรันอยู่)
  2. Annotation Processing — generate code ตอน compile (ขั้นตอนแปลโค้ดก่อนรัน)

บทนี้สอนทั้งคู่ — จบแล้วคุณจะ "เห็น" Spring ทั้งระบบว่ามันคุยกันยังไง

ใช้เวลา 3-4 ชั่วโมง


Part 1: Reflection คืออะไร

📝 หมายเหตุเรื่อง snippet ในบทนี้: โค้ดทุกชิ้นสมมติว่าอยู่ใน method ที่ประกาศ throws Exception (เพื่อไม่ให้ต้องเขียน try/catch ทุกบรรทัด) และต้อง import java.lang.reflect.*; เสมอ ตัวอย่างรูปแบบ main แบบเต็มเป็นดังนี้:

java
import java.lang.reflect.*;

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        // วาง snippet จากบทนี้ตรงนี้
    }
}

1.1 นิยาม

Reflection = ความสามารถของ program ที่จะ ตรวจ + แก้โครงสร้าง (structure) ของตัวเอง ตอน runtime เช่น ดูว่าคลาสมี field อะไร, method อะไร, ติด annotation อะไรไว้

ปกติเราเขียน:

java
User u = new User();
u.setName("Alice");

→ compiler รู้ตั้งแต่เขียนว่า User มี setName(String)

Reflection ทำเหมือนกันได้ แต่ โดยไม่รู้ class ตอน compile (ขั้นตอนแปลโค้ดก่อนรัน):

java
String className = "com.example.User";
Class<?> clazz = Class.forName(className);
Object obj = clazz.getDeclaredConstructor().newInstance();
Method m = clazz.getMethod("setName", String.class);
m.invoke(obj, "Alice");

→ ใช้แค่ String "User" + "setName" ก็ทำได้

1.2 ทำไมต้องมี Reflection

Library/Framework ที่ต้อง รัน code ที่ "ไม่รู้จัก" ตอน compile:

Frameworkใช้ทำอะไร
Springสร้าง bean จาก class config + inject dependency
Hibernatemap field → column, set value จาก ResultSet
Jackson / Gsonแปลง JSON ↔ object
JUnit / TestNGหา method ที่มี @Test แล้วรัน
JPA EntityManagerpersist object โดยไม่รู้ class
Dependency Injectionสร้าง object + inject ตาม annotation

ถ้าไม่มี reflection → Spring ต้องสร้างโค้ดเฉพาะของแต่ละแอปไว้ล่วงหน้าตอน compile


Part 2: Class — จุดเริ่มของ Reflection

📝 เตือนความจำ: โค้ดทุกชิ้นในบทนี้ต้องการ import java.lang.reflect.*; และ method ที่ใส่ snippet ต้องประกาศ throws Exception — ดูตัวอย่างเต็มใน Part 1 ด้านบน

2.1 ได้ Class<?> ยังไง

จุดเริ่มต้นของ reflection คือได้ object Class<?> ที่เป็น "ข้อมูลของคลาส" — มี 3 วิธี: จาก instance (obj.getClass()), จาก class literal (User.class), หรือจากชื่อ string (Class.forName(...)) ซึ่งวิธีหลังโหลดคลาสแบบ dynamic ได้ (plugin, jar):

java
// 1. จาก instance
User u = new User();
Class<?> c1 = u.getClass();

// 2. จาก class literal
Class<User> c2 = User.class;

// 3. จาก string name (dynamic)
Class<?> c3 = Class.forName("com.example.User");

วิธีที่ 3 ใช้ได้กับ class loader (ตัวโหลดคลาส) → load class จาก jar (ไฟล์รวมโค้ด Java), plugin (ปลั๊กอิน), scripting (สคริปต์)

2.2 ดูข้อมูล class

เมื่อมี Class<?> แล้ว เราถามข้อมูลของคลาสได้ทุกอย่างตอน runtime — ชื่อ, package, super class, interface, modifier และตรวจว่าเป็น interface/enum/array หรือไม่:

java
Class<?> c = User.class;

c.getName();              // "com.example.User"
c.getSimpleName();        // "User"
c.getPackage();           // Package: "com.example"
c.getSuperclass();        // Class<Object>
c.getInterfaces();        // Class[] ของ interface ที่ implement
c.getModifiers();         // int — public/abstract/final (ใช้ Modifier.isPublic ตรวจ)
c.isInterface();          // boolean
c.isEnum();
c.isAnnotation();
c.isArray();

2.3 ดู Field

java
Field[] fields = c.getDeclaredFields();    // ทุก field รวม private
Field[] publicOnly = c.getFields();        // เฉพาะ public + inherited

for (Field f : fields) {
    System.out.println(f.getName() + " : " + f.getType());
    System.out.println("modifiers: " + Modifier.toString(f.getModifiers()));
}

ความต่าง:

  • getFields() — public + ของ super class
  • getDeclaredFields() — ทุก modifier แต่ของ class นี้เท่านั้น (ไม่รวม super)

2.4 อ่าน / แก้ field

java
class User {
    private String name = "default";
}

User u = new User();
Field f = User.class.getDeclaredField("name");
f.setAccessible(true);                  // bypass private! — เปิดให้เข้าถึง field แม้ประกาศเป็น private
System.out.println(f.get(u));           // "default"
f.set(u, "Alice");
System.out.println(f.get(u));           // "Alice"

⚠️ setAccessible(true) = bypass encapsulation

  • กับ class ของเราเอง ที่อยู่ใน unnamed module (project ทั่วไปที่ไม่มี module-info.java) — ยังเรียก setAccessible(true) ได้ปกติ แต่ถ้าโปรเจกต์ใช้ JPMS (มี module-info.java) ต้องเปิด package ด้วย opens ใน module-info หรือ --add-opens
  • กับ JDK internal (เช่น field ใน java.lang.String) — ต้องเปิด module ด้วย --add-opens java.base/java.lang=ALL-UNNAMED ตอน run (ไม่ใช่ทุกกรณีที่ใช้ reflection จะต้องใส่ flag นี้)

2.5 หา + เรียก Method

reflection เรียก method ได้ตอน runtime โดยไม่ต้องรู้ชื่อตอน compile — หา method ด้วย getDeclaredMethod(ชื่อ, ชนิดพารามิเตอร์) แล้วเรียกด้วย invoke(target, args) (target เป็น null ถ้าเป็น static):

java
Method m = User.class.getDeclaredMethod("setName", String.class);
m.setAccessible(true);
m.invoke(u, "Bob");

invoke(target, args...):

  • target = object (หรือ null สำหรับ static)
  • args = parameter

return = Object (ถ้า void → null. ถ้า primitive → box ถูกแปลงเป็น object เช่น int → Integer)

2.6 Constructor

java
Constructor<User> ctor = User.class.getDeclaredConstructor(String.class, int.class);
User u = ctor.newInstance("Alice", 25);

ใช้ constructor ที่ไม่ใช่ default:

java
Class<?> c = Class.forName("com.example.User");
Constructor<?> ctor = c.getDeclaredConstructor(String.class);
Object u = ctor.newInstance("Alice");

Part 3: ลองเขียน — สร้าง mini DI Container

ลองเขียน DI container ขนาดเล็ก เพื่อให้เห็นว่า Spring ใต้พรมเป็นยังไง

3.1 Annotation @Inject

เริ่มจากสร้าง annotation @Inject ของเราเองสำหรับทำเครื่องหมาย field ที่ต้องการให้ container ฉีด dependency ให้ — ต้องตั้ง @Retention(RUNTIME) เพื่อให้ reflection อ่านได้ตอนรัน:

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject { }

(อธิบาย annotation ละเอียดในส่วน Part 5)

3.2 Service ที่ทดสอบ

ก่อนอื่น นิยาม User ที่ใช้ใน Part 3 ให้ครบ (มี constructor, toString):

java
public class User {
    public final long id;
    public final String name;

    public User(long id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{id=" + id + ", name='" + name + "'}";
    }
}

เตรียมคลาสตัวอย่างไว้ทดสอบ DI containerUserRepository (dependency) และ UserService ที่มี field @Inject ชี้ไป repository:

java
public class UserRepository {
    public User findById(long id) {
        return new User(id, "Alice");
    }
}

public class UserService {
    @Inject
    UserRepository repo;

    public User get(long id) {
        return repo.findById(id);
    }
}

3.3 Container

หัวใจของ DI container — ใช้ reflection สร้าง instance, สแกนหา field ที่มี @Inject, แล้วฉีด dependency เข้าไปอัตโนมัติ นี่คือกลไกเบื้องหลังที่ Spring ทำให้เราในระดับใหญ่:

java
public class MiniContainer {
    private final Map<Class<?>, Object> beans = new HashMap<>();

    public <T> T getBean(Class<T> type) throws Exception {
        // 1. ถ้าเคยสร้างแล้ว — คืน (singleton)
        if (beans.containsKey(type)) {
            return type.cast(beans.get(type));
        }

        // 2. สร้าง instance (ใช้ default constructor)
        T instance = type.getDeclaredConstructor().newInstance();
        beans.put(type, instance);

        // 3. inject ทุก field ที่มี @Inject
        for (Field f : type.getDeclaredFields()) {
            if (f.isAnnotationPresent(Inject.class)) {
                f.setAccessible(true);
                Object dep = getBean(f.getType());   // recursive
                f.set(instance, dep);
            }
        }

        return instance;
    }
}

3.4 ใช้

java
MiniContainer ctx = new MiniContainer();
UserService svc = ctx.getBean(UserService.class);
System.out.println(svc.get(1));     // "User{id=1, name='Alice'}"

ทำงานได้! — repo ถูก inject ให้อัตโนมัติ

นี่คือ ~5% ของสิ่งที่ Spring ทำ — แต่หลักการเดียวกัน

⚠️ ข้อจำกัดของ MiniContainer: ระวัง circular dependency — ถ้า A ต้องการ B และ B ต้องการ A (วนเป็นวงกลม) MiniContainer จะ crash ด้วย StackOverflowError Spring Boot ก็ปฏิเสธ pattern นี้เช่นกัน ออกแบบให้ dependency ไหลทิศทางเดียวเสมอ

🛠️ Checkpoint 3.1: ปรับ MiniContainer ให้ support:

  • constructor injection (@Inject ที่ constructor) — hint: ต้องแก้ @Target ของ @Inject ให้รวม ElementType.CONSTRUCTOR ก่อน (ปัจจุบัน @Target(ElementType.FIELD) ติด constructor ไม่ได้)
  • interface → implementation mapping (ถ้า inject UserRepository ที่เป็น interface)
  • prototype scope (สร้างใหม่ทุกครั้ง — ไม่ cache)

Part 4: Generic Type Erasure — Reflection กับ generics

4.1 ปัญหา: type erasure (การลบ type ตอน compile)

reflection กับ generics มีข้อจำกัดสำคัญที่ต้องรู้ — Java ใช้ type erasure คือลบข้อมูล generic ทิ้งตอน compile ดังนั้นตอน runtime List<String> กับ List<Integer> หน้าตาเหมือนกันหมด (เป็น List เฉย ๆ) reflection จึงอ่าน type parameter ตรง ๆ ไม่ได้ในหลายกรณี:

java
List<String> list = new ArrayList<>();
// ตอน runtime: List ธรรมดา (ไม่รู้ว่าเป็น <String>)

list.getClass() == ArrayList.class;       // true
list.getClass().getTypeParameters();      // [E] — แค่ TypeVariable

4.2 แต่ generic ที่ method/field declaration ยังเหลือ

📝 ศัพท์ที่จะเจอ:

  • declaration site (จุดที่ประกาศ) — ตำแหน่งที่เขียน type ไว้ เช่น ตรง return type ของ method หรือ type ของ field
  • ParameterizedType — type ที่มี generic parameter เช่น List<String> หรือ Map<String, Integer>
  • ActualTypeArguments — generic type ที่ใส่จริง ๆ เช่น String ใน List<String>
java
class Repository {
    public List<User> findAll() { return null; }
}

Method m = Repository.class.getMethod("findAll");
Type returnType = m.getGenericReturnType();    // ParameterizedType: List<User>

if (returnType instanceof ParameterizedType pt) {
    Type[] args = pt.getActualTypeArguments();
    System.out.println(args[0]);    // class com.example.User
}

→ Reflection อ่าน generic ที่ declaration site (จุดที่ประกาศ เช่น ตรงที่เขียน return type ของ method หรือ type ของ field) ได้

นี่คือเหตุผลที่ Spring/Jackson อ่าน List<User> ใน field/return ได้

4.3 TypeReference trick

ใช้ใน Jackson:

java
// ต้อง import จาก Jackson: com.fasterxml.jackson.core.type.TypeReference
ObjectMapper objectMapper = new ObjectMapper(); // มาจาก Jackson (เรียนแล้วในบทที่ 11)
TypeReference<List<User>> ref = new TypeReference<List<User>>() {};
List<User> users = objectMapper.readValue(json, ref);

new TypeReference<...>() {} = สร้าง anonymous subclass (คลาสย่อยนิรนาม — คลาสที่สร้างขึ้นมาแบบไม่ตั้งชื่อ ในรูป {}) → generic ติดที่ class declaration → reflection อ่านได้

ทำไมถึงอ่านได้ทั้งที่ 4.1 บอกว่า generic ถูกลบตอน compile? เพราะตอนสร้าง anonymous subclass เราต้องเขียน generic parameter ไว้ตรง ๆ ใน source (TypeReference<List<User>>) — มันจึงถูกบันทึกไว้ที่ class declaration ของ subclass นั้นเอง ซึ่งเป็นจุดที่ reflection อ่านได้ตามหลักใน 4.2 (ต่างจาก List<String> list = ... ที่ generic แค่ "ผ่าน" ตัวแปร ไม่ได้ติดอยู่กับ class ไหนเลย)


Part 5: Annotation ลึก

5.1 ประกาศ annotation

java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Timed {
    String value() default "";
    int threshold() default 1000;
}

3 element สำคัญ:

  • @Retention: เก็บ annotation ที่ระดับไหน
    • SOURCE — มีแค่ใน source code (เช่น @Override) — compiler ใช้ตรวจแล้วทิ้ง
    • CLASS — เก็บใน .class แต่ runtime ไม่เห็น (default ถ้าไม่ระบุ)
    • RUNTIME — runtime อ่านได้ (Reflection)

⚠️ กับดักที่พลาดบ่อย: ถ้าลืมใส่ @Retention(RetentionPolicy.RUNTIME) ตอนสร้าง annotation เอง มันจะตกไปใช้ CLASS โดยอัตโนมัติ (default) ซึ่ง annotation จะยังคง compile ผ่านปกติ แต่พอลองใช้ reflection (isAnnotationPresent, getAnnotation) ตอน runtime จะไม่เจอ annotation เลย โดยไม่มี error ใด ๆ เตือน — เป็นบั๊กที่ debug ยากเพราะโค้ด "ดูถูกต้อง" ทุกอย่าง ต้องจำไว้ว่า: annotation ที่จะให้ framework (Spring, JUnit) หรือ reflection ของเราเองอ่านได้ตอนรัน ต้องระบุ RUNTIME เสมอ ไม่งั้นจะเงียบหายไปเฉย ๆ

  • @Target: ใช้ติดที่ไหนได้ — ค่าที่พบบ่อย: TYPE (ติดที่คลาส), METHOD (ติดที่ฟังก์ชัน), FIELD (ติดที่ตัวแปร), PARAMETER (ติดที่พารามิเตอร์), CONSTRUCTOR (ติดที่ constructor) ค่าอื่น ๆ: LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, MODULE, RECORD_COMPONENT
  • Element — สมาชิกใน annotation:
    • มี return type (ปกติเป็น String, primitive, enum, Class, annotation, array ของพวกนี้)
    • มี default ได้

5.2 ใช้

เมื่อนิยาม annotation @Timed แล้ว นำไปติดบน method ที่ต้องการ พร้อมกำหนดค่า element (value, threshold):

java
@Timed(value = "userService.get", threshold = 500)
public User get(long id) { ... }

5.3 อ่านด้วย Reflection

annotation จะมีประโยชน์เมื่อมีโค้ดอ่านมัน — ใช้ reflection เช็ค isAnnotationPresent() แล้วดึงค่าด้วย getAnnotation() นี่คือวิธีที่ framework (Spring, JUnit) ตอบสนองต่อ annotation ที่เราติด:

java
Method m = UserService.class.getMethod("get", long.class);
if (m.isAnnotationPresent(Timed.class)) {
    Timed t = m.getAnnotation(Timed.class);
    System.out.println(t.value());        // "userService.get"
    System.out.println(t.threshold());    // 500
}

5.4 Meta-annotation — annotation ที่ใช้กับ annotation

java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component                              // ← annotation อื่นมาเป็น meta! (@Component มาจาก org.springframework.stereotype.Component)
public @interface Service { }

📝 ตัวอย่างนี้เป็นภาพประกอบแนวคิด meta-annotation — @Component ต้องมี dependency Spring (org.springframework.stereotype) ถึงจะ compile ได้ ไม่จำเป็นต้องพิมพ์ตามตอนนี้ถ้ายังไม่ได้ setup Spring

→ ทุกอย่างที่ติด @Service ก็เหมือนติด @Component ไปด้วย

Spring ใช้เทคนิคนี้กับ @RestController, @Configuration, ฯลฯ

5.5 Repeatable annotation (Java 8+)

ปกติติด annotation ชนิดเดียวกันซ้ำบน element เดียวไม่ได้ — ตั้งแต่ Java 8 ทำได้ด้วย @Repeatable (ต้องมี annotation "ตัวห่อ" ที่เก็บ array) เหมาะกับกรณีอย่าง @Role("admin") @Role("editor"):

java
@Repeatable(Roles.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface Role {
    String value();
}

@Retention(RetentionPolicy.RUNTIME)
public @interface Roles {
    Role[] value();
}

// ใช้:
@Role("admin")
@Role("editor")
public class Document { }

อ่าน:

java
Role[] roles = Document.class.getAnnotationsByType(Role.class);

Part 6: ใช้ Reflection จริง — สร้าง Mini ORM

ทำ ORM mini ที่ map class → INSERT SQL

6.1 Annotation

มาสร้าง ORM จิ๋ว (map object↔ตาราง) เพื่อเห็นว่า Hibernate/JPA ทำงานยังไง เริ่มจากนิยาม annotation @Table (ชื่อตาราง) และ @Column (ชื่อ/คุณสมบัติคอลัมน์) สำหรับติดบน entity:

java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
    String value();
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    String value() default "";
    boolean primaryKey() default false;
}

6.2 Entity

จากนั้นสร้าง "entity" — คลาสข้อมูลที่ติด annotation บอก mapping ไปยังตาราง/คอลัมน์ ซึ่ง ORM จะอ่านด้วย reflection เพื่อ generate SQL:

java
// หมายเหตุ: field เป็น package-private (ไม่มี modifier) ดังนั้นโค้ดที่ใช้ต้องอยู่ใน package เดียวกัน
// ถ้าต้องการใช้ข้ามหลาย package ให้เปลี่ยนเป็น public หรือตรวจสอบ setAccessible(true) ใน ORM
// ตัวอย่าง: ถ้า MiniOrm อยู่คนละ package กับ User → field.setAccessible(true) ใน MiniOrm.insertSql() จะจัดการให้
@Table("users")
public class User {
    @Column(primaryKey = true) long id;
    @Column String name;
    @Column("email_address") String email;
}

6.3 ORM mini

🔴 DEMO ONLY — โค้ดข้างล่างมีช่องโหว่ SQL Injection โดยตั้งใจ เพื่อโชว์ "เครื่องจักร reflection" ของ ORM ห้ามใช้ pattern string-concat แบบนี้กับ user input จริงเด็ดขาด — ถ้าใส่ name = '); DROP TABLE users;-- (คำสั่ง SQL ที่แอบแฝงมากับ input) โปรแกรมจะส่ง SQL 2 คำสั่งไป DB (ฐานข้อมูล) คือ INSERT ของจริง + DROP TABLE ที่ลบตารางทั้งหมด Production ORM (Hibernate, MyBatis, jOOQ) ใช้ PreparedStatement + parameter binding เสมอ — ดู Checkpoint 6.1 ด้านล่างที่บังคับให้แก้เป็น PreparedStatement

java
// ⚠️ EDUCATIONAL — vulnerable to SQL injection. ห้าม copy ไป production
public class MiniOrm {
    public String insertSql(Object entity) throws Exception {
        Class<?> clazz = entity.getClass();
        // หมายเหตุ: production code ควรตรวจ null เช่น if (tableAnn == null) throw new IllegalArgumentException(clazz + " is missing @Table")
        String table = clazz.getAnnotation(Table.class).value();

        List<String> cols = new ArrayList<>();
        List<String> values = new ArrayList<>();

        for (Field f : clazz.getDeclaredFields()) {
            if (!f.isAnnotationPresent(Column.class)) continue;
            f.setAccessible(true);

            Column col = f.getAnnotation(Column.class);
            String colName = col.value().isEmpty() ? f.getName() : col.value();
            cols.add(colName);

            Object value = f.get(entity);
            // ⚠️ string concat — injectable! ของจริงต้อง bind ผ่าน PreparedStatement
            values.add(value instanceof String ? "'" + value + "'" : String.valueOf(value));
        }

        return "INSERT INTO " + table +
               " (" + String.join(", ", cols) + ")" +
               " VALUES (" + String.join(", ", values) + ")";
    }
}

ใช้:

java
User u = new User();
u.id = 1; u.name = "Alice"; u.email = "a@x.com";

MiniOrm orm = new MiniOrm();
System.out.println(orm.insertSql(u));
// INSERT INTO users (id, name, email_address) VALUES (1, 'Alice', 'a@x.com')

6.3.1 เวอร์ชันที่ปลอดภัย — ใช้ PreparedStatement

โค้ดข้างบนใช้ string concat เพื่อเน้นกลไก reflection ให้ดูง่าย — แต่ห้ามใช้ pattern แบบนั้นกับ user input จริง เวอร์ชันนี้คือวิธีที่ถูกต้อง: build SQL ด้วย ? placeholder แล้ว bind ค่าผ่าน PreparedStatement (DB driver จะ escape และกัน injection ให้):

java
// ✅ ปลอดภัย — bind ค่าผ่าน PreparedStatement
public class SafeMiniOrm {
    public PreparedStatement prepareInsert(Connection conn, Object entity) throws Exception {
        Class<?> clazz = entity.getClass();
        String table = clazz.getAnnotation(Table.class).value();

        List<String> cols = new ArrayList<>();
        List<String> placeholders = new ArrayList<>();
        List<Object> values = new ArrayList<>();

        for (Field f : clazz.getDeclaredFields()) {
            if (!f.isAnnotationPresent(Column.class)) continue;
            f.setAccessible(true);

            Column col = f.getAnnotation(Column.class);
            String colName = col.value().isEmpty() ? f.getName() : col.value();
            cols.add(colName);
            placeholders.add("?");                 // ← ใช้ placeholder ไม่ใช่ค่าตรง
            values.add(f.get(entity));             // เก็บค่าไว้ bind ทีหลัง
        }

        String sql = "INSERT INTO " + table +
                     " (" + String.join(", ", cols) + ")" +
                     " VALUES (" + String.join(", ", placeholders) + ")";

        PreparedStatement ps = conn.prepareStatement(sql);
        for (int i = 0; i < values.size(); i++) {
            ps.setObject(i + 1, values.get(i));    // index เริ่ม 1
        }
        return ps;
    }
}

ใช้:

java
try (PreparedStatement ps = new SafeMiniOrm().prepareInsert(conn, u)) {
    ps.executeUpdate();
}

เปรียบเทียบ 2 เวอร์ชัน:

เวอร์ชันSQL ที่สร้างถ้า name = '); DROP TABLE users;--
MiniOrm (string concat)... VALUES (1, ''); DROP TABLE users;--', ...)DB ลบตาราง!
SafeMiniOrm (PreparedStatement)... VALUES (?, ?, ?) + bindค่าถูก escape — DB เห็นเป็น string ปกติ

หลักจำ: structure ของ SQL (table, column, operator) ใส่ใน string ได้ — แต่ค่าที่มาจากภายนอกต้องผ่าน parameter binding (การผูกค่าเข้ากับ SQL ผ่าน ? placeholder แทนที่จะใส่ค่าตรง ๆ ใน string) เสมอ

🛠️ Checkpoint 6.1: ขยาย SafeMiniOrm ให้:

  • มี select(Class, id) ที่อ่าน DB → instantiate entity
  • support @JoinColumn สำหรับ relationship

Part 7: Dynamic Proxy — สร้าง object ตอน runtime

7.1 ปัญหา

ต้องการ "ห่อ" method call ของ interface โดยที่ไม่รู้ implementation:

  • log ก่อน/หลัง method
  • transaction begin/commit
  • cache result

7.2 JDK Dynamic Proxy (interface เท่านั้น)

java
public interface UserService {
    User get(long id);
}

public class LoggingHandler implements InvocationHandler {
    private final Object target;
    public LoggingHandler(Object target) { this.target = target; }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("→ " + method.getName() + " args=" + Arrays.toString(args));
        long t0 = System.nanoTime(); // บันทึกเวลาเริ่ม (หน่วย nanosecond)
        try {
            return method.invoke(target, args);
        } finally {
            System.out.printf("← %s took %d us%n",
                method.getName(), (System.nanoTime() - t0) / 1000); // เวลาที่ใช้ไป หารด้วย 1000 แปลงเป็น microsecond (µs)
        }
    }
}

// ใช้
UserService real = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class[]{UserService.class},
    new LoggingHandler(real)
);
proxy.get(1);
// → get args=[1]
// ← get took 123 us

นี่คือเทคนิคที่ Spring AOP ใช้สำหรับ @Transactional, @Cacheable, ฯลฯ

7.3 CGLIB / ByteBuddy — proxy class ที่ไม่ใช่ interface

JDK proxy ทำได้แค่ interface. ถ้า target เป็น concrete class (คลาสปกติที่สร้าง object ได้โดยตรง ไม่ใช่ interface) → ใช้ CGLIB หรือ ByteBuddy:

⚠️ ตัวอย่างนี้ต้องการ dependency เพิ่มเติม — ต้องเพิ่ม byte-buddy ใน pom.xml ก่อน (ดูใน comment ในโค้ดด้านล่าง) ถ้ายังไม่เพิ่ม dependency จะ compile ไม่ผ่าน

java
// ตัวอย่างนี้เป็น pseudo-code แสดงแนวคิด — ต้องมี dependency byte-buddy ใน pom.xml:
// <dependency>
//   <groupId>net.bytebuddy</groupId>
//   <artifactId>byte-buddy</artifactId>
//   <version>1.14.18</version>  <!-- ตรวจ version ล่าสุดที่ https://bytebuddy.net -->
// </dependency>
//
// imports ที่ต้องใช้:
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import static net.bytebuddy.matcher.ElementMatchers.any;

// คลาสที่จะสร้าง proxy (ไม่ใช่ interface)
public class UserServiceImpl {
    public User get(long id) { return new User(id, "Alice"); }
}

// interceptor (ต้องมี @RuntimeType หรือ signature ตรงตาม ByteBuddy convention)
public class LoggingInterceptor {
    @RuntimeType
    public static Object intercept(@Origin Method method,
                                   @SuperCall Callable<?> callable) throws Exception {
        System.out.println("→ " + method.getName());
        return callable.call();
    }
}

// สร้าง proxy
Class<? extends UserServiceImpl> proxyClass = new ByteBuddy()
    .subclass(UserServiceImpl.class)
    .method(any())
    .intercept(MethodDelegation.to(LoggingInterceptor.class))
    .make()
    .load(UserServiceImpl.class.getClassLoader())
    .getLoaded();

UserServiceImpl proxy = proxyClass.getDeclaredConstructor().newInstance();

CGLIB/ByteBuddy = generate bytecode ตอน runtime → subclass (คลาสลูก) พร้อม override (เขียนทับ method)

Spring ใช้ CGLIB ถ้า bean ไม่มี interface

📝 หมายเหตุ Spring Boot 3.x: Spring Boot 3.x ยังใช้ CGLIB สำหรับ class-based proxy แต่มีข้อจำกัดใน Java module system (อาจต้องการ --add-opens หรือ opens ใน module-info.java) และใน GraalVM Native Image CGLIB ถูกแทนที่ด้วย AOT-generated proxy


Part 8: ปัญหา + ต้นทุนของ Reflection

8.1 ช้ากว่า direct call

text
Direct method call:      ~1 ns
Reflection invoke:       ~50-100 ns (first), ~5-10 ns (after JIT inline)

(ns = nanosecond นาโนวินาที = 1 ส่วนพันล้านของวินาที — เร็วมาก)

(ตัวเลขโดยประมาณ — วัดบน JDK 21 HotSpot, x86-64 / Linux; แตกต่างได้ตาม JDK version + platform + workload ควรวัดบนเครื่องตัวเองด้วย JMH)

JIT (Just-In-Time compiler — ตัวแปลงโค้ดให้เร็วขึ้นขณะรัน) optimize reflection ได้ดีในยุคหลัง — แต่ยังช้ากว่า direct

⚠️ ตัวเลข "~5-10 ns หลัง JIT inline" เป็นกรณีดีที่สุด (best case) วัดด้วย JMH บน call site ที่เรียก class เดิมซ้ำ ๆ และ argument ไม่ใช่ primitive (ไม่ต้อง box/unbox) ในโค้ดจริงที่ call site เปลี่ยน class บ่อย หรือมี argument เป็น primitive ต้อง box/unbox ทุกครั้ง overhead จะสูงกว่านี้ — อย่าคาดหวังว่า reflection จะเร็วเท่า direct call เสมอไปแค่เพราะ cache ไว้แล้ว

ใน hot path (เส้นทางโค้ดที่ถูกเรียกบ่อยมาก เช่น วนลูปล้านครั้ง) → cache Method object + ใช้ MethodHandle (Java 7+)

8.2 MethodHandle — รุ่นเร็วของ Reflection

java
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle handle = lookup.findVirtual(
    UserService.class,
    "get",
    MethodType.methodType(User.class, long.class)
);
User result = (User) handle.invokeExact(svc, 1L);
  • เร็วกว่า Method.invoke ประมาณ 5-10x (ตัวเลขนี้เป็นการประมาณ ขึ้นกับ JIT optimization และ workload จริง — ควรวัดด้วย JMH บน environment จริงก่อนตัดสินใจ)
  • JIT optimize ได้ดีกว่า
  • type safety เข้มกว่า

LambdaMetafactory (กลไก JVM สำหรับสร้าง lambda อย่างมีประสิทธิภาพ — มันก็สร้างบน MethodHandle ตัวเดียวกันนี้) ใช้ตรงนี้ — สร้าง functional interface (interface ที่มี method เดียว เช่น Runnable, Comparator) แบบ dynamic ตอน runtime

📝 LambdaMetafactory เป็น advanced topic สำหรับคนเขียน library/framework — ถ้าใช้งาน Java ทั่วไปหรือแค่เขียน Spring Boot app ไม่จำเป็นต้องรู้ ข้ามไปได้

privateLookupIn — เข้าถึง private member อย่างปลอดภัย (Java 9+)

แทน setAccessible(true) (เก่า — อ่อน security ใน Java 9+ module system):

java
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.VarHandle;

class Secret {
    private int value = 42;
}

Lookup lookup = MethodHandles.privateLookupIn(Secret.class, MethodHandles.lookup());

// VarHandle = handle ของ field (อ่าน/เขียนแบบ atomic — ทำงานสมบูรณ์หรือไม่ทำเลย ป้องกัน race condition)
VarHandle vh = lookup.findVarHandle(Secret.class, "value", int.class);

Secret s = new Secret();
int v = (int) vh.get(s);              // 42
vh.set(s, 100);

ข้อดี:

  • ทำงานกับ module system ได้ (ต้อง --add-opens หรือ module ที่ open ให้)
  • type-safe กว่า Field.set/get
  • JIT inline ได้ดี → ใกล้ความเร็ว direct access
  • VarHandle รองรับ atomic operations (compareAndSet, getAcquire, ...) → ใช้แทน AtomicInteger ได้ในงาน performance-critical (งานที่ความเร็วสำคัญมาก เช่น ระบบ trading หรือ game engine)

ใช้ใน: Jackson, modern serialization library ที่ต้อง bypass private (ระวัง — ต้อง declare module-info opens ให้ถูกต้อง)

Class-Data Sharing สำหรับ Reflection-heavy app

(ดูบทที่ 16 หัวข้อ AppCDS / Class-Data Sharing) — AppCDS / AOT cache ลด overhead ของ class metadata loading สำคัญสำหรับ framework ที่ scan class จำนวนมากตอน boot (Spring, Hibernate)

8.3 ปัญหา security

reflection มีข้อเสียด้านความปลอดภัย — setAccessible(true) ข้าม private ได้ (ทำลาย encapsulation) ดังนั้น Java 9+ จึงจำกัดการเข้าถึง internal ของ JDK ผ่าน module system ต้องเปิดด้วย --add-opens หากจำเป็น:

setAccessible(true) = bypass private → security risk Java 9+ module system จำกัด setAccessible ของ JDK internal

ใน Spring Boot 3 / Java 17+ — ใช้ --add-opens หรือ Java agent

8.4 ปัญหา native image (GraalVM)

GraalVM native image compile-ahead → ไม่มี runtime metadata → Reflection ต้อง register class ใน reflect-config.json ก่อน

Spring Boot 3 มี AOT processing (Ahead-Of-Time = การประมวลผลล่วงหน้าก่อน runtime เพื่อเตรียม config ให้พร้อม) ที่ generate config นี้ให้อัตโนมัติ (ดูบท Spring Boot 15)


Part 9: Annotation Processing — ทำงานตอน compile

9.1 ความต่างจาก Reflection

ReflectionAnnotation Processing
ทำงานตอนRuntimeCompile time
ต้นทุน runtimeมี (ช้ากว่า direct)ไม่มี (generate code แล้ว)
VisibilityRUNTIME annotationSOURCE / CLASS annotation
ใช้ทำอะไรDI, ORM, JSON, AOPGenerate code (Lombok, MapStruct, Dagger)

9.2 ทำไม Lombok ไม่ใช้ Reflection

ถ้า Lombok ใช้ Reflection แต่ละครั้งที่เรียก getter ก็จะ slow down

แต่ Lombok ใช้ annotation processor — ตอน javac compile, Lombok แทรก getter/setter ลง bytecode

→ runtime = method ปกติ. ไม่มี overhead

9.3 เขียน Annotation Processor

Annotation

java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)    // ← SOURCE — ใช้แค่ตอน compile
public @interface Builder { }

Processor

java
@SupportedAnnotationTypes("com.example.Builder")
// แนะนำ: override getSupportedSourceVersion() แทนการใช้ @SupportedSourceVersion
// เพราะ @SupportedSourceVersion(SourceVersion.RELEASE_21) จะทำให้ compiler เตือน
// "Supported source version 'RELEASE_21' from annotation processor ... less than -source 25"
// เมื่อ compile ด้วย Java 22+ — ดูวิธีที่ถูกต้องใน comment ข้างใน
public class BuilderProcessor extends AbstractProcessor {

    // ✅ วิธีที่แนะนำ: override method แทน @SupportedSourceVersion annotation
    // เพื่อรองรับ Java ทุก version โดยไม่ต้องแก้ไขเมื่อ upgrade Java
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations,
                           RoundEnvironment env) {
        for (Element e : env.getElementsAnnotatedWith(Builder.class)) {
            if (e.getKind() != ElementKind.CLASS) continue;
            TypeElement classElement = (TypeElement) e;
            try {
                generateBuilder(classElement);
            } catch (IOException ex) { /* ... */ }
        }
        return true;
    }

    private void generateBuilder(TypeElement classElement) throws IOException {
        String className = classElement.getSimpleName().toString();
        String packageName = processingEnv.getElementUtils()
            .getPackageOf(classElement).getQualifiedName().toString();

        JavaFileObject file = processingEnv.getFiler()
            .createSourceFile(packageName + "." + className + "Builder");

        try (Writer w = file.openWriter()) {
            w.write("package " + packageName + ";\n");
            w.write("public class " + className + "Builder {\n");
            // ... generate field, withXxx(), build()
            w.write("}\n");
        }
    }
}

Register

สร้างไฟล์ registration ที่ path นี้ (เป็นกลไก Java ServiceLoader ที่บอก compiler ว่ามี processor อยู่ที่ไหน — เรียนละเอียดแล้วในบทที่ 14 Annotations, Modules, Build Tools ถ้าจำไม่ได้ ย้อนกลับไปดูได้):

src/main/resources/META-INF/services/javax.annotation.processing.Processor

📝 หมายเหตุ: ชื่อไฟล์ใช้ javax.annotation.processing.Processor ซึ่งเป็น JDK API ที่ยังคง javax.* namespace (ไม่ใช่ Jakarta EE) ดังนั้น ไม่ต้องเปลี่ยนเป็น jakarta.* แม้จะใช้ Java 17+ หรือ Spring Boot 3+ และต้องสร้าง directory src/main/resources/META-INF/services/ ก่อนด้วย

text
com.example.BuilderProcessor

ใช้

java
@Builder
public class User {
    String name;
    int age;
}

ตอน compile → UserBuilder.java ถูก generate อัตโนมัติ

⚠️ หมายเหตุ: เนื้อหา generateBuilder ข้างบนเขียนแค่โครงคลาสเปล่า (// ... generate field, withXxx(), build()) เป็นตัวอย่างแนวคิด — UserBuilder ที่ได้จริงจะยังไม่มี method withName/withAge/build จนกว่าคุณจะเพิ่มโค้ด generate จริง ลองใช้ JavaPoet ซึ่งง่ายกว่ามาก

วิธีใช้กับ Maven: processor ต้องอยู่ใน project/module แยกต่างหาก แล้ว reference ใน pom.xml ของโปรเจกต์หลัก:

xml
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <annotationProcessorPaths>
          <path>
            <groupId>com.example</groupId>
            <artifactId>my-processor</artifactId>
            <version>1.0.0</version>
          </path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
  </plugins>
</build>

ใช้:

java
User u = new UserBuilder().withName("Alice").withAge(25).build();

ในทางปฏิบัติเราใช้ JavaPoet library ทำ code generation แทนเขียน String — สะดวกกว่า + type safe

9.4 ตัวอย่าง Annotation Processor ที่ใช้จริง

Libraryทำอะไร
Lombok@Getter @Setter @Builder @Data — generate boilerplate
MapStructmapper interface → generate impl
DaggerDI ที่ generate code (ไม่ใช้ reflection ตอน runtime)
AutoValue / Recordsvalue class generation (Records ตอนนี้ built-in)
Immutablesimmutable value class
Spring Boot Configuration Processorgenerate spring-configuration-metadata.json สำหรับ IDE

Part 10: Records + Pattern Matching (Java 21+)

10.1 Record reflection

record (Java 16+) มี API reflection เฉพาะที่สะอาดกว่า — getRecordComponents() ให้รายการ component (ชื่อ + type) ตามลำดับที่ประกาศ ใช้แทน getDeclaredFields() ได้ตรงกว่าเมื่อทำงานกับ record:

📝 ตัวอย่างนี้ใช้ record UserRecord(...) — ตั้งชื่อไม่ให้ชนกับ class User ที่นิยามไว้ใน Part 3 (ถ้าเอาทั้งสองไปไว้ package/ไฟล์เดียวกันจะ compile error เพราะประกาศชื่อ User ซ้ำ)

java
record UserRecord(long id, String name) { }

RecordComponent[] components = UserRecord.class.getRecordComponents();
for (RecordComponent c : components) {
    System.out.println(c.getName() + " : " + c.getType());
}

ใช้แทน getDeclaredFields() — อ่านง่ายกว่าและตรงกับ record โดยตรง (clean = โค้ดที่อ่านง่าย กระชับ)

10.2 Pattern matching with reflection

Java สมัยใหม่ลดความจำเป็นของ reflection ในหลายกรณีด้วย pattern matching — instanceof แบบใหม่ (Java 16+) cast ให้อัตโนมัติ และ pattern matching ใน switch (Java 21+) แตกเคสตามชนิดได้สะอาดกว่าการเช็ค type ด้วย reflection:

java
Object o = ...;

// แบบเดิม
if (o instanceof User) {
    User u = (User) o;
    System.out.println(u.name());
}

// Pattern matching (Java 16+)
if (o instanceof User u) {
    System.out.println(u.name());
}

// Pattern matching for switch (Java 21+)
String desc = switch (o) {
    case User u -> "User: " + u.name();
    case Integer n -> "Number: " + n;
    case null -> "null";
    default -> "unknown";
};

10.3 Deconstruction pattern (Java 21+)

Java 21 เพิ่ม "record pattern" ที่แตก record ออกเป็น field ในบรรทัด instanceof เลย (o instanceof Point(int x, int y)) — ดึงค่าได้โดยไม่ต้อง reflection หรือเรียก getter เอง:

java
record Point(int x, int y) { }

if (o instanceof Point(int x, int y)) {
    System.out.println(x + "," + y);
}

ลด reflection — Java compiler ทำ destructuring ให้


Part 11: Best Practices

11.1 ใช้ Reflection เมื่อจำเป็น

✅ ใช้ตอน:

  • เขียน library/framework
  • Plugin system
  • Test / mocking
  • Serialize/deserialize

❌ อย่าใช้ตอน:

  • เรียก method ของตัวเอง — ใช้ direct call
  • Performance-critical path — ใช้ MethodHandle หรือ pre-generate code

11.2 Cache reflection metadata

reflection ช้ากว่าการเรียกตรง โดยเฉพาะการ "หา" Method/Field (lookup) ทุกครั้ง — ถ้าต้องใช้ซ้ำ ควร cache object Method/Field ไว้ครั้งเดียวแล้ว reuse (framework ทำแบบนี้):

java
// ❌ ช้า — สร้าง Method ทุกครั้ง
public void doIt(Object o) throws Exception {
    Method m = o.getClass().getMethod("hello");
    m.invoke(o);
}

// ✅ cache
private static final Map<Class<?>, Method> CACHE = new ConcurrentHashMap<>();

public void doIt(Object o) throws Exception {
    Method m = CACHE.computeIfAbsent(o.getClass(), c -> {
        try { return c.getMethod("hello"); }
        catch (NoSuchMethodException e) { throw new RuntimeException(e); }
    });
    m.invoke(o);
}

11.3 Prefer Annotation Processor for code gen

ถ้าเป็น boilerplate ที่ pattern ชัด → annotation processor ดีกว่า reflection

  • Zero runtime cost
  • Type safe (compiler ตรวจ generated code)
  • IDE สนับสนุน

11.4 ระวัง security

ช่องโหว่ร้ายแรงของ reflection: อย่า Class.forName() ด้วยชื่อคลาสจาก user input เด็ดขาด เพราะผู้โจมตีอาจใส่ชื่อคลาสอันตราย (เช่น Runtime) แล้วเรียกคำสั่งระบบ = RCE (Remote Code Execution — ผู้โจมตีรันคำสั่งบนเซิร์ฟเวอร์ของเราได้ ถือเป็นช่องโหว่ระดับอันตรายสูงสุด) ถ้าจำเป็นต้องใช้ ให้ whitelist เฉพาะคลาสที่อนุญาต:

📝 หมายเหตุ: ตัวอย่างด้านล่างใช้ request ซึ่งคือข้อมูลที่ผู้ใช้ส่งมาผ่านเว็บ — ถ้ายังไม่เคยเรียน web ให้จำหลักการง่าย ๆ แค่นี้: อย่าเอาชื่อ class ที่ผู้ใช้พิมพ์มาโหลดตรง ๆ ไม่งั้นคนไม่หวังดีอาจเรียกคำสั่งระบบได้

java
// ❌ อย่ารับ class name จาก user input (request คือ HTTP request ใน web app)
Class.forName(request.getParameter("class")).getDeclaredConstructor().newInstance();
// → attacker ใส่ "java.lang.Runtime" + invoke "exec" = RCE!

// ✅ whitelist
Map<String, Class<?>> ALLOWED = Map.of(
    "user", User.class,
    "order", Order.class
);
Class<?> c = ALLOWED.get(request.getParameter("type"));
if (c == null) throw new IllegalArgumentException();

11.5 Handle exceptions ให้ดี

reflection โยน checked exception หลายตัวที่ต้องจัดการ — จุดสำคัญคือ InvocationTargetException "ห่อ" exception จริงที่ method โยนไว้ข้างใน ต้องแกะออกมาด้วย e.getCause() ไม่งั้นจะ debug ยาก:

Reflection throws checked exceptions:

  • ClassNotFoundException (ไม่พบคลาสที่ระบุ)
  • NoSuchMethodException (ไม่พบ method ที่ระบุ)
  • IllegalAccessException (ไม่มีสิทธิ์เข้าถึง)
  • InvocationTargetException (method ที่เรียกโยน exception ออกมา — ต้องแกะด้วย e.getCause())
java
try {
    m.invoke(target, args);
} catch (InvocationTargetException e) {
    // exception จริงอยู่ใน e.getCause()
    Throwable cause = e.getCause();
    if (cause instanceof RuntimeException re) throw re;
    throw new RuntimeException(cause);
}

Part 12: Spring ใช้ Reflection ยังไง (เปิดดูใต้พรม)

12.1 Component scan

java
@ComponentScan("com.example")

Spring:

  1. ใช้ ClassLoader scan jar/directory
  2. หา class ที่มี @Component, @Service, @Repository, @Controller
  3. สำหรับแต่ละ class:
    • สร้าง BeanDefinition (metadata)
    • ตอนสร้าง bean → Class.forNamenewInstance

12.2 Dependency injection

java
@Service
public class UserService {
    @Autowired
    private UserRepository repo;  // field injection — ใช้ที่นี่เพื่อโชว์กลไก reflection ใต้พรม
}

📝 Best practice: field injection แบบนี้ใช้เพื่อแสดงกลไก reflection เท่านั้น — ใน production code ของ Spring Boot 3.x ควรใช้ constructor injection แทน (เพราะทดสอบง่ายกว่า ไม่ซ่อน dependency และ Spring เองแนะนำ)

Spring:

  1. Reflect ทุก field/setter ที่มี @Autowired
  2. หา bean ที่ type ตรง
  3. field.setAccessible(true); field.set(bean, dep);

(สำหรับ constructor injection — ใช้ Constructor.newInstance(deps))

12.3 AOP / @Transactional

@Transactional ทำงานได้เพราะ Spring สร้าง "proxy" ครอบ object จริง — proxy ดักการเรียก method แล้วเปิด/ปิด transaction รอบ ๆ ก่อนเรียกของจริง นี่คือ reflection + dynamic proxy ทำงานร่วมกัน:

⚠️ กับดักที่พลาดบ่อย — self-invocation: proxy ครอบ object "จากภายนอก" เท่านั้น ถ้า method A เรียก method B ที่อยู่ใน object เดียวกัน (เช่น this.saveUser(u) เรียกจากใน class เดียวกัน) การเรียกนั้นจะไม่ผ่าน proxy เลย เพราะเป็นการเรียกตรงผ่าน this ไม่ใช่ผ่าน bean ที่ Spring ห่อไว้ ผลคือ @Transactional บน method B จะไม่ทำงานทั้งที่ดูเหมือนถูกเรียกปกติ — ต้องแยก method ที่ต้องการ transaction ออกไปอีก bean หนึ่ง แล้วเรียกผ่าน bean นั้นแทน

java
@Transactional
public void saveUser(User u) { ... }

Spring:

  1. สร้าง proxy ของ class (JDK Proxy ถ้ามี interface, CGLIB ถ้าไม่มี)
  2. Proxy intercept method call → begin transaction → invoke real method → commit/rollback

12.4 Spring Data JPA Repository

java
public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByNameAndAgeGreaterThan(String name, int age);
}

Spring Data:

  1. ไม่มี implementation ของ interface นี้
  2. Spring สร้าง proxy ที่ implement interface
  3. Method name → parse เป็น query (find + By + Name + And + AgeGreaterThan)
  4. Generate JPQL → execute

นี่คือ method name resolution + dynamic proxy ทำงานร่วมกัน


Part 13: Lab — สร้างของจริง

Lab 1: Mini JSON Serializer

ฝึกใช้ reflection สร้าง JSON serializer ของตัวเอง (โดยไม่ใช้ Jackson) — วน getDeclaredFields() อ่านค่าทุก field แล้วประกอบเป็น JSON เห็นภาพว่า library serialize ทำงานเบื้องหลังยังไง:

java
public class MiniJson {
    public String toJson(Object o) throws Exception {
        if (o == null) return "null";
        if (o instanceof String s) return "\"" + s + "\"";
        if (o instanceof Number || o instanceof Boolean) return o.toString();

        StringBuilder sb = new StringBuilder("{");
        boolean first = true;
        for (Field f : o.getClass().getDeclaredFields()) {
            f.setAccessible(true);
            if (!first) sb.append(",");
            first = false;
            sb.append("\"").append(f.getName()).append("\":");
            sb.append(toJson(f.get(o)));
        }
        sb.append("}");
        return sb.toString();
    }
}

ใช้ (ตั้งชื่อ Person ไม่ให้ชนกับ class User/UserRecord ที่นิยามไว้ก่อนหน้าในบทนี้):

java
record Person(long id, String name) { }
System.out.println(new MiniJson().toJson(new Person(1, "Alice")));
// {"id":1,"name":"Alice"}

📝 ข้อจำกัด: implementation นี้ตัดมุมด้านความสมบูรณ์ — ไม่ escape ตัวอักษรพิเศษเช่น "\", \\\, control characters ถ้า String มีตัวอักษรพิเศษ JSON output จะ invalid นั่นคือเหตุผลที่ควรใช้ Jackson แทนเขียนเอง

Lab 2: Mini Test Runner

ฝึกสร้าง test runner จิ๋วแบบ JUnit — นิยาม annotation @MyTest, ใช้ reflection หา method ที่ติด annotation นั้น, เรียกทีละตัว แล้วรายงานว่าผ่าน/ไม่ผ่าน เข้าใจว่า JUnit ทำงานเบื้องหลังอย่างไร:

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyTest { }

public class TestRunner {
    public static void run(Class<?> testClass) throws Exception {
        Object instance = testClass.getDeclaredConstructor().newInstance();
        int pass = 0, fail = 0;

        for (Method m : testClass.getDeclaredMethods()) {
            if (!m.isAnnotationPresent(MyTest.class)) continue;
            try {
                m.invoke(instance);
                System.out.println("✓ " + m.getName());
                pass++;
            } catch (InvocationTargetException e) {
                System.out.println("✗ " + m.getName() + ": " + e.getCause());
                fail++;
            }
        }

        System.out.printf("Result: %d passed, %d failed%n", pass, fail);
    }
}

ใช้:

java
public class CalcTest {
    @MyTest
    public void testAdd() {
        if (1 + 1 != 2) throw new AssertionError();
    }
    @MyTest
    public void testFail() {
        throw new AssertionError("expected fail");
    }
}

TestRunner.run(CalcTest.class);

Lab 3: ดู Spring bean ทั้งหมด

⚠️ ข้ามไปก่อนจนกว่าจะเรียน Spring Boot จบ — Lab นี้ต้องการ Spring Boot project ที่มี @SpringBootApplication และ dependency spring-boot-starter-web ถ้าลอง run ใน plain Java project จะ error เพราะไม่มี ApplicationContext, @RestController, @GetMapping

ข้ามไปทำ Lab 1-2 ก่อน แล้วกลับมา Lab 3 หลังเรียนบท Spring Boot แล้ว

java
@RestController
public class DebugController {
    private final ApplicationContext ctx;
    public DebugController(ApplicationContext ctx) { this.ctx = ctx; }

    @GetMapping("/beans")
    public Map<String, String> beans() {
        return Arrays.stream(ctx.getBeanDefinitionNames())
            .collect(Collectors.toMap(
                name -> name,
                name -> ctx.getBean(name).getClass().getName()
            ));
    }
}

→ เห็น bean ที่ Spring สร้าง + class จริง (เห็น CGLIB proxy ถ้ามี)


Part 14: Checkpoint

  1. Reflection ทำอะไรได้ที่ direct call ทำไม่ได้?
  2. ความต่างระหว่าง getFields() กับ getDeclaredFields()?
  3. setAccessible(true) ทำอะไร? มี caveat อะไรใน Java 17+?
  4. @Retention(RUNTIME) ต่างกับ @Retention(SOURCE) ยังไง?
  5. JDK Dynamic Proxy ต่างกับ CGLIB ยังไง? ใช้ตอนไหน?
  6. ความต่างระหว่าง Reflection กับ Annotation Processing?
  7. MethodHandle ต่างกับ Method.invoke() ยังไง?
  8. Spring @Autowired ทำงานยังไงใต้พรม?
  9. ทำไม Lombok ใช้ annotation processor แทน reflection?
  10. ใน GraalVM Native Image ทำไม Reflection ต้อง pre-register?

Part 15: สรุปบทนี้

  • Reflection = ดู/แก้ structure ของ class ตอน runtime — รากของ Spring/Hibernate
  • เริ่มจาก Class<?>Field, Method, Constructor, Annotation
  • setAccessible(true) = bypass private แต่ระวัง module system
  • Generic ลบที่ usage แต่เหลือที่ declaration → reflection อ่านได้
  • Annotation = metadata. @Retention กำหนดว่าเก็บถึงไหน
  • Dynamic Proxy = สร้าง object ตอน runtime — Spring AOP ใช้ตรงนี้
  • Reflection ช้ากว่า direct call — cache + ใช้ MethodHandle ถ้า hot path
  • Annotation Processing = generate code ตอน compile (Lombok, MapStruct) — zero runtime cost
  • Spring @Autowired, @Transactional, Spring Data Repository ใช้ reflection + proxy ทั้งหมด

บทต่อไป (สุดท้ายของหมวด Java) — Performance + Profiling — JFR, async-profiler, JMH, flame graph — เครื่องมือสำหรับนัก optimize


← บทที่ 17 | สารบัญ | บทที่ 19: Performance + Profiling →