Java:65-Spring实战以及AOP介绍

Spring实战以及AOP介绍

转账案例:
需求:
使用spring框架整合DBUtils技术,实现用户转账功能
基础功能:
步骤分析:
/*
1. 创建java项目,导入坐标
2. 编写Account实体类
3. 编写AccountDao接口和实现类
4. 编写AccountService接口和实现类
5. 编写spring核心配置文件
6. 编写测试代码
*/
创建java项目,导入坐标:
<dependencies>
    <dependency>
        <!--mysql驱动-->
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <dependency>
        <!--对应连接池-->
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.15</version>
    </dependency>
    <dependency>
        <!--使用连接池的-->
        <groupId>commons-dbutils</groupId>
        <artifactId>commons-dbutils</artifactId>
        <version>1.6</version>
    </dependency>
    <dependency>
        <!--IOC容器需要-->
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.5.RELEASE</version>
    </dependency>
    <dependency>
        <!--注解指定配置,需要与@Test一起-->
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.1.5.RELEASE</version>
    </dependency>
    <dependency>
        <!--@Test-->
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

编写Account实体类:
package com.lagou.domain;

/**
 *
 */
public class Account {
    private Integer id;
    private String name;
    private Double money;

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", money=" + money +
                '}';
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Double getMoney() {
        return money;
    }

    public void setMoney(Double money) {
        this.money = money;
    }
}

编写AccountDao接口和实现类 :
package com.lagou.dao;

/**
 *
 */
public interface AccountDao {

   //转出操作

    public void out(String outUser,double money);

   //转入操作
    public void in(String inUser,double money);

}

package com.lagou.dao.impl;

import com.lagou.dao.AccountDao;
import org.apache.commons.dbutils.QueryRunner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.sql.SQLException;

/**
 *
 */
