Spring的輕量級實現

Grey Zeng發表於2021-11-13

作者: Grey

原文地址:Spring的輕量級實現

本文是參考公眾號:碼農翻身 的從零開始造Spring 教程的學習筆記

原始碼

github

開發方法

使用TDD的開發方法,TDD的開發流程是:

  1. 寫一個測試用例

  2. 執行:失敗

  3. 寫Just enough的程式碼,讓測試通過

  4. 重構程式碼保持測試通過,

然後迴圈往復。

說明

  • 僅實現核心功能

  • 基於spring-framework-3.2.18.RELEASE版本

通過XML例項化一個物件

解析XML檔案,拿到Bean的id和完整路徑,通過反射方式例項化一個物件。

XML格式如下,檔名為:bean-v1.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="userService" class="org.spring.service.v1.UserService"></bean>
</beans>

需要解析上述XML並生成userService物件,呼叫者只需要做如下呼叫即可:

public class BeanFactoryV1Test {
    @Test
    public void testGetBean() {
        BeanFactory factory = new DefaultBeanFactory("bean-v1.xml");
        UserService userService = (UserService) factory.getBean("userService");
        assertNotNull(userService);
    }
}

思路為:

解析XML,並把XML中的類通過反射方式生成物件,最後,把這個生成的物件放到一個Map中,其中Map的key為beanId,如上例就是:userService, Map的Value是UserService的全路徑org.spring.service.v1.UserService

實現細節參考程式碼見:step1

基礎工作和基本封裝

  • 增加日誌支援:log4j2 + SLF4j
  • 增加異常處理,所有異常的父類設計為BeansException
  • 封裝BeanDefinition

由於DefaultBeanFactory中的BEAN_MAP目前只包括了beanClassName資訊,後續如果要擴充套件其他的資訊,肯定需要增加欄位,所以我們需要抽象出一個介面BeanDefinition,方便後續擴充套件其他的欄位。

  • 封裝Resource

在BeanFactory初始化的時候,傳入的是XML格式的配置資訊,比如bean-v1.xml, Spring會把這個抽象成一個Resource,常見Resource有
FileSystemResource: 從檔案地址讀配置
ClassPathResource: 從classpath下讀配置
BeanFactory在建立Bean的時候,只關注Resource即可。

實現細節參考程式碼見:vstep4-2-resource

封裝XML的解析邏輯和Bean的註冊邏輯

設計XmlBeanDefinitionReader,用於解析XML,傳入Resource,即可獲取所有BeanDefinition

public void loadBeanDefinitions(Resource resource) {
    // 從Resource中獲取所有的BeanDefinition
    // 註冊到BEAN_MAP中
}

由於要把BeanDefinition放入BEAN_MAP中,所以XmlBeanDefinitionReader需要持有一個DefaultBeanFactory,且DefaultBeanFactory需要有註冊BeanDefinition和獲取BeanDefintion的能力,這樣DefaultBeanFactory的職責就不單一了,所以需要抽象出一個BeanDefinitionRegistry,這個BeanDefinitionRegistry專門負責註冊BeanDefinition和獲取BeanDefintion

public interface BeanDefinitionRegistry {
    /**
     * 註冊Bean
     * @param beanId
     * @param beanDefinition
     */
    void registerBeanDefinition(String beanId, BeanDefinition beanDefinition);
}

XmlBeanDefinitionReader只需要持有BeanDefinitionRegistry,即可將解析生成的BeanDefinition注入BEAN_MAP中。

實現細節參考程式碼見:vstep5-final

單例多例模式的配置實現

XML檔案中會增加一個屬性,如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="org.spring.service.v1.UserService"></bean>
    <bean id="orgService" class="org.spring.service.v1.OrgService" scope="prototype"></bean>
</beans>

其中orgService這個bean配置成了prototype的屬性,所以在BeanDefinition這個資料結構要增加是否單例,是否多例的邏輯

public interface BeanDefinition {
    ...
    boolean isSingleton();
    boolean isPrototype();
    ...
}

DefaultBeanFactory呼叫getBean的時候,判斷是否單例,如果是單例,則複用物件,如果是多例,則new新的物件。

