2026/7/6

Spring AOP

AOP 概念

Aspect-Oriented-Programming 就是在 java business service method 上加工,製造 method 呼叫前後的切面斷點,用以提供一些通用的商務邏輯,最常見的就是 business method 加上 database transaction 的機制。

博客來-深入淺出Spring Boot 3.x 裡面有一個部分,嘗試用程式的方式,透過 spring 使用的 cglib 套件實作 AOP 的概念。

spring boot 有兩種 proxy bean 的方法:JDK 或 CGLIB,預設是用 cglib,以下是用 cglib 的 Enhancer 產生動態 proxy bean。

  • 產生 service 跟 impl

HelloService.java

package com.test.interceptor.service;

public interface HelloService {
    public void sayHello(String name);
}

HelloServiceImpl.java

package com.test.interceptor.service.impl;

import com.test.interceptor.service.HelloService;

public class HelloServiceImpl implements HelloService {

    @Override
    public void sayHello(String name) {
        if (name == null || "".equals(name.trim())) {
            throw new RuntimeException("parameter is null!!");
        }
        System.out.println("hello " + name);
    }

}
  • interceptor

Interceptor.java

package com.test.interceptor.interceptor;

import com.test.interceptor.Invocation;

public interface Interceptor {
    // before method invoke
    public void before();

    // after method invoke
    public void after();

    /**
     * wrap method to add around actions
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    public Object around(Invocation invocation) throws Throwable;

    // after return if no exception
    public void afterReturning();

    // after return if throws exception
    public void afterThrowing();

    // use around to wrap method
    public default boolean useAround() {
        return false;
    }
}

InterceptorImpl.java

package com.test.interceptor.interceptor;

import com.test.interceptor.Invocation;

public class InterceptorImpl implements Interceptor {

    @Override
    public void before() {
        System.out.println("before ......");
    }

    @Override
    public boolean useAround() {
        return true;
    }

    @Override
    public void after() {
        System.out.println("after ......");
    }

    @Override
    public Object around(Invocation invocation) throws Throwable {
        System.out.println("around before ......");
        Object obj = invocation.proceed();
        System.out.println("around after ......");
        return obj;
    }

    @Override
    public void afterReturning() {
        System.out.println("afterReturning......");
    }

    @Override
    public void afterThrowing() {
        System.out.println("afterThrowing......");
    }
}

透過 Invocation 製作這個流程

  1. 呼叫 interceptor 的 before()

  2. 呼叫 target object method

  3. 當 method throw exception,呼叫 afterThrowing()

  4. 當 method 正常執行完成,呼叫 afterReturning()

  5. 呼叫 after()

  6. 如果 interceptor 的 useAround() 回傳 true,會用 around() 取代上面的流程

Invocation.java

package com.test.interceptor;

import com.test.interceptor.interceptor.Interceptor;
import lombok.Data;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

@Data
public class Invocation {
    private Object[] params;
    private Method method;
    private Object target;
    private Interceptor interceptor;

    public Invocation(Object target, Method method, Object[] params, Interceptor interceptor) {
        this.target = target;
        this.method = method;
        this.params = params;
        this.interceptor = interceptor;
    }

    // reflection
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        Object retObj = null; // return object
        boolean exceptionFlag = false; // exception flag
        // invoke before()
        this.interceptor.before();
        try {
            // use reflection to call method
            retObj = method.invoke(target, params);
        } catch (Exception ex) {
            // exception flag
            exceptionFlag = true;
        }
        if (exceptionFlag) {
            this.interceptor.afterThrowing();
        } else {
            this.interceptor.afterReturning();
        }
        this.interceptor.after();
        return retObj;
    }

}
  • 將 target object 跟 interceptor 合併為流程

ProxyBean.java

package com.test.interceptor;

import com.test.interceptor.interceptor.Interceptor;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class ProxyBean implements MethodInterceptor {
    private Interceptor interceptor = null;
    private Object target = null;

    /**
     * create proxy bean
     * @param target
     * @param interceptor
     * @return
     */
    public static Object getProxy(Object target, Interceptor interceptor) {
        var proxyBean = new ProxyBean();
        // create cglib Enhancer
        var enhancer = new Enhancer();
        // set proxy bean class
        enhancer.setSuperclass(target.getClass());
        // set proxy bean interfaces
        enhancer.setInterfaces(target.getClass().getInterfaces());
        // target object
        proxyBean.target = target;
        // interceptor
        proxyBean.interceptor = interceptor;
        // call proxyBean intercept()
        enhancer.setCallback(proxyBean);
        // create proxy bean
        var proxy = enhancer.create();
        return proxy;
    }

    /**
     * @param proxy  proxy bean
     * @param method interceptor method
     * @param args   method args
     * @param mproxy method proxy
     * @return return object
     */
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy mproxy) throws Throwable {
        // invoke proxy bean
        var invocation = new Invocation(this.target, method, args, this.interceptor);
        Object result = null;
        if (this.interceptor.useAround()) { // enable around
            result = this.interceptor.around(invocation);
        } else {
            result = invocation.proceed();
        }
        return result;
    }
}
  • 測試

InterceptorTest.java

package com.test.interceptor;

import com.test.interceptor.interceptor.InterceptorImpl;
import com.test.interceptor.service.HelloService;
import com.test.interceptor.service.impl.HelloServiceImpl;

public class InterceptorTest {

    public static void main(String[] args) {
        testProxy();
    }

    public static void testProxy() {
        // target object
        var helloService = new HelloServiceImpl();
        // create proxy bean & interceptor
        var proxy = (HelloService) ProxyBean.getProxy(helloService, new InterceptorImpl());
        // call proxy bean method
        proxy.sayHello("Hello World");
        System.out.println("###############Testing Exception###############");
        proxy.sayHello(null);
    }

}
  • 執行結果
around before ......
before ......
hello Hello World
afterReturning......
after ......
around after ......
###############Testing Exception###############
around before ......
before ......
afterThrowing......
after ......
around after ......

AOP 術語

  • join point 連接點

    連接點就是告訴 AOP 在哪裡需要做 AOP,因為 AOP 只支援 method,被攔截的一定只能是 method

  • point cut 切點

    有時候需要啟動 AOP 的不是單一個 method,而是多個類別的不同 method。這邊可透過 regular expressoin 與指示器的規則定義 point cut,讓 AOP 根據定義匹配多個 methods。

  • advice 通知

    分為 before advice, after advice, around advice, afterReturning advice, afterThrowing advice

  • target 目標對象

    代理的物件對象,例如上面例子的 HelloServiceImpl

  • introduction 引用

    引用新的類別及 method,可增強現有bean的功能

  • weaving

    透過動態代理的技術,為目標物件生成代理物件。利用切點 join cut 的定義匹配的 join point 決定各類 advice 通知加入流程的過程

  • aspect 切面

    一個類別,透過它定義 AOP 的 point cut, advice

flowchart TB
    a1["target: HelloServiceImpl.sayHello()"]-->b2
    subgraph one [service]
    a1-->a2["cglib: weaving InterceptorImpl"]
    end
    subgraph two [InterceptorImpl, around advice]
    b1["before advice"]-->b2
    b2["method reflection"]-->b3{exception?}
    b3--yes-->b4
    b3--no-->b5
    b4["afterThrowing advice"]-->b6["after advice"]
    b5["afterReturning advice"]-->b6
    end

開發

pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

確認 target

假設有一個 UserService, UserServiceImpl,裡面有 printUser method,printUser 就是 target

UserService.java

package com.test.aspect.service;

import com.test.aspect.pojo.User;

public interface UserService {
    public void printUser(User user);

    public void multiAspects();
}

UserServiceImpl.java

package com.test.aspect.service.impl;

import com.test.aspect.service.UserService;
import com.test.aspect.pojo.User;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    @Override
    public void printUser(User user) {
        if (user == null) {
            throw new RuntimeException("user is null!");
        }
        System.out.println("user=" + user);
    }

    @Override
    public void multiAspects() {
        System.out.println("testing multiAspects");
    }
}

開發切面 @Aspect

內容主要是各種 Apsect advice

@Before, @After, @AfterReturning, @AfterThrowing 分別用一個相同的 regular expression 定義哪個地方要啟用 AOP,匹配 target 的 join point 連結點(method)

MyAspectTemp.java

package com.test.aspect;

import com.test.aspect.pojo.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;

// 宣告為 Aspect,要加上 Component 註冊到 IoC
@Aspect
@Component
public class MyAspectTemp {
    // 以 regular expression,指定 join point (包含 class, method)
    // execution  代表在執行時,要攔截匹配的 regluar expression
    // * 代表不在意 method 回傳任何資料型別
    // com.test.aspect.service.impl.UserServiceImpl  指定 target object
    // printUser  指定 method
    // (..) 代表匹配任意參數
    private static final String aopExp = "execution(* " + "com.test.aspect.service.impl.UserServiceImpl.printUser(..))";

    @Before(aopExp)
    public void before() {
        System.out.println("before ......");
    }

    @Before(aopExp)
    public void beforeParam(JoinPoint jp, User user) {
        System.out.println("beforeParam ......");
    }

    @After(aopExp)
    public void after() {
        System.out.println("after ......");
    }

    @AfterReturning(aopExp)
    public void afterReturning() {
        System.out.println("afterReturning ......");
    }

    @AfterThrowing(aopExp)
    public void afterThrowing() {
        System.out.println("afterThrowing ......");
    }

}

定義切點 Point cut

剛剛的 Aspect0.java 每個註解都有相同的 regular expression,這邊改用一個 method 定義切點 point cut @Pointcut(aopExp),再用該 point cut 定義 AOP advices

MyAspect.java

package com.test.aspect;

import com.test.aspect.validator.UserValidator;
import com.test.aspect.validator.impl.UserValidatorImpl;
import com.test.aspect.pojo.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

// 宣告為 Aspect,要加上 Component 註冊到 IoC
@Aspect
@Component
public class MyAspect {
    // 以 regular expression,指定 join point (包含 class, method)
    // execution  代表在執行時,要攔截匹配的 regluar expression
    // * 代表不在意 method 回傳任何資料型別
    // com.test.aspect.service.impl.UserServiceImpl  指定 target object
    // printUser  指定 method
    // (..) 代表匹配任意參數
    private static final String aopExp = "execution(* " + "com.test.aspect.service.impl.UserServiceImpl.printUser(..))";

//    // 定義 Enhancer bean 與 class
//    @DeclareParents(
//            // 需要引入增强的Bean
//            value = "com.test.aspect.service.impl.UserServiceImpl",
//            // 用這個類別增強
//            defaultImpl = UserValidatorImpl.class)
//    // Enhancer interface
//    public UserValidator userValidator;

    // 用 @Pointcut 定義切點,後面的 advice 可用這個 method 指定 join point
    @Pointcut(aopExp)
    public void pointCut() {
    }

    @Before("pointCut()") // 用 point cut 定義
    public void before() {
        System.out.println("before ......");
    }

    // args(user) 代表要傳遞的參數
    @Before("pointCut() && args(user)")
    public void beforeParam(JoinPoint jp, User user) {
        System.out.println("beforeParam ......");
    }

    @After("pointCut()") // 用 point cut 定義
    public void after() {
        System.out.println("after ......");
    }

    @AfterReturning("pointCut()") // 用 point cut 定義
    public void afterReturning() {
        System.out.println("afterReturning ......");
    }

