Spring学习笔记3

Spring学习笔记-day3

第五章 Spring AOP之动态代理技术

5.1演示事务相关问题

搭建项目:转账案例

在IAccountService接口中添加转账方法:

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

在AccountServiceImpl添加转账方法的具体实现:

 public void transfer(String sourceName, String targetName, float money) {
        /*
            1.根据名称查询转入账户
            2.根据名称查询转入账户
            3.转出账户
            4.转入账户
            5.更新转出账户
            6.更新转入账户
         */
        Account accounSource = iAccountDao.findAccountByName(sourceName);
        if(accounSource == null){
            System.out.println("转出用户不存在");
            return;
        }
        Account accountTarget = iAccountDao.findAccountByName(targetName);
        if(accounSource == null){
            System.out.println("转入用户不存在");
            return;
        }
        if (accounSource.getMoney() < money){
            System.out.println("转出用户金额不足");
            return;
        }
        accounSource.setMoney(accounSource.getMoney() - money);
        accountTarget.setMoney(accountTarget.getMoney() + money);
        iAccountDao.updateAccount(accounSource);
        iAccountDao.updateAccount(accountTarget);
    }

测试方法:

package com.jzt.test;

import com.jzt.entity.Account;
import com.jzt.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

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

    @Test
    public void test1(){
        ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
        IAccountService accountService = (IAccountService) ac.getBean("accountService");
        List<Account> accoutList = accountService.getAll();
        System.out.println(accoutList);
    }
}

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pb7UarGT-1601116638521)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200922202009327.png)]

我们可以看到我们的转账成功了。

但是这种情况是一种理想情况,是在我们转账过程中没有碰到任何干扰的情况下才能正常进行,当我们在转账过程中碰到异常时,就不能保证转账能够正常了。如下,我们在转账流程中认为添加异常:

 public void transfer(String sourceName, String targetName, float money) {
        /*
            1.根据名称查询转入账户
            2.根据名称查询转入账户
            3.转出账户
            4.转入账户
            5.更新转出账户
            6.更新转入账户
         */
        Account accounSource = iAccountDao.findAccountByName(sourceName);
        if(accounSource == null){
            System.out.println("转出用户不存在");
            return;
        }
        Account accountTarget = iAccountDao.findAccountByName(targetName);
        if(accounSource == null){
            System.out.println("转入用户不存在");
            return;
        }
        if (accounSource.getMoney() < money){
            System.out.println("转出用户金额不足");
            return;
        }
        accounSource.setMoney(accounSource.getMoney() - money);
        accountTarget.setMoney(accountTarget.getMoney() + money);
        iAccountDao.updateAccount(accounSource);
        int num = 1 / 0;
        iAccountDao.updateAccount(accountTarget);
    }

再次执行,结果:

发现程序抛出异常:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PpW8ZlNo-1601116638522)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200922202414828.png)]

查看数据库:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yyj2eSIK-1601116638523)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200922202443156.png)]

发现aaa账户扣款成功,但是bbb账户没有收到款,显然是不满足转账操作的。

name如何解决,我们首先想到的便是事务。但是这里我们先要解决一个问题,就是我们在持久层进行数据操作时,实际上一次操作就会与数据库建立一个connection连接,但是我们的转账操作是一个整体,必须要在一个连接下进行,具体分析如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QyrOMrRM-1601116638525)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200922202932772.png)]

那么我们创建一个连接工具类,来实现使用ThreadLocal对象绑定connection。

package com.jzt.util;

import javax.sql.DataSource;
import java.sql.Connection;

