SpringAOP

在人生的道路上,不管是潇洒走一回,或者是千山独行,皆须是自己想走的路,虽然,有的人并不是很快就能找到自己的方向和道路,不过,只要坚持到底,我相信,就一定可以找到自己的路,只要找到路,就不必怕路途遥远了。

导读:本篇文章讲解 SpringAOP,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

8.SpringAOP

8.1 AOP面向切面编程

  • AOP 的全称是“Aspect Oriented Programming”,译为“面向切面编程”,和 OOP(面向对象编程)类似,它也是一种编程思想原文地址

  • AOP 通过预编译方式和运行期间动态代理实现程序功能的统一维护技术。事函数式编程的衍生泛型。

在这里插入图片描述

  • 推论:
  • 通常情况下,我们会根据业务使用 OOP(面向对象)思想,将应用划分为多个不同的业务模块,每个模块的核心功能都只为特定的业务领域提供服务,例如电商系统中的订单模块、商品模块、库存模块就分别是为维护电商系统的订单信息、商品信息以及库存信息而服务的。

  • 但除此之外,应用中往往还存在一些非业务的通用功能,例如日志管理、权限管理、事务管理、异常管理等。这些通用功能虽然与应用的业务无关,但几乎所有的业务模块都会使用到它们,因此这些通用功能代码就只能横向散布式地嵌入到多个不同的业务模块之中。这无疑会产生大量重复性代码,不利于各个模块的复用。

  • 要是可以将这些重复性代码封装成为公共函数,然后在业务模块中显式的调用,不也能减少重复性代码吗?是的,这样做的确能一定程度上减少重复性代码,但这样也增加了业务代码与公共函数的耦合性,任何对于公共函数的修改都会对所有与之相关的业务代码造成影响。

  • 内涵
  • 与 OOP 中纵向的父子继承关系不同,AOP 是通过横向的抽取机制实现的。它将应用中的一些非业务的通用功能抽取出来单独维护,并通过声明的方式(例如配置文件、注解等)定义这些功能要以何种方式作用在那个应用中,而不是在业务模块的代码中直接调用。

  • 这虽然设计公共函数有几分类似,但传统的公共函数除了在代码直接硬调用之外并没有其他手段。AOP 则为这一问题提供了一套灵活多样的实现方法(例如 Proxy 代理、拦截器、字节码翻译技术等),可以在无须修改任何业务代码的基础上完成对这些通用功能的调用和修改。

  • AOP 编程和 OOP 编程的目标是一致的,都是为了减少程序中的重复性代码,让开发人员有更多的精力专注于业务逻辑的开发,只不过两者的实现方式大不相同。

  • OOP 它使用继承和组合方式,仔细地为所有涉及通用功能的模块编制成一套类和对象的体系,以达到减少重复性代码的目标。而 AOP 是一种规定,凡是某包某类下的某方法都一并进行处理。

  • AOP 不是用来替换 OOP 的,而是 OOP 的一种延伸,用来解决 OOP 编程中遇到的问题。

  • 所有的 AOP 框架都应该是对AOP 接口规范(AOP 编程思想所要解决各种问题的最高抽象)的具体实现,这样可以使各个AOP 框架及工具产品之间可以相互移植,因此通常我们也将 AOP 框架称作 AOP 实现。

  • AOP 实现(框架)主要有两个,分别为 Spring AOPAspectJ

AOP 框架 说明
Spring AOP 是一款基于 AOP 编程的框架,它能够有效的减少系统间的重复代码,达到松耦合的目的。 Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。Spring AOP 支持 2 种代理方式,分别是基于接口的 JDK 动态代理和基于继承的 CGLIB 动态代理。
AspectJ 是一个基于 Java 语言的 AOP 框架,从 Spring 2.0 开始,Spring AOP 引入了对 AspectJ 的支持。 AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的植入。
  • AOP 术语
名称 说明
横切关注点 非业务相关公共部分功能跨越应用程序国歌模块的方法和功能。如:日志、缓存、安全。事务…
Joinpoint(连接点) AOP 的核心概念,指的是程序执行期间明确定义的一个点,例如方法的调用、类初始化、对象实例化等。 在 Spring 中,连接点则指可以被动态代理拦截目标类的方法。
Pointcut(切入点) 又称切点,指要对哪些 Joinpoint 进行拦截,即被拦截的连接点。
Advice(通知) 指拦截到 Joinpoint 之后要执行的代码,即对切入点增强的内容。切面类中的方法
Target(目标) 指代理的目标对象,通常也被称为被通知(advised)对象。
Weaving(织入) 指把增强代码应用到目标对象上,生成代理对象的过程。
Proxy(代理) 指生成的代理对象。
Aspect(切面) 切面是切入点(Pointcut)和通知(Advice)的结合。横切关注点被模块化的特殊对象。是一个类

在这里插入图片描述

  • Advice 为通知,也有人将其翻译为“增强处理”,共有 5 种类型,如下表所示。
通知 说明
before(前置通知) 通知方法在目标方法调用之前执行
after(后置通知) 通知方法在目标方法返回或异常后调用
after-returning(返回后通知) 通知方法会在目标方法返回后调用
after-throwing(抛出异常通知) 通知方法会在目标方法抛出异常后调用
around(环绕通知) 通知方法会将目标方法封装起来
  • AOP 的类型,AOP 可以被分为以下 2 个不同的类型。