    @AfterThrowing("pointCut()") // 用 point cut 定義
    public void afterThrowing() {
        System.out.println("afterThrowing ......");
    }

//    @Around("pointCut()")
//    public void around(ProceedingJoinPoint jp) throws Throwable {
//        System.out.println("around before......");
//        // call back target object method
//        jp.proceed();
//        System.out.println("around after......");
//    }
}

@AspectJ關於@Pointcut 的指示器

類型 desc
arg() 限制 join point method 的參數
@args() 以 join point method 參數的註解進行限制
execution() 匹配 join point 的執行 method
this() 匹配當前 AOP 代理物件類別的方法
target target object
@target() 匹配當前 target object 類別的方法,target object 必須標注指定的註解
within 限制 join point 匹配指定的類別
@within() 限制 join point 帶有匹配註解的類別
@annotation 限制帶有指定註解的join point

測試AOP

加上 AopConfig.java

package com.test.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("com.test.*")
@EnableAspectJAutoProxy // 啟動 AOP
public class AopConfig {

}

AopTest1Service.java

package com.test.main;

import com.test.aspect.pojo.User;
import com.test.aspect.service.UserService;
import com.test.aspect.validator.UserValidator;
import com.test.config.AopConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopTest1Service {

    public static void main(String[] args) {
        testAop();
    }

    public static void testAop() {
        var ctx = new AnnotationConfigApplicationContext(AopConfig.class);
        try {
            var userService = ctx.getBean(UserService.class);
            var user = new User();
            user.setId(1L);
            user.setUserName("username1");
            user.setNote("note1");
            userService.printUser(user);
            System.out.println("########## exception test #####");
            userService.printUser(null);
        } finally {
            ctx.close();
        }
    }

}

執行結果

before ......
user=User(id=1, userName=username1, note=note1)
afterReturning ......
after ......
########## exception test #####
before ......
afterThrowing ......
after ......
Exception in thread "main" java.lang.RuntimeException: user is null!

around advice

只有在需要大幅度修改原本 target object 的商業邏輯時,才需要使用 around,一般狀況下,不要使用 around

修改 MyAspect.java 加上 around

    @Around("pointCut()")
    public void around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("around before......");
        // call back target object method
        jp.proceed();
        System.out.println("around after......");
    }

AopTest1Service.java 執行結果

around before......
before ......
beforeParam ......
user=User(id=1, userName=username1, note=note1)
afterReturning ......
after ......
around after......
########## exception test #####
around before......
before ......
beforeParam ......
afterThrowing ......
after ......

Introduction

在使用 UserService 時,會因為 null 發生 exception,可利用 introduction 功能增強,而不需要修改原本的 UserServiceImpl

UserValidator.java

package com.test.aspect.validator;

import com.test.aspect.pojo.User;

public interface UserValidator {
    // testing user object is null or not
    public boolean validate(User user);
}

UserValidatorImpl.java

package com.test.aspect.validator.impl;

import com.test.aspect.validator.UserValidator;
import com.test.aspect.pojo.User;

public class UserValidatorImpl implements UserValidator {
    @Override
    public boolean validate(User user) {
        System.out.println("validate user with class: " + UserValidator.class.getSimpleName());
        return user != null;
    }
}

修改剛剛的 MyAspect.java

    // 定義 Enhancer bean 與 class
    @DeclareParents(
            // 需要 introduction 增强的Bean
            value = "com.test.aspect.service.impl.UserServiceImpl",
            // 用這個類別增強
            defaultImpl = UserValidatorImpl.class)
    // Enhancer interface
    public UserValidator userValidator;

測試

將 userService 轉換為 UserValidator 後使用 validate()

AopTest2Introduction.java

package com.test.main;

import com.test.aspect.pojo.User;
import com.test.aspect.service.UserService;
import com.test.aspect.validator.UserValidator;
import com.test.config.AopConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopTest2Introduction {

    public static void main(String[] args) {
        testIntroduction();
    }

    public static void testIntroduction() {
        var ctx = new AnnotationConfigApplicationContext(AopConfig.class);
        try {
            var userService = ctx.getBean(UserService.class);
            var user = new User();
            user.setId(1L);
            user.setUserName("username1");
            user.setNote("note1");
            // convert to UserValidator interface
            var userValidator = (UserValidator) userService;
            // 检查user is null ot not
            if (userValidator.validate(user)) {
                // 檢查通過,列印user 資訊
                userService.printUser(user);
            }

        } finally {
            ctx.close();
        }
    }

}

執行結果,就不會發生 exception

validate user with class: UserValidator
before ......
beforeParam ......
user=User(id=1, userName=username1, note=note1)
afterReturning ......
after ......

advice parameter

要給 advice 通知加上參數,只需要修改 regular expression 與指示器

    // args(user) 代表要傳遞的參數
    @Before("pointCut() && args(user)")
    public void beforeParam(JoinPoint jp, User user) {
        System.out.println("beforeParam ......");
    }

weaving

先用一個 interface 再加上實作,是 spring 推薦的方式,但 AOP 並不一定只能用在 interface。

動態代理有 JDK, cglib 及 JDK, javaassist。JDK 動態代理是要求代理對象必須要是 interface,但 cglib 沒有限制。預設狀況,如果有 interface,spring 會使用 JDK 動態代理,如果沒有,就用 cglib。

spring boot 預設就是使用 cglib。上面的 AopConfig.java 如果加上 @SpringBootApplication 就代表這是 spring boot

# false: 採用原本 spring 的方法
# true: 預設值,使用 spring boot 的方法,永遠使用 cglib
spring.aop.proxy-target-class=false

多個切面

AOP 可同時對一個 method 支援多個 aspects。

首先定義三個 Aspects

MyAspect1.java

package com.test.aspect.multi;

import org.aspectj.lang.annotation.*;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
//@Order(3)
public class MyAspect1 implements Ordered {

    // 順序
    @Override
    public int getOrder() {
        return 3;
    }

    private static final String exp = "execution(* " + "com.test.aspect.service.impl.UserServiceImpl.multiAspects(..))";

    @Pointcut(exp)
    public void multiAspects() {
    }

    @Before("multiAspects()")
    public void before() {
        System.out.println("MyAspect1 before ......");
    }

    @After("multiAspects()")
    public void after() {
        System.out.println("MyAspect1 after ......");
    }

    @AfterReturning("multiAspects()")
    public void afterReturning() {
        System.out.println("MyAspect1 afterReturning ......");
    }

}

MyApsect2.java

package com.test.aspect.multi;

import org.aspectj.lang.annotation.*;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
//@Order(2)
public class MyAspect2 implements Ordered {

    // 順序
    @Override
    public int getOrder() {
        return 2;
    }

    private static final String exp = "execution(* " + "com.test.aspect.service.impl.UserServiceImpl.multiAspects(..))";

    @Pointcut(exp)
    public void multiAspects() {
    }

    @Before("multiAspects()")
    public void before() {
        System.out.println("MyAspect2 before ......");
    }

    @After("multiAspects()")
    public void after() {
        System.out.println("MyAspect2 after ......");
    }

    @AfterReturning("multiAspects()")
    public void afterReturning() {
        System.out.println("MyAspect2 afterReturning ......");
    }

}

MyAspect3.java

package com.test.aspect.multi;

import org.aspectj.lang.annotation.*;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(1)
public class MyAspect3 {

//    // 順序
//    @Override
//    public int getOrder() {
//        return 1;
//    }

    private static final String exp = "execution(* " + "com.test.aspect.service.impl.UserServiceImpl.multiAspects(..))";

    @Pointcut(exp)
    public void multiAspects() {
    }

    @Before("multiAspects()")
    public void before() {
        System.out.println("MyAspect3 before ......");
    }

    @After("multiAspects()")
    public void after() {
        System.out.println("MyAspect3 after ......");
    }

    @AfterReturning("multiAspects()")
    public void afterReturning() {
        System.out.println("MyAspect3 afterReturning ......");
    }

}

Aspects 的執行先後順序是用 @Order 或是 override Order interface 的 public int getOrder(),上面設定執行順序為 MyAspect3, MyAspect2, MyAspect1

AopTest3MultiAspect.java

package com.test.main;

import com.test.aspect.pojo.User;
import com.test.aspect.service.UserService;
import com.test.aspect.validator.UserValidator;
import com.test.config.AopConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopTest3MultiAspect {

    public static void main(String[] args) {
        testMultiAspects();
    }

    public static void testMultiAspects() {
        var ctx = new AnnotationConfigApplicationContext(AopConfig.class);
        try {
            var userService = ctx.getBean(UserService.class);
            // 測試 multiple aspects
            userService.multiAspects();
        } finally {
            ctx.close();
        }
    }
}

測試結果

MyAspect3 before ......
MyAspect2 before ......
MyAspect1 before ......
testing multiAspects
MyAspect1 afterReturning ......
MyAspect1 after ......
MyAspect2 afterReturning ......
MyAspect2 after ......
MyAspect3 afterReturning ......
MyAspect3 after ......

2026/6/29

Spring IoC

需要管理的物件對象稱為 Spring Bean,Bean 的管理器稱為 Spring IoC Container。IoC container 有兩個功能

  • 管理 bean 的定義、發布、設定、銷毀

  • 描述 bean 之間的依賴關係

所有 IoC Container 都需要實作 BeanFactory Interface,主要的 method:

  • getBean

    依照名稱/類別,取得 bean

    Object getBean(String name) throws BeansException;
    <T> T getBean(String name, Class<T> requiredType) throws BeansException;
    <T> T getBean(Class<T> requiredType) throws BeansException;
    Object getBean(String name, Object... args) throws BeansException;
    <T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
  • getBeanProvider

    <T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
    <T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);
  • containsBean

    boolean containsBean(String name);
  • isSingleton

    IoC 裡面,bean 預設都是 singleton,getBean 預設都會回傳同一個 bean

    boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
  • isPrototype

    getBean 會透過 IoC 產生新的 bean 回傳

    boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
  • isTypeMatch

    boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
    boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
  • getType

    Class<?> getType(String name) throws NoSuchBeanDefinitionException;
    Class<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;
  • getAliases

    String[] getAliases(String name);

spring 在 BeanFactory 基礎上,加上 ApplicationContext,實際的 application 大部分都是使用 ApplicationContext 的 interface


@Component vs @Bean

ref: 注解中@Component和@Bean的区别 - JAVA 牛牛

  • @Component(@Controller、@Service、@Repository)是透過 ComponentScan 掃描來做自動偵測,並自動設定到IoC。@Component 只是一個類別的定義,告知 IoC 要產生這個類別的 bean

  • 在 @Compoent 類別的屬性裡面,加上 @Value,可直接設定該欄位的值

  • @Bean 是使用在 method,在有該 annotation 的 method 會回傳一個 bean 的物件。該 method 裡面是產生 bean 的邏輯。

import lombok.Data;
import org.springframework.stereotype.Component;

@Data
@Component("user")
//@Scope("prototype")
public class User {
    private Long id;
    private String userName;
    private String note;
}
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Data
@Component("admin")
public class Admin {

    @Value("1")
    private Long id;
    @Value("user_name_1")
    private String userName;
    @Value("note_1")
    private String note;
}

AppConfig.java

@Configuration
@ComponentScan(basePackages = "com.test.ioc.*"
//        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes = Service.class)
)
public class AppConfig {
    // name 代表 Bean名稱
    @Bean(name = "user2")
    public User initUser() {
        var user = new User();
        user.setId(2L);
        user.setUserName("user_name_2");
        user.setNote("note_2");
        return user;
    }
}