public class ConnectionUtil {

    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 获取当前线程的连接
     * @return
     */
    public Connection getConnection(){
        try{
            //1.从threadLocal中获取连接
            Connection conn = tl.get();
            //判断ThreadLocal中是否有连接
            if(conn == null){
                //如果没有连接,则从数据源DataSource中获取
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            //返回连接
            return conn;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    /**
     * 释放连接
     */
    public void release(){
        tl.remove();
    }
}

然后我们在创建一个与事务管理相关的类:

package com.jzt.util;

import java.sql.SQLException;

public class TransactionManager {

    private ConnectionUtil connectionUtil;

    public void setConnectionUtil(ConnectionUtil connectionUtil) {
        this.connectionUtil = connectionUtil;
    }

    /**
     * 开启事务
     */
    public void beginTransaction(){
        try {
            connectionUtil.getConnection().setAutoCommit(false);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 提交事务
     */
    public void commit(){
        try {
            connectionUtil.getConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 回滚事务
     */
    public void rollback(){
        try {
            connectionUtil.getConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 释放连接
     */
    public void release(){
        try {
            connectionUtil.getConnection().close();
            connectionUtil.release();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

下面我们将业务层和持久层进行改造,将事务管理器的事务控制加入其中:

package com.jzt.service.impl;

import com.jzt.dao.IAccountDao;
import com.jzt.entity.Account;
import com.jzt.service.IAccountService;
import com.jzt.util.TransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

public class AccountServiceImpl implements IAccountService {

    private IAccountDao iAccountDao;

    private TransactionManager txManager;

    public void setiAccountDao(IAccountDao iAccountDao) {
        this.iAccountDao = iAccountDao;
    }

    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }

	...//省略其他方法

    public void transfer(String sourceName, String targetName, float money) {

        try {
            //开启事务
            txManager.beginTransaction();
            //执行操作
/*
            1.根据名称查询转入账户
            2.根据名称查询转入账户
            3.转出账户
            4.转入账户
            5.更新转出账户
            6.更新转入账户
         */
            Account accounSource = iAccountDao.findAccountByName(sourceName);
            if(accounSource == null){
                System.out.println("转出用户不存在");
                return;
            }
            Account accountTarget = iAccountDao.findAccountByName(targetName);
            if(accounSource == null){
                System.out.println("转入用户不存在");
                return;
            }
            if (accounSource.getMoney() < money){
                System.out.println("转出用户金额不足");
                return;
            }
            accounSource.setMoney(accounSource.getMoney() - money);
            accountTarget.setMoney(accountTarget.getMoney() + money);
            iAccountDao.updateAccount(accounSource);
            int num = 1 / 0;
            iAccountDao.updateAccount(accountTarget);

            //提交事务
            txManager.commit();

        } catch (Exception e) {
            e.printStackTrace();
            //回滚事务
            txManager.rollback();
        }finally {
            //释放连接
            txManager.release();
        }


    }
}

package com.jzt.dao.impl;

import com.jzt.dao.IAccountDao;
import com.jzt.entity.Account;
import com.jzt.util.ConnectionUtil;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.util.List;

public class AccountDaoImpl implements IAccountDao {

    private QueryRunner queryRunner;

    private ConnectionUtil connectionUtil;

    public void setQueryRunner(QueryRunner queryRunner) {
        this.queryRunner = queryRunner;
    }

    public void setConnectionUtil(ConnectionUtil connectionUtil) {
        this.connectionUtil = connectionUtil;
    }

    ...//省略其他方法

    public void updateAccount(Account account) {
        try {
            queryRunner.update(connectionUtil.getConnection(),"update account set money=? where id = ?", account.getMoney(), account.getId());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public Account findAccountByName(String accountName) {
        try {
            return queryRunner.query(connectionUtil.getConnection(),"select * from account where name = ?", new BeanHandler<Account>(Account.class), accountName);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

配置beans.xml将对象数据注入spring容器:

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="accountService" class="com.jzt.service.impl.AccountServiceImpl">
        <property name="iAccountDao" ref="accountDao"></property>
        <property name="txManager" ref="txManager"></property>
    </bean>

    <bean id="accountDao" class="com.jzt.dao.impl.AccountDaoImpl">
        <property name="queryRunner" ref="runner"></property>
        <property name="connectionUtil" ref="connectionUtil"></property>
    </bean>

    <!--  配置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://192.168.31.121:3306/springtest"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>

    <!-- 配置ConnectionUtil工具类 -->
    <bean id="connectionUtil" class="com.jzt.util.ConnectionUtil">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <bean id="txManager" class="com.jzt.util.TransactionManager">
        <property name="connectionUtil" ref="connectionUtil"></property>
    </bean>
</beans>

测试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nDg3ZNHt-1601116638528)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200923095128051.png)]

可以看到,我们已经可以进行事务控制。但是以上的事务全是我们开发自己实现的,那么spring时如何实现的呢?

5.2代理的分析

5.2.1代理的简单分析

在很早之前,我们买东西都是直接跟厂家打交道,要买什么直接找对应的厂家去购买。一对一,直接。

但是现在,我们买东西都很少直接去找厂家,而是通过某淘,某东等购物平台上各个销售点挑选自己喜欢的产品,然后下单购买。这样就是所谓的经销商/代理商,用户之关心购买过程,而后续流程(售后之类的)都是代理商处理。

这样就能加用户和厂商直接解耦。同时代理可以对厂商的产品做出一些增强的业务。例如差价,中间商差价!!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-slAwJ0Dx-1601116638529)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200923160728547.png)]

下面我们用代码演示动态代理模式:

首先简单模拟生产者和消费者:

package com.jzt.proxy;

/**
 * 模拟要生产的行为
 */
public interface IProducer {

    void saleProduct(float money);
}

package com.jzt.proxy;

/**
 * 模拟生产者
 */
public class Producer implements IProducer{

    public void saleProduct(float money) {
        System.out.println("销售产品,拿到钱,"+money);
    }
}

package com.jzt.proxy;

/**
 * 模拟消费者
 */
public class Client {

    public static void main(String[] args) {
        Producer producer = new Producer();
        producer.saleProduct(1000f);
    }

}

这样就完成了上图中所示的对应关系。下面我们来使用动态代理相关知识对工程进行改造。

5.2.2动态代理的实现

java动态代理:

​ 特点:字节码谁用谁创建,谁用谁加载;

​ 分类:

​ 基于接口的动态代理;

​ 基于子类的动态代理;

1)基于接口的动态代理:

​ 涉及的类:proxy;

​ 提供者:JDK官方

​ 如何创建动态代理:

​ 使用Proxy类中的newProxyInstance()方法;

​ 创建代理对象的要求:

​ 被代理对象至少实现一个接口,如果没有则不能使用;

​ newProxyInstance()方法的参数:

​ ClassLoader:类加载器

​ 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。

​ Class[]:字节码数组

​ 用于将代理对象和被代理对象使用相同的方法;固定写法。

​ InvocationHandler:用于提供增强代码

​ 它是让我们如何代理。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。

如下我们基于接口创建一个代理对象:

 //创建代理对象
        IProducer iProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return null;
            }
        });

invoke()方法:

​ 作用:执行代理对象的任何接口方法都会经过该方法。

​ 参数含义:

​ proxy:代理对象的引用;

​ method:当前执行的方法;

​ args:当前执行方法所需要的的参数;

​ 返回值:

​ 和被代理对象有相同的放回值。

那么代理对象就可以在该方法中进行一些增强代码的实现。

//创建代理对象
        IProducer iProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {
            /**
             * 作用:执行代理对象的任何接口方法都会经过该方法。
             * @param proxy     代理对象的引用;
             * @param method    当前执行的方法;
             * @param args      当前执行方法所需要的的参数;
             * @return  和被代理对象有相同的放回值。
             * @throws Throwable
             */
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //增强方法的代码
                Object returnObj = null;
                //1.获取方法执行的参数
                Float meney = (Float) args[0];
                //2.判断方法是不是销售产品的方法
                if(method.getName().equals("saleProduct")){
                    returnObj = method.invoke(producer, meney * 1.2f);
                }
                return returnObj;
            }
        });
        iProducer.saleProduct(1000f);

查看结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ncOQBHQk-1601116638529)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200923195045567.png)]

发现代理对象的代理方法执行成功。

2)基于子类实现动态代理