动态 AOP

  • 动态 AOP 的织入过程是在运行时动态执行的。其中最具代表性的动态 AOP 实现就是 Spring AOP,它会为所有被通知的对象创建代理对象,并通过代理对象对被原对象进行增强。

  • 相较于静态 AOP 而言,动态 AOP 的性能较差,但随着技术的不断发展,它的性能也在不断的稳步提升。

  • 动态 AOP 的优点是它可以轻松地对应用程序的所有切面进行修改,而无须对主程序代码进行重新编译。

静态 AOP

  • 静态 AOP 是通过修改应用程序的实际 Java 字节码,根据需要修改和扩展程序代码来实现织入过程的。最具代表性的静态 AOP 实现是 AspectJ。

  • 相较于动态 AOP 来说,性能较好。但它也有一个明显的缺点,那就是对切面的任何修改都需要重新编译整个应用程序。

  • AOP 的优势

  • AOP 是 Spring 的核心之一,在 Spring 中经常会使用 AOP 来简化编程。

在 Spring 框架中使用 AOP 主要有以下优势。

  • 提供声明式企业服务,特别是作为 EJB 声明式服务的替代品,最重要的是,这种服务是声明式事务管理。
  • 允许用户实现自定义切面。在某些不适合用 OOP 编程的场景中,采用 AOP 来补充。
  • 可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时也提高了开发效率。

8.2 Spring AOP编程

8.2.1 spring aop模块介绍
  • Spring AOP 是 Spring 框架的核心模块之一,纯 Java 实现,不需要专门的编译过程和类加载器,可以在程序运行期通过代理方式向目标类织入增强代码。

  • Spring 在运行期会为目标对象生成一个动态代理对象,并在代理对象中实现对目标对象的增强。

  • Spring AOP 的底层是通过以下 2 种动态代理机制,为目标对象(Target Bean)执行横向织入的。

代理技术 描述
JDK 动态代理 Spring AOP 默认的动态代理方式,若目标对象实现了若干接口,Spring 使用 JDK 的 java.lang.reflect.Proxy 类进行代理。
CGLIB 动态代理 若目标对象没有实现任何接口,Spring 则使用 CGLIB 库生成目标对象的子类,以实现对目标对象的代理。

问题:

  • 1.标记为 final 的方法是无法进行覆盖的,因此Spring AOP提供这两类方法不管是通过 JDK 动态代理机制还是 CGLIB 动态代理机制都是无法完成代理的。

  • 2.Spring AOP 仅支持执行公共(public)非静态方法的调用作为连接点。

  • 解决方式:若向受保护的(protected)或私有的(private)的方法进行增强,此时就需要使用功能更加全面的 AOP 框架来实现,其中使用最多的就是 AspectJ。这个后续在讲:

  • Spring AOP 是一个简化版的 AOP 实现,并没有提供完整版的 AOP 功能。Spring AOP 只支持一种连接点类型:方法调用,是为了易于访问也是相当有用的连接点。

如果需要使用其他类型的连接点(例如成员变量连接点),我们可以将 Spring AOP 与其他的 AOP 实现一起使用,最常见的组合就是 Spring AOP + ApectJ

8.2.2 Spring AOP 通知类型
  • Spring AOP 按照通知(Advice)织入到目标类方法的连接点位置,为 Advice 接口提供了 6 个子接口,org.aopalliance.aop.Interface.Advice 接口是其总接口。如下表。
通知类型 接口 描述
前置通知 org.springframework.aop.MethodBeforeAdvice 在目标方法执行前实施增强。
后置通知 org.springframework.aop.AfterAdvice 在目标方法执行后实施增强。
后置返回通知 org.springframework.aop.AfterReturningAdvice 在目标方法执行完成,并返回一个返回值后实施增强。
环绕通知 org.aopalliance.intercept.MethodInterceptor 在目标方法执行前后实施增强。
异常通知 org.springframework.aop.ThrowsAdvice 在方法抛出异常后实施增强。
引入通知 org.springframework.aop.IntroductionInterceptor 在目标类中添加一些新的方法和属性。
8.2.3 Spring AOP 切面类型
  • org.springframework.aop.Advisor 接口表示切面类,实现对通知(Adivce)和连接点(Joinpoint)的管理。

  • 在 Spring AOP 中,切面可以分为三类:一般切面、切点切面和引介切面。

切面类型 接口 描述
一般切面 org.springframework.aop.Advisor Spring AOP 默认的切面类型。 由于 Advisor 接口仅包含一个 Advice(通知)类型的属性,而没有定义 PointCut(切入点),因此它表示一个不带切点的简单切面。 这样的切面会对目标对象(Target)中的所有方法进行拦截并织入增强代码。由于这个切面太过宽泛,因此我们一般不会直接使用。
切点切面 org.springframework.aop.PointcutAdvisor Advisor 的子接口,用来表示带切点的切面,该接口在 Advisor 的基础上还维护了一个 PointCut(切点)类型的属性。 使用它,我们可以通过包名、类名、方法名等信息更加灵活的定义切面中的切入点,提供更具有适用性的切面。
引介切面 org.springframework.aop.IntroductionAdvisor Advisor 的子接口,用来代表引介切面,引介切面是对应引介增强的特殊的切面,它应用于类层面上,所以引介切面适用 ClassFilter 进行定义。
8.2.4 一般切面开发
  • 在使用 Spring AOP 开发时,若没有对切面进行具体定义,Spring AOP 会通过 Advisor 为我们定义一个一般切面(不带切点的切面),然后对目标对象(Target)中的所有方法连接点进行拦截,并织入增强代码。

  • 举例理解:

  • 编写dao接口及实现类

public interface UserDao {


    void add();

    void delete();

    void update();

