โหมดมืด
บทที่ 14 — Annotations, Modules, Build Tools, Javadoc
📓 โซนอ้างอิง—เปิดตอนต้องใช้ (บท 11-19) บทนี้อยู่ในโซนที่ 2 มือใหม่ข้ามไปก่อนได้ — แต่ส่วน "Build Tools" (Maven/Gradle) จะได้ใช้ค่อนข้างเร็วตอนเริ่มโปรเจกต์จริง/ขึ้น Spring Boot ส่วน Modules (JPMS) กับการเขียน annotation เองนั้นลึกและเจอไม่บ่อย — ข้ามได้สบาย ๆ
บทนี้รวม "เครื่องมือรอบ ๆ ภาษา" ที่ใช้ในทุกโปรเจกต์จริง:
- Annotations (แอนโนเทชัน = ป้ายกำกับบนโค้ดที่ compiler/framework/เครื่องมือต่าง ๆ อ่านได้) — อ่าน + เขียนเอง
- Modules — Java 9+ module system (JPMS = Java Platform Module System = ระบบแบ่งโค้ดเป็นโมดูลของ Java)
- Build Tools (เครื่องมือ build = ตัวรวบรวม/คอมไพล์/จัดการ library ของโปรเจกต์) — Maven vs Gradle
- Javadoc — เขียน document ในโค้ด
ศัพท์ที่จะเจอบ่อยในบทนี้: metadata (ข้อมูลกำกับข้อมูลอีกที — ข้อมูลที่บอก "ข้อมูลนี้คืออะไร"), annotation processor (ตัวประมวลผล annotation ตอน compile เพื่อสร้างโค้ด/ตรวจสอบ), dependency (ดีเพนเดนซี = library ที่โปรเจกต์เราพึ่งพา), artifact (ผลผลิตที่ build ออกมา เช่นไฟล์ .jar), Maven/Gradle (เครื่องมือ build ยอดนิยม 2 ตัวของ Java)
Part 1: Annotations
1. Annotation คืออะไร
Annotation = "metadata" บนโค้ด — ส่วนใหญ่ไม่เปลี่ยน logic การทำงาน แค่เป็นป้ายให้ tool/framework เอาไปอ่านต่อ
ข้อยกเว้นคือ annotation processor บางตัวที่ generate โค้ดใหม่ตอน compile จริง ๆ เช่น Lombok (library ที่ generate getter/setter/constructor ให้อัตโนมัติ — ไม่ต้องเขียนเอง) และ MapStruct (library ที่ generate โค้ดแปลง object จาก type หนึ่งไปอีก type — ใช้บ่อยใน Spring Boot)
java
@Override // บอก compiler ว่าตั้งใจ override
public String toString() { ... }
@Deprecated // บอก IDE ว่า method เก่า
public void oldMethod() { ... }
@SuppressWarnings("unchecked") // ปิด warning
public List<String> cast(List raw) { ... }ในงานจริงเจอบ่อยจาก:
- Spring:
@RestController,@Autowired,@Service— annotation เหล่านี้จะอธิบายละเอียดในบท Spring Boot (โดยเฉพาะ@Autowiredบน field เป็น anti-pattern = รูปแบบที่ดูง่ายแต่มีปัญหาซ่อนอยู่ — รายละเอียดในบท Spring) - JPA:
@Entity,@Id,@Column - JUnit:
@Test,@BeforeEach - Jackson:
@JsonProperty,@JsonIgnore
2. Annotation Built-in ของ Java
Java มี annotation มาตรฐานที่ใช้บ่อย — @Override (ยืนยันว่า override ถูก), @Deprecated (เลิกใช้), @SuppressWarnings (ปิด warning), @FunctionalInterface (interface สำหรับ lambda) ควรรู้จักทั้งหมดเพราะเจอตลอด:
java
@Override // method นี้ override จาก parent — ถ้าพิมพ์ผิด compile fail
@Deprecated(since = "21", forRemoval = true)
@SuppressWarnings({"unchecked", "deprecation"})
@FunctionalInterface // interface นี้ใช้เป็น lambda ได้ (มี abstract method 1 ตัว)
@SafeVarargs // generic varargs ปลอดภัย3. Annotation ทำงานยังไง
มี 3 retention policy (นโยบายการเก็บรักษา annotation):
| Retention | อยู่ถึงไหน | ตัวอย่าง |
|---|---|---|
SOURCE | แค่ compile time → หาย | @Override, @SuppressWarnings |
CLASS | อยู่ใน .class — runtime อ่านไม่ได้ | default |
RUNTIME | อยู่ตลอด — reflection อ่านได้ | @Test, @Autowired |
Framework ใช้ RUNTIME เพื่อ scan ตอน app start
⚠️ กับดักที่พลาดบ่อย: ถ้าเขียน annotation เองแล้วไม่ระบุ
@Retention(RUNTIME)มันจะหายไปตั้งแต่ compile time หรืออยู่ใน.classแบบ reflection อ่านไม่ได้ (default =CLASS) — โค้ด framework ที่ scan หา annotation ตอน runtime จะ "หาไม่เจอ" แบบเงียบ ๆ ไม่มี error เตือน ต้องระบุRUNTIMEตรง ๆ เสมอถ้าจะให้ Spring/JUnit-style scanning ทำงาน
4. เขียน Annotation เอง
java
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) // runtime อ่านได้
@Target(ElementType.METHOD) // ใช้บน method เท่านั้น
public @interface Loggable {
String level() default "INFO";
boolean includeArgs() default false;
}ใช้:
java
public class UserService {
private final UserRepository repo; // ... (inject มาทาง constructor)
@Loggable(level = "DEBUG", includeArgs = true)
public User findUser(Long id) {
return repo.findById(id);
}
}@Target มีอะไรบ้าง
java
ElementType.TYPE // class, interface, enum
ElementType.FIELD // field
ElementType.METHOD // method
ElementType.PARAMETER // parameter
ElementType.CONSTRUCTOR
ElementType.LOCAL_VARIABLE
ElementType.ANNOTATION_TYPE // @interface
ElementType.PACKAGE
ElementType.TYPE_PARAMETER // <T> generic
ElementType.TYPE_USE // ทุกที่ที่เขียน type — @NonNull String s, List<@NonNull String>
ElementType.MODULE
ElementType.RECORD_COMPONENT // component ของ record (Java 16+)Meta-annotation อื่น ๆ
| Meta-annotation | ทำอะไร |
|---|---|
@Retention(SOURCE/CLASS/RUNTIME) | annotation อยู่ถึงไหน — SOURCE = compile แล้วหาย, CLASS = อยู่ใน .class ไม่อ่าน runtime (default), RUNTIME = reflection อ่านได้ |
@Target(...) | ใช้บนอะไรได้ (ดูตาราง) |
@Documented | รวมใน Javadoc |
@Inherited | sub-class สืบทอด annotation จาก super-class |
@Repeatable(C.class) | annotation ใส่ซ้ำได้บน element เดียว |
@Inherited — annotation สืบทอด
java
import java.lang.annotation.*;
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Audited {}
@Audited
class Base {}
class Child extends Base {} // ✅ ถือว่ามี @Audited (เพราะ Base มี + @Inherited)⚠️
@Inheritedทำงานเฉพาะ class inheritance — ไม่มีผลกับ interface
@Repeatable — ใส่ annotation ซ้ำได้ (Java 8+)
java
@Repeatable(Schedules.class)
@Retention(RUNTIME)
@interface Schedule {
String day();
}
@Retention(RUNTIME)
@interface Schedules { // container
Schedule[] value();
}
@Schedule(day = "Monday")
@Schedule(day = "Wednesday")
public void task() { }
// อ่านทั้งหมด (ต้องดึง Method object ผ่าน reflection ก่อน)
Method method = MyClass.class.getMethod("task"); // MyClass = class ที่มี method task()
Schedule[] all = method.getAnnotationsByType(Schedule.class);5. อ่าน Annotation ด้วย Reflection
java
import java.lang.reflect.*;
Class<?> clazz = UserService.class;
for (Method m : clazz.getDeclaredMethods()) {
Loggable ann = m.getAnnotation(Loggable.class);
if (ann != null) {
System.out.println(m.getName() + " — level=" + ann.level()
+ ", includeArgs=" + ann.includeArgs());
}
}ในงานจริง framework (Spring, JUnit) ทำตรงนี้ให้ — เราไม่ค่อยเขียน reflection เอง
6. ตัวอย่างจริง — @RequiresAuth
ใน Spring (ตัวอย่างจริงเขียนเอง):
java
@Retention(RUNTIME)
@Target(METHOD)
public @interface RequiresAuth {
String[] roles() default {};
}
// ใช้
@RequiresAuth(roles = {"ADMIN"})
public void deleteUser(Long id) { ... }แล้วเขียน AOP aspect หรือ interceptor อ่าน annotation → check role
AOP (Aspect-Oriented Programming) = เทคนิคใส่โค้ดพิเศษ (เช่น logging, security check) รอบ method โดยไม่แก้ method ตรง ๆ
- aspect = ส่วนของโค้ดที่ตัดขวางหลาย class พร้อมกัน — เขียนครั้งเดียวแล้ว Spring ฉีดเข้าทุก method ที่มี annotation ให้อัตโนมัติ
- interceptor = ตัวดักจับ request/response ก่อนถึง method จริง
จะเจอ AOP ในบท Spring Boot ที่เรียนต่อไป
Part 2: Java Modules (Java 9+)
7. ปัญหาก่อนมี Module
ก่อน Java 9:
- ไม่มีระบบ "module" — แค่ classpath (เส้นทางที่ JVM ใช้ค้นหา .class file และ library — คล้าย PATH ของ OS แต่ JVM ใช้ค้นหาไฟล์ .class แทน — ทุกอย่างอยู่ในกองเดียวกัน ใครก็เข้าถึงได้ทุก class) ใหญ่ ๆ
- ทุก public class ใน library = ใครก็เรียกได้ (encapsulation พัง)
- JDK เอง ใหญ่ขึ้นเรื่อย ๆ — ทุกคนได้ทั้งก้อน (ทำ container เล็กยาก)
Java 9 มี JPMS (Java Platform Module System) = Project Jigsaw
8. โครงสร้าง Module
ตั้งแต่ Java 9 มี "module system" (JPMS) ที่ให้แบ่งโค้ดเป็น module พร้อมประกาศว่า export อะไรและพึ่งพา module ไหน หัวใจคือไฟล์ module-info.java ที่วางไว้ที่ราก module โครงสร้างไฟล์เป็นแบบด้านล่าง:
text
my-app/
├── src/
│ └── com.example.app/ ← module name
│ ├── module-info.java ← ⭐ ไฟล์สำคัญ
│ └── com/example/app/
│ ├── Main.java
│ └── service/UserService.javamodule-info.java:
java
module com.example.app {
requires java.net.http; // ใช้ HttpClient
requires com.fasterxml.jackson.databind;
exports com.example.app.api; // public class ใน package นี้ คนอื่นเรียกได้
// package อื่น = private อัตโนมัติ — แม้จะเป็น public class
opens com.example.app.entity to com.fasterxml.jackson.databind;
// reflection access เฉพาะ Jackson
}9. Keyword ใน module-info
| Keyword | ใช้ทำอะไร |
|---|---|
requires X | module นี้ใช้ X |
requires transitive X | ใคร require ตัวเรา = ได้ X ฟรี |
exports a.b.c | public class ใน package นี้ เปิดให้คนอื่น |
exports a.b.c to X | เปิดเฉพาะให้ module X |
opens a.b.c | เปิดให้ reflection (Spring/Jackson ต้องใช้) |
uses X | บอกว่าจะใช้ ServiceLoader |
provides X with Y | บอกว่ามี implementation Y ของ X |
10. ทำไมยังไม่ใช้แพร่หลาย?
- เก่าก่อน Java 9 = ไม่มี module ใช้ "classpath" (รายการ path ที่ JVM ค้นหา .class — เปรียบได้กับ PATH ใน OS) → Java 9+ ยังรองรับกลุ่มนี้โดยจัดเป็น "unnamed module" คือ library เก่าที่ไม่มี module-info แต่อยู่บน classpath ทั้งหมด ทุก module ที่ import ได้ก็มองเห็น unnamed module นี้หมด
- Library เก่า ๆ ส่วนใหญ่ ไม่มี
module-info.java→ ใช้ "automatic module" (library เก่าที่ถูกวางบน module-path — ได้ชื่อ module อัตโนมัติจากชื่อไฟล์ jar หรือจากAutomatic-Module-Nameใน manifest) - Spring Boot ส่วนใหญ่ไม่ใช้ JPMS — ใช้ classpath แบบเดิม
สรุป: รู้ว่ามี + อ่านเข้าใจ — เขียนเองตอนทำ library/CLI เท่านั้น
10.5 ServiceLoader — plugin pattern ของ JDK
ServiceLoader = SPI (Service Provider Interface) — ให้ผู้อื่นเสียบ "implementation" ของ interface ของเราโดยไม่ต้อง compile ร่วมกัน
java
// 1. ประกาศ interface
package com.app.payment;
public interface PaymentProcessor {
boolean process(double amount);
}
// 2. มี implementation
package com.stripe;
public class StripeProcessor implements PaymentProcessor {
public boolean process(double amount) { /* ... */ return true; }
}ลงทะเบียน implementation 2 แบบ
แบบ classic — file META-INF/services/com.app.payment.PaymentProcessor (META-INF/services/ คือโฟลเดอร์พิเศษภายใน .jar ที่ JVM ใช้ค้นหา service implementation — สร้างไว้ที่ src/main/resources/META-INF/services/):
text
com.stripe.StripeProcessor
com.paypal.PayPalProcessorแบบ JPMS — ใน module-info.java:
java
module com.stripe {
requires com.app.payment;
provides com.app.payment.PaymentProcessor with com.stripe.StripeProcessor;
}โหลด
java
ServiceLoader<PaymentProcessor> loader = ServiceLoader.load(PaymentProcessor.class);
for (PaymentProcessor p : loader) {
p.process(100);
}
// stream + filter — โหลด provider บางตัว
loader.stream()
.filter(provider -> provider.type().getName().contains("Stripe"))
.findFirst()
.map(ServiceLoader.Provider::get)
.ifPresent(p -> p.process(100));ตัวอย่างจริง: JDBC driver, SLF4J binding, java.time ZoneRulesProvider, security provider — ทุกตัวใช้ ServiceLoader
💡 Spring/Spring Boot ใช้กลไกของตัวเอง (
spring.factories/AutoConfiguration.imports) ที่คล้าย ๆ ServiceLoader
11. jlink — สร้าง custom JRE
ข้อดีจริงของ module: jlink สร้าง JRE ที่มีแค่ module ที่ใช้
📝
$JAVA_HOMEคืออะไร: environment variable (ตัวแปรระบบ) ที่ชี้ไปยังโฟลเดอร์ที่ติดตั้ง JDK ไว้
- ตรวจสอบด้วย
echo $JAVA_HOME(Linux/Mac) หรือecho $env:JAVA_HOME(PowerShell)- ถ้าว่างเปล่า ให้หา path ด้วย
java -XshowSettings:all 2>&1 | grep java.home(Linux/Mac) หรือjava -XshowSettings:all 2>&1 | findstr java.home(Windows)- แล้วตั้งค่า:
export JAVA_HOME=/path/to/jdk(Linux/Mac) หรือ$env:JAVA_HOME = "C:\path\to\jdk"(PowerShell)
bash
# bash (Linux/Mac) — \ ต่อบรรทัด, : คั่น path, $JAVA_HOME
jlink \
--module-path $JAVA_HOME/jmods:mods \
--add-modules com.example.app \
--output custom-jre \
--strip-debug --no-header-files --no-man-pages --compress=zip-6powershell
# PowerShell (Windows) — ` ต่อบรรทัด, ; คั่น path, $env:JAVA_HOME
jlink `
--module-path "$env:JAVA_HOME\jmods;mods" `
--add-modules com.example.app `
--output custom-jre `
--strip-debug --no-header-files --no-man-pages --compress=zip-6ได้ JRE ขนาด ~30-50 MB (เทียบ full JDK 300 MB) — ดีสำหรับ Docker image
⚠️ Spring Boot ใช้ GraalVM native image แทน (เล็กกว่า + เร็วกว่า)
Part 3: Build Tools — Maven vs Gradle
12. ทำไมต้องมี Build Tool?
โปรเจกต์จริง:
- ต้อง download library (Jackson, Spring, JUnit, ...) — กี่ตัว version อะไร?
- ต้อง compile หลายร้อยไฟล์
- ต้อง package เป็น jar
- ต้อง run test
- ต้อง publish ขึ้น artifact repository
ทำเองด้วย javac + jar = ฝันร้าย → ใช้ build tool
| Maven | Gradle | |
|---|---|---|
| ไฟล์ config | pom.xml (XML) | build.gradle (Groovy) หรือ build.gradle.kts (Kotlin) |
| Verbose | XML ยาว | สั้น |
| ความเร็ว | ปานกลาง | เร็ว (incremental, daemon, cache) |
| Plugin | เยอะ + เสถียร | เยอะ + flexible |
| ความยากในการเรียนรู้ | ตรง ๆ | สูงขึ้น |
| Spring Boot default | ✅ (แนะนำสำหรับมือใหม่ — doc/ตัวอย่างเยอะกว่า) | ✅ start.spring.io (เว็บสร้างโปรเจกต์ Spring Boot ใหม่) ปัจจุบัน default เป็น Gradle |
| Android | ❌ | ✅ standard |
สรุป: Spring Boot project มือใหม่ → ใช้ Maven (เริ่มง่าย, doc/ตัวอย่างเยอะกว่า)
Android / โปรเจกต์ใหญ่ → Gradle
⚠️ ถ้าสร้าง project จาก start.spring.io: เว็บนี้ตั้งค่า default เป็น Gradle — ให้เปลี่ยนก่อนกด Generate โดยดูที่ dropdown "Build:" แล้วเลือก Maven แทน Gradle เพื่อให้ตรงกับตัวอย่างในหนังสือนี้
13. Maven — โครงสร้าง
text
my-app/
├── pom.xml ← config
├── src/
│ ├── main/
│ │ ├── java/ ← source code
│ │ └── resources/ ← config, properties, static files
│ └── test/
│ ├── java/ ← test code
│ └── resources/
└── target/ ← output (compile, jar) — gitignoreMaven convention = ทุกโปรเจกต์ structure เหมือนกัน — ลดความคิด
pom.xml ขั้นต่ำ
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId> <!-- บริษัท / org -->
<artifactId>my-app</artifactId> <!-- ชื่อ project -->
<version>1.0.0</version> <!-- version -->
<packaging>jar</packaging> <!-- jar / war / pom -->
<properties>
<!-- ใช้ <release> (Java 9+) แทน source/target — กระชับและถูกต้องกว่า; ปรับเลขตาม JDK ที่ติดตั้งจริง (ที่นี่ใช้ 21 LTS) -->
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.19.0</version>
<!-- ตรวจ version ล่าสุดได้ที่ https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<!-- pom.xml นี้เป็น standalone Java project — ถ้าใช้ Spring Boot parent จะได้ version ของ Jackson และ JUnit จาก BOM โดยอัตโนมัติ ไม่ต้องระบุ version เอง -->
<!-- เมื่อมีหลาย Jackson dep แนะนำให้ใช้ jackson-bom (ดู section BOM ด้านล่าง) แทนการ hardcode version แยกตัว -->
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version> <!-- ถ้าใช้ Spring Boot BOM ให้ลบ <version> ออก — BOM จัดการให้แล้ว -->
<scope>test</scope>
</dependency>
</dependencies>
</project>⚠️ pom.xml ขั้นต่ำนี้ยังไม่มี Maven Surefire Plugin (plugin ที่สั่งรัน JUnit test ตอน
mvn test) — ถ้าเพิ่ม dependency JUnit 5 แล้วรันmvn testแล้วเจอ "No tests found" ให้ดู section Surefire ในบทที่ 15 (Testing + Logging)
Dependency Scope — ตารางครบ
| Scope | compile classpath | test classpath | runtime classpath | อยู่ใน jar สุดท้าย | ตัวอย่าง |
|---|---|---|---|---|---|
compile (default) | ✅ | ✅ | ✅ | ✅ | jackson, slf4j |
provided | ✅ | ✅ | ❌ (มีจาก container) | ❌ | jakarta.servlet-api (Spring Boot 3.x ใช้ Jakarta EE 10+ — ไม่ใช่ javax.*) |
runtime | ❌ | ✅ | ✅ | ✅ | JDBC driver, Logback impl |
test | ❌ | ✅ | ❌ | ❌ | JUnit, Mockito |
system | ✅ | ✅ | ✅ | ❌ | jar นอก repo (ลืม! anti-pattern) |
import | — | — | — | — | ใช้กับ BOM (ดู section ถัดไป) |
💡 Lombok ไม่ได้อยู่ใน scope
provided— ปกติใช้ scopecompile(default) ร่วมกับ<optional>true</optional>ซึ่งเป็นคนละกลไกกับตาราง scope ด้านบน (<optional>true</optional>แค่บอกว่าไม่ต้อง propagate dependency นี้ไปให้ project อื่นที่มาพึ่งเรา)
BOM (Bill of Materials) — แชร์ version ระหว่างหลาย dependency
ปัญหา: Spring Boot มี dependency หลาย dozen ตัว ต้อง compatible version กัน → ใช้ BOM:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.4.x</version> <!-- ⚠️ ใช้ version ล่าสุดของ Spring Boot 3.x — ตรวจสอบที่ https://spring.io/projects/spring-boot (ณ กลาง 2026 อยู่ที่ 3.4.x/3.5.x) -->
<type>pom</type>
<scope>import</scope> <!-- key! -->
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <!-- version มาจาก BOM -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <!-- version มาจาก BOM -->
</dependency>
</dependencies>จุดเด่น:
- ระบุ version ที่เดียว — auto upgrade ทุก dep พร้อมกัน
- กัน "transitive version conflict" (transitive แปลว่า พึ่งพาทางอ้อม = dependency ของ dependency — เช่น lib A พึ่ง jackson, lib B พึ่ง jackson แต่คนละ version → conflict)
- Spring Boot, AWS SDK, Google Cloud — ทุกตัวมี BOM
Multi-module project — โปรเจกต์ใหญ่แบ่งหลาย module
text
parent/
├── pom.xml ← parent POM (packaging=pom)
├── common/
│ └── pom.xml ← module 1
├── api/
│ └── pom.xml ← module 2 (depends on common)
└── service/
└── pom.xml ← module 3 (depends on api)xml
<!-- parent/pom.xml -->
<project>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging> <!-- ไม่ใช่ jar -->
<modules>
<module>common</module>
<module>api</module>
<module>service</module>
</modules>
<dependencyManagement>
<!-- version กลาง — module ลูกใช้ได้โดยไม่ระบุ version -->
</dependencyManagement>
</project>xml
<!-- service/pom.xml -->
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>service</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>api</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>build ทั้งหมดที่ root: mvn install
build เฉพาะ module service + dependency ที่มันต้องการ: mvn -pl service -am install (-pl project list + -am also-make = build deps ก่อน)
14. คำสั่ง Maven ที่ใช้บ่อย
Maven ทำงานผ่าน "lifecycle" — แต่ละคำสั่งรันขั้นก่อนหน้าให้ด้วย (เช่น package จะ compile + test ให้ก่อน) รวมคำสั่งที่ใช้บ่อยไว้ด้านล่างเป็นที่เดียว:
bash
mvn compile # compile src/main
mvn test # compile + run test
mvn package # → target/my-app-1.0.0.jar
mvn install # บันทึกลง local Maven cache (~/.m2) เพื่อให้ module/โปรเจกต์อื่นเรียกใช้ได้ — ไม่ใช่ติดตั้งลงเครื่อง
mvn clean # ลบ target/
mvn dependency:tree # ดู dependency graph
mvn -DskipTests package # package โดยไม่รอ test — ใช้เมื่อ build Docker image หลัง test ผ่านแล้วในขั้นก่อนหน้า
# ใน Spring Boot
mvn spring-boot:run # run appBuild Lifecycle
text
validate → compile → test → package → verify → install → deployแต่ละ phase ครอบคลุม phase ก่อนหน้า — mvn package = compile + test + package
15. Maven Wrapper — มาตรฐาน
ทำให้ทุกเครื่อง build ด้วย Maven version เดียวกัน — แม้ไม่มี Maven installed:
bash
# Setup ครั้งเดียว
mvn wrapper:wrapper
# จากนั้นใช้ ./mvnw (Linux/Mac) หรือ mvnw.cmd (Windows) แทน mvn
./mvnw clean install16. Gradle — ภาพรวม
build.gradle.kts (Kotlin DSL):
kotlin
plugins {
id("java")
id("org.springframework.boot") version "3.4.x" // ใช้ version ล่าสุด — ตรวจสอบที่ https://spring.io/projects/spring-boot
}
group = "com.example"
version = "1.0.0"
java {
sourceCompatibility = JavaVersion.VERSION_21
// สำหรับโปรเจกต์ที่ต้องการ reproducible build แนะนำใช้ Java Toolchain แทน:
// toolchain { languageVersion = JavaLanguageVersion.of(21) }
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.test {
useJUnitPlatform()
}คำสั่ง Gradle
bash
./gradlew build # compile + test + jar
./gradlew test # run test
./gradlew bootRun # run Spring Boot
./gradlew clean
./gradlew dependencies # dependency tree
./gradlew --refresh-dependenciesGradle Kotlin DSL vs Groovy DSL
Gradle มี 2 DSL:
build.gradle— Groovy (เก่า — โครงการเก่ายังเจอ)build.gradle.kts— Kotlin (ใหม่ — แนะนำสำหรับโครงการใหม่)
Kotlin DSL ได้ IDE auto-complete + type safety เต็ม — ไม่ต้องเดา syntax อีก
Version Catalog (libs.versions.toml) — Gradle 7+
Version Catalog (catalog = รายการ library ทุกตัวพร้อม version — เก็บไว้ที่เดียว) ใช้ไฟล์
.toml(รูปแบบ config file อ่านง่าย คล้าย INI แต่มีโครงสร้างชัดกว่า XML)
แทนการ hardcode version ทุกที่:
toml
# gradle/libs.versions.toml
[versions]
spring-boot = "3.4.x" # ใช้ version ล่าสุด — ตรวจสอบที่ https://spring.io/projects/spring-boot
jackson = "2.19.0"
[libraries]
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }kotlin
// build.gradle.kts
plugins {
java
alias(libs.plugins.spring.boot)
}
dependencies {
implementation(libs.spring.boot.starter.web)
implementation(libs.jackson.databind)
}→ มี version ที่เดียว ทุกโมดูลแชร์ได้ — เทียบกับ Maven BOM (<dependencyManagement>)
16.5 jpackage — สร้าง native installer (ตัวติดตั้งสำหรับ OS โดยตรง เช่น .exe/.msi บน Windows, .dmg บน Mac)
ต้องการแจกแอป Java ให้คนที่ ไม่มี JVM? — jpackage (มากับ JDK ตั้งแต่ Java 14) สร้าง installer พร้อม JRE bundled
bash
# สร้าง .exe / .msi (Windows), .dmg / .pkg (Mac), .deb / .rpm (Linux)
jpackage \
--name MyApp \
--input target/ \
--main-jar myapp.jar \
--main-class com.example.Main \
--type msi \
--app-version 1.0.0ผลลัพธ์: installer ที่ผู้ใช้กด install แล้วได้ shortcut + run ได้ทันที (ไม่ต้องลง JDK)
ใช้กับ:
- Desktop app (JavaFX, Swing)
- CLI tool ที่อยากให้ run ง่าย ๆ
- App ภายในองค์กรที่ control environment ไม่ได้
16.6 GraalVM native-image — compile เป็น native binary
แทนการแพ็ก JRE — compile Java เป็น native executable ขนาดเล็ก start เร็ว memory ต่ำ
bash
# ติดตั้ง GraalVM + native-image tool ก่อน
native-image -jar myapp.jar
# ได้ binary ~30-50MB ที่ start ภายใน ~50ms (vs JVM start ~500ms+)ใน Maven/Gradle ใช้ plugin org.graalvm.buildtools.native
| JVM | Native Image | |
|---|---|---|
| Start time | 500ms+ | ~50ms |
| Memory | 100MB+ | 30MB |
| Peak throughput | สูงสุด (JIT optimize) | ต่ำกว่านิด |
| Build time | วินาที | นาที |
| Reflection / dynamic class | ใช้ตรง ๆ ได้ | ต้อง declare config |
ใช้กับ:
- Serverless (รูปแบบ deploy ที่ไม่ต้องดูแล server เอง จ่ายค่าบริการตาม request — เช่น AWS Lambda, Cloud Run) — start เร็ว = ไม่โดน cold start (เวลาที่ใช้เริ่มต้น app ครั้งแรก — ถ้าช้าเกิน user รอนาน)
- CLI tool ที่ต้อง run บ่อย ๆ
- Container ที่ memory จำกัด (k8s pod เล็ก)
Spring Boot 3+ รองรับ native image เป็น first-class — mvn -Pnative native:compile
⚠️ ระวัง reflection-heavy library (Hibernate proxy, dynamic JSON binding) — ต้อง config
reflect-config.jsonหรือใช้ tracing agent (JVM agent ที่รัน app จริงแล้วบันทึกการใช้ reflection ออกมาเป็นไฟล์ config ให้อัตโนมัติ)
17. ⚠️ Build Tools Pitfalls
| Pitfall | แก้ |
|---|---|
| Hardcode version ในหลายที่ | ใช้ <properties> (Maven) หรือ extra (Gradle) |
| Build เร็วเปลี่ยน — ลืม commit lock | ใช้ Maven/Gradle wrapper |
| Test มี dependency กับ DB จริง | ใช้ Testcontainers + scope test |
| Library version conflict | mvn dependency:tree หา conflict |
| Jar ใหญ่เกิน | Spring Boot fat jar = ปกติ — แต่ check ว่าไม่ include test scope |
Part 4: Javadoc
18. Javadoc คืออะไร
Javadoc คือระบบเอกสารในตัวของ Java — เขียนคอมเมนต์พิเศษ /** ... */ เหนือ class/method พร้อม tag (@param, @return, @throws) แล้วเครื่องมือ javadoc แปลงเป็นเว็บ HTML ให้อัตโนมัติ:
Javadoc = comment พิเศษ /** ... */ ที่ tool แปลงเป็น HTML document
java
/**
* บริการสำหรับจัดการ user — สร้าง, อ่าน, อัพเดต, ลบ.
* <p>
* Service นี้ใช้ {@link UserRepository} เพื่อ persistence
* และ {@link PasswordEncoder} สำหรับ hash password.
*
* @author Anna
* @since 1.0.0
*/
public class UserService {
/**
* ค้นหา user จาก id.
*
* @param id user id (must be positive)
* @return user ที่ตรงกับ id
* @throws UserNotFoundException ถ้าไม่เจอ
* @see UserRepository#findById
*/
public User findById(Long id) {
return repo.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
}19. Tag สำคัญ
| Tag | ใช้ทำอะไร |
|---|---|
@param | parameter |
@return | return value |
@throws | exception ที่อาจ throw |
@see | reference ไป class/method อื่น |
@since | version แรกที่มี |
@deprecated | บอกว่า deprecated + แนะนำทางแก้ |
@author | คนเขียน |
{@link X} | inline link ไป X |
{@code x} | inline code |
20. Generate Javadoc
เมื่อเขียน Javadoc comment แล้ว สั่ง generate เป็นเว็บเอกสารได้ผ่าน Maven, Gradle หรือคำสั่ง javadoc ดิบ ๆ — ผลลัพธ์คือชุดไฟล์ HTML ที่เปิดดูได้:
bash
# Maven
mvn javadoc:javadoc # → target/site/apidocs/index.html
# Gradle
./gradlew javadoc # → build/docs/javadoc/index.html
# คำสั่งดิบ (แนะนำใช้แบบนี้ — glob ** ใช้ใน PowerShell/cmd ไม่ได้)
javadoc -d docs -sourcepath src/main/java -subpackages com.example21. ❓ ควรเขียน Javadoc ทุกอย่างไหม?
ไม่
- Public API ของ library / SDK ที่คนอื่นใช้ → เขียนละเอียด
- Internal service class → เขียนเฉพาะที่ WHY ไม่ชัด
- Getter/setter → ไม่ต้อง (IDE generate ได้)
- Private method → ไม่ต้อง
กฎ: เขียนเมื่อโค้ดอ่านแล้วยังไม่เข้าใจ — อย่าย้ำสิ่งที่ method signature บอกอยู่แล้ว
❌ Javadoc ที่ไม่มีประโยชน์:
java
/** Get the name. @return the name */
public String getName() { return name; }✅ Javadoc มีประโยชน์:
java
/**
* คำนวณภาษีตามกฎ ก.ค. 2025 — รวม VAT แต่ไม่รวม service charge.
* Throw ถ้า amount < 0 เพราะระบบหลังบ้านไม่รองรับ credit note.
*/
public BigDecimal calculateTax(BigDecimal amount) { ... }22. Checkpoint
🛠️ Checkpoint 14.1 — Custom Annotation
สร้าง @Timed ที่ใส่บน method แล้วเขียน main ที่:
- scan method ใน class ด้วย reflection
- ถ้ามี
@Timed→ wrap call ด้วย stopwatch → พิมพ์เวลาที่ใช้
🛠️ Checkpoint 14.2 — Maven project
⚠️ ก่อนรันคำสั่งนี้ — ต้องติดตั้ง Maven ก่อน
ตรวจสอบด้วยmvn -vใน terminal — ถ้าเจอ "command not found" หรือ "'mvn' is not recognized" ให้ติดตั้ง Maven ก่อน:
- Windows:
winget install Apache.Mavenหรือโหลดที่ https://maven.apache.org/install.html แล้วเพิ่มใน PATH- macOS:
brew install maven- Linux:
sudo apt install mavenหรือsudo dnf install maven
หลังติดตั้งแล้วเปิด terminal ใหม่แล้วรันmvn -vอีกครั้งเพื่อยืนยัน
สร้างโปรเจกต์ใหม่จาก Maven CLI:
bash
# bash (Linux/Mac)
mvn archetype:generate -DgroupId=com.example -DartifactId=mini-app \
-DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=falsepowershell
# PowerShell (Windows) — เขียนในบรรทัดเดียว หรือใช้ ` ต่อบรรทัด
mvn archetype:generate -DgroupId=com.example -DartifactId=mini-app -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false⚠️ archetype
maven-archetype-quickstartมักสร้าง pom.xml ที่ยังตั้ง Java source/target เป็นเวอร์ชันเก่ามาก — หลัง generate เสร็จให้แก้ pom.xml เพิ่ม<maven.compiler.release>21</maven.compiler.release>(ดูตัวอย่างใน section 13) ก่อน ไม่งั้น syntax ใหม่อย่างvar/record จะ compile ไม่ผ่าน
เพิ่ม Jackson, run test, package เป็น jar, run jar ด้วย java -jar
🛠️ Checkpoint 14.3 — Javadoc your code
เอา class จาก checkpoint ก่อน ๆ มาเพิ่ม Javadoc + generate HTML → เปิดดู
23. สรุปบท
✅ Annotation = metadata บนโค้ด — framework อ่านด้วย reflection
✅ เขียน annotation เอง: @Retention + @Target + @interface
✅ Java Modules (JPMS, Java 9+) = แยก module + control visibility — ส่วนใหญ่ใน app ทั่วไปไม่ใช้
✅ jlink → custom JRE เล็กลง
✅ Build Tools = ตัวจัดการ compile + dependency + test + package
✅ Maven = XML + convention, Gradle = DSL + เร็วกว่า
✅ Spring Boot มือใหม่ → Maven, โปรเจกต์ใหญ่ / Android → Gradle
✅ ใช้ ./mvnw / ./gradlew (wrapper) เพื่อให้ทุกเครื่อง build เหมือนกัน
✅ Javadoc เขียนเมื่อ WHY ไม่ชัด — อย่าย้ำสิ่งที่ signature บอก