深入淺出boot2.0第4章 aop

約定編程 代理

  • 切點 通知 連接點 引入 織入
  • 你需要記住約定的流程是什麼,然後完成對應的任務,卻不需要知道 底層設計者 是怎麼將 約定的內容 織入對應的流程中的。
public interface HelloService {
	public void sayHello(String name);
}


public class HelloServiceImpl implements HelloService {
    @Override
    public void sayHello(String name) {
        if (StringUtils.isEmpty(name)){
            throw new RuntimeException("參數爲空!");
        }
        System.out.println(name.trim()+"你好!!!");
    }
}
  1. 定義攔截器接口

    public interface Interceptor {
        
    	//事前方法
    	public boolean before();
    	
    	//事後方法
    	public void after();
        
        //是否返回方法。事件沒有發生異常執行
    	public void afterReturning();//無異常執行
    	
    	//事後異常方法,當事件發生異常後執行
    	public void afterThrowing();//有異常就執行
        
        
        //是否使用around(下面的)方法取代原有方法
    	boolean useAround();
        
        
    	/**
    	 * 取代原有事件方法
    	 * @param invocation -- 回調參數,可以通過它的proceed方法,回調原有事件
    	 * @return 原有事件返回對象
    	 
    	 * @throws InvocationTargetException 
    	 * @throws IllegalAccessException
    	 */
    	public Object around(Invocation invocation) throws InvocationTargetException, IllegalAccessException;
    	
    	
    }
    

後面會給出約定,將這些發方法織入 流程中

  1. Invocation定義
@Data
public class Invocation {

	private Object[] params;//參數
	private Method method;//方法
	private Object target;//目標
	
    //全參 構造賦值
	public Invocation(Object target, Method method, Object[] params) {
		this.target = target;
		this.method = method;
		this.params = params;
	}
	
	public Object proceed() throws InvocationTargetException, IllegalAccessException {
		return method.invoke(target, params);//方法執行 參數是:  要執行的目標,要傳遞的參數
	}//會以反射的形式 去調用 原有的方法
}

  1. 開發自己的攔截器

    public class MyInterceptor implements Interceptor {
    
    	@Override
    	public boolean before() {
    		System.out.println("before ......");
    		return true;
    	}
    
    	@Override
    	public void after() {
    		System.out.println("after ......");
    	}
        
        @Override
    	public boolean useAround() {
    		return true;
    	}
        
    
    	@Override
    	public Object around(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
    		System.out.println("around before ......");//增強代碼
    		Object obj = invocation.proceed();//執行原來的方法
    		System.out.println("around after ......");//增強代碼
    		return obj;
    	}
    
    	@Override
    	public void afterReturning() {//事後,沒發生於異常執行
    		System.out.println("afterReturning......");
    
    	}
    
    	@Override
    	public void afterThrowing() {//事後,發生了異常執行
    		System.out.println("afterThrowing 。。。。。。");
    	}
    
    }
    
    1. 開始約定了:
    • 約定是Spring Aop的本質

    • 提供一個ProxyBean類,有一個靜態方法 給他人使用

      • ​ public static Object getProxyBean(Object target, Interceptor interceptor) {}

      • target 存在接口。interceptor 是上面定義的接口

      • 返回一個對象記作:proxy,使用target對象進行強制轉換

        		HelloService helloService = new HelloServiceImpl();//定義一個類
        		HelloService proxy = (HelloService) ProxyBean.getProxyBean(helloService, new MyInterceptor());
        		//用這個類得到一個代理,
        		proxy.sayHello("zhangsan");//使用代理調用方法
        		System.out.println("\n###############name is null!!#############\n");
        		proxy.sayHello(null);
        
      • 生成代理對象的時候會走一遍流程:

        before ......攔截器的before方法
        around before ......攔截器的around的方法,invocation.proceed()前,下面執行原方法
        around after ......攔截器的around的方法,invocation.proceed()後
        after ......攔截器的after方法
        afterReturning......原方法執行完畢的執行
        
      • 用這個代理調用方法的時候也會走一遍流程

        before ......攔截器的before方法
        around before ......攔截器的around的方法,invocation.proceed()前,下面執行原方法
        
        zhangsan你好!!!原方法執行了
        
        around after ......攔截器的around的方法,invocation.proceed()後
        
        after ......攔截器的after方法
        afterReturning......原方法執行完畢的執行
        
        cao,下面怎麼又執行了一遍。好像每一個方法,都會執行下面的。哪怕sout.("hello word")
        
        before ......攔截器的before方法
        around before ......攔截器的around的方法,invocation.proceed()前,下面執行原方法
        
        around after ......攔截器的around的方法,invocation.proceed()後
        
        after ......攔截器的after方法
        afterReturning......原方法執行完畢的執行
        
      • 異常的執行了流程

        before ......攔截器的before方法
        around before ......攔截器的around的方法,invocation.proceed()前,下面執行原方法
        
        after ......攔截器的after方法
        afterThrowing 。。。。。。原方法異常了的執行
        
        //又有一個方法執行了
        before ......攔截器的before方法
        around before ......攔截器的around的方法,invocation.proceed()前,下面執行原方法
        
        around after ......攔截器的around的方法,invocation.proceed()後
        
        after ......攔截器的after方法
        afterReturning......原方法執行完畢的執行
        

        我們已經把服務和 攔截器的方法織入約定的流程中了。

  2. ProxyBean 代理生成類

