目录
一、代理:
1.1 什么是动态代理
动态代理利用Java的反射技术(Java Reflection)生成字节码,在运行时创建一个实现某些给定接口的新类(也称"动态代理类")及其实例。
1.2 动态代理的优势
动态代理的优势是实现无侵入式的代码扩展,也就是方法的增强;让你可以在不用修改源码的情况下,增强一些方法;在方法的前后你可以做你任何想做的事情(甚至不去执行这个方法就可以) spring中的AOP是动态代理使用的经典场景。
1.3 基于JDK动态代理实现
在基于JDK的动态代理的实现中有两个重要的类:InvocationHandler, Proxy
- InvocationHandler 是代理实例的调用处理程序实现的接口。每个代理实例都具有一个关联的调用处理程序。对代理实例调用方法时,将对方法调用进行编码并将其指派到它的调用处理程序的 invoke 方法。
- Proxy JDK中动态生成代理类的工具类
一个动态代理的示例:
定义一个接口(基于JDK的动态代理只能使用接口)
public interface ISubject {
void hello(String param);
}
为接口定义实现类
public class SubjectImpl implements ISubject {
@Override
public void hello(String param) {
System.out.println("hello " + param);
}
}
实现一个代理类:
public class JDKProxy implements InvocationHandler {
private Object target;
//调用时传入对象的目标对象(需要实现代理的具体类)
public JDKProxy(Object target) {
this.target = target;
}
//创建代理
public Object newProxy() {
/**
* newProxyInstance,方法有三个参数:
*
* loader: 用哪个类加载器去加载代理对象
*
* interfaces:动态代理类需要实现的接口必须是加载器继承的接口
*
* h:动态代理方法在执行时,会调用h里面的invoke方法去执行 this就相当于调用本类中的
invoke 也可以写别的地方
*/
return (ISubject)Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("---------- 在业务方法调用之前可以进行前置增强 ------------");
//利用反射机制调用方法,invoke为返回值,如果没有返回null
Object invoke = method.invoke(target, args);
System.out.println("---------- 在业务方法调用之前可以进行后置增强 ------------");
return invoke;
}
}
编写代理类实际的调用,利用Proxy类创建代理之后的Subject类
public class JDKProxyDemo {
public static void main(String[] args) {
ISubject subject = new SubjectImpl();
//将具体类赋值给目标对象
JDKProxy subjectProxy = new JDKProxy(subject);
ISubject proxyInstance = (ISubject)subjectProxy.newProxy();
proxyInstance.hello("world");
}
}
运行结果:
二、 AOP
2.1 基本概念
- 连接点 (Joinpoint) 程序执行过程中明确的点,如方法的调用,或者异常的抛出.
- 目标(Target) 被通知(被代理)的对象,如上例中的SubjectImpl
- 通知(Advice) 在某个特定的连接点上执行的动作,同时Advice也是程序代码的具体实现,例如一个实现日志记录的代码(通知有些书上也称为处理),可以理解为AOP真正要实现的功能
- 代理(Proxy) 将通知应用到目标对象后创建的对象(代理=目标+通知),请注意:只有代理对象才有AOP功能,而AOP的代码是写在通知的方法里面的,如上例中的JDKProxy
- 切入点(Pointcut) 多个连接点的集合,定义了通知应该应用到那些连接点。也将Pointcut理解成一个条件 ,此条件决定了容器在什么情况下将通知和目标组合成代理返回给外部程序
- 适配器(Advisor) 适配器=通知(Advice)+切入点(Pointcut)
AOP运行原理:目标对象只负责业务逻辑,通知只负责AOP增强逻辑(如日志,数据验证等),而代理对象则将业务逻辑而AOP增强代码组织起来(组织者)
2.2 AOP带来的好处
AOP是公用的框架代码放置的理想地方,将公共代码与业务代码分离,使我们在处理业务时可以专心的处理业务。 伪代码:
public void doSameBusiness (long lParam,String sParam){
// 记录日志
log.info("调用 doSameBusiness方法,参数是:"+lParam);
// 输入合法性验证
if (lParam<=0){
throws new IllegalArgumentException("xx应该大于0");
}
if (sParam==null || sParam.trim().equals("")){
throws new IllegalArgumentException("xx不能为空");
}
// 异常处理
try{
真正的业务处理
}catch(...){
}catch(...){
}
// 事务控制
tx.commit();
}
通过使用AOP我们可以将日志记录,数据合法性验证,异常处理等功能放入AOP中,那么在编写业务时就可以专心实现真正的业务逻辑代码。
3. Spring AOP
在spring中org.springframework.aop.framework.ProxyFactoryBean用来创建代理对象,在一般情况下它需要注入一下三个属性:
- proxyInterfaces 代理应该实现的接口列表(List)
- interceptorNames 需要应用到目标对象上的通知Bean的名字
- target 目标对象 (Object)
准备工作:创建一个IBookService接口及其实现类,用于演示spring AOP开发示例:
package com.badu.service;
import com.badu.model.Book;
import java.util.List;
public interface IBookService {
List<Book> listBOoks();
/**
*
* 购买 书本
* @param userName
* @param bookName
* @param price
* @return
*/
boolean buy(String userName, String bookName, Double price);
/**
* 评论
* @param userName
* @param comments
*/
void comment(String userName, String comments);
}
在定义 实现类 不适用注解 实现通知
package com.badu.service;
import com.badu.aop.PriceException;
import com.badu.dao.IBookDao;
import com.badu.model.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
//@Service(bookservice) 与下面 结果一个 没有指定id 则就是本内名第一个字母小写
//@Service
public class BookService implements IBookService{
/**
* spring 会根据类型进行 赋值 但 注意 如果又两个实现类则需要指定 否则会直接报错
*/
@Autowired
private IBookDao book;
public void setBook(IBookDao book) {
this.book = book;
}
@Override
public List<Book> listBOoks() {
/* String i=null;
System.out.println(i.length());*/
return book.listBOoks();
}
@Override
public boolean buy(String userName, String bookName, Double price) {
//logger.info("userName={},bookName={},price={}", userName, bookName, price);
// 通过控制台的输出方式模拟购书
System.out.println(userName + " buy " + bookName + ", spend " + price);
//抛出自定义异常
// throw new PriceException();
return true;
}
@Override
public void comment(String userName, String comments) {
System.out.println(userName + " say:" + comments);
}
}
将service配置到spring配置文件中,以便于被spring管理,按自己的实际情况配置
<!-- 目标 -->
<bean id="bookServiceTarget" class="com.badu.service.BookService"/>
3.1 前置通知
前置通知需要实现org.springframework.aop.MethodBeforeAdvice,前置通知将在目标对象调用前调用。示例实现购书系统AOP方式实现日志,简单打印调用的方法及参数
1)首先实现一个前置通知类,实现接口MethodBeforeAdvice,并实现接口中的before方法
package com.badu.aop;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* 前置通知
*/
public class MyMEthodBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
String s = "[前置通知]: "
+ this.getClass() + "."
+ method.getName()
+ "将被调用,参数为:"
+ Arrays.toString(args);
System.out.println(s);
}
}
2)将实现的前置通知配置到Spring.xml中,一遍与被spring管理。需要根据自己的实际情况配置。
<bean id="methodBeforeAdvice" class="com.badu.aop.MyMEthodBeforeAdvice"/>
3)现在需要解决如何将通知和目标联系起来,需要一个组织者 - 代理 固定参数 不可改动
<bean id="bookService" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 配置代理目标 -->
<property name="target" ref="bookServiceTarget"/>
<!-- 配置拦截器列表,拦截器就是通知 -->
<property name="interceptorNames">
<list>
<value>methodBeforeAdvice</value>
</list>
</property>
<!-- 代理要实现的接口,代理类与被代理类需要实现相同接口 -->
<property name="proxyInterfaces">
<list>
<value>com.badu.service.IBookService</value>
</list>
</property>
</bean>
写一个测试类,测试前置通知
public class Demo {
public static void main(String[] args) {
ApplicationContext <u>cxt</u> = new ClassPathXmlApplicationContext("/spring.xml");
IBookService bookService = (IBookService)cxt.getBean("bookService");
System.out.println(bookService.getClass().getName());
bookService.buy("zs", "hlm", 10D);
}
}
3.2 后置通知
在连接点正常完成后执行的通知。定义的后置通知类需要实org.springframework.aop.AfterReturningAdvice
示例:在线购书系统中,要求不修改BookServiceImpl代码的情况下增加如下功能:对买书的用户进行返利:每买本书返利10元,简单打印类似于“[后置通知] 返利10元”即可。开发步骤与前置通知类似
1) 编写一个后置通知实现类
package com.badu.aop;
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
/**
* 后置通知
*/
public class MyAfterReturnAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("后置通知"+method.getName());
}
}
2) 将后置通知实现类配置到spring配置文件中,以便于spring管理
<bean id="methodReturnAdvice" class="com.badu.aop.MyAfterReturnAdvice"/>
3)解决如何将通知和目标联系起来,在实现前置通知时已经配置了代理对象,现在只要将后置通知也配置到拦截器列表当中即可。
<!-- 配置通知 -->
<property name="interceptorNames">
<list>
<!-- 前置通知 -->
<value>methodBeforeAdvice</value>
<!-- 后置通知 -->
<value>methodReturnAdvice</value>
</list>
</property>
运行上例已经实现的测试类,查看后置通知的运行效果。
3.3 环绕通知
包围一个连接点的通知,最大特点是可以修改返回值,由于它在方法前后都加入了自己的逻辑代码,因此功能很强大。自定义的环绕通知需要实现org.aopalliance.intercept.MethodInterceptor接口。 示例:在环绕通知中输出日志和返回值
1)实现一个环绕通知,该类要实现MethodInterceptor接口
package com.badu.aop;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* 环绕通知i
*/
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//获取目标对象
Object target = invocation.getThis();
//获取参数
Object[] args = invocation.getArguments();
//获取方法
Method method = invocation.getMethod();
System.out.println("[环绕通知] 前:将调用"+target.getClass()+"."+ method.getName()+"方法,参数为"+Arrays.toString(args)+""
);
//调用目标对象上的方法
Object val = invocation.proceed();
System.out.println("[环绕通知] 前:将调用"+target.getClass()+"."+ method.getName()+"方法,返回值为"+val+""
);
return val;
}
}
2)在spring的配置文件中配置环绕通知,以便于spring管理
<bean id="methodInterceptor" class="com.badu.aop.MyMethodInterceptor"/>
3)解决如何将通知和目标联系起来,在实现前置通知时已经配置了代理对象,现在只要将环绕通知也配置到拦截器列表当中即可。
<!-- 配置通知 -->
<property name="interceptorNames">
<list>
<!-- 前置通知 -->
<value>methodBeforeAdvice</value>
<!-- 后置通知 -->
<value>methodReturnAdvice</value>
<!-- 环绕通知 -->
<value>methodInterceptor</value>
</list>
</property>
运行上例已经实现的测试类,查看后置通知的运行效果。
3.4 异常通知
异常通知需要实现ThrowsAdvice接口,这个通知会在方法抛出异常退出时执行,与以上演示的前置、后置、环绕通知不同,主要有一下特点:
- 这个接口里面没有定义方法,要求我们的类必须实现afterThrows这个方法
- 以异常类型作为参数,无返回值
示例 1)定义一个自定义异常,继承RuntimeException运行时异常
public class PriceException extends RuntimeException {
public PriceException() {
super();
}
public PriceException(String message, Throwable cause) {
super(message, cause);
}
public PriceException(String message) {
super(message);
}
public PriceException(Throwable cause) {
super(cause);
}
}
2)创建异常通知类,该类实现ThrowsAdvice接口,并实现afterThrowing方法
package com.badu.aop;
import org.springframework.aop.ThrowsAdvice;
/**
* 异常通知类
*/
public class MyThrowsAdvice implements ThrowsAdvice {
/**
* 约定 方法名 不可一定要是 afterThrowing
* @param e
*/
//以异常类型作为参数,无返回值
public void afterThrowing(PriceException e) {
System.out.println("程序发生了PriceException异常");
}
}
剩下的步骤是将异常通知配置到spring配置文件中,及在代理配置中加入异常通知的配置,可参考上面的环绕通知等示例。
3.5 适配器
适配器, 通过正则表达式来定义方法切入点,也就是说定义哪些方法将被拦截器处理。适配器=通知(Advice)+切入点(Pointcut)。 在配置适配器时需要使用org.springframework.aop.support.RegexpMethodPointcutAdvisor
配置适配器示例:
!-- 配置适配器 -->
<bean id="myAdisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<!-- 定义正则表达式,定义需要拦截的方法名 ,本例定义了所有以buy结尾的方法 -->
<property name="patterns">
<list>
<value>.*buy</value>
</list>
</property>
<!-- 定义由那个通知(或者叫拦截器)来处理匹配的方法 -->
<property name="advice">
<ref bean="methodReturnAdvice"/>
</property>
</bean>
在代理中使用刚刚配置的适配器
<!-- 将直接使用后置拦截器 ,改为使用适配器 -->
<!--<value>methodReturnAdvice</value> -->
<!-- 通过适配器使用后置拦截器 -->
<value>myAdisor</value>
修改代理中的拦截器列表(spring配置文件,代理部分),将配置器直接配置在拦截器列表中即可。
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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
<bean id="bookDao" class="com.badu.dao.BookDao">
</bean>
<bean id="bookServiceTarget" class="com.badu.service.BookService">
<property name="book" ref="bookDao"/>
</bean>
<!--定义通知-->
<bean id="methodBeforeAdvice" class="com.badu.aop.MyMEthodBeforeAdvice"/>
<bean id="methodReturnAdvice" class="com.badu.aop.MyAfterReturnAdvice"/>
<bean id="methodInterceptor" class="com.badu.aop.MyMethodInterceptor"/>
<bean id="myThrowsAdvice" class="com.badu.aop.MyThrowsAdvice"/>
<!--定义aop-->
<bean id="bookService" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--配置目标对象-->
<property name="target" ref="bookServiceTarget"/>
<!-- 配置通知 -->
<property name="interceptorNames">
<list>
<value>methodBeforeAdvice</value>
<value>myAdisor</value>
<value>methodInterceptor</value>
<value>myThrowsAdvice</value>
</list>
</property>
<!-- 代理要实现的接口,代理类与被代理类需要实现相同接口 -->
<property name="proxyInterfaces">
<list>
<value>com.badu.service.IBookService</value>
</list>
</property>
</bean>
<!-- 配置适配器 -->
<bean id="myAdisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<!-- 定义正则表达式,定义需要拦截的方法名 ,本例定义了所有以buy结尾的方法 -->
<property name="patterns">
<list>
<value>.*buy</value>
</list>
</property>
<!-- 定义由那个通知(或者叫拦截器)来处理匹配的方法 -->
<property name="advice">
<ref bean="methodReturnAdvice"/>
</property>
</bean>
</beans>
最后运行结果
实例 spring 中定义的异常的使用
在会调用的方法中 抛出我们定义的异常 在前置通知后者后置通知中抛出也可以
演示效果