第十七讲-Spring中的两种切面

第十七讲-Spring中的两种切面

Spring种有两种切面,一种是高级的@Aspect切面,还有一种是低级的Advisor切面,这里的低级指的是比较底层的切面。对于高级切面来讲,@Aspect切面最终都会转为低级切面才会被Spring框架所使用。

如下面的演示:

package com.cherry.chapter1.a17;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.context.support.GenericApplicationContext;


public class A17 {
    public static void main(String[] args) {
        GenericApplicationContext context = new GenericApplicationContext();
        // 注册几个切面类
        context.registerBean("aspect1", Aspect1.class);
        context.registerBean("config",Config.class);
        // 加入bean工厂后处理器解析@Bean
        context.registerBean(ConfigurationClassPostProcessor.class);
        context.refresh();

        for (String beanName : context.getBeanDefinitionNames()){
            System.out.println(beanName);
        }

    }

    static class Target1{
        public void foo(){
            System.out.println("target1 foo");
        }
    }

    static class Target2 {
        public void bar(){
            System.out.println("target2 bar");
        }
    }

    // 创建一个高级切面
    @Aspect
    static class Aspect1 {
        // 定义前置通知和后置通知
        @Before("execution(* foo())")
        public void before(){
            System.out.println("aspect1 前置通知...");
        }

        @After("execution(* foo())")
        public void after(){
            System.out.println("aspect1 前后置通知...");
        }
    }

    // 创建一个低等级切面类(使用配置类的方式)
    @Configuration
    static class Config {
        // 提供一个低等级的切面类

        @Bean   // 低级切面
        public Advisor advisor3(MethodInterceptor advise3){
            // 定义一个切点
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression("execution(* foo())");

            return new DefaultPointcutAdvisor(pointcut, advise3);
        }

        @Bean
        public MethodInterceptor advise3(){
            return new MethodInterceptor() {
                @Override
                public Object invoke(MethodInvocation invocation) throws Throwable {
                    System.out.println("advise3 前后置通知...");
                    Object result = invocation.proceed();
                    System.out.println("advise3 前后置通知...");
                    return result;
                }
            };
        }
    }
}
aspect1
config
org.springframework.context.annotation.ConfigurationClassPostProcessor
advisor3
advise3

我们发现此时的容器中有我们定义的切面类对象,配置类,低级的切面advisor3以及低级切面对应的通知advise3。那么在Spring种这些切面是怎么被找到的呢?即使找到了又是怎么区分高级切面和低级切面的呢?而且不是说高级切面不是最终会转换成低级切面的吗?最终是怎么转换的呢?除此之外,找到切面类后又是如何创建代理对象的呢?接下来我们来分析一下:

完成上面疑问的类,是一个BeanPostProcessor, 该类叫AnnotationAwareAspectJAutoProxyCreator,我们把这个后处理器加入到beanFactory中:

// 用于处理AOP的后处理器
context.registerBean(AnnotationAwareAspectJAutoProxyCreator.class);

这里呢,我们来大致看一下AnnotationAwareAspectJAutoProxyCreator源码

public class AnnotationAwareAspectJAutoProxyCreator extends AspectJAwareAdvisorAutoProxyCreator {
	
    // .....................................
    
    // 找到一些有资格的切面类,该方法找到的是低级切面,如果是找到了高级切面,则会将高级切面转为低级切面再加入到eligibleAdvisors集合中
    protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
        List<Advisor> candidateAdvisors = this.findCandidateAdvisors();
        List<Advisor> eligibleAdvisors = this.findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
        this.extendAdvisors(eligibleAdvisors);
        if (!eligibleAdvisors.isEmpty()) {
            eligibleAdvisors = this.sortAdvisors(eligibleAdvisors);
        }