如何將 服務類 和 攔截方法 織入對應的流程。是ProxyBean的功能。

動態代理:你需要採訪一個兒童時候,需要他的父母同意。在一些問題上父母替他回答。

對於另一些問題,父母覺得不太適合這個小孩。會拒絕掉。

這時父母就是這名兒童的代理了。

通過代理可以增強或者控制對兒童這個真是對象(target)的訪問。

ProxyBean源碼在下面
  1. 約定(攔截器接口的方法)
    1. 會先執行before方法
    2. 如果useAround 返回true(返回false執行target對象的方法)。則執行around方法(不執行target對象的方法)
      1. around 參數invocation對象存在 存在一個 proceed方法,可調用target方法
    3. 無論怎麼樣都會執行after方法。 發生異常執行afterThrowing,不發生異常執行 afterReturning方法
  • jdk提供了類proxy的靜態方法newProxyInstance 生成代理對象

        public static Object newProxyInstance(ClassLoader loader,
                                              Class<?>[] interfaces,
                                              InvocationHandler h)
            throws IllegalArgumentException
        {
        
        }
    
    • ClassLoader 類加載器
    • interfaces綁定的接口,把代理對象綁定到那些接口下
    • InvocationHandler綁定代理對象邏輯實現
  • InvocationHandler 解析

    public Object invoke(Object proxy, Method method, Object[] args)  {}
    目標對象,方法,參數
    
  • ProxyBean源碼

public class ProxyBean implements InvocationHandler {//implements InvocationHandler

	private Object target = null;
	private Interceptor interceptor = null;
	
	/**
	 * 綁定代理對象
	 * @param target 被代理對象
	 * @param interceptor 攔截器
	 * @return 代理對象
	 */
	public static Object getProxyBean(Object target, Interceptor interceptor) {
		ProxyBean proxyBean = new ProxyBean(); // new ProxyBean(); 保存目標對象 和攔截器
		// 保存被代理對象
		proxyBean.target = target;
		// 保存攔截器
		proxyBean.interceptor = interceptor;
		// 生成代理對象 getProxyBean 生成代理對象。 
		Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
				proxyBean);//類加載器,接口, 一個 implements InvocationHandler綁定代理對象
		// 返回代理對象
		return proxy;
	}
    /**
    *生成了一個代理對象,這個代理對象掛在target實現 的 接口之下
    *所以你 可以用 target對象 實現的接口 對這個代理對象 實現 強制裝換
    *並且這個將這個代理對象的邏輯掛在 ProxyBean實例之下
    * 完成了目標對象(target) 和 代理對象(Proxy) 的綁定
    * 最後將代理對象返回給調用者
    
    * HelloService proxy = (HelloService) ProxyBean.getProxyBean(helloService, new MyInterceptor());
    * 當我們使用 代理 調用方法時,就會進入ProxyBean的invoke方法裏
    * 這就是我們通過一定的規則完成約定編成的原因。
    */
    
	
	/**
	 * 處理代理對象方法邏輯
	 * @param proxy 代理對象
	 * @param method 當前方法
	 * @param args  運行參數
	 * @return 方法調用結果
	 * @throws Throwable 異常
	 */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args)  {
		//異常標識
		boolean exceptionFlag = false;
		//創建Invocation對象
		Invocation invocation = new Invocation(target, method, args);
		Object retObj = null; 
		try {
			if (this.interceptor.before()) {//先執行before,如果before爲true
				retObj = this.interceptor.around(invocation);//執行around方法
			} else {
				retObj = method.invoke(target, args);//如果before爲false,還執行原來的方法
			}
		} catch (Exception ex) {
			//產生異常
			exceptionFlag = true;//執行異常了
		}
		this.interceptor.after();//執行after方法
		if (exceptionFlag) {//如果異常了,就執行afterThrowing
			this.interceptor.afterThrowing();
		} else {//否則執行afterReturning方法
			this.interceptor.afterReturning();
			return retObj;
		}
		return null;
	}

