3.2大神之路-Spring(四)——AOP

此生再无相见时 提交于 2020-01-28 04:07:02

AOP

1. AOP是什么?

AOP:aspect oriented programming,面向切面编程,通过预编译和运行期动态代理实现程序功能统一维护的一种技术。
AOP可以对业务逻辑的各个部分进行隔离,使业务逻辑耦合度降低,提高程序的可重用性,提高开发效率。
作用
程序运行期间,不修改源码对已有方法进行增强;
优势
减少重复代码
提高开发效率
维护方便

2. Spring AOP怎么用?

通过配置的方式,实现动态代理章节《动态代理》的功能
Spring可以手动控制到底基于接口还是子类的动态代理

2.1 Spring AOP相关术语

在看其他Spring AOP的资料时,特别需要掌握,因为其他人不会再从底层原理开始讲解了,而是会直接会基于Spring AOP说:

Spring AOP术语 说明
Joinpoint(连接点) 指那些被拦截到的点,因为Spring只支持方法类型的连接点,Spring中指的就是方法。如:业务接口的所有方法就都是连接点。连接的是我们的业务,增强方法中的点
Pointcut(切入点) 指我们要对哪些JointPoint进行拦截的定义。指的是那些被增强的方法,有些方法虽然被连接了,其实没有被增强,因为没有写或执行增强代码
Advice(通知/增强) 拦截到JointPoint之后要做的事情。包括前置通知,后置通知,异常通知,最终通知,环绕通知
Introduction(引介) 特殊的通知,不修改代码前提下,可以在运行期为类动态地添加一些方法或Field
Target(目标对象) 被代理对象
Weaving(织入) 把增强应用到目标对象来创建新的代理对象的过程
Proxy(代理) 代理对象
Aspect(切面) 是切入点和通知的结合,建立切入点方法和通知方法在执行调用关系,就是切面,也就是结合

举个栗子

(IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader()
...
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       if("test".equals(method.getName())){
           return method.invoke(accountService, args);
       }
       Object rtValue = null;
       try {
           txManager.beginTransaction();//1.开启事务-前置通知
           //2.执行操作-环绕通知,有明确的切入点方法调用
           rtValue = method.invoke(accountService, args);
           txManager.commit();//3.提交事务-后置通知
           return rtValue;//4.返回结果
       } catch (Exception e) {
           txManager.rollback();//5.回滚操作-异常通知
           throw new RuntimeException(e);
       } finally {
           txManager.release();//6.释放连接-最后通知
       }
   }
//织类
public class TransactionManager {
    //开启事务
    public  void beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
     //提交事务
    public  void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
	...

在上述案例中
接口IAccountService中所有方法都是Joinpoint连接点;
但因为test()不执行增强代码,所以他不是Pointcut切入点;
Advice通知在代码中注释了;
目标对象Target就是accountService
整个newProxyInstance中加入事务支持的过程叫做Weaving(织入) ,所以它有ing;
Aspect(切面) :看上述代码很简单,因为是直接编写,如果用配置,概念必须明确起来。织入方法的调用关系,何时执行,都需要配置。这些关系配置,就是切面;

奥迪为生产线员工配备外骨骼设备

Spring AOP 费曼说明
Joinpoint(连接点) 外骨骼设备上配置的固定点
Pointcut(切入点) 使用人与外骨骼设备的实际连接点,有些工作是坐着的,腿部就不需要连接
织类 外骨骼设备就是织类
Aspect(切面)

2.2 Spring AOP在生产中角色

  1. 开发阶段,编写核心业务代码
  2. 把公用代码抽出来,做成通知
  3. 在配置文件中,声明切入点和通知点的关系,即切面
  4. 运行阶段,Spring框架监控切入点方法的执行,一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类型,在代理对象的对应位置,将通知对应功能织入

2.3 基于XML配置 Spring AOP

public interface IAccountService {
	//模拟保存账户
   void saveAccount();
	//模拟更新账户
   void updateAccount(int i);
	//删除账户
   int  deleteAccount();
}

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

应用案例:把printLog()织入到IAccountService

第一步:编写配置文件

配置文件的头在core.html->aop 里可以找到
spring中基于XML的AOP配置步骤