@Repository("accountDao")  //生成该类实例到IOC容器中
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private QueryRunner queryRunner;

    @Override
    public void out(String outUser, double money) {

        String sql = "update account set money = money - ? where name = ?";
        try {
            queryRunner.update(sql, money, outUser); //没有指定连接,那么从连接池中拿取
            System.out.println(queryRunner);
            //输出对象,会提示一个小错误,但是不影响程序的运行
            //可以在url后面加上&useSSL=false来解决
            //是因为输出这个,触发了安全机制,需要加上&useSSL=false,一般默认为true(一般来说是版本问题,需要这样做)
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void in(String inUser, double money) {
        String sql = "update account set money = money + ? where name = ?";
        try {
            queryRunner.update(sql, money, inUser);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

编写AccountService接口和实现类:
package com.lagou.service;

/**
 *
 */
public interface AccountService {

    //转账方法
    public void transfer(String out, String in, Double money);


}

package com.lagou.service.impl;

import com.lagou.dao.AccountDao;
import com.lagou.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 *
 */
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    //转账方法
    //可以知道,service层不只是在dao层的方法的基础上进行单独接口操作(调用)和对应判断(接口外的代码)
    //也可以进行多个接口操作和对应判断
    //实际上本来就可以,只是我们通常这样做
    //即dao层只是提供接口,而service层是对接口的操作和对应判断
    @Override
    public void transfer(String outUser, String inUser, Double money) {

        //调用了减钱方法
        accountDao.out(outUser, money);
        //调用了加钱方法
        accountDao.in(inUser, money);


    }
}

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

    <!--注解扫描-->
    <context:component-scan base-package="com.lagou"></context:component-scan>

    <!--引入property-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>

    <!--配置DataSource-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!--配置QueryRunner-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>
</beans>
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///spring_db?characterEncoding=utf8
jdbc.username=root
jdbc.password=123456
编写测试代码:
package com.lagou.test;

import com.lagou.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 *
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test //测试方法,基本只能写void返回值,否则报错
    public void textTransfer() {
        accountService.transfer("tom", "jerry", 100d);
        //100d就是这个100是double类型,就如3.14f就是float类型
    }
}

问题分析:
上面的代码事务在dao层,转出转入操作都是一个独立的事务
因为dao层的方法,基本用来编写单个操作的,如转出操作和转入操作
使得service层也只能依次调用,而dao层之所以不合并方法
是为了需要合并中的单个方法时,可以使用(如只需要转入或者转出操作,但这样的业务通常都是一起,虽然也可以多建立方法)
但是方法越少越好(因为实现一次,就要重写一次,为了好维护)
所以实际开发,当有些业务逻辑需要合起来调用时,应该把业务逻辑放在service层,即控制在一个事务中,使得不能分开执行
代码演示:
  @Override
    public void transfer(String outUser, String inUser, Double money) {

        //调用了减钱方法
        accountDao.out(outUser, money);
        
        //int i = 1/0; 
        //若这里报错,后面加钱方法就不会运行
        //由于这两个是分开的,那么在数据库里,可以看到,只有减钱,没有加钱
        //在实际开放中是非常严重的
        
        //调用了加钱方法
        accountDao.in(inUser, money);


    }
从上面的问题出发,看如下解决方式
传统事务:
步骤分析:
/*
1. 编写线程绑定工具类   ThreadLocal
一般的连接若没有关闭,那么基本就是同一个连接,而不是新的连接(单线程情况下,是好的)
那么对于连接中,连接池和ThreadLocal的区别,为什么使用ThreadLocal,后面会有所解释
2. 编写事务管理器
3. 修改service层代码
4. 修改dao层代码
*/
编写线程绑定工具类:
package com.lagou.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

/**
 *连接工具类:从数据源中获取一个连接,并且将获取到的连接与线程进行绑定
 * ThreadLocal:线程内部的存储类,可以在指定的线程内来存储数据
 * 使用类似于map集合的操作进行存储,key为线程,就是当前线程,也就是Thread.currentThread()
 * value为连接(这里是连接)
 */
@Component
public class ConnectionUtils {

    @Autowired
    private DataSource dataSource;

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

    /*
    获取当前线程上绑定的连接:如果获取到的连接为空
    那么就要从数据源中获取连接,并且放到ThreadLocal中(绑定到当前线程)
    key不变,也就是当前线程Thread.currentThread()
    所以得到时,就是get()方法,没有指定key,因为底层帮你指定了
    设置时,就是set(对应连接),这里是连接,因为泛型,也不用指定key,底层也指定了
    代码如下:
    底层get方法如下
     public T get() {
        Thread t = Thread.currentThread(); 这就是key
        ThreadLocalMap map = getMap(t); 得到类似于map集合的key和value
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this); 得到对应key的value值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;  返回value的值
            }
        }
        return setInitialValue();
    }
    底层set方法如下
     public void set(T value) {
        Thread t = Thread.currentThread(); 这就是key
        ThreadLocalMap map = getMap(t); 得到类似于map集合的key和value
        if (map != null) {
            map.set(this, value); 设置key和value
        } else {
            createMap(t, value);
        }
    }

    也就是说,在同一个线程,那么连接一定是相同的,即一个线程一个连接
    这样就基本导致了,一个用户的连接基本不会变
     */
    public Connection getThreadConnection(){

        //先从ThreadLocal上获取连接
        Connection conn = threadLocal.get();

        //判断是否为空,即当前线程中是否有连接
        if(conn == null){

            //从数据源中获取一个连接,并且放到ThreadLocal中
            try {
                //不为null
                conn= dataSource.getConnection();

                threadLocal.set(conn);

            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        return conn;
    }

    //解除当前线程的连接绑定
    public void removeThreadConnection(){
        threadLocal.remove();
        
        /*
        底层remove方法如下
         public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread()); 得到类似于map集合的key和value
         if (m != null) {
             m.remove(this); 删除对应key
         }
     }
         */
    }
}

连接池和ThreadLocal的区别:
//接下来,我们解释为什么不用数据库连接池,而是使用ThreadLocal线程绑定
/*
在这之前,我们需要明白,在数据库里,不同的会话,就是不同的事务,而会话,就是一个连接
对于不理想的连接池(下面有括号里的说明):
我们主要是使用他帮我们创建好的连接,那么这个连接也就不会关闭,那么会有如下情况:
在使用事务时:
同一时间和不同时间(可以出现不同连接)和不同地方(可以出现不同连接),单个线程,那么事务操作,没有什么问题
因为一个线程无论是否是一个连接,再怎么操作,基本也不会出现事务的不合理
同一时间,多个线程,基本一定不是同一个连接,也就基本也不会出现事务的不合理
不同时间和地方(可以出现不同连接),多个线程,假如
可能出现的连接池:不是并发情况下的获取连接
第一个线程正在进行操作(这个操作要很久,插入很多数据)时,归还了连接(并没有关闭连接,且没有设置自动提交)
第二个线程正好得到了这个连接(也就是从连接池中拿),再次使用事务,直接提交(第一个线程还没插入完)
由于是同一个连接,那么可以发现,第一个线程插入不了了(提交了,被中断,插入数据不完善)
超级不理想的连接池:在并发下的获取连接
同时进入,类似于懒汉式的线程不安全的操作,可能会没有对应锁,但一般都会有锁
那么就算设置了自动提交
也会使得其中一个连接提交或者关闭归还自动提交后
另外一个线程还没有操作完(用户有不同的行为的,即可能有些用户慢一点,就会出现这种情况)
很明显,有些数据库连接池,在多个线程中,有可能会是同一个连接的情况,会出现事务的不合理
主要是会再去连接池中拿,导致同一个连接
虽然是可能,但还是有机会
而ThreadLocal线程绑定,那么我们将线程与连接当作联系,那么就出现了,一个线程,必定只有对应的一个连接
那么就基本不会出现事务的不合理
但对于上面出现的连接池,没有设置自动提交,或者并发获取的连接,也是会出现事务的不合理,但通常都会自动提交

所以现在基本不会出现上述不理想的连接池
如果出现了,就算是使用ThreadLocal线程绑定
也是会出现事务的不合理,没有设置自动提交或并发获取连接(并发基本不会出现,即加了锁)
即多个线程获取同一个连接没有关系,主要是对应的没有自动提交(设置了获得同一连接时,一定的相对干净的)
也就是说现在基本都是正常的连接池(加锁,自动提交)
所以ThreadLocal线程绑定的主要功能并不是上述功能,主要是下面的功能,请看如下
我们知道一个连接对应一个事务,在单线程的情况下,是会出现获得不同的连接的
一般的dao层的代码我们都是一个连接来使用全部的方法
但是若一个dao层方法,只对应一个连接(这是可能的情况,大多数情况下,可能会出现)
比如前面使用QueryRunner的对应方法里,没有指定连接,那么就是去连接池里获得连接的
那么就会出现,两个不同的事务,于是,借助上面的转出转入操作,那么转出操作的事务和转入操作的事务,不是同一个
也就是说,又变成了两个独立的事务,很明显,又回到了上面的情况,只是方式不同而已
可能转出提交了,而中间正好报错,导致没有转入,或者转入回滚了,这些情况
那么为了防止不同的dao层方法,获得连接,就出现了新的操作
即就有了ThreadLocal线程绑定,这样会先判断
在使用方法时,是否为同一个线程后,在考虑去连接池里拿连接,一般都是正常连接池


所以现在的数据库连接池优化上,底层一般也是使用ThreadLocal这个方式的
注意:对于大多数连接池,若没有指定连接的话,基本都是获取连接池的连接,来进行操作,由于基本使用这个方式,即基本会出现不同连接,当然,可能有些连接池不是,并且对应的可能有过期时间的,或者说需要我们手动的进行设置,这些都是连接池自身的手段,我们开发者并不能保证是哪种情况,所以我们基本都会统一的认为他们没有使用,但一般的,我们常用的连接池一般都不会默认这样的操作的,所以使用连接池后,我们通常需要操作类似的ThreadLocal优化


对于数据库连接池的归还问题:是一个双向链表存放对应连接,即头部获取,尾部归还(归还连接池)
这就是不在并发情况下的获取相同连接的原理,正好得到了这一个位置的连接



简单一句话,在方法分开的地方,无论你怎么去连接池里拿连接,得到的都是一个连接
*/
ThreadLocal使用图解:

在这里插入图片描述

最后得到的都是同一个对象,也就是同一个连接,与引用的不同没有关系(不同引用可以指向同一个对象)
因为执行方法的,从来不是引用,而是对象,只是用引用来代替而已
编写事务管理器:
package com.lagou.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.sql.Connection;

/**
 *事务管理工具类:包含:开启事务,提交事务,回滚事务,释放资源
 */
@Component
public class TransactionManager {

    @Autowired
    private ConnectionUtils connectionUtils;

    /*
     * 开启事务
     */
    public void beginTransaction(){

            Connection threadConnection = connectionUtils.getThreadConnection();
        try {
            //开启手动事务,也就是关闭自动提交
            threadConnection.setAutoCommit(false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /*
     * 提交事务
     */
    public void commit(){

            Connection threadConnection = connectionUtils.getThreadConnection();
        try {
            //提交事务
            threadConnection.commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /*
    回滚事务
     */
    public void rollback(){

            Connection threadConnection = connectionUtils.getThreadConnection();
        try {
            //回滚事务
            threadConnection.rollback();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
     /*
     释放资源
     */
    public void release(){

        //将手动事务改成自动提交事务,因为我已经不操作了,当然这里实际上我们并没有实际的手动开启,只是通过设置false来开启的,一般手动的开始与是否设置自动是毫无相关的,如果你手动的开始,那么你是否设置自动都是按照你手动开启来操作的
        Connection threadConnection = connectionUtils.getThreadConnection();
        try {
            threadConnection.setAutoCommit(true);

            //将连接归还到连接池中
            connectionUtils.getThreadConnection().close();

            //解除线程绑定,就是删除对应key
            connectionUtils.removeThreadConnection();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }



    }



修改service层代码:
package com.lagou.service.impl;

import com.lagou.dao.AccountDao;
import com.lagou.service.AccountService;
import com.lagou.utils.TransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 *
 */
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Autowired
    private TransactionManager transactionManager;

    @Override
    public void transfer(String outUser, String inUser, Double money) {

        //手动开启事务:调用事务管理器类中的开启事务方法
        transactionManager.beginTransaction();
        try {
            //调用了减钱方法
            accountDao.out(outUser, money);
            
            //int i = 1/0;  这时你就可以通过这个来实验加对应参数连接和不加的区别了

            //调用了加钱方法
            accountDao.in(inUser, money);

            //手动提交事务
            transactionManager.commit();
        } catch (Exception e) {
            e.printStackTrace();
            //手动回滚事务
            transactionManager.rollback();
        }finally{
            //手动释放资源
            transactionManager.release();
        }
        //可以看出,sql的事务操作在实际应用上的重大作用
    }
}

修改dao层代码 :
package com.lagou.dao.impl;

import com.lagou.dao.AccountDao;
import com.lagou.utils.ConnectionUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.sql.SQLException;

/**
 *
 */
@Repository("accountDao")  //生成该类实例到IOC容器中
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private QueryRunner queryRunner;

    @Autowired
    private ConnectionUtils connectionUtils;

    @Override
    public void out(String outUser, double money) {

        String sql = "update account set money = money - ? where name = ?";
        try {
            queryRunner.update(connectionUtils.getThreadConnection(), sql, money, outUser);
            //指定了连接,那么就不会去连接池里获取连接,使得基本出现不同的事务了(很小几率获得同一连接)
            //因为归还,但就算获得了,基本也是自动提交的,所以还是不要去碰这个运气
            System.out.println(queryRunner);
            //输出对象,会提示一个小错误,但是不影响程序的运行
            //可以在url后面加上&useSSL=false来解决
            //是因为输出这个,触发了安全机制,需要加上&useSSL=false
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void in(String inUser, double money) {
        String sql = "update account set money = money + ? where name = ?";
        try {
            queryRunner.update(connectionUtils.getThreadConnection(), sql, money, inUser);

        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

问题分析 :
上面代码,通过对业务层改造,已经可以实现事务控制了,但是由于我们添加了事务控制
也产生了一个新的问题: 业务层方法变得臃肿了,里面充斥着很多重复代码
并且业务层方法和事务控制方法耦合了,违背了面向对象的开发思想
Proxy优化转账案例:
我们可以将业务代码和事务代码进行拆分,通过动态代理的方式,对业务方法进行事务的增强
这样就不会对业务层产生影响,解决了耦合性的问题啦!
常用的动态代理技术:
JDK 代理:
基于接口的动态代理技术·:利用拦截器(必须实现invocationHandler)加上反射机制生成一个代理接口的匿名类
在调用具体方法前调用InvokeHandler来处理,从而实现方法增强
CGLIB代理:
基于父类的动态代理技术:动态生成一个要代理的子类,子类重写要代理的类的所有不是final的方法
不重写也没关系,只是使用的是父类方法的操作然后增强
在子类中采用方法拦截技术拦截所有的父类方法的调用,顺势织入横切逻辑,对方法进行增强

在这里插入图片描述

相当于其他类帮你调用方法(也有自己的一些调用,这些调用,就可以说是增强)
JDK动态代理方式:
Jdk工厂类:
package com.lagou.proxy;

import com.lagou.service.AccountService;
import com.lagou.utils.TransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

/**
 *JDK动态代理工厂类
 */
@Component
public class JDKProxyFactory {

    @Autowired
    private AccountService accountService;

    @Autowired
    private TransactionManager transactionManager;

    /*
    采用JDk动态代理技术来生成目标类的代理对象,即使用代理类Proxy类来生成代理对象
     */
    public AccountService createAccountServiceJdkProxy(){
        /*
        Proxy.newProxyInstance()方法的参数介绍:
        ClassLoader loader,
        类加载器,反射基本也是可以是类加载器来操作的(他可以操作Class,一般也有其他功能,所以这里是类加载器),这里可以借助被代理对象获取到类加载器,由于大多数的类加载器是一样的,所以这个参数基本可以随便使用某个类的加载器(但是呢,建议可以加载到,否则可能加载失败的,虽然通常基本都是系统加载器加载的,可以参考104章博客对类加载器的说明)
        Class<?>[] interfaces,
        对应的接口是被实现的,如果没有被实现那么不行,即会报错的
        被代理类所需要实现的全部接口(一般用来指定代理类的实现接口的,如果给出的接口是对方没有的,或者对方没有实现接口,那么会报错(我又不能继承,自然不能这样哦,cglib则没有这样的限制),一般单纯的打印代理类也会执行一次,而不用是执行对应的接口,这是一个特别的地方,实际上是因为默认public java.lang.String java.lang.Object.toString()(对应的method打印信息),使得的,也就是为什么打印本身会操作,即的确是一个特殊的地方(因为他是类,注意即可),所以也最好不要在里面再次打印proxy,否则会一直循环,使得触发jvm结束的,具体为什么可以百度),所以是数组,注意是接口的Class类型哦,一般这里代表返回值类型(因为代理类与他相关,实现他)
        InvocationHandler h
        当代理对象调用接口中的任意方法时,那么都会执行InvocationHandler的invoke方法

        反射中,只要有方法名,就可以通过invoke方法来调用对应方法
        如Object invoke(Object obj,Object... args)
        使用对象obj来调用此Method对象所表示的成员方法,实参传递args
        具体操作是根据反射出来的类实例,获得Method类,然后根据方法名称,调用反射出来的类的invoke方法
        那么这个反射出来的对应类也就执行了对应方法名称的方法

        那么根据参数,可以知道,通过类加载器来反射出对象(代理),然后根据接口方法名称的数组得到对应名称
        之后调用对应的invoke方法进行执行对应名称的方法
        */

        //getClass()得到的是对应对象的Class对象,因为是对象来调用方法的
        //这点必须理解,后面是以这点为中心
        //而newInstance方法,是创建参数的对象实例,所以不能是接口或者抽象类
        //Class<?>[] getInterfaces(),获取实现的所有接口,Class调用
        //那么就只知道对应对象的实现的接口信息了,一般没去用
        //一般返回格式是,如interface java.lang.Comparable interface java.io.Serializable
        //一般来说该位置代表返回值类型,因为是接口,所以可以是多种,都操作对应的类的(因为他们都是被实现)

        //之所以可以指定对应类型,参数已经指定好了,因为都是对应的对象
        //这就是相当于我们创建了一个一样的对象,帮我们操作原对象的方法
        AccountService o = (AccountService) 
            Proxy.newProxyInstance(accountService.getClass().getClassLoader(), 
                                   accountService.getClass().getInterfaces(), new 
                                   InvocationHandler() { //他返回Object,自然需要强转,当然也可以不强转,只是可能没有对应的提示(使得编译期过不了而已)

            //这里参数名称解释
            //proxy:当前的代理对象引用,一般没有去用,反射好的对象,基本是全局的
            //method:被调用的目标方法引用,自己调用时,出现的对应Method对象
            //args:被调用的目标方法所用到的参数(方法的参数列表,他也正好是Object数组,那么都可以得到),自己调用时,出现的对应参数
            //可以发现,所有的一切操作都在这里执行
            //也就是说proxy是反射出来的实例
            //method就是这个实例的对应Method对象,已经弄好了对应名称,每次调用都是其中一个
            //而args就是对应对象实现的接口格式信息,即包括接口方法的对应参数,每次调用都是其中一组参数
            //之所以没有指定,是进行拦截了自己手动对象调用的信息,而放入的,也就是自己进行调用的方法
            //所以method的名称就是拦截的原信息
            //而args也是拦截的对应参数信息
            //那么对象就可以通过invoke方法,在有methed的情况下(一般来说,如果对应的没有该接口的实现类,比如下面的AccountServiceImpl,那么执行invoke一般会错误,代表了没有实现类来操作,因为是拦截的,所以存在多个实现类,那么method的invoke自然不同,这里对后面的Cglib基本也是一样的说明的)
            //通过方法名称,使用invoke来调用对应名称的接口方法了,就使得出现动态代理,即可以增强方法
            //在下面方法的地方可以再次增加其他操作,即增强的意思
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				
                //手动开启事务:调用事务管理器类中的开启事务方法
                transactionManager.beginTransaction();
                try {
                //System.out.println("目标方法执行之前动态进行了方法增强");
             //被代理的对象的方法执行,我们使用代理对象进行操作时拦截对应信息,然后根据这些信息进行反射执行
                method.invoke(accountService, args); //考虑void是否返回
                //增强的一部分内容
                //System.out.println(1);

                //手动提交事务
                transactionManager.commit();
                } catch (Exception e) {
                    e.printStackTrace();
                    //手动回滚事务
                    transactionManager.rollback();
                }finally{
                    //手动释放资源
                    transactionManager.release();
                }

//只要进入这里,那么返回值就是他,也就是说,只要被拦截,那么你执行的结果返回值就是这个
                return null; //这里我们主要用到上面的invoke方法,这个返回值是什么,基本可以不用管
                 //具体的上面的AccountService o,基本是唯一的,但是在我们执行方法时,返回值则是这个return null,虽然这里是null,即不操作返回值,或者返回值为null,如果后面执行的接口是void,那么其返回值自然不会进行赋值,否则会进行赋值,因为这里只是得到结果,是否赋值还需要看其接口的返回值类型来判断(一般是反射技术),判断其上面一层的代码是否操作返回值(可能代理存在多个判断和方法,这里了解即可)
            }
        });

        return o;
    }

    //所以说,静态代理,是在类里进行放入其他类,而形成增强,而这里是通过反射进行方法确定,而进行增强
    //静态放入其他类,这里我们自己创建一个类,当作拦截对象(也是对应的类型)
    //再根据拦截信息,来操作反射的真正对象的方法(对应方法中,可以加上其他增强)
    //即静态代理创建多个代理类,有不同的增强,而动态代理只有一个增强,但只要一个类或者不需要类即可
    //因为动态的操纵方法来增强所以是动态的,因为需要我们手动的创建类,所以是静态的(代表静止的,而不是静态变量的意思)
    //那么有个问题,动态代理好还是静态代理好,实际上各有好处,其实动态代理只需要继续创建代理就可以完成静态代理的增强,而不用创建类,但是静态代理的唯一好处就是我比较随意,我可以在里面操作两个类或者多个类,而不是你只能操作一个方法,或者有某些限制等等,简单来说在扩展上静态友好,但是在实现上,动态友好,并且大多数我们并不需要进行扩展,而是来解决相同代码问题,所以我们通常都会使用动态代理,而不是静态代理(简称代理模式)

    //最后注意:JDK代理只能操作接口,若操作类就会报错,那是因为对应的对象已经继承了Proxy了
    //那么只能实现自己这个接口(在文件里先写好,然后进行运行)
    //因为基本不可能在运行时修改代码
    //所以使得我们强转时可以变成对应接口类型
     //其字节码相当于这样:public final class $Proxy0 extends Proxy implements AccountService {
    //还需要注意:对应的接口是被实现的,如果没有被实现那么不行,即会报错的

//还有,具体如果invoke的返回值是随便指定的类型(基本类型不会,和Object的不会,实现接口的实现类里面的返回值类型不会(自身不行),其他的通常会,或者在某些版本下面,作为实现参数的接口可能也不会,具体看版本),通常来说,会有警告(代理类调试显示的警告),也会影响程序后续执行,而不会的虽然有警告,但是不会影响程序后续执行

//警告的出现通常在于method.invoke(accountService, args);,如果强转了或者没有操作Object的返回,就会有警告,这是规定


}

//这里要注意,由于自身能打印(toString也会进行触发,所以对应的invoke里面的参数不要写当前代理类(他也是存在对应方法的),否则是会无限循环的)

package com.lagou.service.impl;

import com.lagou.dao.AccountDao;
import com.lagou.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 *
 */
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;


    @Override
    public void transfer(String outUser, String inUser, Double money) {



            //调用了减钱方法
            accountDao.out(outUser, money);

            //int i = 1/0;

            //调用了加钱方法
            accountDao.in(inUser, money);


    }
}

测试代码:
 /*
    测试JDK动态代理优化转账案例
     */
    @Test
    public void testTransferProxyJDK(){

        System.out.println("被代理类的transfer执行了");

        //当前返回的实际上是AccountService的一个代理对象proxy
        AccountService accountServiceJdkProxy = jdkProxyFactory.createAccountServiceJdkProxy();

        //代理对象Proxy调用接口中的任意方法时,都会执行底层的invoke方法
        //因为调用方法的实际操作不是引用,而是对象,所以这里说Proxy对象,因为对应对象继承Proxy类
        accountServiceJdkProxy.transfer("tom", "jerry", 100d);

    }
/*
可以发现,我们原来执行accountServiceJdkProxy.transfer("tom", "jerry", 100d);代码时
很多的事务代码不用写了,若对应原来代码的话来说,若这样的transfer方法一多,那么对应事务代码一定会多
所以我们使得在执行transfer方法时,通过反射在某个方法里执行(拦截)
而这个方法里进行了增强,那么每个这样的方法,就不需要对应的事务代码了,且是动态的,也就是比静态的扩展性高
静态的需要对应类,虽然直接执行也是可以,但是必须手动写对应的方法名称,且一般需要一致
而动态的通过拦截,就不需要手动写对应方法了,所以扩展性高

但是在这里要注意:但凡扩展性高的,基本底层操作也会多点,所以运行效率也会慢一些,但开发效率快

代理对原来的方法进行改变,使得我们使用的是代理的,即代理增强
*/
CGLIB动态代理方式:
Cglib工厂类:
package com.lagou.proxy;

import com.lagou.service.AccountService;
import com.lagou.utils.TransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
//cglib一般是需要依赖的,对应的依赖一般在spring-core里面,而spring-context里面就存在该依赖,所以看起来spring不用引入cglib依赖的
/*
 该类就是采用cglib动态代理来对目标类(AccountServiceImpl)进行方法(transfer)的动态增强(添加上事务控制)
 */
@Component
public class CglibProxyFactory {

    @Autowired
    private AccountService accountService;

    @Autowired
    private TransactionManager transactionManager;

//同样的cglib打印本身(toString也会执行除非,即也是特殊的),但是这里还有一个参数他是直接打印当前代理对象,而不是操作toString的,即methodProxy,是与jdk代理是不同之处,注意,只要被拦截,那么对应的返回值就是intercept的返回值,jdk代理也是这样
    public AccountService createAccountServiceCglibProxy(){
        // 编写cglib对应的API来生成代理对象进行返回
        // 参数1 : 目标类的字节码对象
        // 参数2:  动作类,当代理对象调用目标对象中原方法时,那么会执行intercept方法
        AccountService accountServiceproxy = (AccountService) 
            Enhancer.create(accountService.getClass(), new MethodInterceptor() {

            // o : 代表生成的代理对象,一般没去用,反射到的对象一般是全局的
            // method:调用目标方法的引用,拦截到的
            // objects:方法入参,拦截到的
            // methodProxy:代理方法,一般有对应实现接口的信息,一般没去用
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy 
                                    methodProxy) throws Throwable {

                try {
                    // 手动开启事务:调用事务管理器类中的开启事务方法
                    transactionManager.beginTransaction();

                    method.invoke(accountService, objects);

                    transactionManager.commit();
                } catch (Exception e) {
                    e.printStackTrace();
                    // 手动回滚事务
                    transactionManager.rollback();
                } finally {
                    // 手动释放资源
                    transactionManager.release();
                }


                return null;
            }
        });
        return accountServiceproxy;


    }



}

//这里要注意,由于自身能打印(toString也会进行触发,所以对应的invoke里面的参数不要写当前代理类(他也是存在对应方法的),否则是会无限循环的)

测试代码:
    /*
   测试Cglib动态代理优化转账案例
    */
    @Test
    public void testTransferProxyCglib(){

        //accountServiceCglibProxy:proxy
        AccountService accountServiceCglibProxy = cglibProxyFactory.createAccountServiceCglibProxy();

        accountServiceCglibProxy.transfer("tom", "jerry", 100d);


    }
与JDK动态代理不同,Cglib动态代理的返回值可以是接口
也可以是对应类(需要符合父类指向子类或者本身的规则),即指向规则
而JDK动态代理只可以返回接口,因为JDK动态代理继承了一个类,所以不可再次继承
而Cglib动态代理则没有继承,所以可以继承类,简单来说,JDK是看第二个参数的接口来作为返回值,且他只能是接口,否则报错,而cglib是看第一个参数的类型来作为返回值,若对应的是接口,那么代理类实现他,如果是类,那么继承他(前提是可以),所以返回的代理类就看你如何获取了
可以认为是通过分析进行文件添加(通常考虑是jvm的自身处理,具体考虑动态生成字节码文件并处理,所以还是文件,一般是jvm,或者java自身操作的,考虑类加载器的操作),或者是反射来操作,但他们都是通过拦截来进行操作的
实际上在文件添加时,由于实现或继承了对应接口或者类,在调用对应方法时
那么就会有对应方法的信息(添加时封装好的),即这些信息就可以说是拦截的信息,然后赋值使用
JDK动态代理由于代理继承了类,那么就要对应需要代理的类实现一个接口
而Cglib动态代理虽然可以返回接口和对应类,但他们对应的final的重写不了(为什么这样说,这就要看代理的其他实现方式了,具体可以百度)
JDK动态代理和Cglib动态代理他们都有父子类关系
实际上代理之所以可以操作就在于拦截,也就是说,如果你会拦截,那么你就可以手写动态代理,具体可以百度
其中他们之所以打印自身会拦截,具体是保证不会给其他人直接使用
初识AOP :
什么是AOP:
AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程
AOP 是 OOP(面向对象编程) 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容
利用AOP可以对业务逻辑的各个部分进行隔离
从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率
这样做的好处是:
在程序运行期间,在不修改源码的情况下对方法进行功能增强
逻辑清晰,开发核心业务的时候,不必关注增强业务的代码
减少重复代码,提高开发效率,便于后期维护
AOP底层实现:
实际上,AOP 的底层是通过 Spring 提供的的动态代理技术实现的
在运行期间Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入
在去调用目标对象的方法,从而完成功能的增强
AOP相关术语:
Spring 的 AOP 实现底层就是对上面的动态代理的代码进行了封装,封装后我们只需要对需要关注的部分进行代码编写
并通过配置的方式完成指定目标的方法增强
在正式讲解 AOP 的操作之前,我们必须理解 AOP 的相关术语
常用的术语如下:
/*
Target(目标对象):代理的目标对象  也就是前面说的AccountServiceImpl,在这里也可以叫做AccountService
 
Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类
 
Joinpoint(连接点):所谓连接点是指那些可以被拦截到的点。在spring中,这些点指的是方法,因为
spring只支持方法类型的连接点
 
Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义(代表一个)
 
Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知
分类:前置通知、后置通知、异常通知、最终通知、环绕通知(可以说是前面四个的整体,即整体的通知)
这个整体也包括执行方法,也就是说,使用环绕通知的话,执行方法需要自己手动执行了(相当于替代执行方法的位置)
 
Aspect(切面):是切入点和通知(引介)的结合,但是它也有时指定是增强的代码所关注的⽅⾯,把这些相关的增强代码定义到⼀个类中,这个类就是切⾯类,例如,事务切⾯,它⾥⾯定义的⽅法就是和事务相关的,像开启事务,提交事务,回滚事务等等
 
Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织
入,而AspectJ采用编译期织入和类装载期织入
*/
图解:

在这里插入图片描述

在这里插入图片描述

对应理解的代码如下:
package com.lagou.proxy;

import com.lagou.service.AccountService;
import com.lagou.utils.TransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

/**
 *JDK动态代理工厂类
 */
@Component
public class JDKProxyFactory {

    @Autowired
    private AccountService accountService;

    @Autowired
    private TransactionManager transactionManager;

    public AccountService createAccountServiceJdkProxy(){

        AccountService o = (AccountService) 
            Proxy.newProxyInstance(accountService.getClass().getClassLoader(), 
                                   accountService.getClass().getInterfaces(), new InvocationHandler() 
                                   {

            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {


                try {
                    //可以通过Method的getName()方法得到对应的方法名称
                    if(method.getName().equals("transfer")) {
                        System.out.println("进行了前置增强");
                        //手动开启事务:调用事务管理器类中的开启事务方法
                        transactionManager.beginTransaction();

                        method.invoke(accountService, args);
                        //手动提交事务
                        transactionManager.commit();
                        System.out.println("进行了后置增强");
                    }else{
                        method.invoke(accountService, args);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    //手动回滚事务
                    transactionManager.rollback();
                }finally{
                    //手动释放资源
                    transactionManager.release();
                }

                return null;
            }
        });

        return o;
    }


}

package com.lagou.service;

/**
 *
 */
public interface AccountService {

    //转账方法
    public void transfer(String out, String in, Double money);

    public void save();

    public void update();

    public void delete();



}

package com.lagou.service.impl;

import com.lagou.dao.AccountDao;
import com.lagou.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 *
 */
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;


    @Override
    public void transfer(String outUser, String inUser, Double money) {



            //调用了减钱方法
            accountDao.out(outUser, money);

            //int i = 1/0;

            //调用了加钱方法
            accountDao.in(inUser, money);


    }

    @Override
    public void save() {
        System.out.println("save方法");
    }

    @Override
    public void update() {

        System.out.println("update方法");
    }

    @Override
    public void delete() {
        System.out.println("delete方法");

    }
}

  /*
    测试JDK动态代理优化转账案例
     */
    @Test
    public void testTransferProxyJDK(){

        System.out.println("被代理类的transfer执行了");

        //当前返回的实际上是AccountService的一个代理对象proxy
        AccountService accountServiceJdkProxy = jdkProxyFactory.createAccountServiceJdkProxy();

      
        //accountServiceJdkProxy.transfer("tom", "jerry", 100d);

        accountServiceJdkProxy.save();
    }
AOP开发明确事项:
开发阶段(我们做的)
编写核心业务代码(目标类的目标方法) 切入点
把公用代码抽取出来,制作成通知(增强功能方法) 通知
在配置文件中,声明切入点与通知间的关系,即切面
运行阶段(Spring框架完成的):
Spring 框架监控切入点方法的执行
一旦监控到切入点方法被运行,拦截,并使用代理机制,动态创建目标对象的代理对象来进行执行方法
根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行
底层代理实现:
在 Spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式
当bean实现接口时,会用JDK代理模式
当bean没有实现接口,用cglib实现
也可以强制使用cglib,在spring配置中加入如下配置(后面会讲到)
<aop:aspectj-autoproxy proxy-target-class="true"/> <!--当然,也可以强制使用jdk,即这里设置为false即可-->
知识小结:
/*
aop:面向切面编程
aop底层实现:基于JDK的动态代理 和 基于Cglib的动态代理
aop的重点概念:
 Pointcut(切入点):真正被增强的方法
 Advice(通知/ 增强):封装增强业务逻辑的方法
 Aspect(切面):切点+通知
 Weaving(织入):将切点与通知结合,产生代理对象的过程
*/
基于XML的AOP开发:
快速入门:
步骤分析:
/*
1. 创建java项目,导入AOP相关坐标
2. 创建目标接口和目标实现类(定义切入点)
3. 创建通知类及方法(定义通知)
4. 将目标类和通知类对象创建权交给spring
5. 在核心配置文件中配置织入关系,及切面
6. 编写测试代码

*/
创建java项目,导入AOP相关坐标:
<dependencies>
    <!--导入spring的context坐标,由于对应aop的命名空间和约束空间起作用,aop没有对应依赖
当然,程序上context也是依赖aop,因为aop基本是需要context的(该依赖里面有aop,所以导入这个即可)-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.5.RELEASE</version>
    </dependency>
    <!-- aspectj的织入(切点表达式需要用到该jar包) -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.13</version>
    </dependency>
    <!--spring整合junit-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId> 
        <version>5.1.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

创建目标接口和目标实现类:
package com.lagou.service;

/**
 *
 */
public interface AccountService {

    /*
    目标方法:(切入点:要进行拦截增强的方法)
     */
    public void transfer();
}

package com.lagou.service.impl;

import com.lagou.service.AccountService;

/**
 *
 */
public class AccountServiceImpl implements AccountService {

    /*
    目标方法:(切入点:要进行拦截增强的方法)
     */
    @Override
    public void transfer() {
        System.out.println("转账方法执行了");
    }
}

创建通知类:
package com.lagou.advice;

/**
 *通知类
 */
public class MyAdvice {

    public void before() {
        System.out.println("前置通知执行了");
    }
}

将目标类和通知类对象创建权交给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: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会判断的,即不加就会报错
-->
    

    <!--目标类放入IOC容器-->
<bean id="accountService" class="com.lagou.service.impl.AccountServiceImpl"></bean>

    <!--通知类放入IOC容器-->
<bean id="myAdvice" class="com.lagou.advice.MyAdvice"></bean>
    
    
    
<!--
execution([修饰符] 返回值类型 包名.类名.方法名(参数))  []包括的是可以省略的   若对应参数的String类型,那么
execution(public void com.lagou.service.impl.AccountServiceImpl.transfer(java.lang.String)) 这样写
-->

    <!--AOP配置-->
    <aop:config>
        <!--配置切面:切入点+通知-->
        <aop:aspect ref="myAdvice"> <!--ref指定通知的类名位置,即上面的id-->
            <aop:before method="before" 
                        pointcut="execution(void 
                                  com.lagou.service.impl.AccountServiceImpl.transfer())"/>
            <!--
            aop:before:将通知放在哪个地方,这里是前置
            method:操作哪个通知方法
            pointcut:操作的切入点的方法,需要对应的格式,即execution后面是对应格式,一般是当ioc的类执行时,会操作拦截代理增强(当然,可能每次都是代理,但是通常是因为这里找到了才会操作代理的,即给他变成代理对象,一般只要有aop:config就会,那么后面的位置就是判断而已,来使得是否处理对应方法的执行,这里面是很多判断的)
            其中public这个位置是修饰符,一般省略不写(也就是默认为public),若写成private或者protected
            则监听不到对应切入点方法,也就没有进行动态代理了,因为接口默认为public,所以不一样就找不到
            后面的void这个代表返回值,一般就是void不变,且需要与对应方法返回值一样
		   否则也是监听不到对应切入点方法,也就没有动态代理了
		   所以上面两个没找到,那么就是普通的方法执行,没有被拦截
		   再后面就是类路径名,加上对应方法调用
            当我们读取这个配置文件时,先将全部读取,其中aop:config这个标签也是全部读取
            但是他是先放入对应对象,然后操作具体内容,不会放入IOC容器,且是一个全局的操作
            当读取到aop:aspect标签时,根据属性ref获得对应实例的对象并进行赋值
            然后再根据aop:before标签的method的这个名称进行反射调用方法(这个先不执行)
            还需要看后面的pointcut标签,通过读取属性,可以得到切入点的方法
            以及对应的反射出来的实例,和方法名称调用,操作内容时,在这里进行监听
		   若有一样的方法,那么进行拦截,然后进行下面操作(主要是使用对应实例的方法,自己创建的不会)
		   也就是说AOP基本只会操作IOC容器的实例,也就是监听IOC容器的实例
            由于这时我们能知道他实现了接口(IOC容器有的),那么通过他实现的接口,进行JDK动态代理
            将根据名称的方法,和aop:before的含义,进行位置的放置,即通知方法在前,切入点方法在后
            这就是我理解的底层操作
            

            -->
        </aop:aspect>
    </aop:config>

</beans>
编写测试代码:
package com.lagou.test;

import com.lagou.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 *
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")

public class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() {
        accountService.transfer();

    }
}

XML配置AOP详解:
切点表达式:
表达式语法:
//execution([修饰符] 返回值类型 包名.类名.方法名(参数))  []包括的是可以省略的
访问修饰符可以省略
/*
execution(void com.lagou.service.impl.AccountServiceImpl.transfer(java.lang.String))
*/
返回值类型、包名、类名、方法名可以使用星号 * 代替,代表任意
/*
execution(void com.lagou.service.impl.AccountServiceImpl.transfer(java.lang.String))
execution(* *.*.*.*.*.*())
代表任意返回值类型,任意包,任意类,的所有方法,且参数是无参的都会被监听
访问修饰没有*,因为默认为public的,所以就没有设置*这个东西
*/
包名与类名之间一个点 . 代表当前包下的类,两个点 … 表示当前包及其子包下的类
/*
execution(* *..*.*())
代表任意返回值类型,任意包及其子包下的所有类的所有方法,且无参的都会被监听
也就相当于execution(* *.*.*.*.*.*())
即*.*.*.*省略成了*..
最后都到达对应的类
*/
参数列表可以使用两个点 … 表示任意个数,任意类型的参数列表
/*
execution(* *..*.*(..))
代表任意返回值类型,任意包及其子包下的所有类的所有方法,且参数是所有参数类型及其任意个数的都会被监听
即代表任意返回值类型,任意包及其子包下的所有类的所有方法(不用指定参数了)都会被监听
而execution(* *..*.*(*)) 代表一个参数无视参数类型,当然也是可以与其他参数一起的,用,隔开
如execution(* *..*.*(java.lang.String,*)) 两个参数,第二个参数无视参数类型
*/
还有如下例子:
/*
execution(public void com.lagou.service.impl.AccountServiceImpl.transfer())
execution(void com.lagou.service.impl.AccountServiceImpl.*(..))
execution(* com.lagou.service.impl.*.*(..))
execution(* com.lagou.service..*.*(..))
*/
最后注意一点:使用@Test的测试方法,public void 方法名称(){}是固定格式,也就是说
访问权限必须是public,返回值必须是void,方法参数必须是无参,有一个不满足就会报错
使用后置通知,修改配置文件和加上对应方法:
   <!--AOP配置-->
<aop:config>
        <!--配置切面:切入点+通知-->
        <aop:aspect ref="myAdvice"> 
            <aop:before method="before" 
                        pointcut="execution(void 
                                  com.lagou.service.impl.AccountServiceImpl.transfer())"/>
       

            <aop:after-returning method="after" 
                                 pointcut="execution(void 
                                           com.lagou.service.impl.AccountServiceImpl.transfer())"/>
<!--在有些Spring版本下
aop:after在aop:after-returning前面位置,但现在一般都是aop:after-returning在前面
但无论怎么回事,都是放在执行方法后面
-->
        </aop:aspect>
    </aop:config>
package com.lagou.advice;

/**
 *通知类
 */
public class MyAdvice {

    public void before() {
        System.out.println("前置通知执行了");
    }
    public void after() {
        System.out.println("后置通知执行了");
    }
}

切点表达式抽取:
当多个增强的切点表达式相同时,可以将切点表达式进行抽取
在增强中使用 pointcut-ref 属性代替 pointcut 属性来引用抽取后的切点表达式
  <!--AOP配置-->
    <aop:config>
        <aop:pointcut id="myPointcut" 
                      expression="execution(void 
                                  com.lagou.service.impl.AccountServiceImpl.transfer())"/>
        <!--配置切面:切入点+通知-->
        <aop:aspect ref="myAdvice"> 
            <aop:before method="before" pointcut-ref="myPointcut"/>
    
           
            <aop:after-returning method="after" pointcut-ref="myPointcut"/>

        </aop:aspect>
    </aop:config>
通知类型:
通知的配置语法:
<aop:通知类型 method=“通知类中方法名” pointcut=“切点表达式"></aop:通知类型>

在这里插入图片描述

注意:通常情况下,环绕通知都是独立使用的
测试代码:
package com.lagou.advice;

/**
 *通知类
 */
public class MyAdvice {

    public void before() {
        System.out.println("前置通知执行了");
    }
    public void after() {
        System.out.println("后置通知执行了");
    }

    public void afterThrowing() {
        System.out.println("异常通知执行了");
    }
    public void afterFinally() {
        System.out.println("最终通知执行了");
    }
}

xml配置文件:
 <!--AOP配置-->
    <aop:config>
        <aop:pointcut id="myPointcut" 
                      expression="execution(void 
                                  com.lagou.service.impl.AccountServiceImpl.transfer())"/>
        <!--配置切面:切入点+通知-->
        <aop:aspect ref="myAdvice">
            <aop:before method="before" pointcut-ref="myPointcut"/>
      
            <aop:after-returning method="after" pointcut-ref="myPointcut"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut"/>
            <aop:after method="afterFinally" pointcut-ref="myPointcut"/>
        </aop:aspect>
    </aop:config>

运行结果:
/*
前置通知执行了     aop:before
转账方法执行了    
后置通知执行了(或异常通知执行了)		aop:after-returning   aop:after-throwing
只能有一个,因为出现异常就不会执行后置通知了
最终通知执行了		aop:after

即可以看成这样
 try{
    前置通知
    转账方法
    后置通知
}catch (Exception e){
    异常通知
    e.printStackTrace();
}finally{
    最终通知
}
大致位置,可能Spring版本的不同,会造成位置的不同,其中后置通知和最终通知可能会变(一般是环绕通知改变的,既然Spring底层可以变,那么环绕也可以进行改变,是可以这样改变的)
*/
使用环绕通知(环绕通知会导致对应的后置通知和最终通知改变位置):
<aop:around method="around" pointcut-ref="myPointcut"></aop:around>
//Proceeding JoinPoint :正在执行的连接点  切点
    //术语中JoinPoint(连接点):所谓连接点是指那些可以被拦截到的点
    //也就是说,我们能够拦截哪些方法的总和,也可以说,对应代理可以执行的方法的总和
    //当我们使用他时,可以知道哪个方法被拦截,得到对应拦截信息
    //那么我们可以通过这个连接点,执行对应方法,那么就相当于执行切入方法
    //一般的在通过监听器监听到切入点之后,然后拦截,自动通过拦截信息进行调用对应方法
    //而由于环绕,使得我们需要手动执行,那么就需要通过自己去使用拦截信息了
    public Object around(ProceedingJoinPoint pjp){
        //切点方法执行
        Object proceed = null;
        try {
            System.out.println("前置通知执行了");
            proceed = pjp.proceed();
            System.out.println("后置通知执行了");
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println("异常通知执行了");
        }finally {
            System.out.println("最终通知执行了");
        }
        return proceed;


        //可以看出,我们手动操作了通知,但实际上还是放在对应地方的
    }
其实相当于替代执行方法的位置
通过实验
 <!--AOP配置-->
    <aop:config>
        <aop:pointcut id="myPointcut" 
                      expression="execution(void 
                                  com.lagou.service.impl.AccountServiceImpl.transfer())"/>
        <!--配置切面:切入点+通知-->
        <aop:aspect ref="myAdvice"> 
            <aop:before method="before" pointcut-ref="myPointcut"/>
            <aop:after-returning method="after" pointcut-ref="myPointcut"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut"/>
            <aop:after method="afterFinally" pointcut-ref="myPointcut"/>
            <aop:around method="around" pointcut-ref="myPointcut"></aop:around>
        </aop:aspect>
    </aop:config>
当他们一起时,对应方法
package com.lagou.advice;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 *通知类
 */
public class MyAdvice {

    public void before() {
        System.out.println("前置通知执行了");
    }
    public void after() {
        System.out.println("后置通知执行了");
    }

    public void afterThrowing() {
        System.out.println("异常通知执行了");
    }
    public void afterFinally() {
        System.out.println("最终通知执行了");
    }

    public Object around(ProceedingJoinPoint pjp){
        //切点方法执行
        Object proceed = null;
        try {
            System.out.println("前置通知执行了11");
            proceed = pjp.proceed();
            System.out.println("后置通知执行了11");
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println("异常通知执行了11");
        }finally {
            System.out.println("最终通知执行了11");
        }
        return proceed;

    }
}

执行结果:
/*
前置通知执行了
前置通知执行了11
转账方法执行了
后置通知执行了11
最终通知执行了11
最终通知执行了    到前面了
后置通知执行了

发现,的确相当于是替换了原来执行方法的位置,所以需要手动执行,但前面的最终方法,会与后置方法交换位置
*/
知识小结:
/*
aop织入的配置
 <aop:config>
        <aop:aspect ref=“通知类”>
            <aop:before method=“通知方法名称” pointcut=“切点表达式"></aop:before>
        </aop:aspect>
    </aop:config>
                                                        
通知的类型
 前置通知、后置通知、异常通知、最终通知
 环绕通知
 
切点表达式
 execution([修饰符] 返回值类型 包名.类名.方法名(参数))

*/
基于注解的AOP开发 :
快速入门:
步骤分析:
/*
1. 创建java项目,导入AOP相关坐标
2. 创建目标接口和目标实现类(定义切入点)
3. 创建通知类(定义通知)
4. 将目标类和通知类对象创建权交给spring
5. 在通知类中使用注解配置织入关系,升级为切面类
6. 在配置文件中开启组件扫描和 AOP 的自动代理
7. 编写测试代码
*/
创建java项目,导入AOP相关坐标:
<dependencies>
    <!--导入spring的context坐标,context依赖aop-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.5.RELEASE</version>
    </dependency>
    <!-- aspectj的织入 -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.13</version>
    </dependency>
    <!--spring整合junit-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.1.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>
创建目标接口和目标实现类:
package com.lagou.service;

/**
 *
 */
public interface AccountService {


    public void transfer();
}

package com.lagou.service.impl;

import com.lagou.service.AccountService;
import org.springframework.stereotype.Service;

/**
 *
 */
@Service
public class AccountServiceImpl implements AccountService {


    @Override
    public void transfer() {
        System.out.println("转账方法执行了");


    }
}

创建通知类:
package com.lagou.advice;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
 *通知类
 */
@Component
@Aspect //升级为切面类:配置切入点和通知的关系,也就是存放通知方法和对应指定的切入方法路径的类
//因为Aspect是是切入点和通知(引介)的结合
public class MyAdvice {

    //很明显,被扫描到时,顺便也有了对应类实例(读取到@Aspect的作用,使得有对应实例)
    //即也通过注解信息,通过了方法添加
    @Before("execution(* com.lagou.service.impl.AccountServiceImpl.*(..))")
    public void before() {
        System.out.println("前置通知执行了");
    }

}

在配置文件中开启组件扫描和 AOP 的自动代理:
<?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"
       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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--开启注解扫描,但只能操作对应的注解-->
    <context:component-scan base-package="com.lagou"></context:component-scan>

    <!--AOP自动代理,使得对应的注解(如@Pointcut)也扫描生效(整个项目上下文,但通常是ioc的扫描路径,即这里是com.lagou,具体看版本,可以百度查看),且spring会采用动态代理完成织入增强,并且生成代理-->
    <aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
    <!--proxy-target-class="true"强制使用cglib动态代理-->

</beans>
编写测试代码 :
package com.lagou.test;

import com.lagou.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 *
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")

public class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() {
        accountService.transfer();

    }

}

注解配置AOP详解 :
切点表达式:
切点表达式的抽取
package com.lagou.advice;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 *通知类
 */
@Component
@Aspect
public class MyAdvice {

    //会先识别这个注解,所以位置不做要求,并讲对应的设置用类名加方法组成
    //后面的注解中,就可以使用这个信息了,当然有的话就会识别
    @Pointcut("execution(* com.lagou.service.impl.AccountServiceImpl.*(..))")
    public void myPoint(){}


    @Before("MyAdvice.myPoint()")
    public void before() {
        System.out.println("前置通知执行了");
    }

    @AfterReturning("MyAdvice.myPoint()")
    public void afterReturning() {
        System.out.println("后置通知执行了");
    }

}

通知类型:
通知的配置语法:@通知注解(“切点表达式")

在这里插入图片描述

注意:
/*
当前四个通知组合在一起时,执行顺序如下:
@Before -> @After -> @AfterReturning(如果有异常:@AfterThrowing)
即最终通知与后置通知交换了位置
*/
package com.lagou.advice;

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

/**
 *通知类
 */
@Component
@Aspect
public class MyAdvice {

    //会先识别这个注解,所以位置不做要求,并讲对应的设置用类名加方法组成
    //后面的注解中,就可以使用这个信息了,当然有的话就会识别
    @Pointcut("execution(* com.lagou.service.impl.AccountServiceImpl.*(..))")
    public void myPoint(){}


    @Before("MyAdvice.myPoint()")
    public void before() {
        System.out.println("前置通知执行了");
    }

    @AfterReturning("MyAdvice.myPoint()")
    public void afterReturning() {
        System.out.println("后置通知执行了");
    }

    @AfterThrowing("MyAdvice.myPoint()")
    public void afterThrowing() {
        System.out.println("异常通知执行了");
    }

    @After("MyAdvice.myPoint()")
    public void after() {
        System.out.println("最终通知执行了");
    }

    @Around("MyAdvice.myPoint()")
    public Object around(ProceedingJoinPoint pjp){
        //切点方法执行
        Object proceed = null;
        try {
            System.out.println("前置通知执行了11");
            proceed = pjp.proceed();
            System.out.println("后置通知执行了11");
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println("异常通知执行了11");
        }finally {
            System.out.println("最终通知执行了11");
        }
        return proceed;

    }

}

运行结果:
/*
前置通知执行了11   在前面了
前置通知执行了
转账方法执行了
后置通知执行了11
最终通知执行了11
最终通知执行了
后置通知执行了
*/
他们的添加方法,有不同的效果,主要是注解或者环绕通知造成的(注解也会造成环绕通知形成的顺序,你可以选中删除环绕通知就知道了)
一般的,我们都会单独使用环绕通知,而不会用其他通知和环绕通知一起,除了顺序可能不对外,可能也会由于我们手动操作时,导致的其他通知出现顺序不对的概率要大,因为是分开的,所以基本使用环绕通知来操作
一般开发中,基本都会使用环绕通知来进行,其他通知基本不会使用,因为环绕通知比较方便
纯注解配置 :
package com.lagou.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
 *
 */
@Configuration
@ComponentScan("com.lagou")
@EnableAspectJAutoProxy //开启AOP自动代理 替代了aop:aspectj-autoproxy(整个项目上下文,但通常是ioc的扫描路径,即这里是com.lagou,具体看版本,可以百度查看)
public class SpringConfig {
}


package com.lagou.test;

import com.lagou.config.SpringConfig;
import com.lagou.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 *
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
//使用classes指定类,不使用基本指定xml,他们不能互换,否则报错

public class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() {
        accountService.transfer();

    }

}

知识小结 :
/*
使用@Aspect注解,标注切面类
使用@Before等注解,标注通知方法
使用@Pointcut注解,抽取切点表达式
配置aop自动代理 <aop:aspectj-autoproxy/> 或 @EnableAspectJAutoProxy

*/

//实际上可以看成两次代理(环绕),或者两个方法

AOP优化转账案例 :
依然使用前面的转账案例,将两个代理工厂对象直接删除!改为spring的aop思想来实现
xml配置实现:
在转账案例的配置文件中加入以下配置文件(注意命名空间和约束路径):
 <!--AOP配置-->
    <aop:config>

        <!--切点表达式-->
        <aop:pointcut id="myPoint" 
                      expression="execution(* com.lagou.service.impl.AccountServiceImpl.*(..))"/>

        <!--切面配置-->
        <aop:aspect ref="transactionManager"> 
            <!--注解和xml都是先放对象,且当xml和注解都操作完后,在进行对象操作-->
            <aop:before method="beginTransaction" pointcut-ref="myPoint"/>
            <aop:after-returning method="commit" pointcut-ref="myPoint"/>
            <aop:after-throwing method="rollback" pointcut-ref="myPoint"/>
            <aop:after method="release" pointcut-ref="myPoint"/>
        </aop:aspect>

    </aop:config>
注意加上依赖:
  <!-- aspectj的织入 切点表达式需要这个jar包,进行解析-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>
执行测试方法:
 @Test
    public void textTransfer() {
        accountService.transfer("tom", "jerry", 100d);
        //100d就是这个100是double类型,就如3.14f就是float类型
    }
注解配置实现 :
在转账案例的配置文件中加入以下配置文件:
 <!--开启AOP自动代理-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
根据下面代码,在转账案例的对应地方加上对应代码:
@Component
@Aspect //先声明为切面类,即通知类
public class TransactionManager {
    @Around("execution(* com.lagou.service.impl.AccountServiceImpl.*(..))")
    //返回值类型也可以是void,或者返回null也可以,会根据返回值类型加上对应的变量的,或者不加
    //所有返回值类型是什么基本无影响
    public Object around(ProceedingJoinPoint pjp){

        Object proceed = null;
        try {
            //开启事务
            connectionUtils.getThreadConnection().setAutoCommit(false);
            proceed = pjp.proceed();
            //提交事务
            connectionUtils.getThreadConnection().commit();
        } catch (Throwable e) {
            e.printStackTrace();
            //回滚事务
            try {
                connectionUtils.getThreadConnection().rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }finally {
            try {
                //释放资源
                connectionUtils.getThreadConnection().setAutoCommit(true);
                //将连接归还到连接池中
                connectionUtils.getThreadConnection().close();

                //解除线程绑定,就是删除对应key
                connectionUtils.removeThreadConnection();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
            return proceed;

    }


}
运行:
 @Test
    public void textTransfer() {
        accountService.transfer("tom", "jerry", 100d);
        //100d就是这个100是double类型,就如3.14f就是float类型
    }
使用注解拦截会先拦截我们写的Proxy类,即在使用注解时,若使用自己的代理类,那么基本会报错
而注解和xml则共生的添加,因为操作是一样的,除非有程序的报错,就如上面的事务,当注解和xml一起使用时
相当于大肠包小肠,即如下
/*
try {
前置
    try {
        前置
        方法
         后置
    }catch (Exception e){
       异常
    }finally {
        最终
    }
    这什么相当于再次替换了方法,具体谁后替换,看注解和xml谁后操作
    使得当上面最终关闭资源后,下面的后置可能就会报错
    
    后置
}catch (Exception e){
      异常
}finally {
    最终
}

即输出
前置
前置
方法
后置(异常)
最终
后置(异常)
最终
*/
一般情况下,环绕通知比较灵活,所以在操作环绕通知时,我们通常不会操作其他通知了,也能保证其他通知的顺序不被改变
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值