    void select();
}
public class UserDaoImpl implements UserDao{
    @Override
    public void add() {
        System.out.println("一般切面-增加");
    }

    @Override
    public void delete() {
        System.out.println("一般切面-删除");
    }

    @Override
    public void update() {
        System.out.println("一般切面-修改");
    }

    @Override
    public void select() {
        System.out.println("一般切面-查询");
    }
}
  • 编写通知
* UserDao环绕通知切面类*/
public class UserDaoMethodAdvice implements MethodInterceptor {
	/*invocation:方法调用连接点*/
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("一般切面-环绕前-动作");
        //执行被代理对象中的逻辑
        Object result = invocation.proceed();
        System.out.println("一般切面-环绕后-动作");
        return result;
    }
}
  • 编写spring配置文件applicationContext.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">
    <!--******Advisor : 代表一般切面,Advice 本身就是一个切面,对目标类所有方法进行拦截(* 不带有切点的切面.针对所有方法进行拦截)*******-->
    <!-- 定义目标(target)对象实现了UserDao接口 -->
    <bean id="userDao" class="com.zk.dao.UserDaoImpl"/>
    <!-- 定义切面类 -->
    <bean id="methodAdvice" class="com.zk.advice.UserDaoMethodAdvice"/>
    <!--Spring 通过配置生成代理-->
    <bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!-- 设置目标对象 -->
        <property name="target" ref="userDao"/>
        <!-- 设置实现的接口 ,value 中写接口的全路径 -->
        <property name="proxyInterfaces" value="com.zk.dao.UserDao"/>
        <!-- 注意这块需要使用value:增强 Bean 的名称 -->
        <property name="interceptorNames" value="methodAdvice"/>
    </bean>

</beans>
  • Spring 能够基于 org.springframework.aop.framework.ProxyFactoryBean 类,根据**目标对象的类型(是否实现了接口)**自动选择使用 JDK 动态代理或 CGLIB 动态代理机制,为目标对象(Target Bean)生成对应的代理对象(Proxy Bean)。

  • ProxyFactoryBean 的常用属性如下表所示。

属性 描述
target 需要代理的目标对象(Bean)
proxyInterfaces 代理需要实现的接口,如果需要实现多个接口,可以通过 元素进行赋值,value 中写接口的全路径
proxyTargetClass 针对类的代理,该属性默认取值为 false(可省略), 表示使用 JDK 动态代理;取值为 true,表示使用 CGlib 动态代理
interceptorNames 拦截器的名字,该属性的取值value既可以是拦截器、也可以是 Advice(通知)类型的 Bean,还可以是切面(Advisor)的 Bean。
singleton 返回的代理对象是否为单例模式,默认值为 true。
optimize 是否对创建的代理进行优化(只适用于CGLIB)。
  • 调用测试
@Test
public void test01(){
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //获取目标对象bean没有切面编码,jdk动态代理接口方式
    //UserDao userDao = context.getBean("userDao", UserDao.class); //一般切面-增加
    UserDao userDao = context.getBean("userDaoProxy", UserDao.class);
    userDao.add();
    /*
        一般切面-环绕前-动作
        一般切面-增加
        一般切面-环绕后-动作
    * */
}
8.2.5 基于 PointcutAdvisor 的 AOP 开发
  • PointCutAdvisor 是 Adivsor 接口的子接口,用来表示带切点的切面。使用它,我们可以通过包名、类名、方法名等信息更加灵活的定义切面中的切入点,提供更具有适用性的切面。

Spring 提供了多个 PointCutAdvisor 的实现,其中常用实现类如如下。

  • NameMatchMethodPointcutAdvisor:指定 Advice 所要应用到的目标方法名称,例如 hello* 代表所有以 hello 开头的所有方法。
  • RegExpMethodPointcutAdvisor:使用正则表达式来定义切点(PointCut),RegExpMethodPointcutAdvisor 包含一个 pattern 属性,该属性使用正则表达式描述需要拦截的方法。
  • 举例理解:采用CGlib 动态代理生成代理实例
  • 编写普通类,未实现接口
public class StudentDaoImpl{
    public void add() {
        System.out.println("切点切面-增加");
    }

    public void delete() {
        System.out.println("切点切面-删除");
    }

    public void update() {
        System.out.println("切点切面-更新");
    }

    public void select() {
        System.out.println("切点切面-查询");
    }

    public void updateOther() {
        System.out.println("切点切面-其他更新");
    }

    public void selectother() {
        System.out.println("切点切面-其他查询");
    }
}
  • 本例通知沿用上面例子中的通知
  • 编写spring配置文件applicationContext.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">
    <!--定义目标(target)对象-->
    <bean id="studentDao" class="com.zk.dao.StudentDaoImpl"/>
    <!-- 定义通知 -->
	<bean id="methodAdvice" class="com.zk.advice.UserDaoMethodAdvice"/>
	
    <!-- 定义切面 包括通知和切点-->
    <bean id="studentPointCutAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <!--定义表达式,规定哪些方法进行拦截 .* 表示所有方法-->
        <!--<property name="pattern" value=".*"></property>-->
        <!--注意如果是pattern表示一个匹配方法,而patterns是一个方法匹配数组-->
        <property name="patterns" value="com.zk.dao.StudentDaoImpl.select.*,com.zk.dao.StudentDaoImpl.update.*">
        </property>
        <property name="advice" ref="methodAdvice"/>
    </bean>

	<!--Spring 通过配置生成代理-->
    <bean id="studentProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="studentDao"/>
        <!-- 针对类的代理,该属性默认取值为 false(可省略), 表示使用 JDK 动态代理;取值为 true,表示使用 CGlib 动态代理-->
        <property name="proxyTargetClass" value="true"/>
        <!-- 在真实目标上使用切面来生成代理对象,使用切面 -->
        <property name="interceptorNames" value="studentPointCutAdvisor"/>
    </bean>