​ 涉及的类:Enhancer;

​ 提供者:第三方cglib库

​ 如何创建动态代理:

​ 使用Enhancer类中的create()方法;

​ 创建代理对象的要求:

​ 被代理类不能是最终类。

​ create()方法的参数:

​ Class:字节码。

​ 用于将代理对象和被代理对象使用相同的方法;固定写法。

​ MethodInterceptor:用于提供增强代码

​ 它是让我们如何代理。我们一般是该接口的子接口实现类。MethodInterceptor

//创建代理对象
Enhancer.create(producer.getClass(), new MethodInterceptor() {
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                return null;
            }
        });

intercept()方法:

参数含义:

​ proxy:代理对象的引用;

​ method:当前执行的方法;

​ args:当前执行方法所需要的的参数;

​ methodProxy 当前执行方法的代理对象

​ 返回值:

​ 和被代理对象有相同的放回值。

那么代理对象就可以在该方法中进行一些增强代码的实现。

//创建代理对象
        Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             * 作用:执行代理对象的任何方法都会经过该方法。
             * @param proxy          代理对象的引用;
             * @param method        当前执行的方法;
             * @param args          当前执行方法所需要的的参数;
             * @param methodProxy   当前执行方法的代理对象
             * @return
             * @throws Throwable
             */
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                //增强方法的代码
                Object returnObj = null;
                //1.获取方法执行的参数
                Float meney = (Float) args[0];
                //2.判断方法是不是销售产品的方法
                if(method.getName().equals("saleProduct")){
                    returnObj = method.invoke(producer, meney * 1.2f);
                }
                return returnObj;
            }
        });
        cglibProducer.saleProduct(1000f);