@Override
    public Object getBean(String beanId) {
        // TODO bean存在與否判斷
        // TODO 異常處理
        // TODO 建構函式帶引數
        BeanDefinition definition = BEAN_MAP.get(beanId);
        if (definition.isSingleton()) {
            Object bean = this.getSingleton(beanId);
            if(bean == null){
                bean = createBean(definition);
                this.registerSingleton(beanId, bean);
            }
            return bean;
        }
        return createBean(definition);
    }

抽象SingletonBeanRegistry這個介面,專門用於註冊和獲取單例物件,

public interface SingletonBeanRegistry {
    void registerSingleton(String beanName, Object singletonObject);
    Object getSingleton(String beanName);
}

DefaultSingletonBeanRegistry實現這個介面,實現對單例物件的註冊

public class DefaultSingletonBeanRegistry implements SingletonBeanRegistry {
    // TODO 考慮執行緒安全的容器
    private final Map<String, Object> singletonObjects = new HashMap<>();

    @Override
    public void registerSingleton(String beanName, Object singletonObject) {
        // 註冊單例Bean
        ...
    }

    @Override
    public Object getSingleton(String beanName) {
  // 獲取單例Bean
        return this.singletonObjects.get(beanName);
    }
}

DefaultBeanFactory繼承DefaultSingletonBeanRegistry這個類,就有了獲取單例Bean和註冊單例Bean的能力。

實現細節參考程式碼見:vstep6-scope

整合並抽象出ApplicationContext

我們使用Spring的時候,一般是這樣做的:

ApplicationContext ctx = new ClassPathXmlApplicationContext("mycontainer.xml");
UserService userService = (UserService) ctx.getBean("userService");

ApplicationContext ctx = new FileSystemApplicationContext("src\\test\\resources\\bean-v1.xml");
UserService userService = (UserService) ctx.getBean("userService");

現在,我們需要抽象出ApplicationContext這個介面來實現如上的功能,其中有如下兩個類去實現這個介面。

ClassPathXmlApplicationContext

從classpath中讀取配置檔案

FileSystemApplicationContext

從檔案中讀取配置檔案

這兩個子類都需要持有DefaultBeanFactory才能有getBean的能力,

ClassPathXmlApplicationContext程式碼如下:

public class ClassPathXmlApplicationContext implements ApplicationContext {
    private final DefaultBeanFactory factory;

    public ClassPathXmlApplicationContext(String configPath) {
        factory = new DefaultBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        reader.loadBeanDefinitions(new ClassPathResource(configPath));
    }
    
    @Override
    public Object getBean(String beanId) {
        return factory.getBean(beanId);
    }
}

FileSystemApplicationContext程式碼如下:

public class FileSystemApplicationContext implements ApplicationContext {
    private final DefaultBeanFactory factory;

    public FileSystemApplicationContext(String configPath) {
        factory = new DefaultBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        reader.loadBeanDefinitions(new FileSystemResource(configPath));
    }
    @Override
    public Object getBean(String beanId) {
        return factory.getBean(beanId);
    }
}

實現細節參考程式碼見:vstep7-applicationcontext-v1

通過觀察發現,ClassPathXmlApplicationContextFileSystemApplicationContext大部分程式碼都是相同的,只有在獲取Resource的時候,方法不一樣,所以,我們通過模板方法這個設計模式,設計一個抽象類AbstractApplicationContext,程式碼如下:

public abstract class AbstractApplicationContext implements ApplicationContext {
    private DefaultBeanFactory factory;

    public AbstractApplicationContext(String configPath) {
        factory = new DefaultBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        reader.loadBeanDefinitions(getResourceByPath(configPath));
    }

    @Override
    public Object getBean(String beanId) {
        return factory.getBean(beanId);
    }
    protected abstract Resource getResourceByPath(String path);
}

這個抽象類實現除了獲取Resource以外的所有邏輯,ClassPathXmlApplicationContextFileSystemApplicationContext都繼承這個抽象類,完成Resource的獲取邏輯的編寫即可。以FileSystemApplicationContext為例,示例程式碼如下:

public class FileSystemApplicationContext extends AbstractApplicationContext {
    public FileSystemApplicationContext(String configPath) {
        super(configPath);
    }

