約定編程 代理
- 切點 通知 連接點 引入 織入
- 你需要記住約定的流程是什麼,然後完成對應的任務,卻不需要知道 底層設計者 是怎麼將 約定的內容 織入對應的流程中的。
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()+"你好!!!");
}
}
-
定義攔截器接口
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; }
後面會給出約定,將這些發方法織入 流程中
- 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);//方法執行 參數是: 要執行的目標,要傳遞的參數
}//會以反射的形式 去調用 原有的方法
}
-
開發自己的攔截器
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 。。。。。。"); } }
- 開始約定了:
-
約定是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......原方法執行完畢的執行
我們已經把服務和 攔截器的方法織入約定的流程中了。
-
-
ProxyBean 代理生成類
如何將 服務類 和 攔截方法 織入對應的流程。是ProxyBean的功能。
動態代理:你需要採訪一個兒童時候,需要他的父母同意。在一些問題上父母替他回答。
對於另一些問題,父母覺得不太適合這個小孩。會拒絕掉。
這時父母就是這名兒童的代理了。
通過代理可以增強或者控制對兒童這個真是對象(target)的訪問。
ProxyBean源碼在下面
- 約定(攔截器接口的方法)
- 會先執行before方法
- 如果useAround 返回true(返回false執行target對象的方法)。則執行around方法(不執行target對象的方法)
- around 參數invocation對象存在 存在一個 proceed方法,可調用target方法
- 無論怎麼樣都會執行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註解
-
爲什麼要用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¬e=哈哈哈
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 ......