</beans>
  • 测试
@Test
public void test02(){
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //获取代理对象,Cglib动态代理方式
    StudentDaoImpl studentProxy = context.getBean("studentProxy", StudentDaoImpl.class);
    studentProxy.add();
    studentProxy.delete();
    studentProxy.select();
    studentProxy.selectother();
    studentProxy.update();
    studentProxy.updateOther();
    /*
        切点切面-增加
    切点切面-删除
    一般切面-环绕前-动作
    切点切面-查询
    一般切面-环绕后-动作
    一般切面-环绕前-动作
    切点切面-其他查询
    一般切面-环绕后-动作
    一般切面-环绕前-动作
    切点切面-更新
    一般切面-环绕后-动作
    一般切面-环绕前-动作
    切点切面-其他更新
    一般切面-环绕后-动作
    * */
}
8.2.6 自动代理方式
  • 之前的案例中,所有目标对象(Target Bean)的代理对象(Proxy Bean)都是在 XML 配置中通过 ProxyFactoryBean 创建的。但在实际开发中,一个项目中往往包含非常多的 Bean, 如果每个 Bean 都通过 ProxyFactoryBean 创建,那么开发和维护成本会十分巨大。为了解决这个问题,Spring 为我们提供了自动代理机制

  • Spring 提供的自动代理方案,都是基于后处理 Bean 实现的,即在 Bean 创建的过程中完成增强,并将目标对象替换为自动生成的代理对象。通过 Spring 的自动代理,我们在程序中直接拿到的 Bean 就已经是 Spring 自动生成的代理对象了。

Spring 为我们提供了 3 种自动代理方案:

  • BeanNameAutoProxyCreator:根据 Bean 名称创建代理对象。
  • DefaultAdvisorAutoProxyCreator:根据 Advisor 本身包含信息创建代理对象。
  • AnnotationAwareAspectJAutoProxyCreator:基于 Bean 中的 AspectJ 注解进行自动代理对象。
  • 举例:根据 Bean 名称创建代理对象

  • 编写后置通知类

public class MyAfterAdvice implements AfterReturningAdvice {
     /**
         *returnValue:返回值
         * method:目标方法
         *args:参数
         * target:目标对象 */
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
    System.out.println("正在执行"+target.getClass().getName()+"类后置通知操作…………目标对象方法是"+method.getName()+"返回值为"+returnValue);

    }
}
  • 编写spring配置文件bean1.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="userDao2" class="com.zk.dao.UserDaoImpl"/>
    <bean id="studentDao2" class="com.zk.dao.StudentDaoImpl"/>

    <!--定义通知-->
    <bean id="methodAdvice2" class="com.zk.advice.UserDaoMethodAdvice"/>
    <bean class="com.zk.advice.MyAfterAdvice" id="myAfterAdvice"/>

    <!--Spring 自动代理:根据 Bean 名称创建代理独享-->
    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <!--设置应该自动被代理包装的 bean 的名称。一个名称可以指定一个前缀以“*”结尾来匹配,例如“myBean,tx*”将匹配名为“myBean”的bean和所有名称以“tx”开头的bean。-->
        <property name="beanNames" value="*Dao*"/>
        <!--设置常用拦截器。这些必须是当前容器中的 bean 名称。它们可以是 Spring 支持的任何通知-->
        <property name="interceptorNames" value="methodAdvice2,myAfterAdvice"/>
    </bean>
</beans>
  • 测试及结果
@Test
public void test03(){
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
    //获取代理对象,JDK动态代理的是一个接口
    UserDao userDao = context.getBean("userDao2", UserDao.class);
    userDao.add();
    userDao.delete();
    userDao.select();
    userDao.update();
    System.out.println("===============================================");
    //获取代理对象,CGlib动态代理的是一个普通类
    StudentDaoImpl studentDao = context.getBean("studentDao2", StudentDaoImpl.class);
    studentDao.add();
    studentDao.delete();
    studentDao.select();
    studentDao.selectother();
    studentDao.update();
    studentDao.updateOther();
}