    @Override
    protected Resource getResourceByPath(String path) {
        return new FileSystemResource(path);
    }
}

實現細節參考程式碼見:vstep7-applicationcontext-v2

注入Bean和字串常量

我們需要對於如下型別的XML配置檔案進行解析:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="org.spring.service.v2.UserService">
        <property name="accountDao" ref="accountDao"/>
        <property name="itemDao" ref="itemDao"/>
        <property name="owner" value="test"/>
        <property name="version" value="2"/>
        <property name="checked" value="on"/>
    </bean>
    <bean id="accountDao" class="org.spring.dao.v2.AccountDao">
    </bean>
    <bean id="itemDao" class="org.spring.dao.v2.ItemDao">
    </bean>
</beans>

需要達到的目的就是:可以把整型,字串型別,簡單物件型別注入到一個Bean中,我們需要解決如下兩個問題:

第一個問題是:把字串轉成各種各樣的Value,比如把String轉換成Integer或者轉換成Boolean。jdk中java.bean包中的PropertyEditorSupport這個類來完成的,我們新建了CustomBooleanEditorCustomNumberEditor兩個類,這兩個類都繼承於PropertyEditorSupport,分別實現了String型別轉換成Boolean型別和String型別轉換成Integer型別的功能。其他的型別轉換也可以通過類似的方法來實現。然後抽象出了TypeConvert這個介面,並把這些轉換器加入一個特定的Map中,Mapkey就是要轉換的目標的型別,Value就是對應的轉換器的實現類,即可實現型別轉換。

public interface TypeConverter {
    // TODO 抽象出:TypeMismatchException
    <T> T convertIfNecessary(Object value, Class<T> requiredType);
}

第二個問題是:我們呼叫Bean的setXXX方法把這些Value值set到目標Bean中,做法是抽象出PropertyValue

public class PropertyValue {
    private final String name;
    private final Object value;
    // 省略構造方法和get/set方法
}

BeanDefiniton需要增加方法獲取PropertyValue的邏輯,BeanDefiniton的所有子類,例如:GenericBeanDefinition中需要增加

private List<PropertyValue> propertyValues = new ArrayList<>();

在解析XML檔案的時候,就需要把List<PropertyValue>識別出來並加入BeanDefinition中(RuntimeBeanReference,TypedStringValue),使用BeanDefinitionValueResolver把對應的PropertyValue給初始化好,如下程式碼:

public class BeanDefinitionValueResolver {
    ...
    public Object resolveValueIfNecessary(Object value) {
        if (value instanceof RuntimeBeanReference) {
            ...
        } else if (value instanceof TypedStringValue) {
            return ((TypedStringValue) value).getValue();
        } else {
            //TODO
            throw new RuntimeException("the value " + value + " has not implemented");
        }
    }
    ...
}

而setXXX的背後實現利用的是jdk原生java.beans.Introspector來實現,見DefaultBeanFactorypopulateBean方法

private void populateBean(BeanDefinition bd, Object bean) {
        ....
        try {
            for (PropertyValue pv : pvs) {
                String propertyName = pv.getName();
                Object originalValue = pv.getValue();
                Object resolvedValue = valueResolver.resolveValueIfNecessary(originalValue);
                BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());
                PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
                for (PropertyDescriptor pd : pds) {
                    if (pd.getName().equals(propertyName)) {
                        Object convertedValue = converter.convertIfNecessary(resolvedValue, pd.getPropertyType());
                        pd.getWriteMethod().invoke(bean, convertedValue);
                        break;
                    }
                }
            }
        } catch (Exception ex) {
            // TODO 封裝Exception
            throw new RuntimeException("Failed to obtain BeanInfo for class [" + bd.getBeanClassName() + "]", ex);
        }
    }

其中

pd.getWriteMethod().invoke(bean, convertedValue);

就是對bean的屬性進行賦值操作(即:setXXX方法)

實現細節參考程式碼見:vstep8-inject

實現構造器注入

處理形如以下的配置:

<bean id="userService" class="org.spring.service.v3.UserService">
    <constructor-arg ref="accountDao"/>
    <constructor-arg ref="itemDao"/>
    <constructor-arg value="1"/>