測試

public class IoCTest {

    public static void main(String[] args) {
        var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        try {
            var admimbean1 = ctx.getBean(Admin.class);
            var admimbean2 = ctx.getBean(Admin.class);
            var admimbean3 = ctx.getBean("admin");
            System.out.println("Admin: admimbean1, name=" + admimbean1.getUserName());
            System.out.println("Admin 2==3:" + (admimbean1 == admimbean2)+", name="+admimbean2.getUserName());
            System.out.println("Admin 1==3:" + (admimbean1 == admimbean3)+", name="+((Admin)admimbean3).getUserName());

            var userbaen1 = ctx.getBean("user");
            var userbaen2 = ctx.getBean("user2");
//            var userbaen3 = ctx.getBean(User.class);
            System.out.println("User: userbaen1, name=" + ((User)userbaen1).getUserName());
            System.out.println("User: userbaen2, name=" + ((User)userbaen2).getUserName());
//            System.out.println("User: userbaen3, name=" + ((User)userbaen3).getUserName());
            System.out.println("User 1==2:" + (userbaen1 == userbaen2)+", name="+((User)userbaen1).getUserName());
//            System.out.println("User 2==3:" + (userbaen2 == userbaen3)+", name="+((User)userbaen3).getUserName());
        } finally {
            ctx.close();
        }
    }
}

結果

Admin: admimbean1, name=user_name_1
Admin 2==3:true, name=user_name_1
Admin 1==3:true, name=user_name_1
User: userbaen1, name=null
User: userbaen2, name=user_name_2
User 1==2:false, name=null

@ComponentScan 可使用的參數

  • basePackages

  • basePackageClasses

  • includeFilters

  • excludeFilters

  • lazyInit

includeFilters, excludeFilters 都需要有 @ComponentScan.Filter 定義

  • type: 選擇 filter type: ANNOTATION/ASSIGNABLE_TYPE/ASPECTJ/REGEX/CUSTOM

  • classes

  • pattern

ex:

@ComponentScan(basePackages = "com.test.ioc.*",
        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes = Service.class)
)
@ComponentScan(basePackages = {"com.test.ioc.*"})

@ComponentScan("com.test.ioc.*")

@ComponentScan(basePackageClasses = {"User.class"})

library bean

使用第三方函式庫的物件時,可透過 @Bean 產生 bean 放到 IoC container 管理

pom.xml 使用 mysql jdbc driver

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