接口也是成功实现了代理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2OuHJZCw-1601116638530)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200923203726923.png)]

5.3使用动态代理实现事务

首先创建一个BeanFactory用于创建代理对象,然后可以将service中的自己实现的事务抽取出来放到这个代理方法中:

package com.jzt.factory;

import com.jzt.service.IAccountService;
import com.jzt.util.TransactionManager;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 使用动态代理实现spring AOP
 */
public class BeanFactory {

    private IAccountService accountService;

    private TransactionManager txManager;

    public void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }

    public IAccountService getAccountService(){
        IAccountService proxyAccountService = (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object resultObj = null;
                try {
                    //开启事务
                    txManager.beginTransaction();
                    //执行操作
                    resultObj = method.invoke(accountService, args);
                    //提交事务
                    txManager.commit();
                } catch (Exception e) {
                    e.printStackTrace();
                    //回滚事务
                    txManager.rollback();
                }finally {
                    //释放连接
                    txManager.release();
                }
                return resultObj;
            }
        });
        return proxyAccountService;
    }
}

service中的方法回归到原本的业务,不在关心事务控制:

@Override
    public void transfer(String sourceName, String targetName, float money) {
        Account accounSource = iAccountDao.findAccountByName(sourceName);
        if(accounSource == null){
            System.out.println("转出用户不存在");
            return;
        }
        Account accountTarget = iAccountDao.findAccountByName(targetName);
        if(accounSource == null){
            System.out.println("转入用户不存在");
            return;
        }
        if (accounSource.getMoney() < money){
            System.out.println("转出用户金额不足");
            return;
        }
        accounSource.setMoney(accounSource.getMoney() - money);
        accountTarget.setMoney(accountTarget.getMoney() + money);
        iAccountDao.updateAccount(accounSource);
//        int num = 1 / 0;
        iAccountDao.updateAccount(accountTarget);

    }

配置:

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 配置bean的代理工厂-->
    <bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>

    <!-- 配置BeanFactory-->
    <bean id="beanFactory" class="com.jzt.factory.BeanFactory">
        <property name="accountService" ref="accountService"></property>
        <property name="txManager" ref="txManager"></property>
    </bean>

    <bean id="accountService" class="com.jzt.service.impl.AccountServiceImpl">
        <property name="iAccountDao" ref="accountDao"></property>
    </bean>

    <bean id="accountDao" class="com.jzt.dao.impl.AccountDaoImpl">
        <property name="queryRunner" ref="runner"></property>
        <!--  注入connection连接工具类  -->
        <property name="connectionUtil" ref="connectionUtil"></property>
    </bean>

    <!--  配置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://192.168.31.121:3306/springtest"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>

    <!-- 配置ConnectionUtil工具类 -->
    <bean id="connectionUtil" class="com.jzt.util.ConnectionUtil">
        <!--  注入数据源  -->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 配置事务管理器 -->
    <bean id="txManager" class="com.jzt.util.TransactionManager">
        <property name="connectionUtil" ref="connectionUtil"></property>
    </bean>
