3. SpringAOP詳解

2.2 AOP介紹

在軟件業,AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程,通過預編譯方式和運行期間動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。

2.2.1 爲什麼使用AOP

在上一節中講到定義了約定好的方法代理執行的流程(方法可前後和異常處理),那麼我們編寫的其他方法也可以按照這種方式織入實現約定好的流程中,避免了寫大量重複的代碼,也提高了代碼的可維護性。而SpringBoot中是使用註解的方式將方法織入約定的流程中。

下面以數據庫事務處理的情景說明其概念。傳統的JDBC實現插入用戶的代碼。

public class UserService{
    public int insertUser(){
        UserDao userDao = new UserDao();
        user user = new User();
        user.setUsername("lei");
        Connection cnnn = null;
        int result = 0;
        try{
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","1234");
            //非自動提交事務
            conn.setAutoCommit(false);
            result = userDao.insertUser(conn, user);
            //提交事務
            conn.commit();
        }catch (Exception e){
            try{
                //回滾事務
                conn.rollback();
            }catch (SQLException e){
                e.printStackTrace();
            }
            e.printStackTrace();
        }finally{
            if(conn != null){
                try{
                    conn.close();
                }catch(SQLException e){
                    e.printStackTrace();
                }
            }
        }
        return result;
    }
}
public class UserDao{
    public int insertUser(Connect conn, User user) throws SQLException{
        PreparedStatement ps = null;
        try{
            ps = conn.prepareStatement("insert into user(username) values(?)");
            ps.setString(1,user.getUsername());
            return ps.excuteUpdate();
        }finally{
            ps.close();
        }
    }
}

從代碼中可以看到從獲取數據庫連接、事務操控和關閉數據庫連接的過程,都需要使用大量的try…catch…finally語句,這顯然是大量重複的工作。這個處理流程是固定的,我們是否可以把執行SQL的方法織入這個流程中執行呢?這樣就可以省略大量的重複代碼。下面先看一下這個約定的流程。

UTOOLS1578108155410.png

我們需要的是隻編寫執行SQL的語句,可以改造成如下

UTOOLS1578108235529.png

使用這種將方法織入流程的方式後,在Spring中,我們的代碼可以變成非常簡潔

@AutoWired
private UserDao = null;

@Transactional
public int inserUser(User user){
    return userDao.insertUser(user);
}

僅僅一個@Transactional就表明該方法需要事務運行,沒有任何數據庫打開和關閉,也沒有事務回滾和提交的代碼,卻實現了數據庫資源的打開、關閉、事務回滾和提交。這看起來和很棒。

2.2.2 AOP基本要素

在AOP中把上面說到的一些概念進行整合,並分別以相應的名稱命名。

  1. 連接點( join point)
    對應的是具體被攔截的對象,因爲 Spring只能支持方法,所以被攔截的對象往往就是指特定的方法,例如,我們前面提到的 HelloServicelmpl的 sayHello方法就是一個連接點,AOP將通過動態代理技術把它織入對應的流程中

  2. 切點( point cut)
    有時候,我們的切面不單單應用於單個方法,也可能是多個類的不同方法,這時,可以通過正則式和指示器的規則去定義,從而適配連接點。切點就是提供這樣個功能的概念。

  3. 通知( advice)

    就是按照約定的流程下的方法,分爲前置通知( before advice)、後置通知(after advice)、環繞通知( around advice)、事後返回通知( after Returning advice)和異常通知( after Throwing advice),它會根據約定織入流程中,需要弄明白它們在流程中的順序和運行的條件。目標對象( target):即被代理對象,例如,約定編程中的 HelloServicelmpl實例就是一個目標對象,它被代理了。

  4. 引入( introduction)
    是指引入新的類和其方法,增強現有Bean的功能

  5. 織入( weaving)
    它是一個通過動態代理技術,爲原有服務對象生成代理對象,然後將與切點定義匹配的連接點攔截,並按約定將各類通知織入約定流程的過程

  6. 切面( aspect)
    是一個可以定義切點、各類通知和引入的內容, Spring AOP將通過它的信息來增強Bean的功能或者將對應的方法織入流程。

UTOOLS1578117414055.png

2.2.2 Spring AOP簡單例子

上面說到了AOP基本概念及其作用,我們可以知道AOP可以很大的提高開發效率。下面以一個簡單的例子說明Spring中的AOP使用方法。

確定鏈接點,也就是需要織入約定流程的方法。

public interface UserService {
    void printUser(User user);
}
@Service
public class UserServiceImpl implements UserService {
    @Override
    public void printUser(User user){
        if(user == null){
            throw new NullPointerException("用戶爲空");
        }
        System.out.println("用戶名 = "+user.getName());
    }
}

如之前說的MyInterceptor攔截器就像是一個切面,這裏定義一個切面(約定的流程),同時定義切點(確定需要織入流程的方法)

@Aspect
public class MyAspect {
    //定義切點,用於確定哪些方法需要使用織入切面中
    @Pointcut("execution(* xyz.mxlei.aop.UserServiceImpl.printUser(..))")
    public void pointCut(){
    }
    
    @Before("pointCut()")
    public void before(){
        System.out.println("before");
    }

    @After("pointCut()")
    public void after(){
        System.out.println("after");
    }

    @AfterReturning("pointCut()")
    public void afterReturning(){
        System.out.println("afterReturning");
    }

    @AfterThrowing("pointCut()")
    public void afterThrowing(){
        System.out.println("afterThrowing");
    }
}

使用Controller來測試AOP是否成功

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService = null;
    
    @RequestMapping("/print")
    @ResponseBody
    public User printUser(String name) {
        User user = new User();
        user.setName(name);
        userService.printUser(name == null ? null : user);
        return user;
    }
}