</bean>
<bean id="accountDao" class="org.spring.dao.v3.AccountDao"></bean>
<bean id="itemDao" class="org.spring.dao.v3.ItemDao"></bean>

和上例中注入Bean和字串常量一樣,我們抽象出ConstructorArgument用於表示一個建構函式資訊,每個BeanDefinition中持有這個物件,

public class ConstructorArgument {
    private final List<ValueHolder> argumentValues = new LinkedList<>();
    public ConstructorArgument() {}
    public void addArgumentValue(ValueHolder valueHolder) {
        this.argumentValues.add(valueHolder);
    }
    public List<ValueHolder> getArgumentValues() {
        return Collections.unmodifiableList(this.argumentValues);
    }
    public int getArgumentCount() {
        return this.argumentValues.size();
    }
    public boolean isEmpty() {
        return this.argumentValues.isEmpty();
    }
    /**
     * Clear this holder, removing all argument values.
     */
    public void clear() {
        this.argumentValues.clear();
    }
    public static class ValueHolder {
        private Object value;
        private String type;
        private String name;
        // 省略get/set和構造方法
    }
}

在解析XML的時候,XmlBeanDefinitionReader需要負責解析出ConstuctorArgumentDefaultBeanFactory通過指定建構函式來生成Bean物件並通過ConstructorResolver注入Bean例項到構造方法中。

public class ConstructorResolver {
  ....
    public Object autowireConstructor(final BeanDefinition bd) {
       // ...通過bd找到一個合適的建構函式
        try {
            // 找到了一個合適的建構函式,則用這個建構函式初始化Bean物件初始化Bean物件
            return constructorToUse.newInstance(argsToUse);
        } catch (Exception e) {
            // TODO throw new BeanCreationException(bd.getID(), "can't find a create instance using " + constructorToUse);        }
            throw new RuntimeException(bd.getID() + "can't find a create instance using " + constructorToUse);
        }

    }
  ....
}

注:這裡指定的建構函式的查詢邏輯為:解析出XML的建構函式的引數列表,和通過反射拿到對應的建構函式的引數列表進行對比(每個引數的型別和個數必須一樣)

Constructor<?>[] candidates = beanClass.getConstructors();
        BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this.beanFactory);
        ConstructorArgument cargs = bd.getConstructorArgument();
        TypeConverter typeConverter = new SimpleTypeConverter();
        for (int i = 0; i < candidates.length; i++) {
            // 匹配引數型別和個數,要完全對應上才可以
            Class<?>[] parameterTypes = candidates[i].getParameterTypes();
            if (parameterTypes.length != cargs.getArgumentCount()) {
                continue;
            }
            argsToUse = new Object[parameterTypes.length];
            boolean result = this.valuesMatchTypes(parameterTypes,
                    cargs.getArgumentValues(),
                    argsToUse,
                    valueResolver,
                    typeConverter);
            if (result) {
                constructorToUse = candidates[i];
                break;
            }
        }

實現細節參考程式碼見:vstep9-constructor

實現註解

實現兩個註解:@Component @Autowired(只針對屬性注入,暫時不考慮方法注入)
且需要實現如下的XML的解析,即實現某個包下的Bean掃描。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.spring.service.v4,org.spring.dao.v4"></context:component-scan>
</beans>

我們首先需要定義註解Component ,Autowired,程式碼如下:

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    boolean required() default true;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {
    String value() default "";
}

其次,我們需要實現一個功能,即:給一個包名,掃描獲取到這個包以及子包下面的所有Class,示例程式碼如下:

  public Resource[] getResources(String basePackage) throws IOException {
        Assert.notNull(basePackage, "basePackage  must not be null");
        // 把包名中的.轉成/, 即可獲取包的路徑
        String location = ClassUtils.convertClassNameToResourcePath(basePackage);
        // TODO  ClassLoader cl = getClassLoader();
        URL url = Thread.currentThread().getContextClassLoader().getResource(location);
        File rootDir = new File(url.getFile());

        Set<File> matchingFiles = retrieveMatchingFiles(rootDir);
        Resource[] result = new Resource[matchingFiles.size()];
        int i = 0;
        for (File file : matchingFiles) {
            result[i++] = new FileSystemResource(file);
        }
        return result;
    }

