作者: Grey
原文地址:Spring的輕量級實現
本文是參考公眾號:碼農翻身 的從零開始造Spring 教程的學習筆記
原始碼
開發方法
使用TDD的開發方法,TDD的開發流程是:
-
寫一個測試用例
-
執行:失敗
-
寫Just enough的程式碼,讓測試通過
-
重構程式碼保持測試通過,
然後迴圈往復。
說明
-
僅實現核心功能
-
基於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
通過觀察發現,ClassPathXmlApplicationContext
和FileSystemApplicationContext
大部分程式碼都是相同的,只有在獲取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
以外的所有邏輯,ClassPathXmlApplicationContext
和FileSystemApplicationContext
都繼承這個抽象類,完成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
這個類來完成的,我們新建了CustomBooleanEditor
和CustomNumberEditor
兩個類,這兩個類都繼承於PropertyEditorSupport
,分別實現了String
型別轉換成Boolean
型別和String
型別轉換成Integer
型別的功能。其他的型別轉換也可以通過類似的方法來實現。然後抽象出了TypeConvert
這個介面,並把這些轉換器加入一個特定的Map
中,Map
的key
就是要轉換的目標的型別,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
來實現,見DefaultBeanFactory
的populateBean
方法
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
需要負責解析出ConstuctorArgument
,DefaultBeanFactory
通過指定建構函式來生成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
的原生方式解析不太方便,解析ClassMetaData
和Annotation
都需要定義一個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生命週期中各個鉤子函式,參考如下圖
實現細節參考程式碼見: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這個元件來實現,具體使用參考AspectJExpressionPointcut
和PointcutTest
這兩個類。
其次,我們需要通過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
順序呼叫。
具體可檢視ReflectiveMethodInvocation
和ReflectiveMethodInvocationTest
這兩個類。
@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自帶的動態代理,具體可參考CGlibTest
和CglibAopProxyTest
實現細節參考程式碼見:vaop-v3