  1. 把通知Bean也交给spring来管理
  2. 使用aop:config标签表明开始AOP的配置
  3. 使用aop:aspect标签表明配置切面
    id属性:是给切面提供一个唯一标识
    ref属性:是指定通知类bean的Id
  4. aop:aspect标签的内部使用对应标签来配置通知的类型

切入点表达式写法

Spring AOP 费曼说明
关键字 execution(表达式)
表达式 访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)
标准的表达式写法 public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略 void com.itheima.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值 * com.itheima.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个 * ....AccountServiceImpl.saveAccount())
包名可以使用…表示当前包及其子包 * *…AccountServiceImpl.saveAccount()
类名和方法名都可以使用*来实现通配 * .*()
全通配写法 * .*(…)

可以直接写数据类型:

  • 基本类型直接写名称 int
  • 引用类型写包名.类名的方式 java.lang.String
  • 可以使用通配符表示任意类型,但是必须有参数
  • 可以使用…表示有无参数均可,有参数可以是任意类型

实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法:execution(* com.itheima.service.impl.*.*(..))

<!-- 配置Spring的IOC-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!-- 1.配置通知Bean -->
<bean id="logger" class="com.itheima.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
    <!--配置切面 -->
    <aop:aspect id="logAdvice" ref="logger">
        <!-- 配置通知的类型,配置切入点方法与通知方法的关联-->
        <aop:before method="printLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:before>
        ...
    </aop:aspect>
</aop:config>
2.3.2 通知类型

其属性如下(pointcut-refpointref属性只能有其中一个)

  • method: 指定通知类中的增强方法名.
  • ponitcut-ref: 指定切入点的表达式的id
  • poinitcut: 指定切入点表达式
通知类型 说明
<aop:before> 前置通知 指定的增强方法在切入点方法之前执行
<aop:after-returning>后置通知 指定的增强方法在切入点方法正常执行之后执行
<aop:after-throwing>异常通知 指定的增强方法在切入点方法产生异常后执行
<aop:after>最终通知 无论切入点方法执行时是否发生异常,指定的增强方法都会最后执行
<aop:around>环绕通知 可以在代码中手动控制增强代码的执行时机
配置<aop:pointcut>切入点
<aop:config>
	<!-- 配置切入点表达式 
	id属性用于指定表达式的唯一标识,expression属性用于指定表达式内容
     此标签写在aop:aspect标签内部只能当前切面使用。
     它还可以写在aop:aspect外面,此时就变成了所有切面可用
     pointcut-ref中都可以换成 pt1的 pointcut引用 
     头文件约束:aop:pointcut  必须在<aop:aspect 之前,不然会报错--> 
    <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
    
    <aop:aspect id="logAdvice" ref="logger">
    	<!-- 配置前置通知:在切入点方法执行之前执行-->
        <aop:before method="beforePrintLog" pointcut-ref="execution(* com.itheima.service.impl.*.*(..))" ></aop:before>
        <!-- 配置后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个-->
        <aop:after-returning method="afterReturningPrintLog" 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>
配置<aop:around环绕通知

spring中的环绕通知:spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式;

通知类型 说明
问题 当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了
分析 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有
解决 Spring框架为我们提供了一个接口ProceedingJoinPoint,该接口有一个方法proceed(),此方法就相当于明确调用切入点方法

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

  */
public Object aroundPringLog(ProceedingJoinPoint pjp){
    Object rtValue = null;
    try{
        Object[] args = pjp.getArgs();//得到方法执行所需的参数
        System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");
        rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
        System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");
        return rtValue;
    }catch (Throwable t){
        System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
        throw new RuntimeException(t);
    }finally {
        System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
    }
}
<!-- 配置环绕通知 详细的注释请看Logger类中-->
	...
        <aop:around method="aroundPringLog" pointcut-ref="pt1"></aop:around>
    </aop:aspect>
总结:XML的AOP的配置步骤
  1. 配置通知的bean
  2. <aop:config>声明
  3. 配置切面<aop:aspect>
  4. 配置通知类型<aop:xxx

2.4 基于注解配置化Spring AOP

2.4.1 配置XML文件
  • 扫描包
  • 开启aop注解
<!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!-- 配置spring开启注解AOP的支持 -->
 <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
