一、代理模式
代理模式是常用的Java 设计模式,它的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。
注意:
委托类对象就是我们后面说到的 目标对象(需要【被】代理的对象)
代理类对象就是我们后面说到的 代理对象(目标对象就是需要这个对象做为代理)
按照代理类的创建时期,代理类可分为两种。
静态代理类:
由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
动态代理类:在程序运行时,运用反射机制动态创建而成。
动态代理分为 ① JDK 动态代理 ② cgLib 代理
cgLib 是第三方的功能,但现在 Spring 已经将 cgLib 集成进来了。
二、
静态代理:
例如:
接口:HelloService
委托类:HelloServiceImpl
代理类:HelloServiceProxy
HelloService.java
public interface HelloService{
public String echo(String msg);
public Date getTime();
}
HelloServiceImpl.java
public class HelloServiceImpl implements HelloService{
public String echo(String msg){
return "echo:"+msg;
}
public Date getTime(){
return new Date();
}
}
HelloServiceProxy.java
public class HelloServiceProxy implements HelloService{
private HelloService helloService; //表示被代理的HelloService 实例
public HelloServiceProxy(HelloService helloService){
this.helloService=helloService;
}
public void setHelloServiceProxy(HelloService helloService){
this.helloService=helloService;
}
public String echo(String msg){
System.out.println("before calling echo()"); //目标方法调前处理
//调用委托类对象的方法(也就是目标对象方法/被代理对象方法)
//这个方法才是我们真正要执行的方法
String result=helloService.echo(msg);
System.out.println("after calling echo()"); //目标方法调用后处理
return result;
}
public Date getTime(){
System.out.println("before calling getTime()"); //目标方法调前处理
//调用委托类对象的方法(也就是目标对象方法/被代理对象方法)
//这个方法才是我们真正要执行的方法
Date date=helloService.getTime();
System.out.println("after calling getTime()"); //目标方法调用后处理
return date;
}
}
main.java
HelloService helloService=new HelloServiceImpl();
HelloService helloServiceProxy=new HelloServiceProxy(helloService);
System.out.println(helloServiceProxy.echo("hello"));
大家可以看到静态代理其实就是提供同名方法,在代理方法中调用目标方法。
缺点:
1)代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,静态代理在程序规模稍大时就无法胜任了。
2)如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。
JDK动态代理
动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。动态代理类不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java 反射机制可以生成任意类型的动态代理类。java.lang.reflect 包下面的Proxy类和InvocationHandler 接口提供了生成动态代理类的能力。
例子:
接口:
public interface IStudentService {
void save(Student s);
void delete(long id);
Student find(long id);
}
日志类:
public class StudentLogger {
public void log(String msg){
System.out.println("log: "+msg);
}
}
实现类
public class StudentServiceImpl implements IStudentService {
public void delete(long id) {
// 记录日志
System.out.println("student is deleted...");
}
public Student find(long id) {
// 记录日志
System.out.println("student is found...");
return null;
}
public void save(Student s) {
// 记录日志
System.out.println("student is saved...");
}
}
//InvocationHandler接口的实现类,java的动态代理中需要使用
public class MyHandler implements InvocationHandler {
//目标对象
private Object target;
private StudentLogger logger = new StudentLogger();
public MyHandler() {
}
public MyHandler(Object target) {
this.target = target;
}
// 参数1 将来所产生的代理对象 Proxy4$
// 参数2 将来需要调用到的目标对象里面真正的那个方法的镜像
// 参数3 将来调用方法的时候所传的参数
public Object invoke(Object proxy, Method m, Object[] args)
throws Throwable {
// 获得将来所调用方法的名字
String methodName = m.getName();
// 用日志记录输出一下
logger.log(methodName + " is invoked...");
// 用反射的方式去调用将来需要真正调用的方法.
Object o = m.invoke(target, args);
return o;
}
get/set
....
}
main:
//目标对象
IStudentService service = new StudentServiceImpl();
//service是我们的目标对象。
//我们要给目标对象产生代理对象。
//目标对象service只能单独执行delete方法。
//但是我们需要的是:先执行log日志方法再执行delete方法。
//目标对象service做不到这个要求,所以我们要给目标对象service
//生成一个代理对象去完成这俩个操作.
//怎么给目标对象生成代理对象:
//JDK动态代理的方式
//获得目标对象的Class对象
Class c = service.getClass();
//获得目标对象的类加载器对象
ClassLoader classLoader = c.getClassLoader();
//获得目标对象所实现的所有接口
Class[] interfaces = c.getInterfaces();
//获得一个InvocationHandler接口的实现类对象,并把目标对象传进去
InvocationHandler h =
new MyHandler(service);
//参数1 目标对象的类加载器对象
//参数2 目标对象所实现的所有接口. Class类型数组
//参数3 InvocationHandler接口的实现类对象
IStudentService proxy =
(IStudentService)Proxy.newProxyInstance
(classLoader, interfaces, h);
//这里的proxy是一个实现了IStudentService接口动态生成的代理类的对象
proxy.delete();
CGLib代理
JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为目标对象创建一个子类对象,并在子类对象中拦截所有父类方法的调用,然后在方法调用前后调用后都可以加入自己想要执行的代码。JDK动态代理与CGLib动态代理都是Spring AOP的采用的代理方式。
简单的实现:
这是一个需要被代理的类,也就是父类,通过字节码技术创建这个类的子类,实现动态代理。
public class SayHello {
public void say(){
System.out.println("hello everyone");
}
}
注意:在cglib方式中,目标对象作为父类,代理对象作为目标对象动态生成的子类对象
该类实现了创建一个类的子类的方法(cglib给一个类生成代理对象的方式)
getProxy(SuperClass.class)方法通过参数即父类的class对象,创建出它的一个子类对象,也就是cglib方式的代理对象
intercept()方法拦截所有目标类方法的调用,
obj表示将来生成的代理对象,
method为目标类中方法的反射对象,args为方法的动态入参,
mproxy为代理类(子类)中方法的反射对象。
mproxy.invokeSuper(obj, args)通过代理类调用目标对象(父类)中的方法。
public class CglibProxy implements MethodInterceptor{
public Object getProxy(Class clazz){
Enhancer enhancer = new Enhancer();
//设置谁是父类
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
//通过字节码技术动态创建子类实例
return enhancer.create();
}
//实现MethodInterceptor接口方法
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy mproxy) throws Throwable {
System.out.println("前置代理");
//通过代理类调用父类中的方法
Object result = mproxy.invokeSuper(obj, args);
System.out.println("后置代理");
return result;
}
}
main:
CglibProxy proxy = new CglibProxy();
//通过生成子类的方式创建代理类
SayHello proxyImp = (SayHello)proxy.getProxy(SayHello.class);
proxyImp.say();
输出结果:
前置代理
hello everyone
后置代理
三、Spring实现AOP(Aspect Oriented Programming)是依赖JDK动态代理和CGLIB代理(不同情况spring会自己选择一种方式)。
JDK动态代理和CGLIB代理的对比:
JDK动态代理:其代理对象必须是某个接口的实现,它是通过在运行期间创建一个接口的实现类来完成对目标对象的代理。
CGLIB代理:实现原理类似于JDK动态代理,只是它在运行期间生成的代理对象是针对目标类扩展的子类。CGLIB是高效的代码生成包,底层是依靠ASM(开源的java字节码编辑类库)操作字节码实现的。
所以spring会有以下俩种选择动态代理实现方式的情况:
*如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
*如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间自动选择;
可能大家对 AOP 不是很理解,那这里就了解下。
在传统的编写业务逻辑处理代码时,我们通常会习惯性地做几件事情:日志记录、事务控制及权限控制等,然后才是编写核心的业务逻辑处理代码。当代码编写完成回头再看时,不禁发现,扬扬洒洒上百行代码中,真正用于核心业务逻辑处理才那么几行,如图所示。方法复方法,类复类,就这样子带着无可奈何遗憾地度过了多少个春秋。这倒也罢,倘若到了项目的尾声,突然决定在权限控制上需要进行大的变动时,成千上万个方法又得一一”登门拜访”,痛苦”雪上加霜”。
如果能把上图中众多方法中的所有共有代码全部抽取出来,放置到某个地方集中管理,然后在具体运行时,再由容器动态织入这些共有代码的话,最起码可以解决两个问题:
①Java EE程序员在编写具体的业务逻辑处理方法时,只需关心核心的业务逻辑处理,既提高了工作效率,又使代码变更简洁优雅。
②在日后的维护中由于业务逻辑代码与共有代码分开存放,而且共有代码是集中存放的,因此使维护工作变得简单轻松。
面向切面编程AOP技术就是为解决这个问题而诞生的,切面就是横切面,如图所示,代表的是一个普遍存在的共有功能,例如,日志切面、权限切面及事务切面等。
下面我们以用户管理业务逻辑组件UserService的AOP实现过程为例,深度剖析一下AOP技术的实现原理。AOP技术是建立在Java语言的反射机制与动态代理机制之上的。业务逻辑组件在运行过程中,AOP容器会动态创建一个代理对象供使用者调用,该代理对象已经按Java EE程序员的意图将切面成功切入到目标方法的连接点上,从而使切面的功能与业务逻辑的功能同时得以执行。从原理上讲,调用者直接调用的其实是AOP容器动态生成的代理对象,再由代理对象调用目标对象完成原始的业务逻辑处理,而代理对象则已经将切面与业务逻辑方法进行了合成。
当然上图涉及到的一些概念了解下:
切面(Aspect):其实就是共有功能的实现。如日志切面、权限切面、事务切面等。在实际应用中通常是一个存放共有功能实现的普通Java类,之所以能被AOP容器识别成切面,是在配置中指定的。
通知(Advice):是切面的具体实现。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际应用中通常是切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。
连接点(Joinpoint):就是程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出或字段修改等,但Spring只支持方法级的连接点。
切入点(Pointcut):用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。
目标对象(Target):就是那些即将切入切面的对象,也就是那些被通知的对象。这些对象中已经只剩下干干净净的核心业务逻辑代码了,所有的共有功能代码等待AOP容器的切入。
代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象的核心业务逻辑功能加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。
织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期,当然不同的发生点有着不同的前提条件。譬如发生在编译期的话,就要求有一个支持这种AOP实现的特殊编译器;发生在类装载期,就要求有一个支持AOP实现的特殊类装载器;只有发生在运行期,则可直接通过Java语言的反射机制与动态代理机制来动态实现。
前置通知(Before advice):
在某连接点(join point)之前执行的通知
返回后通知(After returning advice):
在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
抛出异常后通知(After throwing advice):
在方法抛出异常退出时执行的通知。
后通知(After (finally) advice):
当某连接点退出的时候执行的通知
环绕通知(Around Advice):
包围一个连接点(join point)的通知,例如事务的处理,就需要这样的通知,因为事务需要在方法前开启,在方法后提交
7 在Spring中,Advice是由spring中的几个接口来指定(就像action类由struts2中的action接口来指定一样),主要有以下几种:
1) Before Advice
例如:
//有一下几个类或者接口:
Account.java
private int id;
private String name;
private double balance;//余额
get/set
...
AccountDao.java
//取款 账号减去多少钱
void withdraw(Account acc,double amt);
//存款 账号加上多少钱
void deposit(Account acc,double amt);
AccountDaoImpl.java
//简单的实现接口中的抽象方式
IAccountService.java
//银行账号的一个操作:例如转账
void bankAction();
AccountServiceImpl.java
private AccountDao accountDao;
private Account account;
//转账
public void bankAction(){
accountDao.withdraw(account, 100);
accountDao.deposit(account, 100);
}
get/set
...
//切面类
public class MyLogger {
public void log(String msg){
System.out.println("log:"+msg);
}
}
/*我们要做的事情:在转账方法(bankAction)执行之前进行一个日志输出*/
//前置通知
public class BeforeAdvice implements MethodBeforeAdvice {
//切面类
private MyLogger logger;
// 参数1 将来我们需要调用的目标对象中的方法镜像
// 参数2 将来调用方法的时候所传过来的参数
// 参数3 目标对象
//将来在调用目标对象方法之前,会先执行这个before方法
public void before(Method m, Object[] args, Object target) throws Throwable {
logger.log(m.getName() + " is invoked..");
/*
* 注意:这里一定不要自己手动的用反射去 调用这个目标对象中的方法,
* 因为spring 会帮我们去调用的,如果我们这个再去调用这个方法,
* 那么这这个方法会被调用俩次.
*
* m.invoke(target,args);
*
*/
}
get/set
}
配置xml文件: 注意ProxyFactoryBean的配置,htmlsingle中搜索即可
<!-- 配置切面类 -->
<bean name="logger" class="com.briup.aop.aspect.MyLogger"></bean>
<!-- 配置advice -->
<bean name="beforeAdvice"
class="com.briup.aop.before.BeforeAdvice">
<!-- 注入切面类对象 -->
<property name="logger" ref="logger"></property>
</bean>
<!-- 配置dao层对象 -->
<bean id="dao" class="com.briup.aop.dao.AccountDaoImpl"/>
<!-- 配置目标对象 -->
<bean name="target"
class="com.briup.aop.service.AccountServiceImpl">
<property name="accountDao" ref="dao"></property>
</bean>
<!-- 配置代理对象 -->
<!-- 这里使用的是spring的一个代理对象工厂类产生的 -->
<bean name="proxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 注入目标对象 -->
<property name="target" ref="target"></property>
<!-- 注入目标对象所实现的接口 可以有多个接口 -->
<property name="proxyInterfaces">
<list>
<value>com.briup.aop.service.IAccountService</value>
</list>
</property>
<!-- 注入advice 可以有多个 -->
<property name="interceptorNames">
<list>
<value>beforeAdvice</value>
</list>
</property>
</bean>
2) After advice
例如:
public class AfterAdvice implements AfterReturningAdvice {
private MyLogger logger;
//参数1 目标对象中的方法执行完返回值
//参数2 所执行方法的镜像对象
//参数3 执行方法时候所传的参数
//参数4 目标对象
//将来调用目标对象的方法之后会执行这个afterReturning方法
public void afterReturning(Object returnValue, Method method,
Object[] args, Object target) throws Throwable {
logger.log("after returning " + " target=" + target
+ " method Name=" + method.getName() + " args are:" + args
+ " returnValue=" + returnValue);
}
get/set
}
xml配置文件:
<!-- 配置切面类 -->
<bean name="logger" class="com.briup.aop.aspect.MyLogger"></bean>
<!-- 配置advice -->
<bean name="afterAdvice"
class="com.briup.aop.after.AfterAdvice">
<property name="logger" ref="logger"></property>
</bean>
<!-- 配置dao层对象 -->
<bean id="dao" class="com.briup.aop.dao.AccountDaoImpl" />
<!-- 配置目标对象 -->
<bean name="target"
class="com.briup.aop.service.AccountServiceImpl">
<property name="accountDao" ref="dao"></property>
</bean>
<!-- 配置代理对象 -->
<!-- 这里使用的是spring的一个代理对象工厂类产生的 -->
<bean name="proxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 注入目标对象 -->
<property name="target" ref="target"></property>
<!-- 注入目标对象所实现的接口 可以有多个接口 -->
<property name="proxyInterfaces">
<list>
<value>com.briup.aop.service.IAccountService</value>
</list>
</property>
<!-- 注入advice 可以有多个 -->
<property name="interceptorNames">
<list>
<value>afterAdvice</value>
</list>
</property>
</bean>
注意:另外一个返回后通知接口:AfterReturningAdvice的使用方式和这个是类似的,但是需要注意它们俩个之间的区别
3) 环绕Advice:
例如:
public class AroundAdvice implements MethodInterceptor {
private MyLogger logger;
public Object invoke(MethodInvocation mi) throws Throwable {
// mi.getMethod()获得将来要调用的方法的镜像
//在目标方法执行之前做日志
logger.log(mi.getMethod().getName() + " is start...");
// 这个方法就是用来调用目标对象中的方法的
Object returnValue = mi.proceed();
//在目标方法执行之后做日志
logger.log(mi.getMethod().getName() + " is end...");
return returnValue;
}
get/set
xml配置文件
<!-- 配置切面类 -->
<bean name="logger" class="com.briup.aop.aspect.MyLogger"></bean>
<!-- 配置advice -->
<bean name="aroundAdvice"
class="com.briup.aop.around.AroundAdvice">
<!-- 注入切面类对象 -->
<property name="logger" ref="logger"></property>
</bean>
<!-- 配置dao层对象 -->
<bean name="dao" class="com.briup.aop.dao.AccountDaoImpl"/>
<!-- 配置目标对象 -->
<bean name="target"
class="com.briup.aop.service.AccountServiceImpl">
<property name="accountDao" ref="dao"></property>
</bean>
<!-- 配置代理对象 -->
<!-- 这里使用的是spring的一个代理对象工厂类产生的 -->
<bean name="proxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 注入目标对象 -->
<property name="target" ref="target"></property>
<!-- 注入目标对象所实现的接口 可以有多个接口 -->
<property name="proxyInterfaces">
<list>
<value>com.briup.aop.service.IAccountService</value>
</list>
</property>
<!-- 注入advice 可以有多个 -->
<property name="interceptorNames">
<list>
<value>aroundAdvice</value>
</list>
</property>
</bean>
4)Throws Advice
//ThrowsAdvice 是一个空接口,起标识作用
例如:
public class ThrowingAdvice implements ThrowsAdvice {
private MyLogger logger;
public MyLogger getLogger() {
return logger;
}
public void setLogger(MyLogger logger) {
this.logger = logger;
}
//这里这个方法的名字一定要叫afterThrowing
//参数可以是1个也可以是四个
//1个参数的时候只能是一个异常类型的参数
//如果是4个参数的话,参数的顺序也一定要是下面的顺序
public void afterThrowing(Method method, Object[] args, Object target,Exception e) {
logger.log(e.getMessage());
}
//下面这样写也可以
/*
public void afterThrowing(Exception e) {
logger.log(e.getMessage());
}
*/
get/set
}
配置xml文件:
<!-- 配置切面类 -->
<bean name="logger" class="com.briup.aop.aspect.MyLogger"></bean>
<!-- 配置advice -->
<bean name="throwAdvice" class="com.briup.aop.throwException.ThrowingAdvice">
<!-- 注入切面类对象 -->
<property name="logger" ref="logger"></property>
</bean>
<!-- 配置dao层对象 -->
<bean id="dao" class="com.briup.aop.dao.AccountDaoImpl"/>
<!-- 配置目标对象 -->
<bean name="target"
class="com.briup.aop.service.AccountServiceImpl">
<property name="accountDao" ref="dao"></property>
</bean>
<!-- 配置代理对象 -->
<!-- 这里使用的是spring的一个代理对象工厂类产生的 -->
<bean name="proxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 注入目标对象 -->
<property name="target" ref="target"></property>
<!-- 注入目标对象所实现的接口 可以有多个接口 -->
<property name="proxyInterfaces">
<list>
<value>com.briup.aop.service.IAccountService</value>
</list>
</property>
<!-- 注入advice 可以有多个 -->
<property name="interceptorNames">
<list>
<value>throwAdvice</value>
</list>
</property>
</bean>