AOP面向切面编程

         对于家庭来说,监控用电量是一个很重要的功能,但并不是大多数家庭重点关注的问题,监控房屋的用电量是一个被动事件。
    软件系统中的一些功能就像我们家里的电表一样,这些功能需要用到应用程序的多个地方,但是我们又不想在每个点都明确调用它们。 日志、安全和事务管理的确都很重要,但是它们是否为应用对象主动参与的行为呢?如果让应用对象只关注于自己所针对的业务领域问题,而其他方面的问题由其他应用对象来处理,会不会更好呢?
    以上摘自那本厚厚的《Spring实战》,上述描述的逻辑使我想到了一个熟悉的设计模式——动态代理,在接口中的 那整块方法,其 都可以再自定义操作,那么上述的业务领域就相当于接口中的方法,一定会被执行,但是日志、安全、事务等,可以作为前、后才执行的。

1.关于 CGLIB

    之前学习动态代理时,使用 JDK 生成的动态代理的前提是 目标类必须有实现的接口。 所以如果类没有实现接口,就不能使用 JDK 动态代理。 CGLIB 【Code Generation Library 代码生成库】代理就是解决这个问题的。
    CGLIB 是以动态生成的子类 继承目标 的方式实现,在运行期动态的在内存中构建一个子类,如下:
    CGLIB 使用的前提是目标类不能为 final 修饰,因为 final 修饰的类不能被继承。

2.引入 AOP 的概念

    现在,我们可以看看 AOP 的定义:面向切面编程,核心原理是使用动态代理模式在方法执行前后或出现异常时加入相关逻辑。
    通过定义可以发现:
(1)AOP 是基于动态代理模式。
(2)AOP 是方法级别的。
(3)AOP 可以分离业务代码和关注点代码(重复代码),在执行业务代码时,动态的注入关注点代码。切面就是关注点代码形成的类。
    关于(3)的详细解说:
    在软件开发中,散布于应用中多处的功能被称为横切关注点(corss-cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑中),把这些横切关注点和业务逻辑相分离正是面向切面编程AOP所要解决的问题。
    之前的 DI依赖注入 是应用对象之间的解耦,而AOP可以实现横切关注点与它们所影响对象之间的解耦。
    日志(较常用)、声明式事务、安全和缓存,都是应用切面的应用场景。
     在使用面向切面编程时,仍然是在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式 在何处应用,而无需修改受影响的类。横切关注点可以被模块化成特殊的类,这些类被称为切面(aspect)。

3.简单实现 AOP

    优秀的 Spring 框架把两种方式在底层都集成了进去,那么,Spring是如何生成代理对象的?
AOP面向切面编程_第1张图片

    创建容器对象的时候,根据切入点【地点】表达式拦截的类,生成代理对象。
    如果目标对象有实现接口,使用 JDK 代理。如果目标对象没有实现接口,则使用 CGLIB 代理。然后从容器获取代理后的对象,在运行期植入 “切面” 类的方法。来看 Spring 源码, DefaultAopProxyFactory 类:

@SuppressWarnings("serial")
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

    @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);
        }
    }

    简单的从字面意思看出:如果有接口,则使用 JDK 代理,反之使用 CGLIB ,这刚好印证了前文所阐述的内容。Spring AOP 综合两种代理方式的使用前提有会如下结论:如果目标类没有实现接口,且 class 为 final 修饰的,则不能进行 Spring AOP 编程。

AOP面向切面编程_第2张图片
了解相关术语:
通知(Advice):
切面的目标,它必须要完成的任务,它的工作被称为通知。
通知定义了切面是什么以及何时使用。(除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。)
Spring通知是Java编写的。
连接点(Join point):
我们的应用可能有数以千计的时机应用通知,这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时,甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
切点(Pointcut):
通知定义了切面的“什么”和“何时”,切点定义了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所要匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。
切面(Aspect):
通知和切点的结合。通知和切点共同定义了切面的全部内容。
引入(Introdution):
引入允许我们向现有的类添加新方法或属性。
织入(Weaving):
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期中有多个点可以进行织入:编译期,类加载期,运行期。

首先新建项目,选择 webapps 骨架,在 pom.xml 中导入 jar 包依赖:

<?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.jia</groupId>
  <artifactId>AOPDemo</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>
  <name>AOPDemo Maven Webapp</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.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>commons-dbutils</groupId>
      <artifactId>commons-dbutils</artifactId>
      <version>1.4</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>

    <dependency>
      <groupId>c3p0</groupId>
      <artifactId>c3p0</artifactId>
      <version>0.9.1.2</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>AOPDemo</finalName>
    <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.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.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>

接下来把账户 Account 相关的代码拷贝过来:
AOP面向切面编程_第3张图片
在 resources 包中新建 bean.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

<!--使用注解,需要告知Spring 在创建容器时需要扫描的包-->
<context:component-scan base-package="com.jia">
</context:component-scan>


    <!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--注入数据源-->
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
        <property name="user" value="root"></property>
        <property name="password" value="jdpy1229jiajia"></property>
    </bean>


</beans>

AOP面向切面编程_第4张图片

在业务类接口中新增转账方法:

 /**
     * 转账
     * @param sourceName 转出账户
     * @param targetName 转入账户
     * @param money 转账金额
     */
    void transfer(String sourceName,String targetName,float money);

转账需要 6 个步骤:

 1.根据名称查询转入账户
 2.根据名称查询转出账户
 3.转出账户减钱
 4.转入账户加钱
 5.更新转出账户
 6.更新转入账户

因此需要在 IAccountDao 中新建一个 根据名称查询用户的方法:

 /**
     * 根据名称查询账户
     * @param accountName
     * @return 如果有唯一结果就返回,如果没有结果就返回 null
     *         如果结果集个数超过 1 ,就抛异常
     */
    Account findAccountByNames(String accountName);

实现:

 @Override
    public Account findAccountByNames(String accountName) {
        try{
            List<Account> accounts=runner.query("select * from account where name = ? ",new BeanListHandler<Account>(Account.class),accountName);
        if(accounts==null||accounts.size()==0)
            return null;
        if(accounts.size()>1)
            throw new RuntimeException("结果集不唯一,数据有问题");
        return accounts.get(0);
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

完善转账方法:

   @Override
    public void transfer(String sourceName,String targetName,float money)
    {
        //1.根据名称查询转入账户
        Account source=accountDao.findAccountByNames(sourceName);
       //2.根据名称查询转出账户
        Account target=accountDao.findAccountByNames(targetName);
        //3.转出账户减钱
        source.setMoney(source.getMoney()-money);
        //4.转入账户加钱
        target.setMoney(target.getMoney()+money);
        //5.更新转出账户
        accountDao.updateAccount(source);
        //6.更新转入账户
        accountDao.updateAccount(target);
    }

测试转账方法:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccoutServiceTest {

 @Autowired
    private IAccountService as;
    
        @Test
    public void testTransfer()
        {
           as.transfer("aaa","bbb",100);
        }
    }

转账成功:
AOP面向切面编程_第5张图片
但是如果在更新转入与转出之间加上 1/0:

//5.更新转出账户
        accountDao.updateAccount(source);
        
        int i=1/0;
        
        //6.更新转入账户
        accountDao.updateAccount(target);

执行测试会报异常,而数据 不满足一致性原则:
AOP面向切面编程_第6张图片

你可能感兴趣的:(学习笔记,Spring)