/*
一般切面-环绕前-动作
一般切面-增加
正在执行com.zk.dao.UserDaoImpl类后置通知操作…………目标对象方法是add返回值为null
一般切面-环绕后-动作
一般切面-环绕前-动作
一般切面-删除
正在执行com.zk.dao.UserDaoImpl类后置通知操作…………目标对象方法是delete返回值为null
一般切面-环绕后-动作
一般切面-环绕前-动作
一般切面-查询
正在执行com.zk.dao.UserDaoImpl类后置通知操作…………目标对象方法是select返回值为null
一般切面-环绕后-动作
一般切面-环绕前-动作
一般切面-修改
正在执行com.zk.dao.UserDaoImpl类后置通知操作…………目标对象方法是update返回值为null
一般切面-环绕后-动作
===============================================
一般切面-环绕前-动作
切点切面-增加
正在执行com.zk.dao.StudentDaoImpl类后置通知操作…………目标对象方法是add返回值为null
一般切面-环绕后-动作
一般切面-环绕前-动作
切点切面-删除
正在执行com.zk.dao.StudentDaoImpl类后置通知操作…………目标对象方法是delete返回值为null
一般切面-环绕后-动作
一般切面-环绕前-动作
切点切面-查询
正在执行com.zk.dao.StudentDaoImpl类后置通知操作…………目标对象方法是select返回值为null
一般切面-环绕后-动作
一般切面-环绕前-动作
切点切面-其他查询
正在执行com.zk.dao.StudentDaoImpl类后置通知操作…………目标对象方法是selectother返回值为null
一般切面-环绕后-动作
一般切面-环绕前-动作
切点切面-更新
正在执行com.zk.dao.StudentDaoImpl类后置通知操作…………目标对象方法是update返回值为null
一般切面-环绕后-动作
一般切面-环绕前-动作
切点切面-其他更新
正在执行com.zk.dao.StudentDaoImpl类后置通知操作…………目标对象方法是updateOther返回值为null
一般切面-环绕后-动作

Process finished with exit code 0

*/
  • 举例:根据 Advisor 切面本身包含信息创建代理对象。

  • 编写spring配置文件bean2.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="userDao2" class="com.zk.dao.UserDaoImpl"/>
    <bean id="studentDao2" class="com.zk.dao.StudentDaoImpl"/>

    <!--定义通知-->
    <bean id="methodAdvice2" class="com.zk.advice.UserDaoMethodAdvice"/>
    <bean class="com.zk.advice.MyAfterAdvice" id="myAfterAdvice"/>

    <!-- 定义切面 包括通知和切点-->
    <bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <!--定义表达式,规定哪些方法进行拦截 .* 表示所有方法-->
        <property name="patterns" value="com.zk.dao.UserDao.add,com.zk.dao.StudentDaoImpl.select.*"/>
        <!--加一个后置通知-->
        <property name="advice" ref="myAfterAdvice"/>
    </bean>
    <!--Spring 自动代理:根据切面 myPointCutAdvisor 中信息创建代理对象-->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
</beans>
  • 测试及结果
@Test
public void test04(){
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean2..xml");
    //获取代理对象,JDK动态代理的是一个接口
    UserDao userDao = context.getBean("userDao2", UserDao.class);
    userDao.add();
    userDao.select();
    System.out.println("===============================================");
     //获取代理对象,CGlib动态代理的是一个普通类
    StudentDaoImpl studentDao = context.getBean("studentDao2", StudentDaoImpl.class);
    studentDao.add();
    studentDao.select();
    studentDao.selectother();
    /*一般切面-增加
    正在执行com.zk.dao.UserDaoImpl类后置通知操作…………目标对象方法是add返回值为null
    一般切面-查询
    ===============================================
    切点切面-增加
    切点切面-查询
    正在执行com.zk.dao.StudentDaoImpl类后置通知操作…………目标对象方法是select返回值为null
    切点切面-其他查询
    正在执行com.zk.dao.StudentDaoImpl类后置通知操作…………目标对象方法是selectother返回值为null

        Process finished with exit code 0
        */
}

8.3 Spring集成AspectJ

  • Spring AOP 仅支持执行公共(public)非静态方法的调用作为连接点,如果我们需要向受保护的(protected)或私有的(private)的方法进行增强,此时就需要使用功能更加全面的 AOP 框架来实现,其中使用最多的就是 AspectJ。
  • AspectJ 是一个基于 Java 语言的全功能的 AOP 框架,它并不是 Spring 组成部分,是一款独立的 AOP 框架。但由于 AspectJ 支持通过 Spring 配置 AspectJ 切面,因此它是 Spring AOP 的完美补充,通常情况下,我们都是将 AspectJ 和 Spirng 框架一起使用,简化 AOP 操作。

基于 AspectJ 实现 AOP 操作提供了两种方式

  • 基于 xml 配置实现 AspectJ 的 AOP 开发
  • 基于注解方式实现 AspectJ 的 AOP 开发
  • 使用前需要导入AspectJ 依赖
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.9.1</version>
</dependency>
8.3.1 基于 xml 配置实现 AspectJ 的 AOP 开发
  • 在 Spring 项目中通过 XML 配置,对切面(Aspect 或 Advisor)、切点(PointCut)以及通知(Advice)进行定义和管理,以实现基于 AspectJ 的 AOP 开发。
  • 在applicationContext.xml中配置aop:导入aop约束
定义切入点<aop:pointcut><?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">

</beans>
  • 定义切入点aop:pointcut

aop:pointcut 用来定义一个切入点,用来表示对哪个类中的那个方法进行增强。它既可以在 aop:pointcut 元素中使用,也可以在 aop:aspect 元素下使用。

  • 当 aop:pointcut元素作为 aop:config 元素的子元素定义时,表示该切入点是全局切入点,它可被多个切面所共享;
  • 当 aop:pointcut 元素作为 aop:aspect 元素的子元素时,表示该切入点只对当前切面有效。
<aop:config>
    <aop:pointcut id="myPointCut"
        expression="execution(* com.zk.service.*.*(..))"/>
</aop:config>

id 用于指定切入点的唯一标识名称,execution 用于指定切入点关联的切入点表达式。