2.4.2 注解切面类
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
	//注解切入点
    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    private void pt1(){}
    @Before("pt1()")//前置通知
    public  void beforePrintLog(){
        System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
    }
    @AfterReturning("pt1()")//后置通知
    public  void afterReturningPrintLog(){
        System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
    }
    @AfterThrowing("pt1()")//异常通知
    public  void afterThrowingPrintLog(){
        System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
    }
    @After("pt1()")  //最终通知
    public  void afterPrintLog(){
        System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
    }
    @Around("pt1()")//环绕通知
    public Object aroundPringLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try{
            Object[] args = pjp.getArgs();//得到方法执行所需的参数
            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");
            rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");
            return rtValue;
        }catch (Throwable t){
            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
            throw new RuntimeException(t);
        }finally {
            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
        }
    }
}
总结

基于注解的Spring AOP也太方便了,记下来

3. Spring AOP 生产中案例

Caching 缓存,Context passing 内容传递,,Lazy loading 懒加载,Debugging 调试, tracing 跟踪, profiling 优化,monitoring 校准,Performance optimization 性能优化,Persistence 持久化,Resource pooling资源池,Synchronization 同步事务

3.1 事务机制-Transactions

3.1.1 XML配置事务AOP

编码业务代码

public class AccountServiceImpl implements IAccountService{
    private IAccountDao accountDao;
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }
    @Override
    public List<Account> findAllAccount() {
       return accountDao.findAllAccount();
    }
    @Override
    public Account findAccountById(Integer accountId) {
        return accountDao.findAccountById(accountId);
    }
    ...

配置文件-配置切面类

<!-- 配置事务管理器-->
<bean id="txManager" class="com.itheima.utils.TransactionManager">
    <!-- 注入ConnectionUtils -->
    <property name="connectionUtils" ref="connectionUtils"></property>
</bean>

配置文件-配置aop

  1. 定义切入点表达式<aop:pointcut
  2. 定义<aop:aspect
  3. 定义通知类型<aop:before
<aop:config>
    <!--配置通用切入点表达式-->
    <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
    <aop:aspect id="txAdvice" ref="txManager">
        <!--配置前置通知:开启事务-->
        <aop:before method="beginTransaction" pointcut-ref="pt1"></aop:before>
        <!--配置后置通知:提交事务-->
        <aop:after-returning method="commit" pointcut-ref="pt1"></aop:after-returning>
        <!--配置异常通知:回滚事务-->
        <aop:after-throwing method="rollback" pointcut-ref="pt1"></aop:after-throwing>
        <!--配置最终通知:释放连接-->
        <aop:after method="release" pointcut-ref="pt1"></aop:after>
    </aop:aspect>
</aop:config>
3.1.2 注解配置事务AOP
3.1.2.1 配置IOC的注解

配置文件里头,加头文件context声明

<beans xmlns="http://www.springframework.org/schema/beans"
       ...
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       ...
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

<!--配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
3.1.2.2 开发业务代码
//账户的业务层实现类-事务控制应该都是在业务层
@Service("accountService")
public class AccountServiceImpl implements IAccountService{
	@Autowired
	private IAccountDao accountDao;
	...
}
//账户的持久层实现类
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
    @Autowired
    private QueryRunner runner;
    @Autowired
    private ConnectionUtils connectionUtils;
	...
}
3.1.2.3 配置业务代码注解
<!--配置QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></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="1234"></property>
</bean>

<!-- 配置Service -->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
    <!-- 注入dao -->
    <property name="accountDao" ref="accountDao"></property>
</bean>

<!--配置Dao对象-->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
    <!-- 注入QueryRunner -->
    <property name="runner" ref="runner"></property>
    <!-- 注入ConnectionUtils -->
    <property name="connectionUtils" ref="connectionUtils"></property>
</bean>
3.1.2.4 开启aop注解
<!--开启spring对注解AOP的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
3.1.2.5 编写切面类
// 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接
@Component("txManager")
@Aspect
public class TransactionManager {
    @Autowired
    private ConnectionUtils connectionUtils;
    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    private void pt1(){}
	//开启事务
    public  void beginTransaction(){
		...
	connectionUtils.getThreadConnection().setAutoCommit(false);
    	...
    }
   	...
3.1.3 隐藏的问题

执行手续有问题:程序默认先调用最终通知,再调用后置通知

public void transfer(){
	...
	accountDao.updateAccount(source);
	int i=1/0;//出现异常
}

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

@After("pt1()")
public  void release(){
	...
    connectionUtils.getThreadConnection().close();//还回连接池中
    connectionUtils.removeConnection();
    ...
}

可以看出,最终通知一执行,connection已经关闭了,线程也解绑。

@AfterReturning("pt1()")
 public  void commit(){
	...
	connectionUtils.getThreadConnection().commit();
	...
 }

if (conn == null) {
   	conn = dataSource.getConnection();
   	tl.set(conn);
}

再次调用后置通知的时候,代码会再次创建一个connection然后提交,这个connection的自动提交已经是true,再次提交,所以报错!
这样的程序对数据库也不会有任何操作,因为第二个connection里没有任何操作;
在这里插入图片描述
解决四个通知的顺序问题,使用环绕通知来解决
定义环绕通知

@Around("pt1()")
public Object aroundAdvice(ProceedingJoinPoint pjp){
    Object rtValue = null;
    try {
        Object[] args = pjp.getArgs();//1.获取参数
        this.beginTransaction();//2.开启事务
        rtValue = pjp.proceed(args);//3.执行方法
        this.commit();//4.提交事务
        return  rtValue;//返回结果
    }catch (Throwable e){
        this.rollback();//5.回滚事务
        throw new RuntimeException(e);
    }finally {
        this.release();//6.释放资源
    }
}
3.1.4 纯注解配置实现AOP

可以自己实现

数据库资源文件

jdbc.url=jdbc:mysql://localhost:3306/zzj0301
jdbc.driver=com.mysql.jdbc.Driver
jdbc.user=root
jdbc.password=root
jdbc.characterEncoding=UTF-8

配置数据源

@PropertySource("classpath:jdbc.properties")
@Component
public class MyDataSource extends DriverManagerDataSource {
    public MyDataSource(@Value("${jdbc.driver}") String driver,
    @Value("${jdbc.url}") String url,
    @Value("${jdbc.user}") String username,
    @Value("${jdbc.password}") String password,
    @Value("${jdbc.characterEncoding}") String characterEncoding) {
        super.setDriverClassName(driver);
        super.setUrl(url);
        super.setUsername(username);
        super.setPassword(password);
        Properties properties = new Properties();
        properties.setProperty("characterEncoding",characterEncoding);
        super.setConnectionProperties(properties);
    }
}

数据库Java类

@Getter
@Setter
public class Student {
    private int id;
    private String sname;
    private int age;
    private String gender;
    private String nickname;
}

对数据库进行操作的java类

@Setter//lombok注解
@Getter
@Component
public class StudentDaoImpl {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    /**
     * 插入一条student数据
     * @return 影响的行数
     */
    @Transactional(rollbackFor = {Exception.class})
    public int insertStudent(Map map){
        Object[] args = new Object[]{
                map.get("sname"),
                map.get("age"),
                map.get("gender"),
                map.get("nickname")
        };
        int row = jdbcTemplate.update("insert into student (sname,age,gender,nickname) values (?,?,?,?)",args);
        
        //模拟事务异常,使程序回滚
        //int a = 2 / 0;
        return row;
    }
}

代码运行

@Configuration
@ComponentScan(basePackages = "com.lanou3g.spring.transaction.annotation")
@EnableTransactionManagement    //开启事务相关注解支持
public class App {
    /**
     * 定义JdbcTemplate对象,Spring给我们封装了所有的JDBC操作
     * @param dataSource
     * 相当于xml配置中的
     * <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
     *      <property name="dataSource" ref="dataSource" />
     * </bean>
     */
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }
    /**
     *定义事务管理器
     * @param dataSource
     * 相当于xml配置中的
     * <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
     *      <property name="dataSource" ref="dataSource" />
     * </bean> */
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){    //DataSourceTransactionManager是PlatformTransactionManager的一个实现类
        return new DataSourceTransactionManager(dataSource);
    }
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(App.class);
		Map<String,Object> map = new HashMap<>();
        map.put("sname","欧阳锋");
        map.put("age",42);
        map.put("gender","男");
        map.put("nickname","西毒");
        int row = ctx.getBean(StudentDaoImpl.class).insertStudent(map);
        System.out.println("影响了" + row + "行");
    }
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!