• Spring 创建增强类


    Spring 使用增强类定义横切逻辑,同时由于 Spring 只支持方法连接点,增强还包括在方法的哪一点加入横切代码的方位信息,所以增强既包含横切逻辑,又包含部分连接点的信息。

    1.增强类型

    AOP 联盟为增强定义了 org.aopalliance.aop.Advice 接口,Spring 支持5种类型的增强,先来了解一下增强接口继承关系图,如下图所示。

    带 <<spring>> 标识的接口是 Spring 所定义的扩展增强接口;带 <<aoppalliance>> 标识的接口则是 AOP 联盟定义的接口。按照增强在目标类方法中的连接点位置,可以分为以下5类。

    1)前置增强:org.springframework.aop.BeforeAdvice 代表前置增强。因为 Spring 支持方法级的增强,所以MethodBeforeAdvice 是目前可用的前置增强,表示在目标方法执行前实施增强,而 BeforeAdvice 是为了将来版本扩展需要而定义的。

    2)后置增强:org.springframework.aop.AfterReturningAdvice 代表后置增强,表示在目标方法执行后实施增强。

    3)环绕增强:org.aopalliance.intercept.MethodInterceptor 代表环绕增强,表示在目标方法执行前后实施增强。

    4)异常抛出增强:org.springframework.aopThrowsAdvice 代表抛出异常增强,表示在目标方法抛出异常后实施增强。

    5)引介增强:org.springframework.aop.lntroductionlnterceptor代表引介增强,表示在目标类中添加一些新的方法和属性。

    这些增强接口都有一些方法,通过实现这些接口方法,并在接口方法中定义横切逻辑,就可以将它们织入目标类方法的相应连接点位置。

    2.前置增强

    “热情待客、礼貌服务”已经成为服务行业的基本经营理念,下面通过前置增强对服务生的服务用语进行强制规范。假设服务生只做两件事:第一,欢迎顾客;第二,对顾客提供服务。

    1 )保证使用礼貌用语的实例

    来看一个保证使用礼貌用语的实例,如下面代码所示。

    public interface Waiter {
       void greetTo(String name);
       void serveTo(String name);
    }

    现在来看一个训练不足的服务生的服务情况,如下面代码所示。

    public class NaiveWaiter implements Waiter {
        public void greetTo(String name) {
            System.out.println("greet to "+name+"...");
        }
        
        public void serveTo(String name){
            System.out.println("serving "+name+"...");
        }
    }

    NaiveWaiter 只是简单地向顾客打招呼,闷不作声地走到顾客跟前,直接提供服务。下面对 NaiveWaiter 的服务行为进行规范,让他们在打招呼和提供服务之前,必须先对顾客使用礼貌用语,如下面代码所示。

    public class GreetingBeforeAdvice implements MethodBeforeAdvice {
        public void before(Method method, Object[] args, Object obj) throws Throwable {
            String clientName = (String)args[0];
            System.out.println("How are you!Mr."+clientName+".");
        }
    }

    BeforeAdvice 是前置增强的接口,方法前置增强的 MethodBeforeAdvice 接口是其子类。Spring 目前只提供方法调用的前置增强,在以后的版本中可能会看到 Spring 提供的其他类型的前置增强,这正是 BeforeAdvice 接口存在的意义。MethodBeforeAdvice 接口仅定义了唯一的方法:before(Method method,Object[] args,Object obj)throws Throwable。其中,method 为目标类的方法;args 为目标类方法的入参;而 obj 为目标类实例。当该方法发生异常时,将阻止目标类方法的执行。

    礼貌用语的前置增强制定好后,下面着手强制在服务生队伍中应用这个规定,来看具体的实施情况,如下面代码所示。

    public class BeforeAdviceTest {
        public void before() {
            Waiter target = new NaiveWaiter();
            BeforeAdvice  advice = new GreetingBeforeAdvice();
         //①Spring提供的代理类
            ProxyFactory pf = new ProxyFactory();
         //②设置代理类        
         pf.setTarget(target);
         //③为代理目标添加增强
            pf.addAdvice(advice);
        
    //④生成代理实例 Waiter proxy = (Waiter)pf.getProxy(); proxy.greetTo("John"); proxy.serveTo("Tom"); } }

    运行上面的代码,可以看到以下输出信息:

    How are you!Mr.John!
    greet to John...
    How are you!Mr.Tom!
    serving Tom...

    正如我们期望看到的一样,礼貌待客的优质服务理念得到了坚决、彻底的贯彻。

    2)解剖 ProxyFactory

    在 BeforeAdviceTest 中,使用 org.springframework.aop.framework.ProxyFactory 代理工厂将 GreetingBeforeAdvice 的增强织入目标类 NaiveWaiter 中。回想一下,前面介绍的 JDK 和 CGLib 动态代理技术是否有一些相似之处?不错,ProxyFactory 内部就是使用 JDK 或 CGLib 动态代理技术将增强应用到目标类中的。

    Spring 定义了 org.springframework.aop.framework.AopProxy 接口,并提供了两个 final 类型的实现类,如下图所示。

    其中,Cglib2AopProxy 使用 CGLib 动态代理技术创建代理,而 JdkDynamicAopProxy 使用 JDK 动态代理技术创建代理。如果通过 ProxyFactory 的 setlnterfaces(Class[] interfaces) 方法指定目标接口进行代理,则ProxyFactory 使用 JdkDynamicAopProxy:如果是针对类的代理,则使用 Cglib2AopProxy。此外,还可以通过 ProxyFactory 的 setOptimize(true) 方法让 ProxyFactory 启动优化代理方式,这样,针对接口的代理也会使用 Cglib2AopProxy。

    值得注意的一点是,在使用 CGLib 动态代理技术时,必须引入类库。

    现在回过头来对上面代码进行分析。BeforeAdviceTest 使用的是 CGLib 动态代理技术,当我们指定针对接口进行代理时,将使用 JDK 动态代理技术。

    ProxyFactory pf = new ProxyFactory();
    pf.setInterfaces(target.getClass().getInterfaces());//①指定对接口进行代理
    pf.setTarget(target);
    pf.addAdvice(advice);

    如果指定启用代理优化,则 ProxyFactory 还将使用 Cglib2AopProxy 代理。

    ProxyFactory pf = new ProxyFactory();
    pf.setInterfaces(target.getClass().getInterfaces());//①指定对接口进行代理
    pf.setOptimize(true);//②启用优化       
    pf.setTarget(target);
    pf.addAdvice(advice);

    读者可能己经注意到,ProxyFactory 通过 addAdvice(Advice) 方法添加一个增强,用户可以使用该方法添加多个增强。多个增强形成一个增强链,它们的调用顺序和添加顺序一致,可以通过 addAdvice(int,Advice)方法将增强添加到增强链的具体位置(第一个位置为0)。

    3)在 Spring 中配置

    使用 ProxyFactory 比直接使用 CGLib 或 JDK 动态代理技术创建代理省了很多事,如大家预想的一样,可以通过 Spring 的配置以“很Spring的方式”声明一个代理,如下面代码所示。

    <bean id="greetingAdvice" class="com.smart.advice.GreetingBeforeAdvice" />//①
    <bean id="target" class="com.smart.advice.NaiveWaiter" />//②
    <bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean"
      p:proxyInterfaces="com.smart.advice.Waiter" //③指定代理的接口,如果有多个接口,请使用<list>元素
      p:interceptorNames="greetingAdvice"//④指定使用的增强(①处)
      p:target-ref="target"//⑤指定对哪个Bean进行代理(②处)
    />

    ProxyFactoryBean 是 FactoryBean 接口的实现类,它负责实例化一个 Bean。ProxyFactoryBean 负责为其他 Bean 创建代理实例,它在内部使用 ProxyFactory 来完成这项工作。下面进一步了解一下 ProxyFactoryBean 的几个常用的可配置属性。

    (1)target:代理的目标对象。

    (2)proxylnterfaces:代理所要实现的接口,可以是多个接口。该属性还有一个别名属性 interfaces。

    (3)interceptorNames:需要织入目标对象的 Bean 列表,采用 Bean 的名称指定。这些 Bean 必须是实现了 org.aopalliance.intercept.MethodInterceptor 或 org.springframework.aop.Advisor 的 Bean,配置中的顺序对应调用的顺序。

    (4)singleton:返回的代理是否是单实例,默认为单实例。

    (5)optimize:当设置为 true 时,强制使用 CGLib 动态代理。对于 singleton 的代理,我们推荐使用 CGLib;对于其他作用域类型的代理,最好使用 JDK 动态代理。原因是虽然 CGLib 创建代理时速度慢,但其创建出的代理对象运行效率较高:而使用 JDK 创建代理的表现正好相反。

    (6)proxyTargetClass:是否对类进行代理(而不是对接口进行代理)。当设置为 true 时,使用 CGLib 动态代理。

    运行如下面所示的测试代码。

    String configPath = "com/smart/advice/beans.xm1";
    ApplicationContext ctx = new ClassPathXmlApp1icationContext(configPath);
    Waiter waiter=(Waiter)ctx.getBean("waiter");
    waiter.greetTo("John");

    输出以下信息:

    How are you!Mr.John!
    greet to John...

    这时,ProxyFactoryBean 使用了 JDK 动态代理技术。可以调整配置,使用 CGLib 动态代理技术通过动态创建 NaiveWaiter 的子类来代理 NaiveWalter对象,如下:

    <bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean"
      p:interceptorNames="greetingAdvice"
      p:target-ref="target"
      p:proxyTargetClass="true"/>

    将 proxyTargetClass 设置为 true 后,无须再设置 proxylnterfaces 属性,即使设置也会被 ProxyFactoryBean 忽略。

    3.后置增强

    后置增强在目标类方法调用后执行。假设服务生在每次服务后也需要使用规范的礼貌用语,则可以通过一个后置增强来实施这一要求,如下面所示。

    public class GreetingAfterAdvice implements AfterReturningAdvice {
      //①在目标类方法调用后执行
        public void afterReturning(Object returnObj, Method method, Object[] args,
                Object obj) throws Throwable {
            System.out.println("Please enjoy yourself!");
        }
    }

    通过实现AfterReturmngAdvice来定义后置增强的逻辑,
    AfterReturningAdvice 接口也仅定义了唯一的方法 afterReturning(Object returnObj,Method method,Object[] args,Object obj)throws Throwable。其中,returnObj 为目标实例方法返回的结果;method 为目标类的方法:args 为目标实例方法的入参;而 obj为 目标类实例。假设在后置增强中抛出异常,如果该异常是目标方法声明的异常,则该异常归并到目标方法中;如果不是目标方法所声明的异常,则 Spring 将其转为运行期异常抛出。

    下面将这个后置增强添加到上面的实例中,如下面代码所示。

    <bean id="greetingBefore" class="com.smart.advice.GreetingBeforeAdvice" />
    <bean id="greetingAfter" class="com.smart.advice.GreetingAfterAdvice" />
    <bean id="target" class="com.smart.advice.NaiveWaiter" />
    <bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean"
        p:proxyInterfaces="com.smart.advice.Waiter" 
        p:target-ref="target"
        p:interceptorNames="greetingBefore,greetingAfter"/>

    实战经验
    interceptorNames 是 String[] 类型的,它接收增强 Bean 的名称而非增强 Bean 的实例。这是因为 ProxyBeanFactory 内部在生成代理类时,需要使用增强 Bean 的类,而非增强 Bean 的实例,以织入增强类中所写的横切逻辑代码,因而可以说增强是类级别的。

    对于属性是字符数组类型且数组元素是 Bean 名称的配置,我们最好使用 <idref local="xxx">标签,这样在一般的 IDE 环境下编辑 Spring 配置文件时,IDE 会即时发现配置错误并给出报警,以便开发者及早消除配置错误,如下:

    <property name="interceptorNames">
      <list>
        <idref local="greetingBefore"/>
        <idref local="greetingAfter"/>
      </list>
    </property>

    当然,对于希望尽量简化配置文件的开发者来说,也可以采用逗号、分号或空格分隔的方式进行配置(字符串数组编辑器支持这种配置),如下:

    <property name="interceptorNames" value="greetingBefore,greetingAfter">

    4.环绕增强

    介绍完前置、后置增强,环绕增强的作用就显而易见了。环绕增强允许在目标类方法调用前后织入横切逻辑,它综合实现了前置、后置增强的功能。下面用环绕增强同时实现前礼貌用语和后礼貌用语。

    public class GreetingInterceptor implements MethodInterceptor {
    
        public Object invoke(MethodInvocation invocation) throws Throwable {//①截获目标类方法的执行,并在前后添加横切逻辑
            Object[] args = invocation.getArguments();//目标方法入参
            String clientName = (String)args[0];
            System.out.println("How are you!Mr."+clientName+".");//②在目标方法执行前调用
            
            Object obj = invocation.proceed();//③通过反射机制调用目标方法
            
            System.out.println("Please enjoy yourself!");//②在目标方法执行后调用
            
            return obj;
        }
    }

    Spring 直接使用 AOP 联盟所定义的 Methodlnterceptor 作为环绕增强的接口。该接口拥有唯一的接口方法 Object invoke(MethodInvocation invocation)throws Throwable。Methodlnvocation 不但封装了目标方法及其入参数组,还封装了目标方法所在的实例对象,通过 Methodlnvocation 的 getArguments() 方法可以获取目标方法的入参数组,通过 proceed() 方法反射调用目标实例相应的方法,如③处所示。通过在实现类中定义横切逻辑,可以很容易地实现方法前后的增强。

    下面使用环绕增强替换前置和后置增强,如下面代码所示。

    <bean id="greetingAround" class="com.smart.advice.GreetingInterceptor" />
    <bean id="target" class="com.smart.advice.NaiveWaiter" />
    <bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean"
      p:proxyInterfaces="com.smart.advice.Waiter" 
      p:target-ref
    ="target"   p:interceptorNames="greetingAround" />

    5.异常抛出增强

    异常抛出增强最适合的应用场景是事务管理,当参与事务的某个 DAO 发生异常时,事务管理器就必须回滚事务。在这里,仅仅给出一个模拟性的实例,用于说明异常抛出增强的使用方法,如下面代码所示。

    public class ForumService {
        public void removeForum(int forumId) {
            // do sth...
            throw new RuntimeException("运行异常。");
        }
        public void updateForum(Forum forum) throws Exception{
            // do sth...
            throw new SQLException("数据更新操作异常。");
            
        }
    }

    在模拟业务类 ForumService 中定义了两个业务方法,removeForum() 抛出运行期异常,而 updateForum() 抛出 SQLException。下面试图通过 TransactionManager 这个异常抛出增强对业务方法进行增强处理,统一捕捉抛出的异常并回滚事务,如下面代码所示。

    public class TransactionManager implements ThrowsAdvice {
      //①定义增强逻辑    
      public void afterThrowing(Method method, Object[] args, Object target,
                Exception ex) throws Throwable {
            System.out.println("-----------");
            System.out.println("method:" + method.getName());
            System.out.println("抛出异常:" + ex.getMessage());
            System.out.println("成功回滚事务。");
        }
    }

    ThrowsAdvice 异常抛出增强接口没有定义任何方法,它是一个标签接口,在运行期 Spring 使用反射机制自行判断,必须采用以下签名形式定义异常抛出的增强方法:

    void afterThrowing([Method method,Object[] args,Object target],Throwable);

    方法名必须为 afterThrowing,方法入参规定如下:前3个入参 Method method、Object[] args、Object target 是可选的(3个入参要么提供,要么不提供),而最后一个入参是 Throwable 或其子类。如以下方法都是合法的:

    (1)afterThrowing(SQLException e)。

    (2)afterThrowmg(RuntimeException e)。

    (3)afterThrowing(Method method,Object[] args,Object target, RuntimeException e)。

    可以在同一个异常抛出增强中定义多个 afterThrowing() 方法,当目标类方法抛出异常时,Spring 会自动选用最匹配的增强方法。假设在增强中定义了两个方法:afterThrowing(SQLException e) 和 afterThrowmg(Throwable e)。

    当目标方法抛出一个 SQLException 时,将调用 afterThrowing(SQLException e)而非 afterThrowing(Throwable e)进行增强。在类的继承树上,两个类的距离越近,就说这两个类的相似度越高。目标方法抛出异常后,优先选取拥有异常入参和抛出的异常相似度最高的 afterThrowing() 方法。

    提示:

    标签接口是没有任何方法和属性的接口,它不对实现类有任何语义上的要求,仅仅表明它的实现类属于一个特定的类型。它非常类似于 Web2.0 中 TAG 的概念,Java 使用它标识某一类对象。它主要有两个用途:第一,通过标签接口标识同一类型的类,这些类本身可能并不具有相同的方法,如 Advice 接口;第二,通过标签接口使程序或 JVM 采取一些特殊处理,如 java.io.Serializable,它告诉 JVM 对象可以被序列化

    在 Spring 中对这个异常抛出增强进行配置,如下:

    <bean id="forumServiceTarget" class="com.smart.advice.ForumService" />
    <bean id="transactionManager" class="com.smart.advice.TransactionManager" />
    <bean id="forumService" class="org.springframework.aop.framework.ProxyFactoryBean"
      p:interceptorNames="transactionManager"
      p:target-ref="forumServiceTarget"
      p:proxyTargetClass="true"/>//因ForumService是类,使用CGLib代理

    可见,ForumService 的两个方法所抛出的异常都被 TransactionManager 这个异常抛出增强捕获并成功处理。这样 ForumService 就从事务管理繁复的代码中解放出来,历史揭开了崭新的一页!

    6.引介增强

    引介增强是一种比较特殊的增强类型,它不是在目标方法周围织入增强,而是为目标类创建新的方法和属性,所以引介增强的连接点是类级别的,而非方法级别的。通过引介增强,可以为目标类添加一个接口的实现,即原来目标类未实现某个接口,通过引介增强可以为目标类创建实现某接口的代理。这种功能富有吸引力,因为它能够在横向上定义接口的实现方法,思考问题的角度发生了很大的变化。

    Spring 定义了引介增强接口 Introductionlnterceptor,该接口没有定义任何方法,Spring 为该接口提供了 Delegatinglntroductionlnterceptor 实现类。一般情况下,通过扩展该实现类定义自己的引介增强类。

    回到本章前面性能监视的例子,我们对所有的业务类都织入了性能监视的增强。由于性能监视会影响业务系统的性能,所以是否启用性能监视应该是可控的,即维护人员可以手工打开或关闭性能监视的功能。但原来的例子只简单地添加了运行性能监视逻辑,未提供任何控制的功能,现在可以用引介增强来实现这一诱人的功能。

    首先定义一个用于标识目标类是否支持性能监视的接口,如下面代码所示。

    public interface Monitorable {
       void setMonitorActive(boolean active);
    }

    该接口仅包括一个 setMonitorActive(boolean active) 方法,我们期望通过该接口方法控制业务类性能监视功能的激活和关闭状态。

    下面通过扩展 Delegatinglntroductionlnterceptor 为目标类引入性能监视的可控功能,如下面代码所示。

    public class ControllablePerformaceMonitor extends DelegatingIntroductionInterceptor 
        implements Monitorable, Testable { private ThreadLocal<Boolean> MonitorStatusMap = new ThreadLocal<Boolean>();// public void setMonitorActive(boolean active) {// MonitorStatusMap.set(active); }   //③拦截方法 public Object invoke(MethodInvocation mi) throws Throwable { Object obj = null;      //④对于支持性能监视可控代理,通过判断其状态决定是否开启性能监控功能      if (MonitorStatusMap.get() != null && MonitorStatusMap.get()) { PerformanceMonitor.begin(mi.getClass().getName() + "." + mi.getMethod().getName()); obj = super.invoke(mi); PerformanceMonitor.end(); } else { obj = super.invoke(mi); } return obj; } public void test() { System.out.println("dd"); } }

    ControllablePerformanceMonitor 在扩展 Delegatinglntroductionlnterceptor 的同时,还必须实现 Monitorable 接口,提供接口方法的实现。在①处定义了一个ThreadLocal类型的变量,用于保存性能监视开关状态。之所以使用 ThreadLocal 变量,是因为这个控制状态使代理类变成了非线程安全的实例,为了解决单实例线程安全的问题,通过 ThreadLocaI 让每个线程单独使用一个状态。

    在③处覆盖了父类中的 invoke() 方法,该方法用于拦截目标类方法的调用,根据监视开关的状态有条件地对目标实例方法进行性能监视。④处的粗体代码所示部分可能有点难以理解,它使用了Java5.0的自动拆包功能,MonitorStatusMap.get() 方法返回的 Boolean 被自动拆包为 boolean 类型的值。

    下面通过 Spring 的配置,将这个引介增强织入业务类 ForumService 中,具体配置如下面所示。

    <bean id="pmonitor" class="com.smart.introduce.ControllablePerformaceMonitor" />
    <bean id="forumServiceTarget" class="com.smart.introduce.ForumService" />
    <bean id="forumService" class="org.springframework.aop.framework.ProxyFactoryBean"
        p:interfaces="com.smart.introduce.Monitorable"//①引介增强所实现的接口
        p:target-ref="forumServiceTarget"
        p:interceptorNames="pmonitor" 
        p:proxyTargetClass="true" />//②由于引介增强一点要通过创建子类来生成代理,所以需要使用CGLib,否则会报错

    引介增强的配置与一般的配置有较大的区别:首先,需要指定引介增强所实现的接口,如①处所示,这里的引介增强实现了 Monitorable 接口;其次,由于只能通过为目标类创建子类的方式生成引介增强的代理,所以必须将 proxyTargetClass 设置为true。

    如果没有对 ControllablePerformanceMonitor 进行线程安全的特殊处理,就必须将 singleton 属性设置为 ture,让 ProxyFactoryBean 产生 prototype 作用域类型的代理。这就带来了一个严重的性能问题,因为动态创建代理的性能很低,而每次通过 getBean() 方法从容器中获取作用域类型为 prototype 的 Bean 时都将返回一个新的代理实例,所以这种性能的影响是巨大的,这也是为什么在代码中通过 ThreadLocal 对 ControllablePerformanceMonitor 的开关状态进行线程安全化处理的原因。通过线程安全化处理后,就可以使用默认的 singleton Bean作用域,这样创建代理的动作仅发生一次。

    测试代码如下:

    public class IntroduceTest {
        public void introduce(){
            String configPath = "com/smart/introduce/beans.xml";
            ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);
            ForumService forumService = (ForumService)ctx.getBean("forumService");
            
         forumService.removeForum(
    10);//①默认情况下,未开启性能监视功能 forumService.removeTopic(1022);
    Monitorable moniterable
    = (Monitorable)forumService;//②开启性能监视功能 moniterable.setMonitorActive(true);
    forumService.removeForum(
    10);//③在性能监视功能开启的情况下,再次调用业务方法 forumService.removeTopic(1022); } }

    注意②处的 (Monitorable)forumService 代码,强制性地将 forumService 转换为 Momtorable 类型。代码的成功执行表示从 Spring 容器中返回的代理确实引入了 Monitorable 接口方法的实现。

    执行以上代码,可以看到以下输出信息:

    模拟删除Forum记录:10
    模拟删除Topic记录:1022
    
    begin monitor...
    模拟删除Forum记录:10
    end monitor...
    org.springframework.aop.framework.Cglib2AopProxy$CgIibMethodInvocation.removeForum
    花费47毫秒。
    begin monitor...
    模拟删除Topic记录:1022
    end monitor...
    org.springframework.aop.framework.Cglib2AopProxy$CgIibMethodInvocation.removeTopic
    花费16毫秒。

    在①处,只有业务逻辑被执行,性能监视功能没有被执行;而在②处,性能监视功能正常启用,两个业务方法都启用了性能监视功能。

    提示:在 Spring4.0 中,基于 CGLib 的类代理不再要求目标类必须有无参构造函数。这是一个不错的特性,这样在使用 CGLib 类时,不再需要特别关注目标类是否有无参构造函数。取消这个限制后,增强的目标 Bean 就可以使用构造函数注入了。Spring 到底如何实现这个功能?这就要归根于 Spring 内联了 objenesis 类库,感兴趣的读者可到其官网(http://objenesis.org)查看

  • 相关阅读:
    图标插件——heightcharts
    垂直居中——登录界面
    javaEE(web开发)私人学习笔记
    javaSE(java基础库)私人学习笔记
    commons-fileupload(apache开源文件上传组件)使用方式
    fastjson与spring mvc整合的配置
    ehcache的xml配置
    dubbo用于传输数据的bean必须有空构造器的原因
    修复dubbo注解与spring aop冲突的问题
    dom4j(XML解析)私人学习笔记
  • 原文地址:https://www.cnblogs.com/jwen1994/p/11108509.html
Copyright © 2020-2023  润新知