        return eligibleAdvisors;
    }
    
    
    // 该方法是创建代理的方法
    protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
        if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
            return bean;
        } else if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
        } else if (!this.isInfrastructureClass(bean.getClass()) && !this.shouldSkip(bean.getClass(), beanName)) {
            Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, (TargetSource)null);
            if (specificInterceptors != DO_NOT_PROXY) {
                this.advisedBeans.put(cacheKey, Boolean.TRUE);
                Object proxy = this.createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
                this.proxyTypes.put(cacheKey, proxy.getClass());
                return proxy;
            } else {
                this.advisedBeans.put(cacheKey, Boolean.FALSE);
                return bean;
            }
        } else {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }
    }
    
    // .....................
}

注意,由于上述的两种方法都是受保护的,使用反射调用太麻烦了,因此我们可以将我们的测试代码放到和AnnotationAwareAspectJAutoProxyCreator同一个包下,如下面的截图:

image-20240803131317556

// 第一个重要的方法findEligibleAdvisors就是所有[有资格]的advisors
AnnotationAwareAspectJAutoProxyCreator creator = context.getBean(AnnotationAwareAspectJAutoProxyCreator.class);
List<Advisor> advisors = creator.findCandidateAdvisors(Target1.class, "target1");// 根据目标来判断是否具备的advisors
advisors.forEach(advisor -> System.out.println(advisor));

image-20240803132027242

我们发现一共有4个切面,第一个是Spring给所有代理都加的切面,剩下三个都是我们自己写的,包括低级切面和转为低级切面的高级切面。

接下来我们看一下AnnotationAwareAspectJAutoProxyCreator处理器的第二个方法:wrapIfNecessary是否有必要为目标创建代理,只要findEligibleAdvisors返回的集合不为空,则表示需要创建代理

Object o1 = creator.wrapIfNecessary(new Target1(), "target1", "target1");
Object o2 = creator.wrapIfNecessary(new Target2(), "target2", "target2");
System.out.println(o1.getClass());
System.out.println(o2.getClass());

image-20240803135015806

我们发现,Spring为Target1创建了代理对象,并未Target2创建代理对象,因为bar()方法和切点并未匹配,因此该类并不需要创建代理了。

现在我们来看一下代理类的创建时机,之前我们提过,Bean的创建时机有:创建实例 -> (*)依赖注入 -> 初始化(*)。

代理类的创建时机有两个位置,一个是在创建实例和依赖注入之间,另一个位置是在初始化之后,这两个位置是二选一的。这里先说结论:出现循环依赖会提前创建代理对象,否则会在初始化后创建代理对象。下面呢,我们验证一下:

package org.springframework.aop.framework.autoproxy;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.context.support.GenericApplicationContext;

import javax.annotation.PostConstruct;

public class A17_1 {

    public static void main(String[] args) {
        GenericApplicationContext context = new GenericApplicationContext();
        context.registerBean(ConfigurationClassPostProcessor.class);
        context.registerBean(Config.class);
        context.refresh();
        context.close();
        // 创建 -> (*) 依赖注入 -> 初始化 (*)
        /*
            学到了什么
                a. 代理的创建时机
                    1. 初始化之后 (无循环依赖时)
                    2. 实例创建后, 依赖注入前 (有循环依赖时), 并暂存于二级缓存
                b. 依赖注入与初始化不应该被增强, 仍应被施加于原始对象
         */
    }

    @Configuration
    static class Config {
        @Bean // 解析 @Aspect、产生代理
        public AnnotationAwareAspectJAutoProxyCreator annotationAwareAspectJAutoProxyCreator() {
            return new AnnotationAwareAspectJAutoProxyCreator();
        }

        @Bean // 解析 @Autowired
        public AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor() {
            return new AutowiredAnnotationBeanPostProcessor();
        }

        @Bean // 解析 @PostConstruct
        public CommonAnnotationBeanPostProcessor commonAnnotationBeanPostProcessor() {
            return new CommonAnnotationBeanPostProcessor();
        }

        @Bean
        public Advisor advisor(MethodInterceptor advice) {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression("execution(* foo())");
            return new DefaultPointcutAdvisor(pointcut, advice);
        }

        @Bean
        public MethodInterceptor advice() {
            return (MethodInvocation invocation) -> {
                System.out.println("before...");
                return invocation.proceed();
            };
        }

        @Bean
        public Bean1 bean1() {
            return new Bean1();
        }