主要思路是將包名轉換成檔案路徑,然後遞迴獲取路徑下的Class檔案。

protected Set<File> retrieveMatchingFiles(File rootDir) throws IOException {
        if (!rootDir.exists()) {
            // Silently skip non-existing directories.
            /*if (logger.isDebugEnabled()) {
                logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist");
            }*/
            return Collections.emptySet();
        }
        if (!rootDir.isDirectory()) {
            // Complain louder if it exists but is no directory.
           /* if (logger.isWarnEnabled()) {
                logger.warn("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory");
            }*/
            return Collections.emptySet();
        }
        if (!rootDir.canRead()) {
            /*if (logger.isWarnEnabled()) {
                logger.warn("Cannot search for matching files underneath directory [" + rootDir.getAbsolutePath() +
                        "] because the application is not allowed to read the directory");
            }*/
            return Collections.emptySet();
        }
  /*String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
  if (!pattern.startsWith("/")) {
   fullPattern += "/";
  }
  fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
  */
        Set<File> result = new LinkedHashSet<>(8);
        doRetrieveMatchingFiles(rootDir, result);
        return result;
    }

    protected void doRetrieveMatchingFiles(File dir, Set<File> result) throws IOException {
        File[] dirContents = dir.listFiles();
        if (dirContents == null) {
           /* if (logger.isWarnEnabled()) {
                logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
            }*/
            return;
        }
        for (File content : dirContents) {
            if (content.isDirectory()) {
                if (!content.canRead()) {
             /*       if (logger.isDebugEnabled()) {
                        logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
                                "] because the application is not allowed to read the directory");
                    }*/
                } else {
                    doRetrieveMatchingFiles(content, result);
                }
            } else {
                result.add(content);
            }

        }
    }

由於註解的Bean不像之前的xml定義的Bean那樣,會對Bean配置一個id,所以,這裡解析出來的Bean定義需要自動生成一個BeanId(預設先取註解中的value的配置,否則就就是類名第一個字母小寫,抽象BeanNameGenerator來專門對Bean定義ID),同時,Spring中單獨新建了一個AnnotatedBeanDefinition介面來定義包含註解的BeanDefinition

我們得到了對應的Class檔案,我們需要通過某種方式去解析這個Class檔案,拿到這個Class中的所有資訊,特別是註解資訊。可以使用ASM這個來解析Class的資訊,用ASM的原生方式解析不太方便,解析ClassMetaDataAnnotation都需要定義一個Visitor,所以Spring抽象了一個介面MetadataReader來封裝ASM的實現

public interface MetadataReader {
    /**
     * Read basic class metadata for the underlying class.
     */
    ClassMetadata getClassMetadata();

    /**
     * Read full annotation metadata for the underlying class,
     * including metadata for annotated methods.
     */
    AnnotationMetadata getAnnotationMetadata();
}

然後,我們需要拿到Bean中的所有Field(帶註解的),並把他例項化成一個物件,並將這個物件注入目標Bean中,示例程式碼如下:

public class AutowiredFieldElement extends InjectionElement {
    ...
    @Override
    public void inject(Object target) {
        Field field = getField();
        try {
            DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
            Object value = factory.resolveDependency(desc);
            if (value != null) {
                ReflectionUtils.makeAccessible(field);
                field.set(target, value);
            }
        } catch (Throwable ex) {
            // TODO 異常處理 throw new BeanCreationException("Could not autowire field: " + field, ex);
            throw new RuntimeException("Could not autowire field: " + field);
        }
    }
}

針對於XML的解析,新建了一個ScannedGenericBeanDefinition來處理掃描包下的所有Bean定義。

使用AutowiredAnnotationProcessor來將上述流程整合起來,同時涉及Bean生命週期的鉤子函式設計, 相關示例程式碼如下:

public interface BeanPostProcessor {
    Object beforeInitialization(Object bean, String beanName) throws BeansException;

    Object afterInitialization(Object bean, String beanName) throws BeansException;
}