</beans>

测试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KKGEOyDH-1601116638530)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200923220124659.png)]

可以看到,通过代理以及实现了事务控制,同时将业务层代码与事务控制逻辑分离开。

第六章 Spring中的AOP

6.1AOP概述

6.1.1什么是AOP

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wO2uaTx7-1601116638532)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200924072844626.png)]

6.1.2AOP的作用和优势

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qsJ0izll-1601116638532)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200924073000161.png)]

6.1.3.AOP的实现方式

使用动态代理技术。

6.2AOP的实现细节

6.2.1AOP的相关术语
Joinpoint(连接点):

​ 所谓连接点是指那些被拦截到的店。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。例如在以上转账案例中,业务层service中所有方法都是连接点。

Pointcut(切入点):

​ 所谓切入点是指我们要对那些Joinpoint进行拦截的定义。例如我们上例中,业务层service中那些被事务管理通过事务器增强的方法就是切入点。切入点也是连接点。但连接点不一定是切入点。

Advice(通知/增强):

​ 所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。在上例中,事务管理器中的事务控制方法就是通知。

​ 通知的类型:前置通知后置通知异常通知最终通知环绕通知。结合上例中的代码分析各种通知类型对应如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RBjXeWap-1601116638533)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200920165829349.png)]

Introduction(引介):

​ 它是一种特殊的通知,在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法或者Field。

Target(目标对象):

​ 代理的目标对象,也就是被代理对象。

​ 在我们上例中也就是AccountServiceImpl对象。

Weaving(织入)

​ 值把增强应用到目标对象来创建新的代理对象的过程。

​ spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

​ 在上例中我们通过动态代理的技术在accountService的方法中加入了事务的支持的过程就是织入的过程。

Proxy(代理)

​ 一个类被AOP织入增强后,就产生了一个结果代理类。

​ 在上例中通过动态代理返回的proxyAccountService就是一个代理对象。

Aspect(切面)

​ 是切点和通知(引介)的结合。

6.2.2学习Spring中AOP需要明确的事

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3NsCFHl-1601116638533)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200924080535470.png)]

6.2.3关于代理的选择

在Spring中,spring框架会根据目标类(被代理类)是否实现了接口来决定采用那种动态代理的方式。

6.3Spring中的AOP具体使用

6.3.1实现基于XML的AOP配置
1)简单实现AOP

pom文件导包:

<?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.jzt</groupId>
    <artifactId>day03_spring_03springAOP</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.0</version>
        </dependency>
    </dependencies>
</project>

业务层

package com.jzt.service;

/**
 * 模拟业务层代码
 */
public interface IAccountService {

    void getAllAccount();

    void saveAccount();

    void updateAccount();

    void deleteAccount();
}

package com.jzt.service.impl;

import com.jzt.service.IAccountService;

/**
 * 模拟业务层实现
 */
public class AccountServiceImpl implements IAccountService {

    public void getAllAccount() {
        System.out.println("执行了getAllAccount方法");
    }

    public void saveAccount() {
        System.out.println("执行了saveAccount方法");
    }

    public void updateAccount() {
        System.out.println("执行了updateAccount方法");
    }

    public void deleteAccount() {
        System.out.println("执行了deleteAccount方法");
    }
}

日志工具:

package com.jzt.util;

/**
 * 模拟日志记录,里面提供记录日志的公共方法
 */
public class LogUtil {

    /**
     * 用于打印日志,计划让其在切入点方法之前执行,切入点方法就是业务层方法。
     */
    public void printLog() {
        System.out.println("LogUtil类中的方法printLog()开始执行了。。。");
    }
}

2)基于xml的aop配置步骤

​ 1)把通知bean也交给spring来管理;

​ 2)使用<aop:config>标签表明开始aop配置;

