โหมดมืด
บทที่ 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 DI Dependency Injection — การฉีด object ที่ต้องใช้เข้ามาให้ แทนที่จะสร้างเอง ORM Object-Relational Mapping — แปลง object ↔ ตารางฐานข้อมูล annotation processor ตัวประมวลผล annotation ตอน compile เพื่อสร้างโค้ดให้ (เช่นที่ Lombok ใช้) JMH Java Microbenchmark Harness — เครื่องมือวัดความเร็วโค้ดอย่างแม่นยำ (ดูบทที่ 19) bytecode โค้ดกลางที่ JVM อ่าน
เคยสงสัยไหมว่า:
- ทำไม Spring แค่ใส่
@Autowiredแล้ว field ก็มี value? (ยังไม่เคยใช้ Spring ก็ข้ามคำถามนี้ได้ ค่อยกลับมาตอนเรียน Spring จบ) - ทำไม Jackson แปลง JSON เป็น object ได้โดยไม่ต้องสอน?
- ทำไม
@Entityของ JPA ทำให้ class กลายเป็น table? - Lombok เพิ่ม getter/setter อัตโนมัติยังไง?
- @SpringBootApplication ทำงานยังไง?
คำตอบทั้งหมดอยู่ที่ 2 เทคโนโลยี:
- Reflection — ตรวจ + ใช้ class ตอน runtime (ขณะโปรแกรมรันอยู่)
- Annotation Processing — generate code ตอน compile (ขั้นตอนแปลโค้ดก่อนรัน)
บทนี้สอนทั้งคู่ — จบแล้วคุณจะ "เห็น" Spring ทั้งระบบว่ามันคุยกันยังไง
ใช้เวลา 3-4 ชั่วโมง
Part 1: Reflection คืออะไร
📝 หมายเหตุเรื่อง snippet ในบทนี้: โค้ดทุกชิ้นสมมติว่าอยู่ใน method ที่ประกาศ
throws Exception(เพื่อไม่ให้ต้องเขียน try/catch ทุกบรรทัด) และต้องimport java.lang.reflect.*;เสมอ ตัวอย่างรูปแบบ main แบบเต็มเป็นดังนี้:javaimport 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 |
| Hibernate | map field → column, set value จาก ResultSet |
| Jackson / Gson | แปลง JSON ↔ object |
| JUnit / TestNG | หา method ที่มี @Test แล้วรัน |
| JPA EntityManager | persist 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 classgetDeclaredFields()— ทุก 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 container — UserRepository (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 ด้วย
StackOverflowErrorSpring 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] — แค่ TypeVariable4.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
| Reflection | Annotation Processing | |
|---|---|---|
| ทำงานตอน | Runtime | Compile time |
| ต้นทุน runtime | มี (ช้ากว่า direct) | ไม่มี (generate code แล้ว) |
| Visibility | RUNTIME annotation | SOURCE / CLASS annotation |
| ใช้ทำอะไร | DI, ORM, JSON, AOP | Generate 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+ และต้องสร้าง directorysrc/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ที่ได้จริงจะยังไม่มี methodwithName/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 |
| MapStruct | mapper interface → generate impl |
| Dagger | DI ที่ generate code (ไม่ใช้ reflection ตอน runtime) |
| AutoValue / Records | value class generation (Records ตอนนี้ built-in) |
| Immutables | immutable value class |
| Spring Boot Configuration Processor | generate 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:
- ใช้ ClassLoader scan jar/directory
- หา class ที่มี
@Component,@Service,@Repository,@Controller - สำหรับแต่ละ class:
- สร้าง
BeanDefinition(metadata) - ตอนสร้าง bean →
Class.forName→newInstance
- สร้าง
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:
- Reflect ทุก field/setter ที่มี
@Autowired - หา bean ที่ type ตรง
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:
- สร้าง proxy ของ class (JDK Proxy ถ้ามี interface, CGLIB ถ้าไม่มี)
- 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:
- ไม่มี implementation ของ interface นี้
- Spring สร้าง proxy ที่ implement interface
- Method name → parse เป็น query (find + By + Name + And + AgeGreaterThan)
- 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และ dependencyspring-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
- Reflection ทำอะไรได้ที่ direct call ทำไม่ได้?
- ความต่างระหว่าง
getFields()กับgetDeclaredFields()? setAccessible(true)ทำอะไร? มี caveat อะไรใน Java 17+?@Retention(RUNTIME)ต่างกับ@Retention(SOURCE)ยังไง?- JDK Dynamic Proxy ต่างกับ CGLIB ยังไง? ใช้ตอนไหน?
- ความต่างระหว่าง Reflection กับ Annotation Processing?
MethodHandleต่างกับMethod.invoke()ยังไง?- Spring
@Autowiredทำงานยังไงใต้พรม? - ทำไม Lombok ใช้ annotation processor แทน reflection?
- ใน GraalVM Native Image ทำไม Reflection ต้อง pre-register?
Part 15: สรุปบทนี้
- Reflection = ดู/แก้ structure ของ class ตอน runtime — รากของ Spring/Hibernate
- เริ่มจาก
Class<?>→Field,Method,Constructor,Annotation setAccessible(true)= bypassprivateแต่ระวัง 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