        @Bean
        public Bean2 bean2() {
            return new Bean2();
        }
    }

    static class Bean1 {
        public void foo() {

        }
        public Bean1() {
            System.out.println("Bean1()");
        }
        //@Autowired public void setBean2(Bean2 bean2) {
        //    System.out.println("Bean1 setBean2(bean2) class is: " + bean2.getClass());
        // }
        @PostConstruct public void init() {
            System.out.println("Bean1 init()");
        }
    }

    static class Bean2 {
        public Bean2() {
            System.out.println("Bean2()");
        }
        @Autowired public void setBean1(Bean1 bean1) {
            System.out.println("Bean2 setBean1(bean1) class is: " + bean1.getClass());
        }
        @PostConstruct public void init() {
            System.out.println("Bean2 init()");
        }
    }
}

image-20240803173652345

我们发现,首先打印了Bean1的构造,然后是Bean1的初始化,之后有一条关键的日志。这是代理创建器打印的日志:为Bean1创建了代理,因此是在Bean对象初始化之后创建了代理。

紧接着执行了Bean2的构造方法,并需要一个Bean1对象,那么是将Bean1的原始对象给它还是将Bean1的代理对象给它呢?肯定要给代理对象,因为Bean2调用使用Bean1对象肯定会调用Bean1的方法,当然是希望调用Bean1中增强的方法。因此我们给Bean2注入的是Bean1的代理对象,紧接着Bean2完成了初始化。

接着我们打开注释重新运行,现在Bean1和Bean2是双向相互引用的关系(循环依赖),我们再次运行看一下:

image-20240803181923889

我们发现,首先调用了Bean1的构造方法,此时要进行依赖注入阶段,而此时的依赖注入需要Bean2,但现在并没有Bean2对象,因此会暂停Bean1的后续步骤,进入Bean2的流程,

接下来调用了Bean2的构造方法,但是此时的Bean2d的依赖注入需要Bean1,而Bean1现在也没有初始化好。因为卡在了依赖注入阶段。因此,这个Bean1的代理就会在Bean2注入之前被创建出来,因此代理对象会提前创建出来,因此我们发现:存在循环引用的情况下,代理对象在构造方法之后,依赖注入之前会创建出来,其目的就是给需要的Bean使用,最后完成注入阶段和初始化阶段。

这里我们总结一下:

代理创建的时机有两种:

  • 一种是在初始化之后(无循环依赖时)
  • 另一种是构造方法执行之后,依赖注入之前(有循环依赖时),会将代理提前创建出来,并暂存于二级缓存中。

此外,在依赖注入和初始化阶段不应该被增强,应该施加于原始对象上

接下来我们讲一下切面的顺序控制

如果有多个切面的时候,它们的顺序是怎么样的呢?我们看一下下面的代码:

package org.springframework.aop.framework.autoproxy;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.annotation.Order;

import java.util.List;

public class A17 {
    public static void main(String[] args) {
        GenericApplicationContext context = new GenericApplicationContext();
        context.registerBean("aspect1", Aspect1.class);
        context.registerBean("config", Config.class);
        context.registerBean(ConfigurationClassPostProcessor.class);
        context.registerBean(AnnotationAwareAspectJAutoProxyCreator.class);
        // BeanPostProcessor
        // 创建 -> (*) 依赖注入 -> 初始化 (*)

        context.refresh();
//        for (String name : context.getBeanDefinitionNames()) {
//            System.out.println(name);
//        }

        /*
            第一个重要方法 findEligibleAdvisors 找到有【资格】的 Advisors
                a. 有【资格】的 Advisor 一部分是低级的, 可以由自己编写, 如下例中的 advisor3
                b. 有【资格】的 Advisor 另一部分是高级的, 由本章的主角解析 @Aspect 后获得
         */
        AnnotationAwareAspectJAutoProxyCreator creator = context.getBean(AnnotationAwareAspectJAutoProxyCreator.class);
        List<Advisor> advisors = creator.findEligibleAdvisors(Target2.class, "target2");
        /*for (Advisor advisor : advisors) {
            System.out.println(advisor);
        }*/

        /*
            第二个重要方法 wrapIfNecessary
                a. 它内部调用 findEligibleAdvisors, 只要返回集合不空, 则表示需要创建代理
         */
        Object o1 = creator.wrapIfNecessary(new Target1(), "target1", "target1");
        System.out.println(o1.getClass());
        Object o2 = creator.wrapIfNecessary(new Target2(), "target2", "target2");
        System.out.println(o2.getClass());

        ((Target1) o1).foo();
        /*
            学到了什么
                a. 自动代理后处理器 AnnotationAwareAspectJAutoProxyCreator 会帮我们创建代理
                b. 通常代理创建的活在原始对象初始化后执行, 但碰到循环依赖会提前至依赖注入之前执行
                c. 高级的 @Aspect 切面会转换为低级的 Advisor 切面, 理解原理, 大道至简
         */
    }