​ 3)使用<aop:aspect>标签表明配置切面;

​ id属性:是给切面提供一个唯一标识;

​ ref属性:指定通知类bean的id;

​ 4)在<aop:aspect>标签内部使用对应的标签来配置对应的类型;

​ <aop:before>:表示配置前置通知;

​ method属性:用于指定切面中哪些方法是前置通知;

配置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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置accountService-->
    <bean id="accountService" class="com.jzt.service.impl.AccountServiceImpl"></bean>

    <!-- 配置日志工具类-->
    <bean id="logUtil" class="com.jzt.util.LogUtil"></bean>

    <!--
      **spring中基于xml的aop配置步骤:**
	    1)把通知bean也交给spring来管理;
	    2)使用<aop:config>标签表明开始aop配置;
	    3)使用<aop:aspect>标签表明配置切面;
				id属性:是给切面提供一个唯一标识;
				ref属性:指定通知类bean的id;
	    4)在<aop:aspect>标签内部使用对应的标签来配置对应的类型;
				<aop:before>:表示配置前置通知;
						method属性:用于指定切面中哪些方法是前置通知;
      -->

    <!-- 配置AOP-->
    <aop:config>
        <!-- 配置切面-->
        <aop:aspect id="logAdvice" ref="logUtil">
            <!--配置通知类型,并建立切入方法和切入点方法的关联-->
            <aop:before method="printLog" pointcut="execution(public void com.jzt.service.impl.AccountServiceImpl.getAllAccount())"></aop:before>
        </aop:aspect>
    </aop:config>
</beans>

测试:

package com.jzt.test;

import com.jzt.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringAOPTest {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
        IAccountService accountService = (IAccountService) ac.getBean("accountService");
        accountService.getAllAccount();
    }
}

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rlo1U9ya-1601116638534)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200925080120679.png)]

可以看到,spring通过配置文件实现了AOP的功能。

3)切入点表达式的写法

关键字:execution(表达式)

表达式:

	访问修饰符 返回值 报名.报名.报名...类名.方法名(参数列表)

标准的表达式写法:

	public void com.jzt.service.impl.AccountServiceImpl.getAllAccount()

访问修饰符可以省略:

	void com.jzt.service.impl.AccountServiceImpl.getAllAccount()

返回值可以使用通配符,表示任意返回值:

	* com.jzt.service.impl.AccountServiceImpl.getAllAccount()

包名可以使用通配符,表示任意包。但是有几级包,就要写几个*.:

	* *.*.*.*.AccountServiceImpl.getAllAccount()

包名可以使用…表示当前包及其子包:

	* *..AccountServiceImpl.getAllAccount()

类名和方法名都可以使用*来通配:

	* *..*.*()

参数列表:

​ 可以直接写数据类型:

​ 基本类型可以直接写名称: int

​ 引用类型写包名.类名方式: java.lang.String

​ 可以使用通配符表示任意类型,但是必须有参数;

​ 可以使用…表示有无参数均可,有参数可以是任意类型;

全通配写法:

	* *..*.*(..)

实际开发中切入点常用写法:

​ 切到业务层实现类下的所有方法

* com.jzt.service.impl.*.*(..)
4)四种常用的通知类型

在通知类中实现四个方法:

 /**
     * 前置通知:在切入点方法之前调用
     */
    public void beforePrintLog() {
        System.out.println("LogUtil类中的方法beforePrintLog()开始执行了。。。");
    }

    /**
     * 后置通知:在切入点方法正常执行之后调用,它与异常通知只能出现一个
     */
    public void afterReturnPrintLog() {
        System.out.println("LogUtil类中的方法afterReturnPrintLog()开始执行了。。。");
    }

    /**
     * 异常通知:在切入点方法出现异常之后执行
     */
    public void afterThrowingPrintLog() {
        System.out.println("LogUtil类中的方法afterThrowingPrintLog()开始执行了。。。");
    }

    /**
     * 最终通知:无论切入点方法能否正常执行,该方法都会在它之后执行
     */
    public void afterPrintLog() {
        System.out.println("LogUtil类中的方法afterPrintLog()开始执行了。。。");
    }