只要提供一定的約定規則,按照約定編程後

就可以把自己開發的代碼織入約定的流程中

jdbc經典代碼



Connection conn = null;
		int result = 0;
		try {
			//先註冊驅動
			Class.forName("com.mysql.jdbc.Driver");
			//在通過驅動管理器,獲取數據事務連接
			conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
			//非自動提交事務
			conn.setAutoCommit(false);
			//執行層
			PreparedStatement ps = null;
			try {
				//預設sql
				ps = conn.prepareStatement("insert into user(user_name,note) values (?,?)");
				//參數指定
				ps.setString(1, "張三");
				ps.setString(2, "study");
				//執行
				result = ps.executeUpdate();
			} finally {
				//怎樣都關閉連接
				ps.close();
			}

			//提交事務
			conn.commit();
		} catch (Exception e) {
			try {
				//回滾事務
				conn.rollback();
			} catch (SQLException ex) {
				ex.printStackTrace();
			}
			e.printStackTrace();
		} finally {
			//釋放連接池
			if (conn != null) {
				try {
					conn.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
		}
		if (result == 0) {
		} else {
		}

存在一個默認的過程

  • 打開數據庫連接,然後對其進行設置
  • 這行SQL語句
  • 如果沒有異常,就提交
  • 如果發生異常,則回滾
  • 關閉數據庫事務連接

這個默認流程 可以通過 Aop來實現,你只需要編寫SQL這一步,然後織入流程中

AOP概念

  • Aop 也是一種約定流程的編程
  • @AspectJ註解
  1. 爲什麼要用aop

    數據庫事務的管控,當我們要保存一個用戶時,連同它的角色信息一併保存到數據庫中。

    要麼一起成功,要麼一起失敗。(oop無能爲力)

@Transactional //實現了 數據庫的打開和關閉 ,事務的回滾 和 提交
public int inserUser(User user){
	return userDao.insertUser(user);
}
  • spring Aop 可以處理 OOP實現的業務邏輯

  • Spring Aop是一種基於方法的aop

SpringAop 流程約定:

  • 連接點 join point 。 HelloServiceImpl sayHello方法。是具體被攔截的對象。

  •     public void sayHello(String name) {
            if (StringUtils.isEmpty(name)){
                throw new RuntimeException("參數爲空! 異常方法執行了");
            }
            System.out.println(name.trim()+"你好!!!原方法執行了");
        }
    
  • 切面 aspect 。 MyInterceptor 。包含前置通知 before ,環繞通知around,後置通知after,事後返回通知 afterReturning ,和 異常通知 afterThrowiing

    	@Override
    	public void after() {
    		System.out.println("after ......攔截器的after方法");
    	}
    
    	@Override
    	public boolean useAround() {
    		return true;
    	}
    	
    	@Override
    	public Object around(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
    		System.out.println("around before ......攔截器的around的方法,invocation.proceed()前,下面執行原方法");
    		Object obj = invocation.proceed();
    		System.out.println("around after ......攔截器的around的方法,invocation.proceed()後");
    		return obj;
    	}
    
  • 切點 point cut 。有時候不單是單個方法,也可能是多個類的不同方法。通過正則 和 指示器的規則 去定義

  • 通知 advice,就是切面中的那些通知

  • 目標對象 target 。即:被代理對象,HelloServiceImpl 實例

  • 引入 introduction。引入新的類和其他方法,增強現有的bean功能

  • 織入 weaving。通過動態代理技術,爲原服務生成代理對象。

aop 開發詳細

確定連接點

public interface UserService {
	public void printUser(User user);
}

@Service
public class UserServiceImpl implements UserService {
	
	@Override
	public void printUser(User user) {
		if (user == null) {
			throw new RuntimeException("檢查用戶參數是否爲空......");
		}
		System.out.print("id =" + user.getId());
		System.out.print("\tusername =" + user.getUsername());
		System.out.println("\tnote =" + user.getNote());
	}
}

開發切面

1.最簡單的切面

@Aspect
public class MySimpleAspect {

	//定義切點
	@Pointcut("execution(* com.springboot.chapter4.aspect.service.impl.UserServiceImpl.printUser(..))")
	public void pointCut() {
	}

	//前置通知 1
	@Before("pointCut()")
	public void before() {
		System.out.println("before ......"+new Date(System.currentTimeMillis()));
	}

	//環繞通知 2
	@Around("pointCut()")
	public void around(ProceedingJoinPoint jp) throws Throwable {
		System.out.println("around before......");
		jp.proceed();
		System.out.println("around after......");
	}

	//後置通知 3
	@After("pointCut()")
	public void after() {
		System.out.println("after ......"+new Date(System.currentTimeMillis()));
	}
	
	//事後返回通知 4
	@AfterReturning("pointCut()")
	public void afterReturning() {
		System.out.println("afterReturning ......");
	}

	//異常通知
	@AfterThrowing("pointCut()")
	public void afterThrowing() {
		System.out.println("afterThrowing ......");
	}
	
}
  • 畢竟不是所有的功能都是需要啓用Aop的,spring通過這個正則去匹配,去確定對應的方法,是否啓用切面編程。

“execution( * com.springboot.chapter4.aspect.service.impl.UserServiceImpl.printUser(…) )”

execution 執行的時候,攔截裏面的正則匹配的方法

*任意返回類型的方法

com.****.pringUser 指定目標對象的方法

(…) 任意參數進行匹配

AspectJ關於 Spring Aop切點的指示器

項目類型 描述
arg() 限定連接點方法參數
@args 連接點方法參數上的註解進行限定
execution() 連接點的執行方法
this() 限制連接點匹配Aop代理Bean引用爲指定的類型
target 目標(被代理)對象
@target() 限制目標對下你給的配置了指定的註解
within 限制連接點匹配指定的註解
@within() 限制連接點帶有匹配註解類型
@annotation() 限制帶有指定註解的連接點
	@Pointcut("execution(* com.springboot.chapter4.*.*.*.*.print(..)) && bean('userServiceImpl')") bean沒用,並且報錯。前面的可用
	public void pointCut() {
	}

execution(* com.springboot.chapter4.*.*.*.*.printUser(..)) && bean('userServiceImpl')
			com.springboot.chapter4.aspect.service.impl.UserServiceImpl.printUser

&& 並且

bean(‘userServiceImpl’) 代表對Spring Bean名稱的限定

測試Aop

pom

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId> //必要依賴
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-jasper</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-integration</artifactId>
		</dependency>
	</dependencies>

控制器

//定義控制器
@Controller
// 定義類請求路徑
@RequestMapping("/user")
public class UserController {

	// 注入用戶服務
	@Autowired
	private UserService userService = null;

	// 定義請求
	@RequestMapping("/print")
	// 返回json
	@ResponseBody
	public User printUser(Long id, String userName, String note) {
		User user = new User();
		user.setId(id);
		user.setUsername(userName);
		user.setNote(note);
		userService.printUser(user);
		return user;
	}
}

配置

@SpringBootApplication(scanBasePackages = { "com.springboot.chapter4.aspect" })
public class Chapter4Application {

	// 啓動切面
	public static void main(String[] args) {
		SpringApplication.run(Chapter4Application.class, args);
	}

	// 定義切面
	@Bean(name = "myAspect")
	public MyAspect initMyAspect() {
		return new MyAspect();
	}
}

userService 可以看出是個 jdk 動態代理對象

http://localhost:8080/user/print/?id=111&note=哈哈哈

before ......Sun May 03 18:27:10 CST 2020   之前
id =111	username =null	note =哈哈哈 方法體
after ......Sun May 03 18:27:25 CST 2020  之後
afterReturning ...... 事後返回

環繞通知

  • 最強大的通知,也意味着難以控制

  • 場景:你需要 大幅度修改原有 目標對象的服務邏輯時。否則:儘可能用其他通知。

  • 是一個取代 原有目標對象 方法的通知。也提供了回調原方法的能力。

    	切面類:
    
    	//環繞通知 2
    	@Around("pointCut()")
    	public void around(ProceedingJoinPoint jp) throws Throwable {
    		System.out.println("around before......");
    		jp.proceed();//回到原有的放
    		System.out.println("around after......");
    	}
    
around before......   這個順序是錯的,它很強大,卻很危險

before ......Sun May 03 21:43:56 CST 2020 。這個在應該放在 第一位

id =111	username =null	note =哈哈哈
around after......

after ......Sun May 03 21:44:06 CST 2020

afterReturning ......

spring 4.3.9 xml測試,沒問題

引入

  • 引入用戶檢測的接口

    public interface UserValidator {
    	public boolean validate(User user);
    }
    //檢測用戶信息是否爲空,如果爲空 則不再打印
    public class UserValidatorImpl implements UserValidator {
    	@Override
    	public boolean validate(User user) {
    		System.out.println("引入新的接口:"+ UserValidator.class.getSimpleName());
    		return user != null && user.getUsername() != null;//大概需要這樣校驗
    	}
    }
    
    
  • 使用

    @Aspect
    public class MyAspect {
    	@DeclareParents(value= "com.springboot.chapter4.aspect.service.impl.UserServiceImpl+", defaultImpl=UserValidatorImpl.class)
    	public UserValidator userValidator;
    }
    
    @DeclareParents(value= "com.UserServiceImpl+", defaultImpl=UserValidatorImpl.class)
    
    • @DeclareParents 引入新的類來增強服務
    • value XX + 要增強的對象
    • defaultImpl 引入增強的類
  • action 測試引入的驗證器

    		// 注入用戶服務
    	@Autowired
    	private UserService userService = null;
    
    	// 定義請求
    	@RequestMapping("/vp")
    	// 返回json
    	@ResponseBody
    	public User validateAndPrint(Long id, String userName, String note) {
    		User user = new User();
    		user.setId(id);
    		user.setUsername(userName);
    		user.setNote(note);
    		// 強制轉換
    		UserValidator userValidator = (UserValidator) userService;
    		// 驗證用戶是否爲空
    		if (userValidator.validate(user)) {
    			userService.printUser(user);
    		}
    		return user;
    
        }
    
  • 原理

Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
				proxyBean);
//類加載器,接口, 一個 implements InvocationHandler綁定代理對象

	源碼:
    @CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
  • 第二個參數是一個對象數據。生成代理對象 會把 UserService 和 UserValidator 兩個參數傳遞進去
  • 讓代理對象掛到這兩個接口下,

獲取通知的參數

  • 傳遞參數給通知

  • 只需要在切點處 加入對應 的正則式 就可以了。

  • 非環繞通知 還可以 使用 一個連接點 JoinPoint 類型的參數

  • 環繞通知 還是 那個可執行 原方法的對象 ProceedingJoinPoint jp;jp.proceed();

    	@Before("pointCut() && args(user)")
    	public void beforeParam(JoinPoint point, User user) {
    		Object[] args = point.getArgs();
    		System.out.println("before ......");
    	} 
    
    
    
    	//前置通知 1
    	@Before("pointCut() && args(user)")
    	public void before(JoinPoint point, User user) {
    		Object[] args = point.getArgs();
    		System.out.println("所有的參數:"+args.toString());
    		System.out.println("before ......"+new Date(System.currentTimeMillis()));
    	}
    

織入

  • 是一個生成動態代理對象 並且將切面 和 目標對象方法 編織 成爲約定流程 的過程

  • 動態代理,jdk,cglib,javassist,asm,

  • jdk 要求 ,被代理的目標對象 必須擁有接口。cglib都可以

  • 當你需要使用Aop的類擁有接口時,它會以Jdk動態代理運行。否則cglib。

  • 注意最新版的:5.2.5 我看到的,都是cglib

  • 最重要的還是這一句話

  Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
  				proxyBean);
  //類加載器,接口, 一個 implements InvocationHandler綁定代理對象