配置Springboot啓動類和注入切面到容器中。

@SpringBootApplication
public class AopApplication {
    
    @Bean
    public MyAspect myAspect(){
        return new MyAspect();
    }
    
    public static void main(String[] args) {
        SpringApplication.run(AopApplication.class, args);
    }

}

項目啓動後,瀏覽器輸入
http://localhost:8080/user/print?name=lei

測試結果,方法printUser織入切面MyAspect中。

before
用戶名 = lei
after
afterReturning

瀏覽器輸入

http://localhost:8080/user/print

測試結果,方法printUser織入切面MyAspect中,異常正確處理

before
after
afterThrowing

2.2.3 AOP環繞通知

環繞通知在AOP中是最強大的通知,但是也難以控制,如果使用不當很容易出現問題。環繞通知可以取代原有目標對象的方法。也就說使用了環繞通知,調用的方法就不是原來的方法了,原來的方法將不會再執行了。當然它也能再環繞通知裏面調用原來的對象的方法。

在上面的MyAspect切面中加入環繞通知

@Around("pointCut()")
public void arround(ProceedingJoinPoint joinPoint) throws Throwable{
    System.out.println("arround before");
    //回調原有目標對象的方法
    joinPoint.proceed();
    System.out.println("arround after");
}

執行後可以看到打印結果

before
arround before
用戶名 = lei
arround after
after
afterReturning

2.2.4 引入

在上面的程序中,當user爲空時會拋出異常,從而進入切面的afterThrowing流程。但是當user不爲空,但是用戶名爲空時確實正常執行沒有異常的。這不是我們需要,假設需要的是用戶名爲空時不執行打印,也不進去切面流程。而在開發中也會遇到其他的接口我們需要增強其功能(不能修改原接口)的情況。這時我們可以引入新的接口來增強原來的接口。

public interface UserValidator {
    boolean validate(User user);
}
public class UserValidatorImpl implements UserValidator {
    @Override
    public boolean validate(User user) {
        System.out.println("引入新的接口:"+UserValidator.class.getSimpleName());
        return !user.getName().isEmpty();
    }
}

在MyAspect切面中引入新的類來增強服務

/**
 * 引入新的類來增強服務
 * value指要增強的目標對象
 * defaultImpl指引入增強功能的類
 * */
@DeclareParents(value = "xyz.mxlei.aop.UserServiceImpl+", defaultImpl = UserValidatorImpl.class)
public UserValidator userValidator;

測試上面引入的增強功能

@RequestMapping("/printValidate")
@ResponseBody
public User printUserValidate(String name) {
    User user = new User();
    user.setName(name);
    //強制轉換
    UserValidator userValidator = (UserValidator) userService;
    //驗證用戶名是否爲空
    if(userValidator.validate(user)){
        userService.printUser(user);
    }
    return user;
}

執行發現,當name爲空時,不會進入切面流程;當name不爲空時,切面流程正常執行。

在上面的代碼中,發現UserService對象怎麼能夠強制類型轉換爲UserValidator對象呢?。那麼它是根據什麼原理來增強原有對象功能的呢?,在切面生成代理對象的代碼爲:

Object proxy Proxy. newProxyInstance(
    target getclass().getclassLoader(),
    target getclass().getInterfaces(),
    proxyBean);

這裏的 newProxylnstance的第二個參數爲一個對象數組,也就是說這裏生產代理對象時, Spring
會把 UserService和 UserValidator兩個接口傳遞進去,讓代理對象下掛到這兩個接口下,這樣這個代理對象就能夠相互轉換並且使用它們的方法了。所以可以看到代碼清單4-20中強制轉換的代碼,爲了驗證這點,我們可以加入斷點進行測試。

2.2.5 參數傳遞到切面通知

在切面中的before,after等方法中,除了arround環繞通知默認帶參數ProceedingJoinPoint可以獲取原方法的參數,其他的通知也可以獲取原參數,可以在切點處聲明args(參數名)的方式傳遞參數過來。如下

//定義切點
@Pointcut("execution(* xyz.mxlei.aop.UserServiceImpl.printUser(..)) && args(user)")
public void pointCut(User user){
}

@Before("pointCut(user)")
public void before(User user){
    System.out.println("before");
}

另外,也可以用JoinPoint傳遞參數。

2.2.6 織入

上面的例子中,printUser()方法其實就是織入了約定的流程(Aspect切面)中。在JDK中默認被織入的對象必須有接口才能被動態代理所處理運行,而在CGLIB中則不需要被織入的對象帶有接口形式。Spring會自動判斷被織入的對象是否帶有接口,從而選擇使用JDK或者CGLIB的實現。

2.2.7 多切面的執行順序

Spring中可以多個切面同時作用於一個方法,這是各切面的通知執行順序是亂的。這個時候有兩種方式確定切面的執行順序。

  1. @Order註解切面類
  2. 切面類實現Ordered接口

推薦使用@Order註解更爲方便。

@Aspect
@Order(1)
public class MyAspect1 {
    @Before("execution(* xyz.mxlei.aop.UserServiceImpl.printUser(..))")
    public void before(User user) {
        System.out.println("before");
    }
}
@Aspect
@Order(2)
public class MyAspect2 {
    @Before("execution(* xyz.mxlei.aop.UserServiceImpl.printUser(..))")
    public void before(User user) {
        System.out.println("before");
    }
}

執行順序爲前置通知before根據order值從小到大,後置通知從大到小,這是典型的責任鏈模式的順序。

UTOOLS1578123017810.png
附上源代碼

https://github.com/mxxlei/study-spring-aop

發佈了123 篇原創文章 · 獲贊 201 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章