前置通知

​ 在切入点方法之前调用;对应xml标签:aop:before

<!--配置前置通知:在切入点方法之前调用-->
<aop:before method="beforePrintLog" pointcut="execution(* com.jzt.service.impl.*.*(..))"></aop:before>

后置通知

​ 在切入点方法正常执行之后调用,它与异常通知只能出现一个;对应xml标签:aop:after-returning

 <!--配置后置通知:在切入点方法正常执行之后调用,它与异常通知只能出现一个-->
<aop:after-returning method="afterReturnPrintLog" pointcut="execution(* com.jzt.service.impl.*.*(..))"></aop:after-returning>

异常通知

​ 在切入点方法出现异常之后执行;对应xml标签:aop:after-throwing

<!--配置异常通知:在切入点方法出现异常之后执行-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(* com.jzt.service.impl.*.*(..))"></aop:after-throwing>

最终通知

​ 无论切入点方法能否正常执行,该方法都会在它之后执行;aop:after

<!--配置最终通知:无论切入点方法能否正常执行,该方法都会在它之后执行-->
<aop:after method="afterPrintLog" pointcut="execution(* com.jzt.service.impl.*.*(..))"></aop:after>
5)通用切入点表达式

​ 上面通过xml配置四种通知时,每一个同志都要配置切入点表达式,其实我们可以通过aop:pointcut标签来配置通用切入点表达式。

​ 配置规则如下:

<aop:pointcut id="pt1" expression="execution(* com.jzt.service.impl.*.*(..))"/>

​ 配置切入点表达式,id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容。

​ 此标签可以写在aop:aspect标签内部,表示该通用切入点表达式只能在当前切面使用;

​ 此标签也可以写在aop:aspect标签外部,表示该切入点表达式对所有切面可用。(注意要在aop:aspect标签之前使用)

​ 具体配置如下:

<!-- 配置AOP-->
    <aop:config>
        <aop:pointcut id="pt1" expression="execution(* com.jzt.service.impl.*.*(..))"/>
        <!-- 配置切面-->
        <aop:aspect id="logAdvice" ref="logUtil">
            <!--配置通知类型,并建立切入方法和切入点方法的关联-->
            <!--配置前置通知:在切入点方法之前调用-->
            <aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
            <!--配置后置通知:在切入点方法正常执行之后调用,它与异常通知只能出现一个-->
            <aop:after-returning method="afterReturnPrintLog" pointcut-ref="pt1"></aop:after-returning>
            <!--配置异常通知:在切入点方法出现异常之后执行-->
            <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
            <!--配置最终通知:无论切入点方法能否正常执行,该方法都会在它之后执行-->
            <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
        </aop:aspect>
    </aop:config>
6)环绕通知

配置环绕通知:

在业务层中增加一个环绕通知方法:

/**
 * 环绕通知:
 */
public void arroundPrintLog(){
    System.out.println("LogUtil类中的方法arroundPrintLog()开始执行了。。。");
}

在xml中配置环绕通知:

 <!-- 配置AOP-->
    <aop:config>
        <aop:pointcut id="pt1" expression="execution(* com.jzt.service.impl.*.*(..))"/>
        <!-- 配置切面-->
        <aop:aspect id="logAdvice" ref="logUtil">
            <aop:around method="arroundPrintLog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>

问题:

​ 当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。

分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BMoUj0JB-1601116638534)(C:\Users\sj\AppData\Roaming\Typora\typora-user-images\image-20200926172519765.png)]

​ 通过对比环绕通知代码,发现动态代理的环绕通知有明确的切点方法调用,而我们的代码中没有。

解决:

​ spring框架为我们提供了一个接口,ProceedingJoinPoint,该接口提供了一个process()方法,此方法就相当于明确调用切入点方法。

​ 该接口可以作为环绕通知的方法参数,在程序设计时,spring框架会为我们提供该接口的实现类供我们使用。

spring中的环绕通知:

