Web基礎之Spring AOP與事務

Spring之AOP

AOP 全程Aspect Oriented Programming,直譯就是面向切面編程。和POP、OOP類似,它也是一種編程思想。OOP強調的是封裝、繼承、多態,也就是功能的模塊化。而AOP則是OOP的補充,它強調的是切面,在運行時動態地將代碼切入到類的指定方法、指定位置上的編程思想,也就是將業務代碼和業務先後的代碼分離出來(解耦),將日誌、權限驗證等功能抽取出來而後重用。
在Spring中,採用動態代理的方式來表達AOP。(並不是全部的AOP都是使用動態代理來,好比AspectJ採用編譯時建立代理對象,比運行時建立效率更高)
動態代理通常有兩種實現方式,一種是JDK原生動態代理,要求被代理對象必須實現接口,而且只能代理接口中的方法(本質是建立一個實現接口的代理對象)。另外一種是CGlib,能夠代理全部的方法(本質是建立一個代理對象,繼承被代理對象)。Spring中使用的是CGlib的方式。java

廢話很少說,直接介紹Spring中的AOP。mysql

Spring AOP相關術語

  • Joinpoint(鏈接點):任何能夠被加強的方法,都稱爲鏈接點。在spring中,這些點指的是方法,由於spring只支持方法類型的鏈接點。
  • Pointcut(切入點):將要被加強的方法。即咱們要對哪些Joinpoint進行攔截的定義。
  • Advice(通知/加強):所謂通知是指攔截到Joinpoint以後所要作的事情就是通知。通知的類型:前置通知,後置通知,異常通知,最終通知,環繞通知。
  • Introduction(引介):Adivice是對方法進行加強的,而Introdution是針對類進行加強的
  • Target(目標對象):被代理的目標對象。
  • Weaving(織入):是指把加強應用到目標對象來建立新的代理對象的過程。spring採用動態代理織入,而AspectJ採用編譯期織入和類裝載期織入。
  • Proxy(代理):一個對象被jdk代理或者cglib代理後的對象,稱爲代理
  • Aspect(切面):多個通知和切入點的配置關係。

基於xml的AOP配置

首先是依賴:
web


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主配置文件: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"
       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表達式的匹配方式:sql

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>

把被代理對象註冊到容器中:
express


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便可。
tomcat


註解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對象。mybatis

  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🤣

相關文章
相關標籤/搜索