execution 的语法格式格式为:execution([权限修饰符] [返回值类型] [类的完全限定名] [方法名称] ([参数列表])

其中:

  • 返回值类型、方法名、参数列表是必须配置的选项,而其它参数则为可选配置项。
  • 返回值类型:*表示可以为任何返回值。如果返回值为对象,则需指定全路径的类名。
  • 类的完全限定名:指定包名 + 类名。
  • 方法名:*代表所有方法,set* 代表以 set 开头的所有方法。
  • 参数列表:(..)代表所有参数;(*)代表只有一个参数,参数类型为任意类型;(*,String)代表有两个参数,第一个参数可以为任何值,第二个为 String 类型的值。
  • 上诉示例表示对com.zk.service包下所有类的所有方法作为切入点,进行增强
  • 定义切面aop:aspect

在 Spring 配置文件中,使用 aop:aspect 元素定义切面。该元素可以将定义好的 Bean 转换为切面 Bean,所以使用 aop:aspect 之前需要先定义一个普通的 Spring Bean。其中,id 用来定义该切面的唯一标识名称,ref 用于引用普通的 Spring Bean。

<aop:config>
    <aop:aspect id="myAspect" ref="beanID">
        ...
    </aop:aspect>
</aop:config>
  • 定义通知,AspectJ 支持 5 种类型的 advice
<aop:aspect id="myAspect" ref="aBean">
    <!-- 前置通知 -->
    <aop:before pointcut-ref="myPointCut" method="..."/>
   
    <!-- 后置通知 -->
    <aop:after-returning pointcut-ref="myPointCut" method="..."/>
    <!-- 环绕通知 -->
    <aop:around pointcut-ref="myPointCut" method="..."/>
    <!-- 异常通知 -->
    <aop:after-throwing pointcut-ref="myPointCut" method="..."/>
    <!-- 最终通知 -->
    <aop:after pointcut-ref="myPointCut" method="..."/>
    .... 
</aop:aspect>
  • 定义环绕
<aop:config>
    <!--切入点,expression切入位置-->
    <aop:pointcut id="myPointcut" expression="execution(* com.zk.service.UserServiceImpl.*(..))"/>
    <!--执行环绕-->
    <aop:advisor advice-ref="methodAdvice" pointcut-ref="myPointcut"/>
</aop:config>

methodAdvice:定义的环绕bean,pointcut-ref:指向定义的切点

8.3.3 举例:使用原生spring API接口
  • 编写目标接口和实现
public interface UserService {
    void add();

    void delete();

    void update();

    void select();
}
public class UserServiceImpl implements UserService{
    @Override
    public void add() {
        System.out.println("正在执行增加业务!");
    }

    @Override
    public void delete() {
        System.out.println("正在执行删除业务!");
    }

    @Override
    public void update() {
        System.out.println("正在执行更新业务!");
    }

    @Override
    public void select() {
        System.out.println("正在执行查询业务!");
    }
}
  • 编写环绕增强
public class MyAdvisor implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println(target.getClass().getName()+"执行方法为"+method.getName());

    }
}
  • 编写applicationContext.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">

    <!-- 定义目标(target)对象 -->
    <bean id="userService" class="com.zk.service.UserServiceImpl"/>

    <!-- 定义通知 -->
    <bean id="methodAdvice" class="com.zk.myadvisor.MyAdvisor"/>
    <!--方式1:使用原生spring API接口-->
    <!--配置aop:导入aop约束-->
    <aop:config>
        <!--切入点,expression切入位置-->
        <aop:pointcut id="myPointcut" expression="execution(* com.zk.service.UserServiceImpl.*(..))"/>
        <!--执行环绕-->
        <aop:advisor advice-ref="methodAdvice" pointcut-ref="myPointcut"/>
    </aop:config>
</beans>
  • 测试及结果
@Test
public void testDemo01(){
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //动态代理的是接口
    UserService userService = context.getBean("userService", UserService.class);
    userService.select();
}
/*
正在执行查询业务!
com.zk.service.UserServiceImpl执行方法为select*/
8.3.4 例2:自定义类来实现AOP
  • 编写接口及实现类
public interface StudentService {
    void add();

    void delete();

    void update();

    int select();

    void other();
}
public class StudentServiceImpl implements StudentService{
    @Override
    public void add() {
        System.out.println("正在执行student增加业务!");
    }

    @Override
    public void delete() {
        System.out.println("正在执行student删除业务!");
    }

    @Override
    public void update() {
        System.out.println("正在执行student更新业务!");
    }

    @Override
    public int select() {
        System.out.println("正在执行student查询业务!");
        return 1;
    }

    @Override
    public void other() {
        //异常
       /* String str = null;
        int a = str.length();*/
        int a = 10 / 0;
        System.out.println("正在执行 student 中的 other异常方法");
    }
}
  • 编写切面类
public class MyAspectAdvic {
    public void before() {
        System.out.println("前置操作……");
    }
    public void after() {
        System.out.println("最终操作……");
    }

    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕前操作……");
        proceedingJoinPoint.proceed();
        System.out.println("环绕后操作……");
    }
    public void afterThrowing(Exception exception) {
        System.out.println("异常操作……"+exception.getMessage());
    }
    public void afterReturning(Object result) {
        System.out.println("后置返回值操作……"+result);
    }
}
  • 配置文件bean1.xml进行代理配置。一个切面类对应一个aop:aspect切面元素