public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {
    Object beforeInstantiation(Class<?> beanClass, String beanName) throws BeansException;

    boolean afterInstantiation(Object bean, String beanName) throws BeansException;

    void postProcessPropertyValues(Object bean, String beanName) throws BeansException;
}

public class AutowiredAnnotationProcessor implements InstantiationAwareBeanPostProcessor {
    // 實現Bean初始化,並且預留Bean的生命週期的鉤子函式
}

關於Bean的生命週期和Bean生命週期中各個鉤子函式,參考如下圖

image

image

實現細節參考程式碼見:vstep10-annotation-final

實現AOP

即要實現如下XML格式的解析

<context:component-scan
  base-package="org.litespring.service.v5,org.litespring.dao.v5">
 </context:component-scan>
 <bean id="tx" class="org.litespring.tx.TransactionManager" />
 <aop:config>
  <aop:aspect ref="tx">
   <aop:pointcut id="placeOrder"  expression="execution(* org.litespring.service.v5.*.placeOrder(..))" />
   <aop:before pointcut-ref="placeOrder" method="start" />
   <aop:after-returning pointcut-ref="placeOrder" method="commit" /> 
   <aop:after-throwing pointcut-ref="placeOrder" method = "rollback"/>  
  </aop:aspect>
 </aop:config>

首先,我們需要實現如下功能,即,給定一個表示式,然後判斷某個類的某個方法是否匹配這個表示式,這需要依賴AspectJ這個元件來實現,具體使用參考AspectJExpressionPointcutPointcutTest這兩個類。

其次,我們需要通過Bean的名稱("tx")和方法名("start")定位到這個Method,然後反射呼叫這個Method,具體可參考MethodLocatingFactoryTest

public class MethodLocatingFactoryTest {
    @Test
    public void testGetMethod() throws Exception{
        DefaultBeanFactory beanFactory = new DefaultBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        Resource resource = new ClassPathResource("bean-v5.xml");
        reader.loadBeanDefinitions(resource);
        MethodLocatingFactory methodLocatingFactory = new MethodLocatingFactory();
        methodLocatingFactory.setTargetBeanName("tx");
        methodLocatingFactory.setMethodName("start");
        methodLocatingFactory.setBeanFactory(beanFactory);
        
        // 獲取到目標方法
        Method m = methodLocatingFactory.getObject();
        
        Assert.assertEquals(TransactionManager.class, m.getDeclaringClass());
        Assert.assertEquals(m, TransactionManager.class.getMethod("start"));

    }
}

然後,我們需要使用AOP Alliance實現指定順序的鏈式呼叫,即根據配置的不同advice順序呼叫。

image

具體可檢視ReflectiveMethodInvocationReflectiveMethodInvocationTest這兩個類。

 @Test
    public void testMethodInvocation() throws Throwable{


        Method targetMethod = UserService.class.getMethod("placeOrder");

        List<MethodInterceptor> interceptors = new ArrayList<>();
        interceptors.add(beforeAdvice);
        interceptors.add(afterAdvice);


        ReflectiveMethodInvocation mi = new ReflectiveMethodInvocation(userService,targetMethod,new Object[0],interceptors);

        mi.proceed();


        List<String> msgs = MessageTracker.getMsgs();
        Assert.assertEquals(3, msgs.size());
        Assert.assertEquals("start tx", msgs.get(0));
        Assert.assertEquals("place order", msgs.get(1));
        Assert.assertEquals("commit tx", msgs.get(2));

    }

其中

  Assert.assertEquals(3, msgs.size());
        Assert.assertEquals("start tx", msgs.get(0));
        Assert.assertEquals("place order", msgs.get(1));
        Assert.assertEquals("commit tx", msgs.get(2));

就是驗證我們配置的advice是否按指定順序執行。

最後,我們需要實現動態代理,在一個方法前後增加一些邏輯,而不用改動原始程式碼。如果是普通類就使用CGLib實現,如果有介面的類可以使用JDK自帶的動態代理,具體可參考CGlibTestCglibAopProxyTest

實現細節參考程式碼見:vaop-v3

完整程式碼

lite-spring

參考資料

從零開始造Spring

相關文章