多個切面

@Aspect
public class MyAspect1 implements Ordered {
	
	@Override
	public int getOrder() {
		return 1; //執行順序爲1
	}

	@Pointcut("execution(* com.example.demobenaware.service.impl.UserServiceImpl.manyAspects(..))")
	public void manyAspects() {
	}

	@Before("manyAspects()")
	public void before() {
		System.out.println("MyAspect1 before ......");
	}

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

	@After("manyAspects()")
	public void afterReturning() {
		System.out.println("MyAspect1 afterReturning ......");
	}

}


@Aspect
@Order(2) //執行順序爲2,任意選擇一種繼承 或者註解都行
public class MyAspect2 implements Ordered {

	@Override
	public int getOrder() {
		return 2;
	}
}


	// 定義切面
	@Bean(name = "myAspect2")
	public MyAspect2 initMyAspect2() {
		return new MyAspect2();
	}

	// 定義切面
	@Bean(name = "myAspect1")
	public MyAspect1 initMyAspect1() {
		return new MyAspect1();
	}

  • http://localhost:8080/user/manyAspects
從外 到裏執行,在從裏到外 執行
MyAspect1 before ......
MyAspect2 before ......
測試多個切面順序
MyAspect2 after ......
MyAspect2 afterReturning ......

MyAspect1 after ......
MyAspect1 afterReturning ......
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章