​ 他是spring框架为我们提供一种可以在代码中手动控制增强方法何时执行的一种方式。

 /**
     * 环绕通知:
     */
    public Object arroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){
        Object[] args = proceedingJoinPoint.getArgs();
        try {
            System.out.println("LogUtil类中的方法arroundPrintLog()开始执行了。。。前置");
            Object obj = proceedingJoinPoint.proceed(args);
            System.out.println("LogUtil类中的方法arroundPrintLog()开始执行了。。。后置");
            return obj;
        } catch (Throwable e) {
            System.out.println("LogUtil类中的方法arroundPrintLog()开始执行了。。。异常");
            throw new RuntimeException(e);
        } finally {
            System.out.println("LogUtil类中的方法arroundPrintLog()开始执行了。。。最终");
        }
    }
6.3.2实现基于注解的AOP配置

相关注解:

@Aspect

​ 作用:表示当前类是一个切面类。对应xml中aop:aspect标签。

​ 注意:使用注解方式实现AOP时,需要在xml中开启基于注解的AOP方式。

<!-- 开启aop注解方式,此步骤不能少,这样java类中的aop注解才会生效 -->
<aop:aspectj-autoproxy/>
@Before

​ 作用:表示该方法是一个前置通知方法。对应xml中的标签aop:before标签。

@AfterReturning

​ 作用:表示该方法是一个后置通知方法。对应xml中的标签aop:after-returning标签。

@AfterThrowing

​ 作用:表示该方法是一个异常通知方法。对应xml中的标签aop:after-throwing标签。

@After

​ 作用:表示该方法是一个最终通知方法。对应xml中的标签aop:after标签。

@Around

​ 作用:表示该方法是一个环绕通知方法。对应xml中的标签aop:around标签。

@pointcut

​ 作用:用于标注切入点表达式。对应xml中的标签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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--开启springAOP注解,这样aspect等注解才能生效-->
    <aop:aspectj-autoproxy/>

    <!-- 配置accountService-->
    <bean id="accountService" class="com.jzt.service.impl.AccountServiceImpl"></bean>

    <!-- 配置日志工具类-->
    <bean id="logUtil" class="com.jzt.util.LogUtil"></bean>

</beans>
package com.jzt.util;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * 模拟日志记录,里面提供记录日志的公共方法
 */
@Component("logUtil")
@Aspect
public class LogUtil {

    @Pointcut("execution(* com.jzt.service.impl.*.*(..))")
    private void pt1(){};

    /**
     * 前置通知:在切入点方法之前调用
     */
    @Before("pt1()")
    public void beforePrintLog() {
        System.out.println("LogUtil类中的方法beforePrintLog()开始执行了。。。");
    }

    /**
     * 后置通知:在切入点方法正常执行之后调用,它与异常通知只能出现一个
     */
    @AfterReturning("pt1()")
    public void afterReturnPrintLog() {
        System.out.println("LogUtil类中的方法afterReturnPrintLog()开始执行了。。。");
    }

    /**
     * 异常通知:在切入点方法出现异常之后执行
     */
    @AfterThrowing("pt1()")
    public void afterThrowingPrintLog() {
        System.out.println("LogUtil类中的方法afterThrowingPrintLog()开始执行了。。。");
    }

    /**
     * 最终通知:无论切入点方法能否正常执行,该方法都会在它之后执行
     */
    @After("pt1()")
    public void afterPrintLog() {
        System.out.println("LogUtil类中的方法afterPrintLog()开始执行了。。。");
    }


    /**
     * 环绕通知:
     */
    //@Around("pt1")
    public Object arroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){
        Object[] args = proceedingJoinPoint.getArgs();
        try {
            System.out.println("LogUtil类中的方法arroundPrintLog()开始执行了。。。前置");
            Object obj = proceedingJoinPoint.proceed(args);
            System.out.println("LogUtil类中的方法arroundPrintLog()开始执行了。。。后置");
            return obj;
        } catch (Throwable e) {
            System.out.println("LogUtil类中的方法arroundPrintLog()开始执行了。。。异常");
            throw new RuntimeException(e);
        } finally {
            System.out.println("LogUtil类中的方法arroundPrintLog()开始执行了。。。最终");
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值