<?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"
       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/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean class="com.zk.service.StudentServiceImpl" id="studentService"/>
    <bean class="com.zk.myadvisor.MyAspectAdvic" id="myAspectAdvic"/>
    
    <aop:config>
        <!--定义全局切点-->
        <aop:pointcut id="beforePointcut" expression="execution(* com.zk.service.StudentService.add(..))"/>
        <aop:pointcut id="afterPointcut" expression="execution(* com.zk.service.StudentService.delete(..))"/>
        <aop:pointcut id="afterReturningPointcut" expression="execution(* com.zk.service.StudentService.select(..))"/>
        <aop:pointcut id="afterThrowingPointcut" expression="execution(* com.zk.service.StudentService.other(..))"/>
        <aop:pointcut id="aroundPointcut" expression="execution(* com.zk.service.StudentService.*(..))"/>
        <!--定义切面-->
        <aop:aspect ref="myAspectAdvic">
            <aop:before method="before" pointcut-ref="beforePointcut"/>
            <aop:after method="after" pointcut-ref="afterPointcut"/>
            <!--注意返回值设置 方法中有返回值,此处要配置-->
            <aop:after-returning method="afterReturning" pointcut-ref="afterReturningPointcut" returning="result"/>
            <!--异常通知有参数,则需要配置抛出异常-->
            <aop:after-throwing method="afterThrowing" pointcut-ref="afterThrowingPointcut" throwing="exception"/>
            <aop:around method="around" pointcut-ref="afterPointcut"/>
        </aop:aspect>
    </aop:config>
</beans>
  • 测试及结果
@Test
public void testDemo02(){
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
    //动态代理的是接口
    StudentService studentService = context.getBean("studentService", StudentService.class);
    studentService.add();
    studentService.delete();
    studentService.update();
    studentService.select();
    studentService.other();

}
前置操作……
正在执行student增加业务!
环绕前操作……
正在执行student删除业务!
环绕后操作……
最终操作……
正在执行student更新业务!
正在执行student查询业务!
后置返回值操作……1
异常操作……/ by zero

java.lang.ArithmeticException: / by zero
8.3.2 基于注解方式实现 AspectJ 的 AOP 开发
  • 在 Spring 中,虽然我们可以使用 XML 配置文件可以实现 AOP 开发,但如果所有的配置都集中在 XML 配置文件中,就势必会造成 XML 配置文件过于臃肿,从而给维护和升级带来一定困难。
  • AspectJ 框架为 AOP 开发提供了一套 @AspectJ 注解。它允许我们直接在 Java 类中通过注解的方式对切面(Aspect)、切入点(Pointcut)和增强(Advice)进行定义,Spring 框架可以根据这些注解生成 AOP 代理。
名称 说明
@Aspect 用于定义一个切面。
@Pointcut 用于定义一个切入点。
@Before 用于定义前置通知,相当于 BeforeAdvice。
@AfterReturning 用于定义后置通知,相当于 AfterReturningAdvice。
@Around 用于定义环绕通知,相当于 MethodInterceptor。
@AfterThrowing 用于定义抛出通知,相当于 ThrowAdvice。
@After 用于定义最终通知,不管是否异常,该通知都会执行。
@DeclareParents 用于定义引介通知,相当于 IntroductionInterceptor(不要求掌握

启用 @AspectJ 注解支持:在使用 @AspectJ 注解进行 AOP 开发前,首先我们要先启用 @AspectJ 注解支持。

  • 通过以下 2 种方式来启用 @AspectJ 注解。
  • 使用 Java 配置类启用

我们可以在 Java 配置类(标注了 @Configuration 注解的类)中,使用 @EnableAspectJAutoProxy 和 @ComponentScan 注解启用 @AspectJ 注解支持。

@Configuration
@ComponentScan(basePackages = "net.biancheng.c") //注解扫描
@EnableAspectJAutoProxy //开启 AspectJ 的自动代理
public class AppConfig {
}
  • 基于 XML 配置启用:在 Spring 的 XML 配置文件中,添加以下元素启用 @AspectJ 注解支持。
<!-- 开启注解扫描 -->
<context:component-scan base-package="net.biancheng.c"></context:component-scan>
<!--开启AspectJ 自动代理-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy proxy-target-class="false">
  • proxy-target-class=”false”是指用jdk动态代理方式,true为cglib动态代理方式

定义切面 @Aspect

  • 半注解方式定义切面即在 Spring 的 XML 配置文件定义bean,用注解方式@Aspect定义切面

  • 全注解方式定义切面

  • @Component // 定义成 Bean,@Aspect //定义为切面

定义切点 @Pointcut

  • 在 AspectJ 中,我们可以使用 @Pointcut 注解用来定义一个切点。需要注意的是,定义为切点的方法,它的返回值类型必须为 void
//定义切入点1
@Pointcut("execution(* com.zk.service.StudentService.select(..))")
public void pointcut1(){

}
//定义切入点2
@Pointcut("execution(* com.zk.service.StudentService.update(..))")
public void pointcut2(){

}

//定义切入点3
@Pointcut("execution(* com.zk.service.StudentService.other(..))")
public void pointcut3(){

}
  • @Pointcut 注解中有一个 value 属性,这个属性的值就是切入点表达式。

  • 除了可以通过切入点表达式(execution)直接对切点进行定义外,还可以通过切入点方法的名称来引用其他的切入点。在使用方法名引用其他切入点时,还可以使用“&&”、“||”和“!”等表示“与”、“或”、“非”的含义,示例代码如下。

/** ! 表示 非 ,即 "不是" 的含义,求补集
 * * && 表示 与,即 ”并且“ ,求交集
 * || 表示 或,即 “或者”,求并集
 */
@Around("!pointcut1()&&!pointcut3()")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    System.out.println("环绕前操作……");
    proceedingJoinPoint.proceed();
    System.out.println("环绕后操作……");
}

//使用切入点表达式
@AfterThrowing(value = "pointcut3()", throwing = "exception")
public void afterThrowing(Exception exception) {
    System.out.println("异常操作……"+exception.getMessage());
}

//使用其他切面类或自己切面类切入点引用
@AfterReturning(value = "MyAspectAdvic.pointcut1()",returning = "result")
public void afterReturning(Object result) {
    System.out.println("后置返回值操作……"+result);
}