    static class Target1 {
        public void foo() {
            System.out.println("target1 foo");
        }
    }

    static class Target2 {
        public void bar() {
            System.out.println("target2 bar");
        }
    }

    @Aspect // 高级切面类
   
    static class Aspect1 {
        @Before("execution(* foo())")
        public void before1() {
            System.out.println("aspect1 before1...");
        }

        @Before("execution(* foo())")
        public void before2() {
            System.out.println("aspect1 before2...");
        }
    }

    @Configuration
    static class Config {
        @Bean // 低级切面
        public Advisor advisor3(MethodInterceptor advice3) {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression("execution(* foo())");
            DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice3);
            return advisor;
        }
        @Bean
        public MethodInterceptor advice3() {
            return invocation -> {
                System.out.println("advice3 before...");
                Object result = invocation.proceed();
                System.out.println("advice3 after...");
                return result;
            };
        }
    }
}

运行如下:

image-20240803190201282

我们发现,低级切面的advice3是在外层,说明它优先被执行。我们可以看到:低级切面优先被执行,其次是高级切面,这个切面的执行顺序我们能不能控制呢?我们可以加@Order注解来排序(数字小的优先级高一些)。现在我们在Aspect1上用Order注解标识,如下面所示:

package org.springframework.aop.framework.autoproxy;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.annotation.Order;

import java.util.List;

public class A17 {
    public static void main(String[] args) {
        GenericApplicationContext context = new GenericApplicationContext();
        context.registerBean("aspect1", Aspect1.class);
        context.registerBean("config", Config.class);
        context.registerBean(ConfigurationClassPostProcessor.class);
        context.registerBean(AnnotationAwareAspectJAutoProxyCreator.class);
        // BeanPostProcessor
        // 创建 -> (*) 依赖注入 -> 初始化 (*)

        context.refresh();
//        for (String name : context.getBeanDefinitionNames()) {
//            System.out.println(name);
//        }

        /*
            第一个重要方法 findEligibleAdvisors 找到有【资格】的 Advisors
                a. 有【资格】的 Advisor 一部分是低级的, 可以由自己编写, 如下例中的 advisor3
                b. 有【资格】的 Advisor 另一部分是高级的, 由本章的主角解析 @Aspect 后获得
         */
        AnnotationAwareAspectJAutoProxyCreator creator = context.getBean(AnnotationAwareAspectJAutoProxyCreator.class);
        List<Advisor> advisors = creator.findEligibleAdvisors(Target2.class, "target2");
        /*for (Advisor advisor : advisors) {
            System.out.println(advisor);
        }*/

        /*
            第二个重要方法 wrapIfNecessary
                a. 它内部调用 findEligibleAdvisors, 只要返回集合不空, 则表示需要创建代理
         */
        Object o1 = creator.wrapIfNecessary(new Target1(), "target1", "target1");
        System.out.println(o1.getClass());
        Object o2 = creator.wrapIfNecessary(new Target2(), "target2", "target2");
        System.out.println(o2.getClass());

        ((Target1) o1).foo();
        /*
            学到了什么
                a. 自动代理后处理器 AnnotationAwareAspectJAutoProxyCreator 会帮我们创建代理
                b. 通常代理创建的活在原始对象初始化后执行, 但碰到循环依赖会提前至依赖注入之前执行
                c. 高级的 @Aspect 切面会转换为低级的 Advisor 切面, 理解原理, 大道至简
         */
    }