在剛剛的 AppConfig.java 加上 dataSource 定義

    // @Bean 產生物件,name為 名稱
    @Bean(name = "dataSource")
    public DataSource getDataSource() {
        var dataSource = new MysqlDataSource();
        try {
            dataSource.setUrl("jdbc:mysql://localhost:3306/testweb");
            dataSource.setUser("root");
            dataSource.setPassword("password");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

回到 IoCTest.java

    public static void main(String[] args) {
        var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        try {
            testDatasource( (DataSource)ctx.getBean("dataSource") );
        } finally {
            ctx.close();
        }
    }

    public static void testDatasource(DataSource ds) {
        System.out.println("testDatasource");
        try (Connection con = ds.getConnection();
             PreparedStatement ps = createPreparedStatement(con, 1);
             ResultSet rs = ps.executeQuery()) {
            // process the resultset here, all resources will be cleaned up
            while (rs.next()) {
                System.out.print("id:" + rs.getInt(1));
                System.out.print(",username:" + rs.getString(2));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    private static PreparedStatement createPreparedStatement(Connection con, int userId) throws SQLException {
        String sql = "SELECT id, username FROM user WHERE id = ?";
        PreparedStatement ps = con.prepareStatement(sql);
        ps.setInt(1, userId);
        return ps;
    }

執行結果

testDatasource
id:1,username:Lily

Dependency Injection

先定義 兩個 interface

Animal.java

package com.test.ioc.dependency.pojo;

public interface Animal {
    public void use();
}

Person.java

package com.test.ioc.dependency.pojo;

public interface Person {
    public void service();
    public void setAnimal(Animal animal);

}

AnimalDog.java implements Animal

package com.test.ioc.dependency.pojo.impl;

import com.test.ioc.dependency.pojo.Animal;
import org.springframework.stereotype.Component;

@Component
public class AnimalDog implements Animal {

    @Override
    public void use() {
        System.out.println("Dog " + AnimalDog.class.getSimpleName() + " for help.");
    }
}

PersonSales.java

使用 @Autowired 自動綁定一個 animal 物件

package com.test.ioc.dependency.pojo.impl;

import com.test.ioc.dependency.pojo.Animal;
import com.test.ioc.dependency.pojo.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class PersonSales implements Person {
    @Autowired
    private Animal animal = null;

    @Override
    public void service() {
        this.animal.use();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
}

AppConfig2.java

package com.test.ioc.dependency;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "com.test.ioc.dependency.*")
public class AppConfig2 {
}

IoCTest2.java

package com.test.ioc.dependency;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.test.ioc.dependency.pojo.impl.PersonSales;

public class IoCTest2 {

    public static void main(String[] args) {
        var ctx = new AnnotationConfigApplicationContext(AppConfig2.class);
        try {
            var person = ctx.getBean(PersonSales.class);
            person.service();
        } finally {
            ctx.close();
        }
    }

}

執行結果

Dog AnimalDog for help.

如果再增加一個 AnimalCat

package com.test.ioc.dependency.pojo.impl;

import com.test.ioc.dependency.pojo.Animal;
import org.springframework.stereotype.Component;

@Component
public class AnimalCat implements Animal {
    @Override
    public void use() {
        System.out.println("Cat " + AnimalCat.class.getSimpleName() + " for rats");
    }
}

執行後會發生 Exception

Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'personSales':
Unsatisfied dependency expressed through field 'animal':
No qualifying bean of type 'com.test.ioc.dependency.pojo.Animal' available:
expected single matching bean but found 2: animalCat,animalDog

因為在 @AutoWired 時,只使用了 animal 屬性,但 container 不確定要使用 Cat 還是 Dog

如果將 animal 改成 animalDog,就很明確。因為 @Autowired 會先根據類別產生 bean,然後再根據屬性名稱匹配。

@Component
public class PersonSales implements Person {
    @Autowired
    private Animal animalDog = null;

    @Override
    public void service() {
        this.animalDog.use();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animalDog = animal;
    }
}

@Autowired 也可以寫在 method 上面

    @Override
    @Autowired
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }

@Primary@Qualifier

在 AnimalCat 加上 @Primary 就看可以確定先使用這個類別

@Component
@Primary
public class AnimalCat implements Animal {
    @Override
    public void use() {
        System.out.println("Cat " + AnimalCat.class.getSimpleName() + " for rats");
    }
}

但如果又在 AnimalDog 加上 @Primary,還是會發生問題。

所以應該在使用 bean 的地方,用 @Qualifier 限制應該要用哪一個 bean

    @Override
    @Autowired
    @Qualifier("animalDog")
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }

在 class constructor 的參數,也可使用 @Autowired

@Component
public class PersonSales implements Person {

    private Animal animal = null;

    public PersonSales(@Autowired @Qualifier("animalDog") Animal animal) {
        this.animal = animal;
    }

    @Override
    public void service() {
        this.animal.use();
    }

    @Override
//    @Autowired
//    @Qualifier("animalDog")
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
}

Bean 的生命週期

bean 的初始化流程

flowchart TB
    A["資源定位\n使用 @ComponentScan"] --> B["Bean 的定義\n存到 BeanDefinition"]
    B --> C["發布 bean 的定義\nIoC Container 載入定義"]
    C --> D["實例化\n產生 bean object"]
    D --> E["DI\n使用 @Autowired 注入資源"]

@ComponentScan 裡面有個 lazyInit 參數,預設為 false,預設不會延遲 bean 初始化,而是在 getBean 時,才會初始化

修改 AppConfig2.java 加上 lazyInit = true

@Configuration
@ComponentScan(basePackages = "com.test.ioc.dependency.*", lazyInit = true)
public class AppConfig2 {
}

Spring 在完成 DI 以後,會依照此流程管理 bean 的 life cycle

flowchart TB
A["初始化"] --> B["DI"]
B --> C["setBeanName()\n介面 BeanNameAware"]
C --> D["setBeanFactory()\n介面 BeanFactoryAware"]
D --> E["setAppicationContext()\n介面 ApplicationContextAware\n容器要實作ApplicationContext才會被呼叫"]
E --> F["postProcessBeforeInitialization()\n BeanPostProcessor method"]
F --> G["自訂初始化 method\n@PostConstruct method"]
G --> H["afterPropertiesSet()\n介面 InitializingBean"]
H --> I["postProcessAfterInitialization()\nBeanPostProcessor method"]
I --> J["bean 的使用期間"]
J --> K["自訂銷毀 method\n@PreDestory method"]
K --> L["destory method\n介面 DisposableBean"]

IoC container 基本是需要實作 BeanFactory 介面,不一定要實作 ApplicationContext

修改 PersonBussiness.java

package com.test.ioc.dependency.pojo.impl;

import com.test.ioc.dependency.pojo.Animal;
import com.test.ioc.dependency.pojo.Person;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class PersonBussiness implements Person, BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean {

    private Animal animal = null;

    @Override
    public void service() {
        this.animal.use();
    }

    @Override
    @Autowired
    @Qualifier("animalDog")
    public void setAnimal(Animal animal) {
        System.out.println("lazyInit");
        this.animal = animal;
    }

    @Override
    public void setBeanName(String beanName) {
        System.out.println("【" + this.getClass().getSimpleName() + "】invoke BeanNameAware.setBeanName");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("【" + this.getClass().getSimpleName() + "】invoke BeanFactoryAware.setBeanFactory");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("【" + this.getClass().getSimpleName() + "】invoke ApplicationContextAware.setApplicationContext");

    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("【" + this.getClass().getSimpleName() + "】invoke InitializingBean.afterPropertiesSet");
    }

    @PostConstruct
    public void init() {
        System.out.println("【" + this.getClass().getSimpleName() + "】@PostConstruct 定義的自訂初始化方法");
    }

    @PreDestroy
    public void destroy1() {
        System.out.println("【" + this.getClass().getSimpleName() + "】@PreDestroy 定義的自訂銷毀方法");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("【" + this.getClass().getSimpleName() + "】 DisposableBean");
    }
}

IoCTest2.java

public class IoCTest2 {

    public static void main(String[] args) {
        var ctx = new AnnotationConfigApplicationContext(AppConfig2.class);
        try {
            var person = ctx.getBean(PersonBussiness.class);
            person.service();
        } finally {
            ctx.close();
        }
    }

}

執行結果

lazyInit
【PersonBussiness】invoke BeanNameAware.setBeanName
【PersonBussiness】invoke BeanFactoryAware.setBeanFactory
【PersonBussiness】invoke ApplicationContextAware.setApplicationContext
【PersonBussiness】@PostConstruct 定義的自訂初始化方法
【PersonBussiness】invoke InitializingBean.afterPropertiesSet
Dog AnimalDog for help.
【PersonBussiness】@PreDestroy 定義的自訂銷毀方法
【PersonBussiness】 DisposableBean

增加 BeanPostProcessorExample.java

package com.test.ioc.dependency.life;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class BeanPostProcessorExample implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("BeanPostProcessor invoke " + "postProcessBeforeInitialization method  參數【" + bean.getClass().getSimpleName() + "】【" + beanName + "】 ");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("BeanPostProcessor invoke " + "postProcessAfterInitialization method  參數【" + bean.getClass().getSimpleName() + "】【" + beanName + "】 ");
        return bean;
    }
}

執行結果

BeanPostProcessor invoke postProcessBeforeInitialization method  參數【AppConfig2$$SpringCGLIB$$0】【appConfig2】 
BeanPostProcessor invoke postProcessAfterInitialization method  參數【AppConfig2$$SpringCGLIB$$0】【appConfig2】 
BeanPostProcessor invoke postProcessBeforeInitialization method  參數【AnimalDog】【animalDog】 
BeanPostProcessor invoke postProcessAfterInitialization method  參數【AnimalDog】【animalDog】 
lazyInit
【PersonBussiness】invoke BeanNameAware.setBeanName
【PersonBussiness】invoke BeanFactoryAware.setBeanFactory
【PersonBussiness】invoke ApplicationContextAware.setApplicationContext
BeanPostProcessor invoke postProcessBeforeInitialization method  參數【PersonBussiness】【personBussiness】 
【PersonBussiness】@PostConstruct 定義的自訂初始化方法
【PersonBussiness】invoke InitializingBean.afterPropertiesSet
BeanPostProcessor invoke postProcessAfterInitialization method  參數【PersonBussiness】【personBussiness】 
Dog AnimalDog for help.
【PersonBussiness】@PreDestroy 定義的自訂銷毀方法
【PersonBussiness】 DisposableBean

Properties file

spring boot 通常使用一個 application.properties 檔案處理設定

先在 pom.xml 加上 spring-boot-configuration-processor,就可以直接使用

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>

用 SpEL 語法,可參考到 database.properties 設定

DataBaseProperties.java

package com.test.ioc.prop.props;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@Data
public class DataBaseProperties {
    @Value("${database.driverName}")
    private String driverName = null;
    @Value("${database.url}")
    private String url = null;
    @Value("${database.username}")
    private String username = null;
    @Value("${database.password}")
    private String password = null;

}

也可以直接用 @ConfigurationProperties("database")

package com.test.ioc.prop.props;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties("database")
@Data
public class DataBaseProperties2 {
    private String driverName = null;
    private String url = null;
    private String username = null;
    private String password = null;

}

IoCTest3.java

package com.test.ioc.prop;

import com.test.ioc.prop.props.DataBaseProperties;
import com.test.ioc.prop.props.DataBaseProperties2;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class IoCTest3 {

    public static void main(String[] args) {
        var ctx = new AnnotationConfigApplicationContext(AppConfig3.class);
        try {
            var prop1 = ctx.getBean(DataBaseProperties.class);
            System.out.println("prop1 url="+prop1.getUrl());
            var prop2 = ctx.getBean(DataBaseProperties2.class);
            System.out.println("prop2 url="+prop2.getUrl());
        } finally {
            ctx.close();
        }
    }

}

執行結果

prop1 url=jdbc:mysql://localhost:3306/testweb
prop2 url=jdbc:mysql://localhost:3306/testweb

@Conditional

有時候會遇到缺少一些參數,讓 bean 無法初始化,可透過 @Conditional 限制 bean 的條件

DatabaseConditional.java

package com.test.ioc.prop.conditional;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class DatabaseConditional implements Condition {

    /**
     * data source condition
     * @param context
     * @param metadata
     * @return true -> init bean
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 取得 env
        var env = context.getEnvironment();
        // 判断 .properties 內容,是否有相關參數
        return env.containsProperty("database.url")
                && env.containsProperty("database.username")
                && env.containsProperty("database.password");
    }
}

PropApplication.java

@SpringBootApplication(scanBasePackages = "com.test.ioc.prop")
@PropertySource(value = "classpath:jdbc.properties", ignoreResourceNotFound = true)
@EnableConfigurationProperties
public class PropApplication {

    public static void main(String[] args) {
        SpringApplication.run(PropApplication.class, args);
    }

    @Bean(name = "dataSource2")
    // 透過 DatabaseConditional 限制 bean 的條件
    @Conditional(DatabaseConditional.class)
    public DataSource getDataSource(
            @Value("${database.url}") String url,
            @Value("${database.username}") String username,
            @Value("${database.password}") String password
    ) {
        System.out.println("init datasource");
        var dataSource = new MysqlDataSource();
        dataSource.setUrl(url);
        dataSource.setUser(username);
        dataSource.setPassword(password);
        return dataSource;
    }
}

如果 database.properties 缺少了一些參數,也不會讓 PropApplication 無法啟動


Bean Scope

一般來說,bean 有 singleton, prototype 兩種,在 Jakarta EE 的 web container 裡面,還增加了 page, reqeust, session, application 四種 scope

page scope 作用範圍是 JSP,Spring 沒有支援

scope 使用範圍 desc
singleton spring application 預設值
prototype spring application 從 IoC container 取得一個 bean 都是產生新的
session spring web application HTTP session
application spring web application web application life cycle
request spring web application web application http request
globalSession spring web application global http session,一個 bean 對應一個 object,不常使用

ScopBean.java

package com.test.ioc.scope;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ScopeBean {
}

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 是定義為 prototype

如果是 spring web application 還可以使用

  • WebApplicationContext.SCOPE_REQUEST

  • WebApplicationContext.SCOPE_SESSION

  • WebApplicationContext.SCOPE_APPLICATION


@Profile

專案開發時,可能會有開發、測試、正式測試、正式環境這些不同的運作環境,不同的環境有不同的設定。

假設有 dev, test 兩種資料庫,可使用 @Profile 切換

ScopeApplication.java

package com.test.ioc.scope;

import com.mysql.cj.jdbc.MysqlDataSource;
import com.test.ioc.prop.conditional.DatabaseConditional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;

import javax.sql.DataSource;

@SpringBootApplication(scanBasePackages = "com.test.ioc.scope")
@PropertySource(value = "classpath:jdbc.properties", ignoreResourceNotFound = true)
@EnableConfigurationProperties
public class ScopeApplication {
    public static void main(String[] args) {
        SpringApplication.run(ScopeApplication.class, args);
    }

    @Bean(name = "dataSource")
    @Profile("dev")
    public DataSource getDevDataSource() {
        System.out.println("getDevDataSource");
        var dataSource = new MysqlDataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/dev_testweb");
        dataSource.setUser("root");
        dataSource.setPassword("password");
        return dataSource;
    }

    @Bean(name = "dataSource")
    @Profile("test")
    public DataSource getTestDataSource() {
        System.out.println("getTestDataSource");
        var dataSource = new MysqlDataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/test_testweb");
        dataSource.setUser("root");
        dataSource.setPassword("password");
        return dataSource;
    }

}

在啟動時切換

JAVA_OPTS="-Dspring.profiles.active=dev"

spring 會自動使用 application-{profile}.properties


SpEL

讀取 properties 的值

@Value("${database.driverName}")
String driver;

記錄 bean 的初始化時間

@Value("#{T(System).currentTimeMillis()}")
private Long initTime = null;

直接設定屬性

    @Value("#{'teststring'}")
    private String str=null;

    @Value("#{3.1E2}")
    private double d;

    @Value("#{3.14}")
    private double e;

取得其他 bean 的屬性

?. 是 null 判斷

    @Value("#{bean2.str}")
    private String otherStr=null;

    @Value("#{bean2.str?.toUpperCase()}")
    private String otherStrUpperCase=null;

運算

@Value("#{1+2}")
private int run;

// 浮點數比較運算
@Value("#{beanName.pi == 3.14f}")
private boolean piFlag;

// 字串比較運算
@Value("#{beanName.str eq 'Spring Boot'}")
private boolean strFlag;

// 字串連接
@Value("#{beanName.str + ' 連接字串'}")
private String strApp = null;

// 三元運算
@Value("#{beanName.d > 1000 ? '大於' : '小於'}")
private String resultDesc = null;

2026/6/22

密碼 hash 演算法比較

比較密碼加密演算法

演算法 可調整參數 記憶體強度 抗 GPU/ASIC 密碼儲存
UnixCrypt 不適合
MD5 不適合
PBKDF2 iterations 一般安全性
bcrypt cost factor 部分 一般網站,推薦使用
scrypt N, r, p 高安全性,錢包,加密金鑰
Argon2 time, mem, parallelism 最高 最高安全性,密碼系統

java profiler

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.maxkit.test</groupId>
    <artifactId>test</artifactId>
    <version>1.0</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.mindrot</groupId>
            <artifactId>jbcrypt</artifactId>
            <version>0.4</version>
        </dependency>

        <dependency>
            <groupId>at.favre.lib</groupId>
            <artifactId>bcrypt</artifactId>
            <version>0.10.2</version>
        </dependency>

        <!-- scrypt, Argon2 -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk18on</artifactId>
            <version>1.82</version>
        </dependency>
    </dependencies>
</project>
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.generators.SCrypt;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.mindrot.jbcrypt.BCrypt;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;

public class PasswordHashBenchmarkWithVerify {
    private static final String PASSWORD = "MyPassword123!";
    private static final SecureRandom RANDOM = new SecureRandom();

    public static void main(String[] args) throws Exception {
        System.out.println("=== Hash + Verify Benchmark ===");
        System.out.println("Password: " + PASSWORD + "\n");

        // 先做一次 warm-up (JVM/JIT)
        warmUp();

        System.out.println("---- PBKDF2 (HmacSHA256) ----");
        pbkdf2HashAndVerify();

        System.out.println("---- bcrypt (jBCrypt) ----");
        bcryptHashAndVerify();

        System.out.println("---- scrypt (BouncyCastle) ----");
        scryptHashAndVerify();

        System.out.println("---- Argon2id (BouncyCastle) ----");
        argon2HashAndVerify();
    }

    private static void warmUp() {
        // 簡單 warmup 幾次,讓 JIT 編譯熱起來
        for (int i = 0; i < 3; i++) {
            try {
                pbkdf2Once();
                bcryptOnce();
                scryptOnce();
                argon2Once();
            } catch (Exception ignored) {}
        }
    }

    // ---------- PBKDF2 ----------
    private static void pbkdf2HashAndVerify() throws Exception {
        byte[] salt = new byte[16];
        RANDOM.nextBytes(salt);
        int iterations = 65536;
        int keyLen = 256; // bits

        long t0 = System.nanoTime();
        PBEKeySpec spec = new PBEKeySpec(PASSWORD.toCharArray(), salt, iterations, keyLen);
        SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        byte[] hash = skf.generateSecret(spec).getEncoded();
        long t1 = System.nanoTime();

        String stored = String.format("PBKDF2$%d$%s$%s",
                iterations,
                Base64.getEncoder().encodeToString(salt),
                Base64.getEncoder().encodeToString(hash));

        System.out.println("Stored: " + stored);
        System.out.println("Hash time: " + ((t1 - t0) / 1_000_000) + " ms");

        // verify
        long v0 = System.nanoTime();
        boolean ok = verifyPBKDF2(PASSWORD, stored);
        long v1 = System.nanoTime();
        System.out.println("Verify: " + ok + " (time: " + ((v1 - v0) / 1_000_000) + " ms)\n");
    }

    private static boolean verifyPBKDF2(String password, String stored) throws Exception {
        // stored format: PBKDF2$iterations$base64salt$base64hash
        String[] parts = stored.split("\\$");
        int iterations = Integer.parseInt(parts[1]);
        byte[] salt = Base64.getDecoder().decode(parts[2]);
        byte[] expected = Base64.getDecoder().decode(parts[3]);

        PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, expected.length * 8);
        SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        byte[] computed = skf.generateSecret(spec).getEncoded();

        return MessageDigest.isEqual(computed, expected);
    }

    private static void pbkdf2Once() throws Exception {
        byte[] salt = new byte[8];
        RANDOM.nextBytes(salt);
        PBEKeySpec spec = new PBEKeySpec(PASSWORD.toCharArray(), salt, 1000, 128);
        SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        skf.generateSecret(spec).getEncoded();
    }

    // ---------- bcrypt ----------
    private static void bcryptHashAndVerify() {
        int cost = 12; // cost factor
        long t0 = System.nanoTime();
        String hash = BCrypt.hashpw(PASSWORD, BCrypt.gensalt(cost));
        long t1 = System.nanoTime();

        System.out.println("Stored: " + hash);
        System.out.println("Hash time: " + ((t1 - t0) / 1_000_000) + " ms");

        long v0 = System.nanoTime();
        boolean ok = BCrypt.checkpw(PASSWORD, hash);
        long v1 = System.nanoTime();
        System.out.println("Verify: " + ok + " (time: " + ((v1 - v0) / 1_000_000) + " ms)\n");
    }

    private static void bcryptOnce() {
        BCrypt.hashpw(PASSWORD, BCrypt.gensalt(8));
    }

    // ---------- scrypt ----------
    private static void scryptHashAndVerify() {
        byte[] salt = new byte[16];
        RANDOM.nextBytes(salt);
        int N = 16384; // CPU/memory cost (2^14)
        int r = 8;
        int p = 1;
        int keyLen = 32;

        long t0 = System.nanoTime();
        byte[] derived = SCrypt.generate(PASSWORD.getBytes(StandardCharsets.UTF_8), salt, N, r, p, keyLen);
        long t1 = System.nanoTime();

        String stored = String.format("scrypt$%d$%d$%d$%s$%s",
                N, r, p,
                Base64.getEncoder().encodeToString(salt),
                Base64.getEncoder().encodeToString(derived));

        System.out.println("Stored: " + stored);
        System.out.println("Hash time: " + ((t1 - t0) / 1_000_000) + " ms");

        long v0 = System.nanoTime();
        boolean ok = verifyScrypt(PASSWORD, stored);
        long v1 = System.nanoTime();
        System.out.println("Verify: " + ok + " (time: " + ((v1 - v0) / 1_000_000) + " ms)\n");
    }

    private static boolean verifyScrypt(String password, String stored) {
        // stored format: scrypt$N$r$p$base64salt$base64hash
        String[] parts = stored.split("\\$");
        int N = Integer.parseInt(parts[1]);
        int r = Integer.parseInt(parts[2]);
        int p = Integer.parseInt(parts[3]);
        byte[] salt = Base64.getDecoder().decode(parts[4]);
        byte[] expected = Base64.getDecoder().decode(parts[5]);

        byte[] computed = SCrypt.generate(password.getBytes(StandardCharsets.UTF_8), salt, N, r, p, expected.length);
        return MessageDigest.isEqual(computed, expected);
    }

    private static void scryptOnce() {
        byte[] salt = new byte[8];
        RANDOM.nextBytes(salt);
        SCrypt.generate(PASSWORD.getBytes(StandardCharsets.UTF_8), salt, 1024, 8, 1, 16);
    }

    // ---------- Argon2id ----------
    private static void argon2HashAndVerify() {
        byte[] salt = new byte[16];
        RANDOM.nextBytes(salt);
        int iterations = 3;
        int memoryKB = 64 * 1024; // 64 MB represented as KB here
        int parallelism = 1;
        int hashLen = 32;

        Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
                .withSalt(salt)
                .withParallelism(parallelism)
                .withMemoryAsKB(memoryKB)
                .withIterations(iterations)
                .build();

        Argon2BytesGenerator gen = new Argon2BytesGenerator();

        long t0 = System.nanoTime();
        gen.init(params);
        byte[] hash = new byte[hashLen];
        gen.generateBytes(PASSWORD.getBytes(StandardCharsets.UTF_8), hash);
        long t1 = System.nanoTime();

        String stored = String.format("argon2id$%d$%d$%d$%s$%s",
                iterations, memoryKB, parallelism,
                Base64.getEncoder().encodeToString(salt),
                Base64.getEncoder().encodeToString(hash));

        System.out.println("Stored: " + stored);
        System.out.println("Hash time: " + ((t1 - t0) / 1_000_000) + " ms");

        long v0 = System.nanoTime();
        boolean ok = verifyArgon2(PASSWORD, stored);
        long v1 = System.nanoTime();
        System.out.println("Verify: " + ok + " (time: " + ((v1 - v0) / 1_000_000) + " ms)\n");
    }

    private static boolean verifyArgon2(String password, String stored) {
        // stored format: argon2id$iterations$memoryKB$parallelism$base64salt$base64hash
        String[] parts = stored.split("\\$");
        int iterations = Integer.parseInt(parts[1]);
        int memoryKB = Integer.parseInt(parts[2]);
        int parallelism = Integer.parseInt(parts[3]);
        byte[] salt = Base64.getDecoder().decode(parts[4]);
        byte[] expected = Base64.getDecoder().decode(parts[5]);

        Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
                .withSalt(salt)
                .withParallelism(parallelism)
                .withMemoryAsKB(memoryKB)
                .withIterations(iterations)
                .build();

        Argon2BytesGenerator gen = new Argon2BytesGenerator();
        gen.init(params);
        byte[] computed = new byte[expected.length];
        gen.generateBytes(password.getBytes(StandardCharsets.UTF_8), computed);

        return MessageDigest.isEqual(computed, expected);
    }

    private static void argon2Once() {
        byte[] salt = new byte[8];
        RANDOM.nextBytes(salt);
        Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
                .withSalt(salt)
                .withParallelism(1)
                .withMemoryAsKB(32)
                .withIterations(1)
                .build();
        Argon2BytesGenerator gen = new Argon2BytesGenerator();
        gen.init(params);
        byte[] out = new byte[16];
        gen.generateBytes(PASSWORD.getBytes(StandardCharsets.UTF_8), out);
    }
}

執行結果

=== Hash + Verify Benchmark ===
Password: MyPassword123!

---- PBKDF2 (HmacSHA256) ----
Stored: PBKDF2$65536$uBq2UzPm/rU5/5QOOHgWwA==$0Ri1XVl8++OB5Pz7pXAXEAJ32M8i8degzp6PZPnyd5o=
Hash time: 61 ms
Verify: true (time: 52 ms)

---- bcrypt (jBCrypt) ----
Stored: $2a$12$FjXjsIfL7MQ8zz8oyNfx.O6sBoSkhG6hcMK/brtWvfFOlfFx0WD/a
Hash time: 215 ms
Verify: true (time: 213 ms)

---- scrypt (BouncyCastle) ----
Stored: scrypt$16384$8$1$C6G1/j3+24iT7CHfW8/Ifg==$HC7JX3pdmDTKk2tCMUdTQ/ezgyey/hLx2ZCAoePzdHs=
Hash time: 22 ms
Verify: true (time: 25 ms)

---- Argon2id (BouncyCastle) ----
Stored: argon2id$3$65536$1$6HpyoNfAlJQ32cFAmflEHQ==$vZ+hNK/DnNpj1Urbj0W76zT2nvm0qDV14HFMbW9O9J0=
Hash time: 136 ms
Verify: true (time: 133 ms)

2026/6/15

Argon2

Argon2 是一種密碼雜湊 (password hashing) 演算法,也是 2015 年密碼雜湊競賽(PHC, Password Hashing Competition) 的獲勝者。它被設計來安全地儲存密碼,抵抗暴力破解和 GPU / ASIC 攻擊。

特性

  1. Memory-hard:需要大量記憶體計算,增加硬體破解成本。

  2. Time-cost adjustable:可設定運算次數(iterations),增加雜湊延遲。

  3. Parallelism:支援多執行緒並行計算。

  4. 三種變體

    • Argon2d:可抵抗 GPU 破解,記憶體存取取決於輸入密碼,但對側信道攻擊 side‑channel attack 敏感。

    • Argon2i:可抵抗側信道攻擊,使用固定存取模式,但稍慢。

    • Argon2id:混合模式,推薦用於大多數應用(兼顧 GPU 攻擊與側信道安全)。

側信道攻擊 side‑channel attack

側信道攻擊不是直接破解演算法數學弱點,而是利用系統在執行時「洩漏的額外資訊」來恢復密碼、金鑰。這些洩漏資訊稱為「側信道」,常見類型有:

  • 時間(Timing):根據運算花費時間推斷內部邏輯或資料(例如不同輸入導致不同記憶體存取時間)。

  • 快取/記憶體訪問模式(Cache / Memory access):透過觀察 CPU cache 命中/未命中或記憶體訪問順序推測密碼相關資料。

  • 電力(Power analysis):量測設備耗電曲線(特別在智慧卡、嵌入式裝置)來推斷內部運算。

  • 電磁輻射(EM):接收設備發出的電磁洩漏訊號。

  • 分支/投機執行漏洞(Spectre/Speculative exec):利用微架構行為取得不該看到的記憶體內容。

  • I/O 或錯誤回應差異:例如錯誤訊息長短或序列差異可洩漏資訊。

側信道攻擊常見條件:攻擊者和目標在同一台機器或共用硬體資源(例如 cloud 共宿主、虛擬機、瀏覽器 JS 高解析計時)或在物理近距離(電力/EM)時更容易成功。遠端網路時延雜訊大,攻擊難度上升但並非不可能(需大量樣本與高解析度計時)。

若攻擊者只有透過網路遠端(無共用硬體且無高解析計時),側信道攻擊較難。

三種變體

在記憶體存取模式上不同

  • Argon2d

    • 記憶體存取依賴輸入(data‑dependent)

    • 優點:對 GPU / ASIC 破解更抗(memory‑hard),但容易洩漏記憶體訪問模式,因此對於有本地或共宿主攻擊者(能觀察 cache/access pattern)的系統 較不安全

  • Argon2i

    • 記憶體存取獨立於輸入(data‑independent),以固定/預定方式存取記憶體。

    • 優點:較能抵抗側信道攻擊(尤其是記憶體訪問/快取型);缺點:通常較慢,對某些攻擊(GPU 的大量平行 brute force)耐性較弱。

  • Argon2id

    • 混合模式:先用 Argon2i 的 data‑independent pass 再用 Argon2d 的 data‑dependent pass(或類似組合)。

    • 建議:大多數情況使用 Argon2id —— 在兼顧側信道與 GPU 抗性間取得平衡,是常見推薦選擇。

java example

        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk18on</artifactId>
            <version>1.82</version>
        </dependency>

Argon2BouncyCastleExample.java

import org.bouncycastle.crypto.params.Argon2Parameters;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Arrays;

public class Argon2BouncyCastleExample {

    // 產生隨機 salt
    private static byte[] generateSalt(int length) {
        byte[] salt = new byte[length];
        new SecureRandom().nextBytes(salt);
        return salt;
    }

    // 將密碼 hash
    public static String hashPassword(String password, byte[] salt) {
        // 設定參數
        Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
                .withSalt(salt)
                .withIterations(3)       // 運算次數
                .withMemoryAsKB(65536)   // 記憶體使用量 KB (64 MB)
                .withParallelism(2);     // 併行線程數

        Argon2BytesGenerator generator = new Argon2BytesGenerator();
        generator.init(builder.build());

        byte[] hash = new byte[32]; // 32 bytes hash
        generator.generateBytes(password.toCharArray(), hash, 0, hash.length);

        // 回傳 base64 編碼
        return Base64.getEncoder().encodeToString(hash);
    }

    // 驗證密碼
    public static boolean verifyPassword(String password, String expectedHashBase64, byte[] salt) {
        String hashToCheck = hashPassword(password, salt);
        return Arrays.equals(Base64.getDecoder().decode(hashToCheck), Base64.getDecoder().decode(expectedHashBase64));
    }

    public static void main(String[] args) {
        String password = "MyPassword123!";
        byte[] salt = generateSalt(16); // 16 bytes salt

        // hash
        String hash = hashPassword(password, salt);
        System.out.println("Salt (Base64): " + Base64.getEncoder().encodeToString(salt));
        System.out.println("Hash (Base64): " + hash);

        // 驗證
        boolean ok = verifyPassword(password, hash, salt);
        System.out.println("Password verified: " + ok);

        // 測試錯誤密碼
        boolean fail = verifyPassword("wrongPassword", hash, salt);
        System.out.println("Wrong password verified: " + fail);
    }
}

執行結果

Salt (Base64): Uivew8iBmhiZaz7Wv2nSDw==
Hash (Base64): Ldm2DksAcnhUIHyJ4v4uzJJwLd5A2YIhTMn3277/tDU=
Password verified: true
Wrong password verified: false

References

Argon2 - 維基百科,自由的百科全書

2026/6/1

scrypt

scrypt 是一種password-based key derivation演算法。是加拿大計算機科學家暨計算機安全研究人員 Colin Percival 於2009年所發明的密鑰派生函數,當初設計用在他所創立的Tarsnap服務上。設計時考慮到大規模的客製硬體攻擊而刻意設計需要大量記憶體運算,可防止 GPU/ASIC 大規模破解。2016年,scrypt 演算法發佈在RFC 7914。scrypt的簡化版被用在數個密碼貨幣的工作量證明(Proof-of-Work)上。

scrypt需要使用大量記憶體的原因來自於產生大量 pseudorandom 資料作為演算法計算的基礎。一旦這些資料被產生後,演算法將會以偽隨機性的順序讀取這些資料產生結果。因此最直接的實做方式將會需要大量記憶體將這些資料儲存在記憶體內供演算法計算。由於偽隨機性資料是透過演算法產生,在實作上也可以在需要存取時再計算以降低記憶體使用量。但由於計算成本很高,這個實作方法將大幅降低演算法的速度。

主要的參數:

透過 r, p 參數,調整記憶體使用量

參數 說明
N 迭代次數,耗費 CPU 與時間成本(必須是 2 的次方)
r 記憶體使用量(block size)
p 並行度(parallelism)
salt 隨機值,避免彩虹表攻擊
dkLen 輸出 hash 長度,通常是 32 bytes / 64 bytes

java sample

pom.xml

        <!-- scrypt -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk18on</artifactId>
            <version>1.82</version>
        </dependency>

ScryptBouncyCastleExample.java

import org.bouncycastle.crypto.generators.SCrypt;
import java.security.SecureRandom;
import java.util.Base64;

public class ScryptBouncyCastleExample {

    // scrypt 參數
    private static final int N = 16384; // CPU cost
    private static final int r = 8;     // Memory cost
    private static final int p = 1;     // Parallelism
    private static final int KEY_LENGTH = 32;

    // 產生隨機 salt
    public static byte[] generateSalt() {
        byte[] salt = new byte[16];
        new SecureRandom().nextBytes(salt);
        return salt;
    }

    // Hash 密碼
    public static String hashPassword(String password, byte[] salt) {
        byte[] hash = SCrypt.generate(password.getBytes(), salt, N, r, p, KEY_LENGTH);
        return Base64.getEncoder().encodeToString(hash);
    }

    // 驗證密碼
    public static boolean verifyPassword(String password, byte[] salt, String storedHash) {
        String hashToVerify = hashPassword(password, salt);
        return hashToVerify.equals(storedHash);
    }

    public static void main(String[] args) {
        String password = "MyPassword123!";

        // 產生 salt
        byte[] salt = generateSalt();

        // 生成 hash
        String hashed = hashPassword(password, salt);
        System.out.println("Salt (Base64): " + Base64.getEncoder().encodeToString(salt));
        System.out.println("Hashed password: " + hashed);

        // 驗證
        boolean match = verifyPassword(password, salt, hashed);
        System.out.println("Password match? " + match);

        boolean wrong = verifyPassword("wrongPassword", salt, hashed);
        System.out.println("Password match with wrong password? " + wrong);
    }
}

執行結果

Salt (Base64): +VF7o5rmURY++GxWmKARhw==
Hashed password: PON5gVuqeiOM4MDszfY4DHBjBacpMdObu07WhdTaoyo=
Password match? true
Password match with wrong password? false

References

scrypt - 維基百科,自由的百科全書

2026/5/25

PBKDF2

PBKDF1 和 PBKDF2 是 Password-Based Key Derivation Function 1 and 2,具有可改變計算成本的金鑰衍生函式。PBKDF 是 RSA 的 PKCS 的一部分。PBKDF1 只能產生 160 bits 長度的金鑰。2000 年發布了 PBKDF2,也就是 PKCS #5 v2.0,以 RFC 2898 發布。2017 年發布了 PBKDF2 的更新版本,也就是 RFC 8018,PKCS #5 v2.1 版。

PBKDF2 將 HMAC (hash-based message authentication code) 與 salt 一起套用於密碼加密,並多次重複這個過程,產生一個 derived key,增加了 CPU 運算,讓破解變得困難。

2000 年的演算法建議最小疊代次數為 1000 次,隨著 CPU 效能提升,2005 年 Kerberos 標準建議改為 4096 次。apple 在 ios3 使用 3000 次,在 ios4 為 10000 次。2023 年,OWASP 建議PBKDF2-HMAC-SHA256使用600,000次迭代,PBKDF2-HMAC-SHA512使用210,000 次迭代。

PBKDF2 雖然可以通過改變迭代次數來任意調整所需的計算時間,但它可以用一個小電路和很少的RAM來實現,透過使用特殊應用積體電路(ASIC)或圖形處理器(GPU)進行暴力攻擊會比較容易。

Bcrypt 密碼雜湊函式需要更大的RAM(但仍然不能單獨調整,由給定的CPU時間決定)並且對此類攻擊的抵抗力稍強,更現代的Scrypt金鑰衍生函式可以任意使用大量主記憶體,因此更能抵抗ASIC和GPU攻擊。

2013 年舉辦的 Password Hashing Competition (PHC) 鼓勵開發更好的密碼 hash 演算法。於 2015/7/20,Argon2 被選為最終的獲勝者。

java sample

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.util.Base64;

public class PBKDF2Example {

    private static final int SALT_LENGTH = 16;      // 16 bytes
    private static final int ITERATIONS = 65536;    // iteration count
    private static final int KEY_LENGTH = 256;      // 256-bit

    // 產生隨機 salt
    public static byte[] generateSalt() {
        SecureRandom sr = new SecureRandom();
        byte[] salt = new byte[SALT_LENGTH];
        sr.nextBytes(salt);
        return salt;
    }

    // 將密碼 + salt 轉為 PBKDF2 hash
    public static byte[] hashPassword(char[] password, byte[] salt) {
        try {
            PBEKeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH);
            SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            return skf.generateSecret(spec).getEncoded();
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new RuntimeException("Error while hashing password", e);
        }
    }

    // 驗證密碼
    public static boolean verifyPassword(char[] password, byte[] salt, byte[] expectedHash) {
        byte[] pwdHash = hashPassword(password, salt);
        if (pwdHash.length != expectedHash.length) return false;
        for (int i = 0; i < pwdHash.length; i++) {
            if (pwdHash[i] != expectedHash[i]) return false;
        }
        return true;
    }

    public static void main(String[] args) {
        String password = "MyPassword123!";

        // 生成 salt
        byte[] salt = generateSalt();

        // 生成 hash
        byte[] hash = hashPassword(password.toCharArray(), salt);

        System.out.println("Salt: " + Base64.getEncoder().encodeToString(salt));
        System.out.println("PBKDF2 Hash: " + Base64.getEncoder().encodeToString(hash));

        // 驗證正確密碼
        boolean valid = verifyPassword(password.toCharArray(), salt, hash);
        System.out.println("Password matched? " + valid);

        // 驗證錯誤密碼
        boolean invalid = verifyPassword("wrongPassword".toCharArray(), salt, hash);
        System.out.println("Wrong password verified? " + invalid);
    }
}

執行結果

Salt: aDRyAM6YJD1p8W0SN8U7pg==
PBKDF2 Hash: 2lXNtraA2JIUdSNL5xcj/seHjOpIuTxkarTxKItRabw=
Password matched? true
Wrong password verified? false

References

PBKDF2 - 維基百科,自由的百科全書

2026/5/18

bcrypt

bcrypt 是一種專為密碼儲存設計的單向雜湊演算法(hashing algorithm)。 由美國電腦科學家 Niels Provos 及 David Mazières 根據 Blowfish 加密演算法所設計的密碼雜湊函式,於1999年在USENIX中展示。並加入了:

  • Salt — 防止字典攻擊與 rainbow table 攻擊

  • 工作因子(cost factor) — 運算次數 = 2^cost,控制計算複雜度,讓暴力破解更慢

Cost 大約耗時(現代CPU) 用途建議
8 約 50ms 測試環境
10 約 100–200ms 一般 Web 登入
12 約 300–500ms 高安全性需求
14+ >1 秒 金融級或低頻操作

bcrypt 運作流程

注意密碼最多只支援 72 bytes

  1. 輸入:
  • 明文密碼(例如 "MyPassword123!"),最多 72 bytes

  • 工作因子(cost,例如 10)

  1. 產生隨機 Salt(16 bytes), 是一個 16 bytes(128 bits) salt value

  2. 進行多輪 Blowfish 加密運算,運算次數 = 2^cost ex: cost=10 → 1024 次加密循環 cost=12 → 4096 次循環

  3. 輸出:產生一個 60 字元的 hash 字串,裡面包含版本, cost factor, salt及 hash 結果

bcrypt 格式

#格式
$2<a/b/x/y>$[cost]$[22 character salt][31 character hash]
# sample
$2a$10$E6h8dSmDsC8Kz.bJb3SuPO0h9msLniD9pD8bZjq4nLZdTIm.CDzMy
$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW

格式說明

$2a$    → bcrypt 版本  
10$     → cost factor  
E6h8dSmDsC8Kz.bJb3SuPO  → Base64 編碼的 salt  
0h9msLniD9pD8bZjq4nLZdTIm.CDzMy  → hash 結果
$2a$    → bcrypt 版本  
12$     → cost factor  
R9h/cIPz0gi.URNNX3kh2O  → Base64 編碼的 salt  
PST9/PgBkqquzi.Ss7KIUgO2t0jWMUW  → hash 結果

Version

$2$ (1999)

最初始的 bcrypt 版本

在 OpenBSD 密碼檔案裡面用以下幾種方式定義使用哪一種加密方式

  • $1$: MD5-based crypt ('md5crypt')
  • $2$: Blowfish-based crypt ('bcrypt')
  • $sha1$: SHA-1-based crypt ('sha1crypt')
  • $5$: SHA-256-based crypt ('sha256crypt')
  • $6$: SHA-512-based crypt ('sha512crypt')

$2a$

初始版本的 bcrypt,沒有定義如何處理 non-ASCII 字元,也沒有處理 null terminator。這個版本的 bcrypt 限制

  • 字串必須要用UTF-8 encoding

  • 必須要包含 null terminator

$2x$, $2y$ (June 2011)

因 2011/6 在 php 版本的 crypt_blowfish 發現了一個 bug,在 8th bit 設定時,會處理錯誤。故建議把 $2a$更新為 $2x$ ,同時做了 $2y$。但 2x/2y 都沒有被廣泛採用

$2b$ (February 2014)

OpenBSD 版本的 bcrypt 發現了一個 bug,是使用 unsigned 8-bit 數值儲存密碼長度。超過 255 bytes 的密碼,會以少於 72 bytes 的長度切割。 ex: 260 bytes 被以 4 bytes 切割。

OpenBSD 修正這個問題,並改版為 $2b$

Java Sample

有兩個 java library,org.mindrot.jbcrypt 版本比較舊,沒有支援新版的演算法,且已經沒有在維護。at.favre.lib.crypto.bcrypt 有支援新的演算法,且有支援超過 72 bytes 密碼的處理方式。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.maxkit.test</groupId>
    <artifactId>test</artifactId>
    <version>1.0</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.mindrot</groupId>
            <artifactId>jbcrypt</artifactId>
            <version>0.4</version>
        </dependency>

        <dependency>
            <groupId>at.favre.lib</groupId>
            <artifactId>bcrypt</artifactId>
            <version>0.10.2</version>
        </dependency>
    </dependencies>
</project>

MindrotBcrypt.java

import org.mindrot.jbcrypt.BCrypt;

public class MindrotBcrypt {
    public static void main(String[] args) {
        String password = "MyPassword123!";

        // cost factor (越大越慢,預設常見是 10~12)
        int cost = 12;

        // 產生 salt
        String salt = BCrypt.gensalt(cost);
        System.out.println("Salt: " + salt);

        // Hash 密碼
        String hashed = BCrypt.hashpw(password, salt);
        System.out.println("BCrypt Hash: " + hashed);

        // 驗證密碼
        boolean matched = BCrypt.checkpw(password, hashed);
        System.out.println("Password matched? " + matched);

        // 測試錯誤密碼
        boolean matchedWrong = BCrypt.checkpw("WrongPassword", hashed);
        System.out.println("Wrong password verified? " + matchedWrong);
    }
}

執行結果

Salt: $2a$12$.ZzlOaYpDmUSvFCq/FJaM.
BCrypt Hash: $2a$12$.ZzlOaYpDmUSvFCq/FJaM.lEUlNjTrXH8JuUAN96fq80yiKSyV0Q.
Password matched? true
Wrong password verified? false

BcryptFavreBcrypt.java

import at.favre.lib.crypto.bcrypt.BCrypt;

public class BcryptFavreBcrypt {
    public static void main(String[] args) {
        String password = "MyPassword123!";
        int cost = 12;

        // 產生 hash
        String bcryptHashString = BCrypt.withDefaults().hashToString(cost, password.toCharArray());
        System.out.println("Hash: " + bcryptHashString);

        // 驗證密碼
        BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), bcryptHashString);
        if (result.verified) {
            System.out.println("Password verified OK");
        } else {
            System.out.println("Password verification failed");
        }

        // 錯誤密碼測試
        BCrypt.Result resultWrong = BCrypt.verifyer().verify("WrongPassword".toCharArray(), bcryptHashString);
        System.out.println("Wrong password verified? " + resultWrong.verified);

        //////
        // 產生 2y hash
        String bcryptHashString2Y = BCrypt.with(BCrypt.Version.VERSION_2Y).hashToString(cost, password.toCharArray());
        System.out.println("2Y Hash: " + bcryptHashString2Y);
        // 驗證密碼
        BCrypt.Result result2Y = BCrypt.verifyer().verify(password.toCharArray(), bcryptHashString2Y);
        if (result2Y.verified) {
            System.out.println("2Y Password verified OK");
        } else {
            System.out.println("2Y Password verification failed");
        }

        // 產生 2b hash
        String bcryptHashString2B = BCrypt.with(BCrypt.Version.VERSION_2Y).hashToString(cost, password.toCharArray());
        System.out.println("2B Hash: " + bcryptHashString2B);
        // 驗證密碼
        BCrypt.Result result2B = BCrypt.verifyer().verify(password.toCharArray(), bcryptHashString2B);
        if (result2B.verified) {
            System.out.println("2B Password verified OK");
        } else {
            System.out.println("2B Password verification failed");
        }
    }
}

執行結果

Hash: $2a$12$BUc45rHJsnLWDkFW8CJN4.5QB.KUSg2ofIcejoeJ24OTmznz8BD8a
Password verified OK
Wrong password verified? false
2Y Hash: $2y$12$vRH74gxo2LZN/wbFj4oKdO6RrGgOgWTHxrhw.OUoMt9ZT1WqPT8Uq
2Y Password verified OK
2B Hash: $2y$12$7y.j7ocPnDqcbspURCddzeTn25HWgtQR1LHy20Wo./N81ygBd7m92
2B Password verified OK

References

bcrypt - 維基百科,自由的百科全書

2026/5/4

Java Atomic Variables

java.util.concurrent.atomic 定義了對單一變數進行 atomic operations 的類別,所有類別都有 get 與 set methods,可讀寫 volatile variables。set 可確定會在 get 的前面先處理。

對於物件管理,如果有兩個 thread 同時做 get/set,會導致資料異常,所以通常會在 set method 加上 synchronized 來限制一次只有一個 thread 進入該 method

例如

Counter 類別的變數 c,會因為多個 thread 同時使用而異常

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

所以會改寫為

class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }

}

synchronized 可解決 multi-thread 問題,但會產生效能問題。

以 AtomicInteger 改寫

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }

}

Atomic Operation

non-blocking 演算法提供了 compare-and-swap CAS 方法,可確保資料完整性。

CAS operation 使用以下三個 operands

  1. M 要操作 operate 的記憶體位置

  2. A 該變數期待的既有原始數值

  3. B 需要被設定的新數值

CAS operation 可自動 atomically 從 M 到 B 更新數值,只會在 M 確認為 A 時,更新為 B

因此可不使用 synchronization lock,完成資料的更新,沒有 thread 會被 suspended,也不需要 context switching。


常用的 Atomic Variables 有 AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference,分別代表 int, long, boolean, object reference 這些數值可自動被安全地更新,主要 methods 有

  • get()

    等同於讀取 volatile variable,可直接取得其他 thread 更新的數值

  • incrementAndGet()

  • set()

    等同於寫入 volatile variable

  • lazySet()

    最終會寫入變數,但不保證可立即被其他 thread 取得。速度比 set 更快。可用在 cache,或被 gc 的資料

  • compareAndSet()

    成功就回傳 true

  • weakCompareAndSetPlain(), weakCompareAndSetVolatile()

    weakCompareAndSet() 已被 deprecated

References

# Atomic Variables Java Tutorial

An Introduction to Atomic Variables in Java | Baeldung

java.util.concurrent.atomic (Java SE 25 & JDK 25)

Atomic Variables in Java with Examples - GeeksforGeeks

2026/4/27

FFMForeign Function and Memory (FFM) API

為了使用外部函式庫,java 需要跟原生的 native code 進行互動,傳統是使用 Java Native Interface JNI 進行開發,但 JNI 的開發過程繁複,需要定義 native method,然後從 Java Source Code 生成 C 的 header file,再用C 語言實作,然後才能編譯並連結原生開發的函式庫。

Foreign Function & Memory API(FFM API)參考了其他語言的方法,實作外部函式庫的 Interface,提供更安全有效率的方法,存取本地端的記憶體及函式,取代了 JNI 的功能。這是自 JDK 22 開始的一組新的 API,用來取代 JNI。能夠呼叫 C/C++/Rust 的非 Java 原生函式庫,可直接操作記憶體。

JNI sample

JNIDemo.java

public class JNIDemo {
    static {
        System.loadLibrary("JNIDemo");
    }

    public static void main(String[] args) {
        new JNIDemo().printHelloWorld();
    }

    private native void printHelloWorld();
}

這裡面,有一個 private native void printHelloWorld(),這就是還沒有實作的原生函式。

用以下指令,編譯 class 並產生 header file

javac -h . JNIDemo.java

JNIDemo.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNIDemo */

#ifndef _Included_JNIDemo
#define _Included_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JNIDemo
 * Method:    printHelloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JNIDemo_printHelloWorld
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

編譯 C library

因為在 macos 先查詢 JAVA_HOME 目錄

$ /usr/libexec/java_home
/opt/local/Library/Java/JavaVirtualMachines/jdk-25-azul-zulu.jdk/Contents/Home

編譯

JAVA_HOME=/opt/local/Library/Java/JavaVirtualMachines/jdk-25-azul-zulu.jdk/Contents/Home
gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin -dynamiclib JNIDemo.c -o libJNIDemo.dylib

會產生 libJNIDemo.dylib

執行

$ java -cp . -Djava.library.path=. JNIDemo
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::loadLibrary has been called by JNIDemo in an unnamed module (file:/Users/charley/Downloads/)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

Hello World!

FFM

FFM API 能夠讓 Java 程式在不使用 JNI 的情況下

  • 更安全地存取記憶體

  • 直接呼叫 C function

  • 呼叫 C/系統的 library

FFM 核心元件:

  • Linker:對應 C 語言呼叫約定的連結器,用來呼叫 native 函式

  • SymbolLookup: 在 native library 或 process 中查找符號(例如 printf

  • MemorySegment: 表示一塊連續的記憶體(on-heap 或 off-heap)

  • Arena:管理 MemorySegment 的生命週期(自動釋放資源)

  • ValueLayout:描述原生型別的記憶體布局,例如 JAVA_INT, C_DOUBLE

  • FunctionDescriptor:描述 native 函式的簽章(參數與回傳型別)

測試1

測試呼叫 strlen

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.nio.charset.StandardCharsets;

public class FFMStrlenDemo {
    public static void main(String[] args) throws Throwable {
        // 1. 取得系統的 Linker(如 SystemV ABI 或 Windows ABI)
        Linker linker = Linker.nativeLinker();

        // 2. 找出 symbol (在 libc 裡)
        SymbolLookup stdlib = Linker.nativeLinker().defaultLookup();
        MemorySegment strlenAddr = stdlib.find("strlen").orElseThrow();

        // 3. 建立 Java 對應的 MethodHandle
        MethodHandle strlen = linker.downcallHandle(
            strlenAddr,
            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
        );

        // 4. 建立要傳入的字串記憶體
        // try (Arena arena = Arena.ofConfined()) {
        //     MemorySegment str = arena.allocateUtf8String("Hello FFM API!");
        //     long len = (long) strlen.invoke(str);
        //     System.out.println("Length = " + len);
        // }
        try (Arena arena = Arena.ofConfined()) {
            // 手動建立 UTF-8 null-terminated string
            byte[] bytes = "Hello FFM API!".getBytes(StandardCharsets.UTF_8);
            MemorySegment str = arena.allocate(bytes.length + 1, 1);
            str.asSlice(0, bytes.length).copyFrom(MemorySegment.ofArray(bytes));
            str.set(ValueLayout.JAVA_BYTE, bytes.length, (byte) 0); // '\0'

            long len = (long) strlen.invoke(str);
            System.out.println("Length = " + len);
        }

        // allocate set/get 測試
        try (Arena arena = Arena.ofConfined()) {
            MemorySegment seg = arena.allocate(ValueLayout.JAVA_INT);
            seg.set(ValueLayout.JAVA_INT, 0, 42); // 寫入 42
            int value = seg.get(ValueLayout.JAVA_INT, 0);
            System.out.println("Read value: " + value);
        } // arena 自動釋放記憶體
    }
}

檢查 allocate API

標準 API 是 java.lang.foreign.Arena.allocateUtf8String(String)

但測試,結果是 allocate

# javap -classpath $JAVA_HOME/lib/modules java.lang.foreign.Arena | grep allocate

public abstract java.lang.foreign.MemorySegment allocate(long, long);

這表示目前這個 openjdk 還是使用舊版的 allocate,缺少 allocateUtf8String

編譯,執行

javac FFMStrlenDemo.java
java --enable-native-access=ALL-UNNAMED FFMStrlenDemo

結果

Length = 14
Read value: 42

測試2

sum.c

// sum.c
#include <stdio.h>

int sum(int a, int b) {
    return a + b;
}

編譯為 library

## linux
# gcc -shared -fPIC -o libsum.so sum.c

## macos
gcc -shared -fPIC -o libsum.dylib sum.c

SumFFMDemo.java 放在同一個目錄

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

public class SumFFMDemo {
    public static void main(String[] args) throws Throwable {
        // 載入我們的動態函式庫
        System.loadLibrary("sum"); // 對應 libsum.so / sum.dll

        // 建立 linker
        Linker linker = Linker.nativeLinker();

        // 查找 symbol
        SymbolLookup lookup = SymbolLookup.libraryLookup("libsum.dylib", Arena.global());
        MemorySegment funcAddr = lookup.find("sum").orElseThrow();

        // 建立函式描述: int (int, int)
        FunctionDescriptor fd = FunctionDescriptor.of(
                ValueLayout.JAVA_INT, // return type
                ValueLayout.JAVA_INT, // param a
                ValueLayout.JAVA_INT  // param b
        );

        // 建立 method handle
        MethodHandle sum = linker.downcallHandle(funcAddr, fd);

        // 呼叫 native 函式
        int result = (int) sum.invoke(12, 30);
        System.out.println("sum(12, 30) = " + result);
    }
}

編譯

javac SumFFMDemo.java
java --enable-native-access=ALL-UNNAMED SumFFMDemo
# java -Djava.library.path=. --enable-native-access=ALL-UNNAMED SumFFMDemo

結果

sum(12, 30) = 42

2026/4/20

Compact Object Header

在 JDK 25 中,Compact Object Headers(緊湊型物件標頭)已經正式成為 HotSpot JVM 的預設功能,並且不再需要使用 -XX:+UseCompactObjectHeaders 啟用這個功能。這是 JEP 519 的一部分,用途是進一步優化 Java 物件的記憶體佈局。在 JDK 24 中,這個功能曾作為實驗性功能引入,在 JDK 25 中轉為正式功能。

在 64 位架構的 HotSpot JVM 中,物件標頭的大小從原本的 12 至 16 字節(取決於 JVM 配置)縮減至 8 字節(64 位)。

  • 減少記憶體佔用:物件標頭變小,整體記憶體佔用降低。

  • 提高快取效率:更緊湊的記憶體佈局有助於提升 CPU 快取的命中率。

  • 降低垃圾回收壓力:減少記憶體佔用量,有助於減少垃圾回收的次數。

  • 提升部署密度:在容器化環境中,減少記憶體佔用量有助於提高部署密度。

Project Lilliput 的目標是將物件標頭的大小進一步縮小至 4 字節。然而,這樣的改變需要更深入的研究和測試,以確保不會影響 JVM 的穩定性和性能。

測試

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryUsage;
import java.util.ArrayList;
import java.util.List;

/**
 * JDK 25 Compact Object Header 功能測試
 * 
 * 功能說明:
 * Compact Object Header (JEP 450) 是 JDK 25 引入的實驗性特性
 * 目的是減少物件頭的記憶體開銷,從傳統的 12-16 bytes 減少到 8 bytes
 * 
 * 啟用方式:
 * java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders YourClass
 * 
 * 主要優勢:
 * 1. 減少記憶體佔用(每個物件節省 4-8 bytes)
 * 2. 提升快取效率(更好的資料局部性)
 * 3. 適合大量小物件的應用場景
 */
public class CompactObjectHeaderTest {

    private static final int OBJECT_COUNT = 1_000_000;

    static class SmallObject {
        private int id;
        private String name;

        public SmallObject(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== JDK 25 Compact Object Header 測試 ===\n");

        // 檢查 JVM 參數
        checkJVMFlags();

        // 顯示初始記憶體狀態
        System.out.println("\n--- 初始記憶體狀態 ---");
        printMemoryUsage();

        // 建立大量物件並測試
        System.out.println("\n--- 建立 " + OBJECT_COUNT + " 個物件 ---");
        List<SmallObject> objects = new ArrayList<>(OBJECT_COUNT);

        long startTime = System.currentTimeMillis();
        long startMemory = getUsedMemory();

        for (int i = 0; i < OBJECT_COUNT; i++) {
            objects.add(new SmallObject(i, "Object_" + i));
        }

        long endTime = System.currentTimeMillis();
        long endMemory = getUsedMemory();

        // 強制垃圾回收以獲得更準確的記憶體使用量
        System.gc();
        Thread.sleep(100);
        long afterGCMemory = getUsedMemory();

        // 顯示測試結果
        System.out.println("\n--- 測試結果 ---");
        System.out.println("建立時間: " + (endTime - startTime) + " ms");
        System.out.println("記憶體增量 (建立後): " + formatBytes(endMemory - startMemory));
        System.out.println("記憶體增量 (GC後): " + formatBytes(afterGCMemory - startMemory));
        System.out.println("平均每個物件: " + formatBytes((afterGCMemory - startMemory) / OBJECT_COUNT));

        // 顯示最終記憶體狀態
        System.out.println("\n--- 最終記憶體狀態 ---");
        printMemoryUsage();

        // 物件頭大小估算
        System.out.println("\n--- 物件頭分析 ---");
        analyzeObjectHeader(afterGCMemory - startMemory);

        // 保持物件存活
        System.out.println("\n物件數量: " + objects.size());
        System.out.println("\n提示: 使用 -XX:+UseCompactObjectHeaders 啟用壓縮物件頭");
        System.out.println("比較指令: java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders CompactObjectHeaderTest");
    }

    private static void checkJVMFlags() {
        System.out.println("JVM 版本: " + System.getProperty("java.version"));
        System.out.println("JVM 供應商: " + System.getProperty("java.vendor"));

        List<String> inputArgs = ManagementFactory.getRuntimeMXBean().getInputArguments();
        System.out.println("\nJVM 參數:");
        for (String arg : inputArgs) {
            System.out.println("  " + arg);
        }

        boolean hasCompactHeadersFlag = inputArgs.stream()
            .anyMatch(arg -> arg.contains("UseCompactObjectHeaders"));
        boolean isEnabled = inputArgs.stream()
            .anyMatch(arg -> arg.contains("+UseCompactObjectHeaders"));
        boolean isDisabled = inputArgs.stream()
            .anyMatch(arg -> arg.contains("-UseCompactObjectHeaders"));

        if (isEnabled) {
            System.out.println("\n✓ Compact Object Headers 已明確啟用 (+UseCompactObjectHeaders)");
        } else if (isDisabled) {
            System.out.println("\n✗ Compact Object Headers 已明確禁用 (-UseCompactObjectHeaders)");
        } else if (hasCompactHeadersFlag) {
            System.out.println("\n? Compact Object Headers 參數已設定");
        } else {
            System.out.println("\n- Compact Object Headers 使用預設設定");
        }
    }

    private static void printMemoryUsage() {
        Runtime runtime = Runtime.getRuntime();
        long totalMemory = runtime.totalMemory();
        long freeMemory = runtime.freeMemory();
        long usedMemory = totalMemory - freeMemory;
        long maxMemory = runtime.maxMemory();

        System.out.println("已使用記憶體: " + formatBytes(usedMemory));
        System.out.println("總分配記憶體: " + formatBytes(totalMemory));
        System.out.println("最大可用記憶體: " + formatBytes(maxMemory));
        System.out.println("可用記憶體: " + formatBytes(freeMemory));
    }

    private static long getUsedMemory() {
        Runtime runtime = Runtime.getRuntime();
        return runtime.totalMemory() - runtime.freeMemory();
    }

    private static String formatBytes(long bytes) {
        if (bytes < 1024) return bytes + " B";
        if (bytes < 1024 * 1024) return String.format("%.2f KB", bytes / 1024.0);
        if (bytes < 1024 * 1024 * 1024) return String.format("%.2f MB", bytes / (1024.0 * 1024));
        return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
    }

    private static void analyzeObjectHeader(long totalMemory) {
        // 估算物件頭大小
        // 傳統物件頭: 12 bytes (32-bit) 或 16 bytes (64-bit 壓縮指標)
        // Compact 物件頭: 8 bytes

        long avgBytesPerObject = totalMemory / OBJECT_COUNT;

        System.out.println("平均每個物件記憶體: " + avgBytesPerObject + " bytes");
        System.out.println("\n理論估算:");
        System.out.println("  - 傳統物件頭: 16 bytes (mark word + klass pointer)");
        System.out.println("  - Compact 物件頭: 8 bytes");
        System.out.println("  - int 欄位: 4 bytes");
        System.out.println("  - String 參考: 4-8 bytes (壓縮指標)");
        System.out.println("  - 對齊填充: 可能需要額外空間");

        if (avgBytesPerObject <= 70) {
            System.out.println("\n✓ 可能正在使用 Compact Object Headers");
        } else {
            System.out.println("\n✗ 可能使用傳統物件頭");
        }
    }
}

編譯後,用這兩種方式執行

# 禁用 Compact Object Headers
java -XX:-UseCompactObjectHeaders -Xms512m -Xmx512m CompactObjectHeaderTest > test_without_compact.log 2>&1
# 啟用
java -XX:+UseCompactObjectHeaders -Xms512m -Xmx512m CompactObjectHeaderTest > test_with_compact.log 2>&1

執行結果比較

傳統模式 - 平均每個物件: 78
壓縮模式 - 平均每個物件: 69

分析

傳統模式 (78 bytes):
├─ 物件頭:16 bytes (mark word 8 + klass pointer 8)
├─ int id:4 bytes
├─ String 參考:8 bytes (未壓縮指標)
└─ 對齊填充:~50 bytes (String 物件本身的開銷)

壓縮模式 (69 bytes):
├─ 物件頭:8 bytes (壓縮後)
├─ int id:4 bytes
├─ String 參考:8 bytes
└─ 對齊填充:~49 bytes

header 部分減少了 8 bytes,實際上節省 9 bytes

如果 application 是大量的小物件的集合,例如 cache,就啟用-XX:+UseCompactObjectHeaders