spring源代码分析:aop的实现
简介
在之前的文章里我们讨论过一些程序构建ProxyFactory的方式来实现AOP。然而,上述的方式有一些不足,就是我们实际实现的逻辑需要显式的和添加的切面进行耦合。我们在真正的应用里不太可能这么去用。更多的会是使用xml配置文件或者annotation的方式来实现。像这篇文章就具体介绍过基于annotation的应用。那么,像采用这两种方式的AOP实现在spring里是怎么实现对它们的支持的呢?这就是我们要讨论的重点。
XML配置示例
在这里,我们先看一个基于xml配置的aop示例。程序的依赖配置如下:
<?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.yunzero</groupId> <artifactId>aopxmlsample</artifactId> <version>1.0-SNAPSHOT</version> <name>aopxmlsample</name> <!-- FIXME change it to the project's website --> <url>http://www.example.com</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.version>5.0.8.RELEASE</spring.version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.1</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.1</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.8</version> </dependency> </dependencies> <build> <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> <plugins> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>3.0.0</version> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.20.1</version> </plugin> <plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> </plugin> <plugin> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> </plugins> </pluginManagement> </build> </project>
这里主要依赖的库有spring-core, spirng-context, aspectjrt, aspectjweaver。在程序里,我们首先定义自己的业务逻辑部分:
package com.yunzero.customer.bo; public interface CustomerBo { void addCustomer(); String addCustomerReturnValue(); void addCustomerThrowException() throws Exception; void addCustomerAround(String name); }
以及这个接口的实现:
package com.yunzero.customer.bo.impl; import com.yunzero.customer.bo.CustomerBo; public class CustomerBoImpl implements CustomerBo { public void addCustomer(){ System.out.println("addCustomer() is running "); } public String addCustomerReturnValue(){ System.out.println("addCustomerReturnValue() is running "); return "abc"; } public void addCustomerThrowException() throws Exception { System.out.println("addCustomerThrowException() is running "); throw new Exception("Generic Error"); } public void addCustomerAround(String name){ System.out.println("addCustomerAround() is running, args : " + name); } }
这里的实现相当简单,就是针对性的输出一些相关的内容。然后我们就需要针对相关的部分定义且面的逻辑。对应的实现如下:
package com.yunzero.aspect; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import java.util.Arrays; public class LoggingAspect { public void logBefore(JoinPoint joinPoint) { System.out.println("logBefore() is running!"); System.out.println("hijacked : " + joinPoint.getSignature().getName()); System.out.println("******"); } public void logAfter(JoinPoint joinPoint) { System.out.println("logAfter() is running!"); System.out.println("hijacked : " + joinPoint.getSignature().getName()); System.out.println("******"); } public void logAfterReturning(JoinPoint joinPoint, Object result) { System.out.println("logAfterReturning() is running!"); System.out.println("hijacked : " + joinPoint.getSignature().getName()); System.out.println("Method returned value is : " + result); System.out.println("******"); } public void logAfterThrowing(JoinPoint joinPoint, Throwable error) { System.out.println("logAfterThrowing() is running!"); System.out.println("hijacked : " + joinPoint.getSignature().getName()); System.out.println("Exception : " + error); System.out.println("******"); } public void logAround(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("logAround() is running!"); System.out.println("hijacked method : " + joinPoint.getSignature().getName()); System.out.println("hijacked arguments : " + Arrays.toString(joinPoint.getArgs())); System.out.println("Around before is running!"); joinPoint.proceed(); System.out.println("Around after is running!"); System.out.println("******"); } }
这里看起来相当的简单,就是一个普通的类和若干方法。稍微有点特殊的就是不同的方法采用了JointPoint, Object等相关的参数。这里的细节比较有意思,像针对方法执行之前和之后的逻辑,它就一个JointPoint参数。而针对afterReturning, afterThrowing等方法因为要考虑针对方法的返回结果或者出错的情况进行处理,所以分别带了一个Object, Throwable类型的参数。
在定义好了切入逻辑之后,剩下的部分重点就是怎么将它们给拼接到一起了。这个就是通过一个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" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <aop:aspectj-autoproxy /> <bean id="customerBo" class="com.yunzero.customer.bo.impl.CustomerBoImpl"/> <bean id="logAspect" class="com.yunzero.aspect.LoggingAspect"/> <aop:config> <aop:aspect id="aspectLoffinf" ref="logAspect"> <aop:pointcut id="pointcutAdd" expression="execution(* com.yunzero.customer.bo.CustomerBo.addCustomer(..))"/> <aop:before method="logBefore" pointcut-ref="pointcutAdd"/> <aop:after method="logAfter" pointcut-ref="pointcutAdd"/> <aop:pointcut id="pointCutAfterReturning" expression="execution(* com.yunzero.customer.bo.CustomerBo.addCustomerReturnValue(..))" /> <aop:after-returning method="logAfterReturning" returning="result" pointcut-ref="pointCutAfterReturning"/> <aop:pointcut id="pointCutAfterThrowing" expression="execution(* com.yunzero.customer.bo.CustomerBo.addCustomerThrowException(..))" /> <aop:after-throwing method="logAfterThrowing" throwing="error" pointcut-ref="pointCutAfterThrowing" /> <aop:pointcut id="pointCutAround" expression="execution(* com.yunzero.customer.bo.CustomerBo.addCustomerAround(..))" /> <aop:around method="logAround" pointcut-ref="pointCutAround" /> </aop:aspect> </aop:config> </beans>
这里有几个比较重要的点,一个就是我们需要配置<aop:aspectj-autoproxy>。还有一个就是在程序里面,针对要切入的方法点,我们定义成<aop:pointcut>, 而具体是在这个切入点的之前还是之后执行呢,则需要通过<aop:before>, <aop:after>等配置项里的method来定义。当然,它们一个这样的<aop:config>对应有多个具体的<aop:aspect>。只有在<aop:aspect>里,会引入具体的aspect切入逻辑对象。比如这里对应的logAspect。
启动运行程序的代码很简单:
package com.yunzero; import com.yunzero.customer.bo.CustomerBo; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class App { public static void main( String[] args ) { ApplicationContext appContext = new ClassPathXmlApplicationContext("Spring-Customer.xml"); CustomerBo customer = (CustomerBo) appContext.getBean("customerBo"); //customer.addCustomer(); //customer.addCustomerReturnValue(); customer.addCustomerAround("frankliu"); } }
运行的结果输出如下:
logAround() is running! hijacked method : addCustomerAround hijacked arguments : [frankliu] Around before is running! addCustomerAround() is running, args : frankliu Around after is running! ******
现在,结合这个示例和前面文章里基于annotation的示例,我们可以大胆的猜测,在spring框架的实现里一定有某个地方支持对xml配置文件的解析以及对annotation的解析。那么,我们可以先从这两个点入手。
xml配置
基于xml配置解析的代码入口在AopNamespaceHandler这里。它的继承关系如下图:
它继承的接口NamespaceHandler主要有3个方法,分别是init, parse和decorate。其中init方法一般用在初始化的时候注册各种parser的。而parse则是找到特定的BeanDefinitionParser,然后调用它的parse方法。而decorate则是通过给定的decorator映射map,然后找到对应的decorator对原有的BeanDefinitionHolder进行包装。其中除了init方法,其他两个方法的实现细节在NamespaceHandlerSupport里。它主要有parsers, decorators以及attributeDecorators3个Map结构。也分别提供了3个对应的register方法。说白了就是往这3个Map里添加对应的key值和对象。因为这一步相对比较简单,就不详细列里面的代码了。
对于类AopNamespaceHandler来说,这个类的实现主要是继承和实现了NamespaceHandlerSupport和接口NamespaceHandler。它的详细实现代码如下:
public class AopNamespaceHandler extends NamespaceHandlerSupport { @Override public void init() { // In 2.0 XSD as well as in 2.1 XSD. registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser()); registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser()); registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator()); // Only in 2.0 XSD: moved to context namespace as of 2.1 registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); } }
这部分代码看起来很简单,它里面注册的各种Parser其实就是对应着前面xml文件里的<aop:xxx>元素的解析器。那么,这里的ConfigBeanDefinitionParser就是用来解析<aop:config>以及它所包含子元素属性部分的。而AspectJAutoProxyBeanDefinitionParser则是用来解析<aop:aspectj-autoproxy>这个部分的。这里,我们重点分析一下这两个部分。因为这两个类都实现了BeanDefinitionParser接口,所以它们实现的重点就是这个接口里定义的parse方法。
ConfigBeanDefinitionParser
在前面的示例里,我们可以看到,在一个<aop:config> 里,它可以定义若干个子元素。一般来说,它可以包含有若干个<aop:aspect>,而一个aspect里面又可以包含有pointcut, advisor, before, after, around等元素。每个元素又有不同的属性,像method, expression等。所以,怎么对这些元素进行归类处理就是它要实现的重点了。
在这个类里面定义了一系列的常量字符,这些就是我们前面xml里面用到的各种属性名。对应的总的解析方法parse定义如下:
public BeanDefinition parse(Element element, ParserContext parserContext) { CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element)); parserContext.pushContainingComponent(compositeDef); configureAutoProxyCreator(parserContext, element); List<Element> childElts = DomUtils.getChildElements(element); for (Element elt: childElts) { String localName = parserContext.getDelegate().getLocalName(elt); if (POINTCUT.equals(localName)) { parsePointcut(elt, parserContext); } else if (ADVISOR.equals(localName)) { parseAdvisor(elt, parserContext); } else if (ASPECT.equals(localName)) { parseAspect(elt, parserContext); } } parserContext.popAndRegisterContainingComponent(); return null; }
上述方法里,前面4行主要是定义一个CompositeComponentDefinition,然后将这个部分给放到parserContext里头。这个配置好的parserContext参数在后面的几个parse方法里都有用到。configAutoProxyCreator会在后面介绍。这里先跳过。在接着的for循环里,我们首先根据当前的config节点找到它所有的子节点,对于属于Element的子节点都放在childElts里。然后遍历这个列表来针对它的子节点进行处理。对这些子节点的处理主要有3个部分,一个是解析pointcut的,一个是解析advisor的,还有一个是解析aspect定义的。它们分别对应里面的元素<aop:pointcut>, <aop:advisor>, <aop:aspect>。
我们先看parsePointcut:
private AbstractBeanDefinition parsePointcut(Element pointcutElement, ParserContext parserContext) { String id = pointcutElement.getAttribute(ID); String expression = pointcutElement.getAttribute(EXPRESSION); AbstractBeanDefinition pointcutDefinition = null; try { this.parseState.push(new PointcutEntry(id)); pointcutDefinition = createPointcutDefinition(expression); pointcutDefinition.setSource(parserContext.extractSource(pointcutElement)); String pointcutBeanName = id; if (StringUtils.hasText(pointcutBeanName)) { parserContext.getRegistry().registerBeanDefinition(pointcutBeanName, pointcutDefinition); } else { pointcutBeanName = parserContext.getReaderContext().registerWithGeneratedName(pointcutDefinition); } parserContext.registerComponent( new PointcutComponentDefinition(pointcutBeanName, pointcutDefinition, expression)); } finally { this.parseState.pop(); } return pointcutDefinition; }
由于它里面的定义主要包含有两个属性,一个是id, 一个是expression。所以这里的代码处理就是围绕这两个部分的解析来的。这部分的逻辑实现还算比较简单。它首先获取里面定义的id, expression两个属性对应的值。在接着的try块里,尝试创建一个pointcutDefinition对象。这里的重点在创建这个对象的createPointcutDefinition方法。而后面接着的部分不过是将创建好的这个对象注册到parserContext里头。
protected AbstractBeanDefinition createPointcutDefinition(String expression) { RootBeanDefinition beanDefinition = new RootBeanDefinition(AspectJExpressionPointcut.class); beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); beanDefinition.setSynthetic(true); beanDefinition.getPropertyValues().add(EXPRESSION, expression); return beanDefinition; }
在这个方法里,主要通过提供的expression以及相关的信息,构建一个RootBeanDefinition对象。
我们接着看看parseAdvisor方法的定义:
private void parseAdvisor(Element advisorElement, ParserContext parserContext) { AbstractBeanDefinition advisorDef = createAdvisorBeanDefinition(advisorElement, parserContext); String id = advisorElement.getAttribute(ID); try { this.parseState.push(new AdvisorEntry(id)); String advisorBeanName = id; if (StringUtils.hasText(advisorBeanName)) { parserContext.getRegistry().registerBeanDefinition(advisorBeanName, advisorDef); } else { advisorBeanName = parserContext.getReaderContext().registerWithGeneratedName(advisorDef); } Object pointcut = parsePointcutProperty(advisorElement, parserContext); if (pointcut instanceof BeanDefinition) { advisorDef.getPropertyValues().add(POINTCUT, pointcut); parserContext.registerComponent( new AdvisorComponentDefinition(advisorBeanName, advisorDef, (BeanDefinition) pointcut)); } else if (pointcut instanceof String) { advisorDef.getPropertyValues().add(POINTCUT, new RuntimeBeanReference((String) pointcut)); parserContext.registerComponent( new AdvisorComponentDefinition(advisorBeanName, advisorDef)); } } finally { this.parseState.pop(); } } private AbstractBeanDefinition createAdvisorBeanDefinition(Element advisorElement, ParserContext parserContext) { RootBeanDefinition advisorDefinition = new RootBeanDefinition(DefaultBeanFactoryPointcutAdvisor.class); advisorDefinition.setSource(parserContext.extractSource(advisorElement)); String adviceRef = advisorElement.getAttribute(ADVICE_REF); if (!StringUtils.hasText(adviceRef)) { parserContext.getReaderContext().error( "'advice-ref' attribute contains empty value.", advisorElement, this.parseState.snapshot()); } else { advisorDefinition.getPropertyValues().add( ADVICE_BEAN_NAME, new RuntimeBeanNameReference(adviceRef)); } if (advisorElement.hasAttribute(ORDER_PROPERTY)) { advisorDefinition.getPropertyValues().add( ORDER_PROPERTY, advisorElement.getAttribute(ORDER_PROPERTY)); } return advisorDefinition; }
上面的这部分代码里,首先createAdvisorBeanDefinition里尝试解析advisor这个元素里的子元素部分,像看看里面是否有advice-ref成员,如果有,则将这个属性设置到对应的advisorDefinition里。同时也检查是否有order这个属性,有的话,也设置对应的属性值。这部分很简单,就是设置了一下属性值,并没有任何对具体这个引用的属性对象进行进一步解析的工作。
在上面的parseAdvisor方法里,调用了createAdvisorBeanDefinition之后,还有一个重要的部分是parsePointcutProperty,这部分的详细实现如下:
private Object parsePointcutProperty(Element element, ParserContext parserContext) { if (element.hasAttribute(POINTCUT) && element.hasAttribute(POINTCUT_REF)) { parserContext.getReaderContext().error( "Cannot define both 'pointcut' and 'pointcut-ref' on <advisor> tag.", element, this.parseState.snapshot()); return null; } else if (element.hasAttribute(POINTCUT)) { // Create a pointcut for the anonymous pc and register it. String expression = element.getAttribute(POINTCUT); AbstractBeanDefinition pointcutDefinition = createPointcutDefinition(expression); pointcutDefinition.setSource(parserContext.extractSource(element)); return pointcutDefinition; } else if (element.hasAttribute(POINTCUT_REF)) { String pointcutRef = element.getAttribute(POINTCUT_REF); if (!StringUtils.hasText(pointcutRef)) { parserContext.getReaderContext().error( "'pointcut-ref' attribute contains empty value.", element, this.parseState.snapshot()); return null; } return pointcutRef; } else { parserContext.getReaderContext().error( "Must define one of 'pointcut' or 'pointcut-ref' on <advisor> tag.", element, this.parseState.snapshot()); return null; } }
这部分的代码看起来比较多,其实也比较简单,主要是针对里面的xml节点元素,如果这个元素里有属性"pointcut"的定义,则根据解析的expression创建pointcutDefinition。而对于其他几种异常的情况则进行对应的处理。
在前面整体的parse方法里,还有一个要处理的方法和元素就是aspect。对应的方法就是parseAspect:
private void parseAspect(Element aspectElement, ParserContext parserContext) { String aspectId = aspectElement.getAttribute(ID); String aspectName = aspectElement.getAttribute(REF); try { this.parseState.push(new AspectEntry(aspectId, aspectName)); List<BeanDefinition> beanDefinitions = new ArrayList<>(); List<BeanReference> beanReferences = new ArrayList<>(); List<Element> declareParents = DomUtils.getChildElementsByTagName(aspectElement, DECLARE_PARENTS); for (int i = METHOD_INDEX; i < declareParents.size(); i++) { Element declareParentsElement = declareParents.get(i); beanDefinitions.add(parseDeclareParents(declareParentsElement, parserContext)); } // We have to parse "advice" and all the advice kinds in one loop, to get the // ordering semantics right. NodeList nodeList = aspectElement.getChildNodes(); boolean adviceFoundAlready = false; for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); if (isAdviceNode(node, parserContext)) { if (!adviceFoundAlready) { adviceFoundAlready = true; if (!StringUtils.hasText(aspectName)) { parserContext.getReaderContext().error( "<aspect> tag needs aspect bean reference via 'ref' attribute when declaring advices.", aspectElement, this.parseState.snapshot()); return; } beanReferences.add(new RuntimeBeanReference(aspectName)); } AbstractBeanDefinition advisorDefinition = parseAdvice( aspectName, i, aspectElement, (Element) node, parserContext, beanDefinitions, beanReferences); beanDefinitions.add(advisorDefinition); } } AspectComponentDefinition aspectComponentDefinition = createAspectComponentDefinition( aspectElement, aspectId, beanDefinitions, beanReferences, parserContext); parserContext.pushContainingComponent(aspectComponentDefinition); List<Element> pointcuts = DomUtils.getChildElementsByTagName(aspectElement, POINTCUT); for (Element pointcutElement : pointcuts) { parsePointcut(pointcutElement, parserContext); } parserContext.popAndRegisterContainingComponent(); } finally { this.parseState.pop(); } }
在上面的这段代码里,前面两行只是简单的获取aspect id和aspect name。如果这个当前的aspect element定义了declare-parents的属性,那么第10行到第14行的代码里首先通过xml解析找到所有带这个属性的元素,然后再通过parseDeclareParents来解析声明的parents元素。
在后面接着的第20行到37行的代码里,主要是解析里面的advice节点。这里是根据node里的名字,然后再去比较它是否为before, after, after-returning, after-throwing, around等几个值。如果是的话,表示这个元素就是advice节点。在确定为advice节点之后会接着调用34行的parseAdvice来解析详细的advice元素。
parseAdvice的实现如下:
private AbstractBeanDefinition parseAdvice( String aspectName, int order, Element aspectElement, Element adviceElement, ParserContext parserContext, List<BeanDefinition> beanDefinitions, List<BeanReference> beanReferences) { try { this.parseState.push(new AdviceEntry(parserContext.getDelegate().getLocalName(adviceElement))); // create the method factory bean RootBeanDefinition methodDefinition = new RootBeanDefinition(MethodLocatingFactoryBean.class); methodDefinition.getPropertyValues().add("targetBeanName", aspectName); methodDefinition.getPropertyValues().add("methodName", adviceElement.getAttribute("method")); methodDefinition.setSynthetic(true); // create instance factory definition RootBeanDefinition aspectFactoryDef = new RootBeanDefinition(SimpleBeanFactoryAwareAspectInstanceFactory.class); aspectFactoryDef.getPropertyValues().add("aspectBeanName", aspectName); aspectFactoryDef.setSynthetic(true); // register the pointcut AbstractBeanDefinition adviceDef = createAdviceDefinition( adviceElement, parserContext, aspectName, order, methodDefinition, aspectFactoryDef, beanDefinitions, beanReferences); // configure the advisor RootBeanDefinition advisorDefinition = new RootBeanDefinition(AspectJPointcutAdvisor.class); advisorDefinition.setSource(parserContext.extractSource(adviceElement)); advisorDefinition.getConstructorArgumentValues().addGenericArgumentValue(adviceDef); if (aspectElement.hasAttribute(ORDER_PROPERTY)) { advisorDefinition.getPropertyValues().add( ORDER_PROPERTY, aspectElement.getAttribute(ORDER_PROPERTY)); } // register the final advisor parserContext.getReaderContext().registerWithGeneratedName(advisorDefinition); return advisorDefinition; } finally { this.parseState.pop(); } }
上面的这段代码主要分为三个部分,第一部分是8到12行的创建MethodLocatingFactoryBean类型的BeanDefinition。第二部分是15到18行的创建SimpleBeanFactoryAwareAspectInstanceFactory类型的BeanDefinition。接着第21的createAdviceDefinition方法创建AdviceBeanDefinition,并注册pointcut。第26行的代码创建一个AspectJPointcutAdvisor类型的BeanDefinition,然后配置对应的AdviceBeanDefinition和Poincut相关的信息。
AspectJAutoProxyBeanDefinitionParser
我们接着再看看对于<aop:aspectj-autoproxy/>部分的解析。它和前面的parser一样,也是实现同样的接口。详细的定义实现如下:
class AspectJAutoProxyBeanDefinitionParser implements BeanDefinitionParser { @Override @Nullable public BeanDefinition parse(Element element, ParserContext parserContext) { // 1-注册AnnotationAwareAspectJAutoProxyCreator AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); // 2-扩展BeanDefinition extendBeanDefinition(element, parserContext); return null; } private void extendBeanDefinition(Element element, ParserContext parserContext) { // 获取BeanName为internalAutoProxyCreator的BeanDefinition,其实就是之前注册的自动代理构建器 BeanDefinition beanDef = parserContext.getRegistry().getBeanDefinition(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME); if (element.hasChildNodes()) { // 如果当前元素有子节点,则给上面获取的Bean定义添加子节点中明确定义的类型值(填充BeanDefinition) addIncludePatterns(element, parserContext, beanDef); } } private void addIncludePatterns(Element element, ParserContext parserContext, BeanDefinition beanDef) { ManagedList<TypedStringValue> includePatterns = new ManagedList<>(); NodeList childNodes = element.getChildNodes();//获取子节点 for (int i = 0; i < childNodes.getLength(); i++) { // 遍历子节点,获取子节点中name属性值,封装到TypeStringValue中 // 在上下文中提取子节点includeElement的元数据保存到TypedStringValue的source属性中 // 最后封装好的TypeStringValue保存到includePatterns列表中 Node node = childNodes.item(i); if (node instanceof Element) { Element includeElement = (Element) node; TypedStringValue valueHolder = new TypedStringValue(includeElement.getAttribute("name")); valueHolder.setSource(parserContext.extractSource(includeElement)); includePatterns.add(valueHolder); } } if (!includePatterns.isEmpty()) { // 从解析上下文parserContext中提取指定节点element的元数据保存到includePatterns的source属性中, // 然后将includePatterns保存到BeanDefinition的propertyValues属性中 includePatterns.setSource(parserContext.extractSource(element)); beanDef.getPropertyValues().add("includePatterns", includePatterns); } } }
上面的代码里主要有两个部分,一个是registerAspectJAnnotationAutoProxyCreatorIfNecessary,它主要是用来注册AnnotationAwareAspectJAutoProxyCreator构建器。这个方法也是前面解析xml配置文件类ConfigBeanDefinitionParser里configureAutoProxyCreator所依赖的部分。这里我们会重点分析。另外一个部分是在构建器构造完成后,往里面填充一些必要的内容。
我们接着看registerAspectJAnnotationAutoProxyCreatorIfNecessary里的实现细节:
public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary( ParserContext parserContext, Element sourceElement) { // 1-注册或升级AnnotationAwareAspectJAutoProxyCreator // parserContext.getRegistry()获取到的是BeanDefinitionRegistry注册器,第二个参数是提取的指定元素的元数据 BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary( parserContext.getRegistry(), parserContext.extractSource(sourceElement)); // 2-校验并设置是否适用基于CGLIB的动态代理实现AOP,和是否要暴露代理类 useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); // 3-注册成组件 registerComponentIfNecessary(beanDefinition, parserContext); } private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, @Nullable Element sourceElement) { if (sourceElement != null) { boolean proxyTargetClass = Boolean.parseBoolean(sourceElement.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE));//获取XML中设置的proxy-target-class属性的值,解析为Boolean值 if (proxyTargetClass) { // 如果为true,则强制自动代理构建器使用基于类的动态代理CGLIB,需要将属性设置到自动代理构建器的BeanDefinition中 AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); } boolean exposeProxy = Boolean.parseBoolean(sourceElement.getAttribute(EXPOSE_PROXY_ATTRIBUTE));//获取XML中配置的expose-proxy属性的值,同样解析为Boolean值 if (exposeProxy) { // 如果为true,强制自动代理构建器暴露代理类,需要将属性设置到自动代理构建器的BeanDefinition中 AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); } } } private static void registerComponentIfNecessary(@Nullable BeanDefinition beanDefinition, ParserContext parserContext) { if (beanDefinition != null) { // 将自动代理构建器包装成为一个Bean组件定义。 // Bean组件定义是将一个BeanDefinition中包含的所有的属性的值(可能为一个BeanDefinition或者BeanReference)全部封装起来成为一个组件包,然后将其注册到解析上下文中 BeanComponentDefinition componentDefinition = new BeanComponentDefinition(beanDefinition, AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME); parserContext.registerComponent(componentDefinition); } }
上面的代码里重点实现了这三个部分的内容:1-注册构建器 2-配置属性 3-组件注册。其中注册构建器是通过AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary方法来实现的。我们继续跟进看看:
public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) { if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { //如果构建器已经加载,获取其BeanDefinition,添加属性proxyTargetClass,值为true BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE); } } public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) { if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { //如果构建器已经加载,获取其BeanDefinition,添加属性exposeProxy,值为true BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); definition.getPropertyValues().add("exposeProxy", Boolean.TRUE); } } @Nullable private static BeanDefinition registerOrEscalateApcAsRequired(Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) { Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { // 1-如果internalAutoProxyCreator已经被注册那么比较新旧自动代理构建器类在列表中的优先级,如果已注册的构建器优先级低,则替换为给定的新构建器 BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); if (!cls.getName().equals(apcDefinition.getBeanClassName())) { int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); int requiredPriority = findPriorityForClass(cls); if (currentPriority < requiredPriority) { apcDefinition.setBeanClassName(cls.getName()); } } return null; } // 尚未注册internalAutoProxyCreator的情况下,将给定的构建器包装成RootBeanDefinition,然后注册这个BeanDefinition RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); // 把元数据保存到BeanDefinition中 beanDefinition.setSource(source); // 设置为最高优先值 beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); // 设置为基础角色 beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); // 2-以internalAutoProxyCreator为beanName注册当前BeanDefinition(AnnotationAwareAspectJAutoProxyCreator类) registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition); return beanDefinition; }
上述代码中的1-中是在已存在一个自动代理构建器的情况下,将其与新的给定的AnnotationAwareAspectJAutoProxyCreator构建器的优先级进行比对,取优先极高的。最后的registerBeanDefinition方法用于注册BeanDefinition。
annotation
我们知道,在目前很多流行的项目中,使用annotation来进行开发和配置也是很常见的。那么,这里一个重要的配置项就是@EnableAspectJAutoProxy:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(AspectJAutoProxyRegistrar.class) public @interface EnableAspectJAutoProxy { /** * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed * to standard Java interface-based proxies. The default is {@code false}. */ boolean proxyTargetClass() default false; /** * Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal} * for retrieval via the {@link org.springframework.aop.framework.AopContext} class. * Off by default, i.e. no guarantees that {@code AopContext} access will work. * @since 4.3.1 */ boolean exposeProxy() default false; }
从定义里可以看到,它主要有两个属性,一个是proxyTargetClass,一个是exposeProxy。它们和对应的xml里的配置属性其实是一样的。
上述代码里还有一个比较重要的部分,就是应用了@Import(AspectJAutoProxyRegistar.class)。这里的@import和xml配置文件里的import是对应的,表示要引入相关的属性依赖配置内容。这里引入的这个AspectJAutoProxyRegistar值得关注。它的实现如下:
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { /** * Register, escalate, and configure the AspectJ auto proxy creator based on the value * of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing * {@code @Configuration} class. */ @Override public void registerBeanDefinitions( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry); AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class); if (enableAspectJAutoProxy != null) { if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) { AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); } if (enableAspectJAutoProxy.getBoolean("exposeProxy")) { AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); } } } }
上面的代码主要是注册自动代理构建器,然后获取元数据里EnableAspectJAutoProxy里定义的数据信息。根据解析获取的结果来判断,如果有proxyTargetClass属性值,且值为true的话,则强制使用CGLib来创建代理类。而exposeProxy表示是否暴露生成的代理类。上述代码中的方法AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary正好和前面基于xml解析的部分重合了。可见,最后它们还是走到同一个执行路径上来了。
AnnotationAwareAspectJAutoProxyCreator
前面的两个部分主要是解析xml或者annotation的内容,然后注册对应的BeanDefinition。其中还有一个比较重要的就是我们具体创建AspectJAutoProxy的类,也就是AnnotationAwareAspectJAutoProxyCreator。这个类所在的层次结构比较深,如下图:
从图中可以看到,虽然里面的类和接口比较多,但是重点是这个类的继承层次里实现了接口BeanPostProcessor。我们都知道这个接口里定义了两个方法,一个是postProcessBeforeInitialization,一个是postProcessAfterInitialization。我们重点看看这个postProcessAfterInitialization,它的实现在类AbstractAutoProxyCreator里:
@Override public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (!this.earlyProxyReferences.contains(cacheKey)) { return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; }
上面的代码里,首先通过getCacheKey来获得给定的bean class和bean名字的key值。如果earlyProxyReferences集合里包含有这个key,则接着调用wrapIfNecessary。这个方法表示如果有必要的话,对返回的类添加代理包装。wrapIfNecessary的实现如下:
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { // 如果已经处理过,直接返回 if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { return bean; } // 如果不需要增强,直接返回 if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { return bean; } // 检测目标类是否是AOP的基础设施类,基础设施类包括Advice、Pointcut、Advisor、AopInfrastructureBean,或者是否需要跳过代理,如果是则将其设置为无需增强 if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; } // Create proxy if we have advice. // 获取对当前bean的增强 Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); if (specificInterceptors != DO_NOT_PROXY) { this.advisedBeans.put(cacheKey, Boolean.TRUE); // 如果获得增强则创建代理 Object proxy = createProxy( bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); this.proxyTypes.put(cacheKey, proxy.getClass()); return proxy; } this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; }
上述的代码里会检查对应的bean是否需要增强,如果有必要的话,会尝试去获取对应的增强。有了这个增强的advice或者advisor之后,再通过createProxy方法来创建结合增强之后的bean对象。所以这里重点的两个方法就是getAdvicesAndAdvisorsForBean和createProxy。
@Override @Nullable protected Object[] getAdvicesAndAdvisorsForBean( Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) { List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName); if (advisors.isEmpty()) { return DO_NOT_PROXY; } return advisors.toArray(); } protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) { //获得所有的增强器列表 List<Advisor> candidateAdvisors = findCandidateAdvisors(); // 获取当前目标对象的增强器列表 List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); // 在通知链的首部添加ExposeInvocationInterceptor extendAdvisors(eligibleAdvisors); if (!eligibleAdvisors.isEmpty()) { eligibleAdvisors = sortAdvisors(eligibleAdvisors); } return eligibleAdvisors; } protected List<Advisor> findCandidateAdvisors() { Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available"); return this.advisorRetrievalHelper.findAdvisorBeans(); } protected List<Advisor> findAdvisorsThatCanApply( List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) { ProxyCreationContext.setCurrentProxiedBeanName(beanName); try { return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass); } finally { ProxyCreationContext.setCurrentProxiedBeanName(null); } }
上述代码的核心在于findEligibleAdvisors。它的主要任务是找出所有的增强列表并获得当前目标对象的增强列表。findCandidateAdvisors的实现依赖于advisorRetrievalHelper.findAdvisorBeans,而findAdvisorsThatCanApply依赖于AopUtils.findAdvisorsThatCanApply。我们继续跟进。先看advisorRetrievalHelper.findAdvisorBeans:
public List<Advisor> findAdvisorBeans() { // Determine list of advisor bean names, if not cached already. String[] advisorNames = null; synchronized (this) { advisorNames = this.cachedAdvisorBeanNames; if (advisorNames == null) { // Do not initialize FactoryBeans here: We need to leave all regular beans // uninitialized to let the auto-proxy creator apply to them! // 获取当前beanFactory及其继承体系中所有为Advisor类型的beanname,排除factorybean advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( this.beanFactory, Advisor.class, true, false); this.cachedAdvisorBeanNames = advisorNames; } } if (advisorNames.length == 0) { return new LinkedList<>(); } List<Advisor> advisors = new LinkedList<>(); for (String name : advisorNames) { if (isEligibleBean(name)) { // 排除创建中的bean if (this.beanFactory.isCurrentlyInCreation(name)) { if (logger.isDebugEnabled()) { logger.debug("Skipping currently created advisor '" + name + "'"); } } else { try { //把剩下的bean添加到通知列表中 advisors.add(this.beanFactory.getBean(name, Advisor.class)); } catch (BeanCreationException ex) { Throwable rootCause = ex.getMostSpecificCause(); if (rootCause instanceof BeanCurrentlyInCreationException) { BeanCreationException bce = (BeanCreationException) rootCause; String bceBeanName = bce.getBeanName(); if (bceBeanName != null && this.beanFactory.isCurrentlyInCreation(bceBeanName)) { if (logger.isDebugEnabled()) { logger.debug("Skipping advisor '" + name + "' with dependency on currently created bean: " + ex.getMessage()); } // Ignore: indicates a reference back to the bean we're trying to advise. // We want to find advisors other than the currently created bean itself. continue; } } throw ex; } } } } return advisors; }
上面的代码主要就是做了前面描述的3件事。1:获取容器中所有的Advisor,并排除FactoryBean。2:排除创建中的bean。3:将剩下的advisor通过getBean的方法创建bean实例并加入到列表中返回。
我们再看看findAdvisorsThatCanApply方法:
public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) { if (candidateAdvisors.isEmpty()) { return candidateAdvisors; } List<Advisor> eligibleAdvisors = new LinkedList<>(); for (Advisor candidate : candidateAdvisors) { // 尝试添加IntroductionAdvisor if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { eligibleAdvisors.add(candidate); } } boolean hasIntroductions = !eligibleAdvisors.isEmpty(); for (Advisor candidate : candidateAdvisors) { if (candidate instanceof IntroductionAdvisor) { // already processed continue; } // 检查其他advisor是否也能应用到当前目标对象中 if (canApply(candidate, clazz, hasIntroductions)) { eligibleAdvisors.add(candidate); } } return eligibleAdvisors; }
上面的代码里对IntroductionAdvisor以及其他类型的Advisor进行了分别的处理。这里两个地方都用到了canApply方法。都是用来判断给定的Advisor能否应用到目标类。我们再跟进去看看canApply方法的实现:
public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) { if (advisor instanceof IntroductionAdvisor) { return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); } else if (advisor instanceof PointcutAdvisor) { PointcutAdvisor pca = (PointcutAdvisor) advisor; return canApply(pca.getPointcut(), targetClass, hasIntroductions); } else { // It doesn't have a pointcut so we assume it applies. return true; } } public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) { Assert.notNull(pc, "Pointcut must not be null"); // 比较classFilter if (!pc.getClassFilter().matches(targetClass)) { return false; } // 获取方法匹配器 MethodMatcher methodMatcher = pc.getMethodMatcher(); if (methodMatcher == MethodMatcher.TRUE) { // No need to iterate the methods if we're matching any method anyway... return true; } IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; if (methodMatcher instanceof IntroductionAwareMethodMatcher) { introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; } Set<Class<?>> classes = new LinkedHashSet<>(); if (!Proxy.isProxyClass(targetClass)) { classes.add(ClassUtils.getUserClass(targetClass)); } classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); for (Class<?> clazz : classes) { Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); for (Method method : methods) { if ((introductionAwareMethodMatcher != null && introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) || methodMatcher.matches(method, targetClass)) { return true; } } } return false; }
上面代码里,首先会判断给定的advisor类型,如果是introductionAdvisor的话,则获取其中的classFilter进行匹配比较。如果是PointcutAdvisor的话,则通过另外一个canApply方法进行校验。在这个额外的检查方法里,它主要检查classFilter是否匹配,然后比较methodMatcher对应的方法是否匹配。所以后面这一大堆的方法就是用来比较方法是否匹配的。
这部分解析结束之后,我们继续回到前面AbstractAutoProxyCreator里的createProxy方法。这一步是在前面的getAdvicesAndAdvisorsForBean方法之后,已经拿到了对应的interceptor了。它的详细实现如下:
protected Object createProxy(Class<?> beanClass, @Nullable String beanName, @Nullable Object[] specificInterceptors, TargetSource targetSource) { if (this.beanFactory instanceof ConfigurableListableBeanFactory) { // 暴露目标类,并将其保存在BeanDefinition中 AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass); } // 创建一个代理工厂,并为其拷贝当前类中的相关配置属性 ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.copyFrom(this); if (!proxyFactory.isProxyTargetClass()) { if (shouldProxyTargetClass(beanClass, beanName)) { //校验该Bean的BeanDefinition中的preserveTargetClass属性,是否被代理工厂设置为true,如果设置为true,则表示代理工厂希望代理类可以强转为目标类 proxyFactory.setProxyTargetClass(true); } else { // 基于接口创建代理 evaluateProxyInterfaces(beanClass, proxyFactory); } } // 将interceptor封装成advisor Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); proxyFactory.addAdvisors(advisors); // 加入增强器 proxyFactory.setTargetSource(targetSource); //设置要代理的类 customizeProxyFactory(proxyFactory); proxyFactory.setFrozen(this.freezeProxy); if (advisorsPreFiltered()) { proxyFactory.setPreFiltered(true); } return proxyFactory.getProxy(getProxyClassLoader()); }
我们接着看getProxy的方法实现。这部分的实现在ProxyCreatorSupport里:
public Object getProxy(@Nullable ClassLoader classLoader) { return createAopProxy().getProxy(classLoader); } protected final synchronized AopProxy createAopProxy() { if (!this.active) { activate(); } return getAopProxyFactory().createAopProxy(this); } private void activate() { this.active = true; for (AdvisedSupportListener listener : this.listeners) { listener.activated(this); } }
上述的代码无非是激活listener,然后调用getAopProxyFactory().createAopProxy方法。这个方法的实现细节在类DefaultAopProxyFactory里:
@Override public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class<?> targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation."); } if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); } else { return new JdkDynamicAopProxy(config); } }
看到这里,是不是有一种特别熟悉的感觉呢?没错,就是之前的这一篇讲ProxyFactory的文章里分析过了这部分的代码。
所以,总的来说,这部分的逻辑又和前面ProxyFactory的部分重合了。至此,我们可以大致的分析出基于AspectJ的AOP实现的一个流程。不管是基于xml还是annotation的实现,它们最终都是通过AnnotationAwareAspectJAutoProxyCreator实现。而其中的核心就是实现了BeanPostProcessor接口。在前面的很多实现分析里可以看见,这几乎成了spring里实现一些功能增强的套路了。
参考材料
https://www.mkyong.com/spring3/spring-aop-aspectj-in-xml-configuration-example/
https://www.cnblogs.com/V1haoge/p/9560803.html
http://shmilyaw-hotmail-com.iteye.com/blog/2418673
推荐阅读
-
Spring源码分析——调试环境搭建(可能是最省事的构建方法)
-
使用spring mvc+localResizeIMG实现HTML5端图片压缩上传的功能
-
spring5 源码深度解析----- AOP代理的生成
-
Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器
-
WPF/Silverlight实现图片局部放大的方法分析
-
shell脚本实现的网站日志分析统计(可以统计9种数据)
-
JS实现的汉字与Unicode码相互转化功能分析
-
asp实现的7xi音乐网的采集源代码
-
Spring Boot Maven 打包可执行Jar文件的实现方法
-
Spring Cloud使用Feign实现Form表单提交的示例