    static class Target1 {
        public void foo() {
            System.out.println("target1 foo");
        }
    }

    static class Target2 {
        public void bar() {
            System.out.println("target2 bar");
        }
    }

    @Aspect // 高级切面类
    @Order(1)
    static class Aspect1 {
        @Before("execution(* foo())")
        public void before1() {
            System.out.println("aspect1 before1...");
        }

        @Before("execution(* foo())")
        public void before2() {
            System.out.println("aspect1 before2...");
        }
    }

    @Configuration
    static class Config {
        @Bean // 低级切面
        public Advisor advisor3(MethodInterceptor advice3) {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression("execution(* foo())");
            DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice3);
            return advisor;
        }
        @Bean
        public MethodInterceptor advice3() {
            return invocation -> {
                System.out.println("advice3 before...");
                Object result = invocation.proceed();
                System.out.println("advice3 after...");
                return result;
            };
        }
    }
}

image-20240803190614739

此时定义的高级切面被优先调用了!

最后我们讲讲高级切面如何转为低级切面。

如果我们使用findEligibleAdvisors方法查找的是高级切面怎么办呢?这里会有一个转换,将Aspect切面转为Advisor切面,这个过程就涉及到@Aspect注解的解析,这里我们大致研究一下注解的解析过程,这里以@Before为例,其它注解类似:

package org.springframework.aop.framework.autoproxy;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Before;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectInstanceFactory;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.aspectj.AspectJMethodBeforeAdvice;
import org.springframework.aop.aspectj.SingletonAspectInstanceFactory;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
import org.springframework.aop.support.DefaultPointcutAdvisor;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class A17_2 {

    static class Aspect {
        @Before("execution(* foo())")
        public void before1() {
            System.out.println("before1");
        }

        @Before("execution(* foo())")
        public void before2() {
            System.out.println("before2");
        }

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

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

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

        public Object around(ProceedingJoinPoint pjp) throws Throwable {
            try {
                System.out.println("around...before");
                return pjp.proceed();
            } finally {
                System.out.println("around...after");
            }
        }
    }

    static class Target {
        public void foo() {
            System.out.println("target foo");
        }
    }

    @SuppressWarnings("all")
    public static void main(String[] args) throws Throwable {

        AspectInstanceFactory factory = new SingletonAspectInstanceFactory(new Aspect());
        // 准备一个存放切面的集合
        List<Advisor> list = new ArrayList<>();
        for (Method method : Aspect.class.getDeclaredMethods()) {
            // 判断方法上有没有@Before注解
            if (method.isAnnotationPresent(Before.class)) {
                // 拿到注解,并获取到切点表达式(@Before注解中的值)
                String expression = method.getAnnotation(Before.class).value();
                // 构造一个切点对象
                AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
                // 将从注解中解析到的切点表达式值设置到切点表达式对象中
                pointcut.setExpression(expression);
                // 准备一个通知方法对象
                AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice(method, pointcut, factory);
                // 准备一个切面类对象,并将切点对象和通知方法对象传递进去,此时就完成了高级切面到低级切面的转换
                Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
                // 将该切面对象加入到集合中
                list.add(advisor);
            }
        }
        for (Advisor advisor : list) {
            System.out.println(advisor);
        }
        /*
            @Before 前置通知会被转换为下面原始的 AspectJMethodBeforeAdvice 形式, 该对象包含了如下信息
                a. 通知代码从哪儿来
                b. 切点是什么(这里为啥要切点, 后面解释)
                c. 通知对象如何创建, 本例共用同一个 Aspect 对象
            类似的通知还有
                1. AspectJAroundAdvice (环绕通知)
                2. AspectJAfterReturningAdvice
                3. AspectJAfterThrowingAdvice
                4. AspectJAfterAdvice (环绕通知)
         */

    }
}
posted @   LilyFlower  阅读(13)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示