定义通知

  • AspectJ 为我们提供了以下 6 个注解,来定义 6 种不同类型的通知(Advice),如下表。
注解 说明
@Before 用于定义前置通知,相当于 BeforeAdvice。
@AfterReturning 用于定义后置通知,相当于 AfterReturningAdvice。
@Around 用于定义环绕通知,相当于 MethodInterceptor。
@AfterThrowing 用于定义抛出通知,相当于 ThrowAdvice。
@After 用于定义最终通知,不管是否异常,该通知都会执行。
@DeclareParents 用于定义引介通知,相当于 IntroductionInterceptor(不要求掌握)。
  • 这些通知注解中都有一个 value 属性,这个 value 属性的取值就是这些通知(Advice)作用的切点(PointCut),它既可以是切入点表达式,也可以是切入点的引用(切入点对应的方法名称)

  • 举例理解,基于 XML 配置启用

  • 编写实体接口和实现

public interface StudentService {
    void add();

    void delete();

    void update();

    int select();

    void other();
}
@Component("studentService")
public class StudentServiceImpl implements StudentService{
    @Override
    public void add() {
        System.out.println("正在执行student增加业务!");
    }

    @Override
    public void delete() {
        System.out.println("正在执行student删除业务!");
    }

    @Override
    public void update() {
        System.out.println("正在执行student更新业务!");
    }

    @Override
    public int select() {
        System.out.println("正在执行student查询业务!");
        return 1;
    }

    @Override
    public void other() {
        //异常
       /* String str = null;
        int a = str.length();*/
        int a = 10 / 0;
        System.out.println("正在执行 student 中的 other异常方法");
    }
}
  • 开启注解扫描和代理支持
<?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"
       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/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--开启包扫描注解-->
    <context:component-scan base-package="com.zk"/>
    <!--开启aspectj注解代理支持-->
    <aop:aspectj-autoproxy/>
</beans>
  • 编写切面类
@Component
@Aspect
public class MyAspectAdvic {

    //定义切入点1
    @Pointcut("execution(* com.zk.service.StudentService.select(..))")
    public void pointcut1(){

    }
    //定义切入点2
    @Pointcut("execution(* com.zk.service.StudentService.update(..))")
    public void pointcut2(){

    }

    //定义切入点3
    @Pointcut("execution(* com.zk.service.StudentService.other(..))")
    public void pointcut3(){

    }

    //使用切入点表达式
    @Before(value = "execution(* com.zk.service.StudentService.add(..))")
    public void before() {
        System.out.println("前置操作……");
    }

    //使用切入点表达式
    @After("execution(* com.zk.service.StudentService.delete(..))")
    public void after() {
        System.out.println("最终操作……");
    }


    /** ! 表示 非 ,即 "不是" 的含义,求补集
     * * && 表示 与,即 ”并且“ ,求交集
     * || 表示 或,即 “或者”,求并集
     */
    @Around("!pointcut1()&&!pointcut3()")
    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕前操作……");
        proceedingJoinPoint.proceed();
        System.out.println("环绕后操作……");
    }

    //使用切入点表达式
    @AfterThrowing(value = "pointcut3()", throwing = "exception")
    public void afterThrowing(Exception exception) {
        System.out.println("异常操作……"+exception.getMessage());
    }

    //使用其他切面类或自己切面类切入点引用
    @AfterReturning(value = "MyAspectAdvic.pointcut1()",returning = "result")
    public void afterReturning(Object result) {
        System.out.println("后置返回值操作……"+result);
    }
}
  • 测试及结果
@Test
public void testDemo01(){
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //动态代理的是接口
    StudentService studentService = context.getBean("studentService", StudentService.class);
    studentService.add();
    studentService.delete();
    studentService.update();
    studentService.select();
    studentService.other();
}
环绕前操作……
前置操作……
正在执行student增加业务!
环绕后操作……
环绕前操作……
正在执行student删除业务!
最终操作……
环绕后操作……
环绕前操作……
正在执行student更新业务!
环绕后操作……
正在执行student查询业务!
后置返回值操作……1
异常操作……/ by zero

java.lang.ArithmeticException: / by zero
  • 基于 Java 配置类启用

  • 可以不要applicationContext.xml配置文件

  • 编写java配置类

@Configuration
@ComponentScan("com.zk")
@EnableAspectJAutoProxy
public class MyConfig {
}
  • 实体接口、实现和切面沿用上例

  • 测试及结果

@Test
public void testDemo02(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
    //动态代理的是接口
    StudentService studentService = context.getBean("studentService", StudentService.class);
    studentService.add();
    studentService.delete();
    studentService.update();
    studentService.select();
    studentService.other();
}
环绕前操作……
前置操作……
正在执行student增加业务!
环绕后操作……
环绕前操作……
正在执行student删除业务!
最终操作……
环绕后操作……
环绕前操作……
正在执行student更新业务!
环绕后操作……
正在执行student查询业务!
后置返回值操作……1
异常操作……/ by zero

java.lang.ArithmeticException: / by zero
  • 环绕前操作……前置操作……正在执行student增加业务!后置返回值操作……最终操作(释放资源)……环绕后操作……
  • 但执行顺序在spring的改版改变过,具体看cpu调度,应该都是多线程的执行的,看线程调度,wait之后就看谁先notify
  • 这个是随机的,可以靠@Order配置优先级,较低的值具有较高的优先级

本专栏下一篇:Spring JDBC

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/123909.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!