首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

J2EE 整合 Spring AOP 與 EJB:從重構到切面化業務擴展實戰

     編輯:Java 相關知識

動機:為什麼在 J2EE 中引入 Spring?

  • 傳統 EJB 依賴 JNDI 查找,耦合度高,單元測試困難
  • Spring IOC 容器管理 POJO,降低依賴,支援 AOP 橫切關注點
  • 非侵入式:無需特殊編譯器、類載入器修改或部署描述符變更

重構步驟:EJB → Spring POJO

1. 原始無狀態 Session Bean

@Stateless
public class TradeManagerImpl implements TradeManager {
    @EJB TradeLocalHome tradeHome; // JNDI 依賴
    public BigDecimal getPrice(String symbol) { /* ... */ }
    public void setPrice(String symbol, BigDecimal price) { /* ... */ }
}

2. 提取業務邏輯為 POJO

public class TradeDao implements TradeManager {
    private TradeLocalHome tradeHome;
    public TradeDao(TradeLocalHome tradeHome) { this.tradeHome = tradeHome; }
    // 純業務邏輯,無 EJB 介面依賴
}

3. EJB 轉為委派代理

public class TradeManagerEJB extends AbstractStatelessSessionBean {
    private TradeManager tradeManager;
    
    protected void onEjbCreate() {
        tradeManager = (TradeManager) getBeanFactory().getBean("tradeManager");
    }
    
    public BigDecimal getPrice(String symbol) { return tradeManager.getPrice(symbol); }
    public void setPrice(String symbol, BigDecimal price) { tradeManager.setPrice(symbol, price); }
}

4. Spring 配置注入 JNDI 資源

<bean id="tradeManager" class="com.example.TradeDao">
    <constructor-arg>
        <bean class="org.springframework.jndi.JndiObjectFactoryBean">
            <property name="jndiName">
                <bean class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
                    <property name="staticField" value="com.example.TradeLocalHome.JNDI_NAME"/>
                </bean>
            </property>
            <property name="proxyInterface" value="com.example.TradeLocalHome"/>
        </bean>
    </constructor-arg>
</bean>

AOP 切面化擴展:外部報價補全

需求

本地無報價時,自動呼叫 Yahoo Finance API 補全。

實作切面

public class ForeignTradeAdvice implements MethodInterceptor {
    private TradeManager externalFeed; // YahooFeed
    
    public ForeignTradeAdvice(TradeManager externalFeed) { this.externalFeed = externalFeed; }
    
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object result = invocation.proceed(); // 執行原始 getPrice
        if (result == null) { // 本地無報價
            String symbol = (String) invocation.getArguments()[0];
            return externalFeed.getPrice(symbol); // 呼叫外部
        }
        return result;
    }
}

Spring AOP 配置

<bean id="yahooFeed" class="com.example.YahooFeed"/>

<bean id="foreignTradeAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
    <property name="mappedName" value="getPrice"/>
    <property name="advice">
        <bean class="com.example.ForeignTradeAdvice">
            <constructor-arg ref="yahooFeed"/>
        </bean>
    </property>
</bean>

<!-- 代理目標 Bean -->
<bean id="tradeManager" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="com.example.TradeManager"/>
    <property name="target" ref="baseTradeManager"/> <!-- 原始 TradeDao -->
    <property name="interceptorNames">
        <list><value>foreignTradeAdvisor</value></list>
    </property>
</bean>

進階:切面鏈與快取優化

YahooFeed 加入快取切面

<bean id="yahooFeed" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target">
        <bean class="com.example.YahooFeed"/>
    </property>
    <property name="interceptorNames">
        <list>
            <value>yahooFeedCachingAdvisor</value> <!-- 快取 -->
            <value>syncPointAdvisor</value>       <!-- 同步防重複請求 -->
        </list>
    </property>
</bean>

同步切面防止並發重複請求

public class SyncPointAdvice implements MethodInterceptor {
    private Map<String, Object[]> locks = new ConcurrentHashMap<>();
    
    public Object invoke(MethodInvocation inv) throws Throwable {
        String symbol = (String) inv.getArguments()[0];
        Object[] lock = locks.computeIfAbsent(symbol, k -> new Object[1]);
        
        synchronized (lock) {
            if (lock[0] != null) return lock[0]; // 已有結果
            lock[0] = inv.proceed(); // 首次請求
            return lock[0];
        }
    }
}

單元測試:Mock 獨立驗證切面邏輯

@Test
public void testForeignTradeAdvice() {
    Mock baseMock = mock(TradeManager.class);
    Mock yahooMock = mock(TradeManager.class);
    
    when(baseMock.getPrice("IBM")).thenReturn(null);
    when(yahooMock.getPrice("IBM")).thenReturn(new BigDecimal("123.45"));
    
    // 使用 BeanPostProcessor 替換 Spring Context 中的 Bean
    ApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml") {
        @Override
        protected void prepareBeanFactory(ConfigurableListableBeanFactory bf) {
            bf.addBeanPostProcessor(new BeanPostProcessor() {
                public Object postProcessBeforeInitialization(Object bean, String name) {
                    if ("baseTradeManager".equals(name)) return baseMock;
                    if ("yahooFeed".equals(name)) return yahooMock;
                    return bean;
                }
            });
        }
    };
    
    TradeManager proxy = (TradeManager) ctx.getBean("tradeManager");
    assertEquals(new BigDecimal("123.45"), proxy.getPrice("IBM"));
    verify(baseMock).getPrice("IBM");
    verify(yahooMock).getPrice("IBM");
}

總結

  1. EJB 保持薄委派層,業務邏輯下沉至 Spring POJO
  2. 2. 利用 AbstractStatelessSessionBean 整合 Spring Context 3. AOP 實現非侵入式功能擴展(外部報價、快取、同步、審計) 4. 切面獨立可測,支援 Mock 驗證邏輯正確性 5. 透過 Spring 配置組裝,無需修改 Java 代碼即可調整行為
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved