Web基础之Spring AOP与事务

Posted lixin-link

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Web基础之Spring AOP与事务相关的知识,希望对你有一定的参考价值。

Spring之AOP

AOP 全程Aspect Oriented Programming,直译就是面向切面编程。和POP、OOP相似,它也是一种编程思想。OOP强调的是封装、继承、多态,也就是功能的模块化。而AOP则是OOP的补充,它强调的是切面,在运行时动态地将代码切入到类的指定方法、指定位置上的编程思想,也就是将业务代码和业务前后的代码分离出来(解耦),将日志、权限验证等功能抽取出来然后重用。
在Spring中,采用动态代理的方式来表达AOP。(并非所有的AOP都是使用动态代理来,比如AspectJ采用编译时创建代理对象,比运行时创建效率更高)
动态代理一般有两种实现方式,一种是JDK原生动态代理,要求被代理对象必须实现接口,并且只能代理接口中的方法(本质是创建一个实现接口的代理对象)。另一种是CGlib,可以代理所有的方法(本质是创建一个代理对象,继承被代理对象)。Spring中使用的是CGlib的方式。

废话不多说,直接介绍Spring中的AOP。

Spring AOP相关术语

  • Joinpoint(连接点):任何可以被增强的方法,都称为连接点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。
  • Pointcut(切入点):将要被增强的方法。即我们要对哪些Joinpoint进行拦截的定义。
  • Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
  • Introduction(引介):Adivice是对方法进行增强的,而Introdution是针对类进行增强的
  • Target(目标对象):被代理的目标对象。
  • Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
  • Proxy(代理):一个对象被jdk代理或者cglib代理后的对象,称为代理
  • Aspect(切面):多个通知和切入点的配置关系。

基于xml的AOP配置

首先是依赖:


AOP依赖

<!--IOC相关依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.6.RELEASE</version>
</dependency>
<!--junit-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
    <version>4.12</version>
</dependency>
<!-- AOP相关依赖 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.0.6.RELEASE</version>
</dependency>
<!-- spring集合junit的依赖 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.0.6.RELEASE</version>
</dependency>

然后配置xml主配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- 待增强的bean -->
    <bean id="aspectService" class="com.bilibili.service.impl.AspectServiceImpl"></bean>
    <!-- 增强的功能bean -->
    <bean id="logger" class="com.bilibili.common.Logger"></bean>
    <!-- 配置AOP -->
    <aop:config>
        <!-- 配置切面
            id:唯一标识
            ref:引用的通知类(bean id)
        -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置前置通知
                method:配置通知的方法(即增强的功能)
                pointcut:配置切面,也就是对哪个方法进行增强(使用AspectJ表达式)
                    execution:使用AspectJ切入点表达式
             -->
            <aop:before method="printLog" pointcut="execution(public void com.bilibili.service.impl.AspectServiceImpl.update())"></aop:before>
        </aop:aspect>
    </aop:config>
</beans>

execution表达式的匹配方式:

execution:匹配方法的执行(常用)
    execution(表达式)
表达式语法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
写法说明:
    全匹配方式:
        public void cn.bilibili.service.impl.AccountServiceImpl.saveAccount(cn.bilibili.domain.Account)
    访问修饰符可以省略   
        void cn.bilibili.service.impl.AccountServiceImpl.saveAccount(cn.bilibili.domain.Account)
    返回值可以使用*号,表示任意返回值
        * cn.bilibili.service.impl.AccountServiceImpl.saveAccount(cn.bilibili.domain.Account)
    包名可以使用*号,表示任意包,但是有几级包,需要写几个*
        * *.*.*.*.AccountServiceImpl.saveAccount(cn.bilibili.domain.Account)
    使用..来表示当前包,及其子包
        * cn..AccountServiceImpl.saveAccount(cn.bilibili.domain.Account)
    类名可以使用*号,表示任意类
        * cn..*.saveAccount(cn.bilibili.domain.Account)
    方法名可以使用*号,表示任意方法
        * cn..*.*( cn.bilibili.domain.Account)
    参数列表可以使用*,表示参数可以是任意数据类型,但是必须有参数
        * cn..*.*(*)
    参数列表可以使用..表示有无参数均可,有参数可以是任意类型
        * cn..*.*(..)
    全通配方式:
        * *..*.*(..)
注:
    通常情况下,我们都是对业务层的方法进行增强,所以切入点表达式都是切到业务层实现类。
    execution(* cn.bilibili.service.impl.*.*(..))

    注意:多个execution可以使用  ||  &&  连接

AOP 常用标签

  • <aop:config>:用于声明开始aop配置
  • <aop:aspect>:切面
    属性:
    • id:给切面提供一个唯一标识。
    • ref:引用配置好的通知类bean的id
  • <aop:point>:切点,方便一个切点多次使用
    属性:
    • id:切点的唯一标识
    • expression:定义切点的表达式
  • <aop:aspect>:前置通知
    属性:
    • method:通知的方法(即增强的功能)
    • pointcut:AspectJ表达式
    • pointcut-ref:引用切点(和pointcut不可同时使用)
  • <aop:after-returning>:后置通知
  • <aop:after-throwing>:异常通知
  • <aop:after>:最终通知(相当于finally)
  • <aop:around>:环绕通知,一般单独使用,该通知(增强的方法)接收一个类型为ProceedingJoinPoint的参数,该类型有一个proceed()方法,用来调用被代理方法。

基于注解的AOP配置

在主配置文件中开启包扫描和注解AOP:

<!-- 开启注解扫描的包 -->
<context:component-scan base-package="com.bilibili"></context:component-scan>

<!-- 开启注解AOP -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

把被代理对象注册到容器中:


AccountServiceImpl类

@Service("accountService")
public class AccountServiceImpl implements AccountService 
    public void update() 
        System.out.println("更新操作");
    

    public void save() 
        System.out.println("保存操作");
    

    public void delete() 
        System.out.println("删除操作");
    

在通知类上添加@Component()进行注册,@Aspect表示切面类。方法上添加@Before()前置通知、@AfterReturning()后置通知、@AfterThrowing()异常通知、@After()最终通知。@Pointcut()注解空方法表示切面。


通知类

@Component("logger")
@Aspect//声明当前是一个切面类(通知类)
public class Logger 
    //注解前置通知,value属性就是切点的AspectJ表达式
    @Before("execution(* com.bilibili.service.impl.AccountServiceImpl.update())")
    public void beforePrintLog()
        System.out.println("<aop:before>标签配置前置通知,即增强的功能在目标方法之前");
    

    //切面
    @Pointcut("execution(* com.bilibili.service.impl.AspectServiceImpl.update())")
    public void pt1() 
    

    //注解后置通知,引用切面
    @AfterReturning("pt1()")
    public void afterReturningPrintLog()
        System.out.println("<aop:after-returning>标签配置后置通知,即增强的功能在目标方法之后");
    
//下面就不一个一个标注了。
    public void afterThrowingPrintLog()
        System.out.println("<aop:after-throwing>标签配置异常通知,即目标方法出现异常的时候执行");
    

    public void afterPrintLog()
        System.out.println("<aop:after>标签配置最终通知。即不管是否出现异常,都会执行,类似finally");
    


    public Object aroundPrintLog(ProceedingJoinPoint pjp)
        Object obj = null;
        try 
            System.out.println("环绕通知,手动在代码中定义何时执行");
            obj = pjp.proceed();//目标方法执行
            System.out.println("环绕通知,手动在代码中定义何时执行");
         catch (Throwable throwable) 
            System.out.println("环绕通知,手动在代码中定义何时执行");
            throwable.printStackTrace();
        finally 
            System.out.println("环绕通知,手动在代码中定义何时执行");
        
        return obj;
    

纯注解配置

只需在IoC的纯注解配置类上添加@EnableAspectJAutoProxy()开启AOP即可。


注解AOP

//声明当前类是一个spring的配置类,用来替代xml配置文件
//获取容器时需要使用AnnotationApplicationContext(@Configuration标注的类.class)
@Configuration
//用于配置容器初始化时需要扫描的包
//和xml配置中<context:component-scan base-package="com.bilibili"/>作用一致
@ComponentScan("com.bilibili")
//导入其他配置类
@Import(JdbcConfig.class)
//开启AOP
@EnableAspectJAutoProxy
public class SpringConfig 

JdbcDaoSupport

继承该类后可以不用手动获取JdbcTemplate对象。

  1. dao层的实现类只需要继承JdbcDaoSupport,然后通过getJdbcTemplate()方法获取jdbcTemplate对象
  2. 在spring的applicationContext.xml中,只需要给dao的实现类注入dataSource数据源即可。因为JdbcDaoSupport中的setDataSource()方法自动创建jdbcTemplate对象。

使用这种方式无法用注解注入DataSource,只能通过xml注入(注入给子类也可以)

Spring 事务

事务处理位于业务层,Spring提供了一个spring-tx包来进行控制事务,事务是基于AOP,原理也比较好理解。

PlatformTransactionManager

PlatformTransactionManager:平台事务管理器,是Spring真正管理事务的对象,是一个接口,常用实现类有如下两个:

  • DataSourceTransactionManager:针对JDBC和mybatis事务管理
  • HibernateTransactionManager:针对Hibernate事务管理

Spring主要通过两个重要的接口来描述一个事务:

  • TransactionDefinition:事务定义的对象,用来定义事务的隔离级别、传播行为、是否只读、超时信息等等
  • TransactionStatus:事务状态信息的对象,用来获取事务是否保存、是否完成等。

Spring框架进行事务的管理,首先使用TransactionDefinition对事务进行定义。通过PlatformTransactionManager根据TransactionDefinition的定义信息进行事务的管理。在事务管理过程中产生一系列的状态:保存到TransactionStatus中。

TransactionDefinition接口具有以下常用方法:

  • String getName():获取事务对象名称
  • int getIsolationLevel():获取事务隔离级别
  • int getPropagationBehavior():获取事务传播行为
  • int getTimeout():获取事务超时时间
  • boolean isReadOnly()获取事务是否只读

事务隔离级别:

  • ISOLATION_DEFAULT:默认级别,会根据不同数据库自动变更(mysql为可重复读,Oracle和Access为读已提交)
  • ISOLATION_READ_UNCOMMITTED:读未提交(会产生脏读)
  • ISOLATION_READ_COMMITTED:读已提交(解决脏读)
  • ISOLATION_REPEATABLE_READ:可重复读
  • ISOLATION_SERIALIZABLE:串行化

事务的传播行为

传播行为解决的问题: 一个业务层事务 调用 另一个业务层事务时,事务之间关系如何处理

事务传播行为PROPAGATION的取值:
    REQUIRED    支持当前事务,如果不存在,就新建一个(默认的传播行为)
        * 删除客户 删除订单, 处于同一个事务,如果 删除订单失败,删除客户也要回滚 
    SUPPORTS    支持当前事务,如果不存在,就不使用事务
    MANDATORY   支持当前事务,如果不存在,抛出异常


    REQUIRES_NEW    如果有事务存在,挂起当前事务,创建一个新的事务
        * 生成订单, 发送通知邮件, 通知邮件会创建一个新的事务,如果邮件失败, 不影响订单生成
    NOT_SUPPORTED   以非事务方式运行,如果有事务存在,挂起当前事务
    NEVER   以非事务方式运行,如果有事务存在,抛出异常
    

    NESTED  如果当前事务存在,则嵌套事务执行
        * 依赖于 JDBC3.0 提供 SavePoint 技术 
        * 删除客户 删除订单, 在删除客户后, 设置SavePoint, 执行删除订单,删除订单和删除客户在同一个事务 ,删除部分订单失败, 事务回滚 SavePoint , 由用户控制是事务提交 还是 回滚 

三个代表:
REQUIRED 一个事务, 要么都成功,要么都失败 
REQUIRES_NEW 两个不同事务,彼此之间没有关系  一个事务失败了 不影响另一个事务 
NESTED  一个事务, 在A事务 调用 B过程中, B失败了, 回滚事务到 之前SavePoint , 用户可以选择提交事务或者回滚事务

超时时间:默认值是-1,没有超时限制。如果有,以秒为单位进行设置。

是否是只读事务:建议查询时设置为只读。

TransactionStatus:事务的运行状态,常用方法如下:

  • void flush():刷新事务
  • boolean hasSavePoint():是否存在存储点
  • boolean idComplated():事务是否完成
  • boolean isNewTransaction():是否为新事物
  • boolean isRollbackOnly():事务是否回滚
  • void setRollbackOnly()设置事务回滚

xml方式配置事务

首先添加依赖:


一堆依赖

主要是spring-tx、spring-aspects这两个不要漏了。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.0.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.38</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.9</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.0.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.0.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.0.6.RELEASE</version>
</dependency>

配置事务管理器的bean

<!-- 
    bean的名字叫做transactionManager,因为在配置事务策略的时候需要指定的事务管理器的默认名字就是transactionManager,如果是其他名字,在配置事务策略的时候,需要手动指定。 
-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>

配置事务策略:

<!-- 配置事务策略 -->
<tx:advice id="tx">
    <tx:attributes>
        <!--
            指定对那些方法使用事务
            name:需要进行事务管理的方法名 *代表所有方法,这里需要填方法的名字即可,不是aspectj那种包名加类名方法名。
            isolation:事务隔离级别
            propagation:事务传播行为
            timeout:超时时间
            ready-only:设置事务是否只读
            rollback-for:指定对哪种异常进行回滚
            no-rollback-for:指定对那种异常不进行回滚
         -->
        <tx:method name="*"  />
    </tx:attributes>
</tx:advice>

配置事务AOP:

<!-- 配置aop -->
<aop:config>
    <aop:pointcut id="pt1" expression="execution(* com.bilibili.service.impl.*.*(..))"></aop:pointcut>
    <!-- 配置事务策略运用到事务管理器 -->
    <aop:advisor advice-ref="tx" pointcut-ref="pt1"></aop:advisor>
</aop:config>

注解AOP

在spring主配置文件中:

<!-- 开启spring的注解扫描 -->
<context:component-scan base-package="com.bilibili"></context:component-scan>

<!-- 开启事务的注解扫描 -->
<tx:annotation-driven></tx:annotation-driven>

然后只在方法或者类或者接口上配置@Transactional即可开启事务

纯注解配置

在配置类上添加@EnableTransactionManagement开启注解事务管理:


纯注解配置类

@Configuration  //声明当前是一个配置类,用来代替applicationContext.xml文件
@ComponentScan("com.bilibili")  //开启注解包扫描
@PropertySource("classpath:jdbc.properties") // 加载外部配置文件
@EnableTransactionManagement // 开启注解事务管理
public class SpringConfig 
    
    @Value("$jdbc.url")//引入外部配置文件中的资源
    private String url;
    @Value(("$jdbc.driverClass"))
    private String driverClass;
    @Value("$jdbc.username")
    private String username;
    @Value("$jdbc.password")
    private String password;

    
    @Bean("dataSource")//将bean装配到spring容器中
    public DataSource getDataSource()
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driverClass);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return  dataSource;
    

    @Bean("jdbcTemplate")
    public JdbcTemplate getJdbcTemplate(@Qualifier("dataSource") DataSource dataSource)
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

        return  jdbcTemplate;
    

    @Bean("transactionManager")
    public DataSourceTransactionManager getDataSourceTransactionManager(@Qualifier("dataSource") DataSource dataSource)
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
        return dataSourceTransactionManager;
    

Spring与Web之监听器

在Tomcat中,由于Servlet是Tomcat创建的,无法放入Spring中,当Servlet需要使用Service的时候是不太方便的,此时就可以使用监听器来自动创建ApplicationContext。

spring监听器原理:监听servletContext创建,创建ApplicationContext并将其放入上下文域。

自己实现:

创建一个servletContext监听器

@WebListener()
public class MyListener implements ServletContextListener 

    /**
     *  在ServletContext对象创建的时候,创建spring容器。
     *  1.创建spring容器,需要配置文件的名字,名字并不是固定的,所以可以配置在web.xml中
     *  2.spring容器创建之后,需要能够被所有的servlet来使用,那么需要将spring容器保存起来,保存到哪里?ServletContext域对象中
     *
     */
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) 
        //获取servletContext域对象
        ServletContext servletContext = servletContextEvent.getServletContext();
        //读取web.xml中的配置参数 -- 即spring的核心配置文件的名字
        String contextConfig = servletContext.getInitParameter("contextConfig");
        //创建spring容器
        ApplicationContext ac = new ClassPathXmlApplicationContext(contextConfig);
        //将spring容器保存到servletContext对象中
        servletContext.setAttribute("ac",ac);
    

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) 

    

web.xml中配置spring配置文件位置:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <!-- 配置spring核心配置文件的名字 -->
    <context-param>
        <param-name>contextConfig</param-name>
        <param-value>applicationContext.xml</param-value>
    </context-param>
</web-app>

然后在servlet中就可以获取servletContext中保存的ApplicationContext了。

使用Spring的监听器:

引入依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.0.6.RELEASE</version>
</dependency>

web.xml中配置spring主文件位置和监听器:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <!-- spring核心配置文件的位置
     key:是固定的
    value:格式固定,classpath:文件名
    -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <!-- 告诉tomcat 用于创建spring容器的监听器的位置 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

然后就可以在servlet中使用下面的方式获取ApplicationContext:

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException 
    ApplicationContext ac = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
    userService = (UserService ) ac.getBean("userService");

    userService.register();

其实用了SpringMVC之后不会这么麻烦23333??

以上是关于Web基础之Spring AOP与事务的主要内容,如果未能解决你的问题,请参考以下文章

Spring基础之AOP

Spring基础:声明式事务

Spring基础

Spring之AOP基础知识

Spring基础:Spring概念介绍

SpringSpring系列5之Spring支持事务处理