【Java EE 学习 51】【Spring学习第三天】【cglib动态代理】【AOP和动态代理】【切入点表达式】

2023-08-02

一、cglib动态代理

  1.简介

    (1)CGlib是一个强大的,高性能,高质量的Code生成类库。它可以在运行期扩展Java类与实现Java接口。

    (2) 用CGlib生成代理类是目标类的子类。

    (3)用CGlib生成 代理类不需要接口

    (4)用CGLib生成的代理类重写了父类的各个方法。

    (5)拦截器中的intercept方法内容正好就是代理类中的方法体

  2.Spring什么时候使用JDK动态代理,什么时候使用CGLib动态代理?

    (1)若目标对象实现了若干接口,spring使用JDK的java.lang.reflect.Proxy类代理。

            优点:因为有接口,所以使系统更加松耦合

            缺点:为每一个目标类创建接口

    (2)若目标对象没有实现任何接口,spring使用CGLIB库生成目标对象的子类。

            优点:因为代理类与目标类是继承关系,所以不需要有接口的存在。

            缺点:因为没有使用接口,所以系统的耦合性没有使用JDK的动态代理好。

  3.不使用Spring,使用CGlib动态代理示例。

 package com.kdyzm.spring.proxy;

 public class Person {
private String name; public Person(String name) {
this.name = name;
} public Person() {
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} @Override
public String toString() {
return "Person [name=" + name + "]";
}
}

com.kdyzm.spring.proxy.Person

 package com.kdyzm.spring.proxy;
/*
* 这里测试的是cplib动态代理,不需要实现某个接口
*/
public class PersonDaoImpl{ public void savePerson() {
System.out.println("保存学生!");
} public Person getPerson() {
Person p=new Person();
p.setName("狗蛋");
return p;
} public void updatePerson() {
System.out.println("更新学生信息!");
} public void deletePerson() {
System.out.println("删除学生!");
} }

com.kdyzm.spring.proxy.PersonDaoImpl

    这里直接使用PersonDaoImpl类,因为使用CGLib动态代理目标类不需要有接口,代理对象是目标类的子类实例。

 package com.kdyzm.spring.proxy;

 public class Transaction {
public void startTransaction(){
System.out.println("开启事务!");
}
public void commit(){
System.out.println("提交事务!");
}
}

com.kdyzm.spring.proxy.Transaction

    最重要的一个类是

com.kdyzm.spring.proxy.PersonDaoInterceptor,该类是实现CGLib动态代理的关键。
 package com.kdyzm.spring.proxy;

 import java.lang.reflect.Method;

 import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy; public class PersonDaoInterceptor implements MethodInterceptor{
private Object target;
private Transaction transaction; public PersonDaoInterceptor(Object target, Transaction transaction) {
this.target = target;
this.transaction = transaction;
} //定义一个获取代理对象的方法
public Object createProxyObject(){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass());
enhancer.setCallback(this);//设置拦截器为回调函数
return enhancer.create();//创建代理对象
} @Override
public Object intercept(Object arg0, Method method, Object[] args,
MethodProxy arg3) throws Throwable {
Object obj=null;
String methodName=method.getName();
if("savePerson".equals(methodName)||"updatePerson".equals(methodName)||"deletePerson".equals(methodName)){
this.transaction.startTransaction();
obj=method.invoke(this.target, args);
this.transaction.commit();
}
else{
System.out.println("方法名为:"+method.getName()+",不拦截!");
obj=method.invoke(this.target, args);
}
return obj;
} }

    最后测试类:com.kdyzm.spring.proxy.PersonDaoInterceptorTest

 package com.kdyzm.spring.proxy;

 import org.junit.Test;

 public class PersonDaoInterceptorTest {
@Test
public void testOne(){
Transaction transaction = new Transaction();
PersonDaoImpl target = new PersonDaoImpl();
PersonDaoInterceptor personDaoInterceptor = new PersonDaoInterceptor(target, transaction);
PersonDaoImpl proxy=(PersonDaoImpl) personDaoInterceptor.createProxyObject();
Person person=proxy.getPerson();
System.out.println(person);
proxy.updatePerson();
}
}

    运行结果:

方法名为:getPerson,不拦截!
Person [name=狗蛋]
开启事务!
更新学生信息!
提交事务!

  4.使用CGLib动态代理需要注意的事项:

    如果不在spring中使用,则只需要导入cglib相关的jar包即可。

二、传统的动态代理实现的缺陷

  不管采用JDK动态代理生成代理类还是采用CGLIB生成动态代理类。目标类中的所有方法都被拦截下来。而在哪个方法里做比如权限的判断、安全性的检查等一系列工做必须在拦截器中作相应的判断。但是这样的编程形式给程序的编写带来了一定的麻烦。

  1. 在拦截器中控制哪些方法将被做权限判断、安全性检查等是一件比较困难的事情。

    (1)采取这样的配置目标类只能是一个,所以如果用这种方法做权限控制,得写很多代理,这样给代码的书写造成了困难。

    (2)每一个类中的每一个方法如果都有不同的权限(实际的系统往往都是这样的),在拦截器中的判断代码书写会很困难。

  2.这样的代码也会导致硬编码,也就是说我们必须在拦截器中写一些权限判断等事情,会导致拦截器中代码量的增大,造成维护的麻烦。

三、AOP编程

  1.几个概念

    (1)Aspect(切面)

      比如说事物、权限等等,和业务逻辑没有关系的部分。

    (2)joinpoint(连接点)

      目标类的目标方法,由客户端调用的时候决定。

    (3)Pointcut(切入点)

      所谓切入点是指我们要对哪些拦截的方法的定义。

      被纳入spring aop中的目标类的方法。

    (4)Advice(通知)

      所谓通知是指拦截到joinpoint之后要做的事情。通知分为前置通知、后置通知、异常通知、最终通知、环绕通知(切面要完成的功能(方法))

    (5)Target(目标对象)

      代理的目标对象

    (6)Weaving(织入)

      是指把切面应用到目标对象来创建新的代理对象的过程。切面在指定的连接点织入到目标对象。    

  2.第一个小案例:

       首先需要引入lib/aspectj文件夹中的两个jar包。

 package com.kdyzm.spring.proxy.xml;

 public class Person {
private String name; public Person(String name) {
this.name = name;
} public Person() {
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} @Override
public String toString() {
return "Person [name=" + name + "]";
}
}

com.kdyzm.spring.proxy.xml.Person

 package com.kdyzm.spring.proxy.xml;
/*
* 这里测试的是cplib动态代理,不需要实现某个接口
*/
public class PersonDaoImpl{ public void savePerson() {
System.out.println("保存学生!");
} public Person getPerson() {
Person p=new Person();
p.setName("狗蛋");
return p;
} public void updatePerson() {
System.out.println("更新学生信息!");
} public void deletePerson() {
System.out.println("删除学生!");
} }

com.kdyzm.spring.proxy.xml.PersonDaoImpl

 package com.kdyzm.spring.proxy.xml;

 public class Transaction {
public void startTransaction(){
System.out.println("开启事务!");
}
public void commit(){
System.out.println("提交事务!");
}
}

com.kdyzm.spring.proxy.xml.Transaction

      最重要的是配置文件,这里不需要明确使用哪种代理方式,因为没有给目标类加上要实现的接口,所以spring会自动使用CGLib动态代理方式,通过debug模式可以看出来。

 <?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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
">
<bean id="personDao" class="com.kdyzm.spring.proxy.xml.PersonDaoImpl"></bean>
<bean id="transaction" class="com.kdyzm.spring.proxy.xml.Transaction"></bean> <aop:config>
<!-- 切入点表达式确定哪个类能够生成代理对象 -->
<aop:pointcut expression="execution(* com.kdyzm.spring.proxy.xml.PersonDaoImpl.*(..))" id="aimClass"/>
<!-- 切面 -->
<aop:aspect ref="transaction">
<aop:before method="startTransaction" pointcut-ref="aimClass"/>
<aop:after method="commit" pointcut-ref="aimClass"/>
</aop:aspect>
</aop:config>
</beans>

      可以发现配置文件的命名空间多了一个,而且需要另外的一个schema约束。

xmlns:aop="http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd

      测试类:com.kdyzm.spring.proxy.xml.AOPTest

 package com.kdyzm.spring.proxy.xml;

 import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext; public class AOPTest {
@Test
public void testOne(){
ApplicationContext context=new ClassPathXmlApplicationContext("com/kdyzm/spring/proxy/xml/applicationContext.xml");
PersonDaoImpl personDaoImpl = (PersonDaoImpl) context.getBean("personDao");
personDaoImpl.deletePerson();
}
}

      运行结果:

开启事务!
删除学生!
提交事务!

      使用的代理方式一定是CGLib:

      

  3.各种通知

    (1)前置通知

      形式

<aop:before method="startTransaction" pointcut-ref="aimClass"/>

      在切面Transaction中的对应方法中,可以加上JoinPoint参数,通过该参数能够获取目标类的目标方法名称、参数值等信息。

public void startTransaction(JoinPoint joinPoint){
System.out.println("开始事务!");
}

    (2)后置通知

<aop:after-returning method="commit" pointcut-ref="aimClass" returning="val"/>

      目标方法执行完成之后执行该通知,通知中可以带有两个参数,JoinPoint joinPoint和Object val。

      joinPoint:通过该对象能够获取目标类和目标方法的一切信息,包括目标方法的参数列表。

      val:通过该对象能够获取目标方法执行之后的返回值,如果没有返回值,则val为null;val一定要和方法中的参数名称完全相同。

    注意:如果在执行目标类的目标方法中遇到异常,则不执行后置通知。

    (3)异常通知

<aop:after-throwing method="exceptionMethod" pointcut-ref="aimClass" throwing="ex"/>

      切面中的通知可以带有两个参数:JoinPoint joinPoint,Throwalbe ex

      ex一定要与throwing="ex"中的相同。

    (4)环绕通知

<aop:around method="aroundTest" pointcut-ref="aimClass"/>

      环绕通知的作用就是拦截目标方法的执行,可以在执行之前和执行之后做一些动作。

      切面中的通知可以有一个参数:ProceedingJoinPoint joinPoint,该接口是JoinPoint的子接口,所以通过该接口的引用仍然能够获取想要获取到的有关目标类和目标方法的所有信息。另外,该类提供了procced方法,用于执行目标方法。

public void aroundTest(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("拦截方法的执行!");
joinPoint.proceed();
}

    (5)最终通知

<aop:after method="finallyMethod" pointcut-ref="aimClass"/>

      最终通知大多数用于释放在执行目标方法的时候占用的资源,即使产生了异常,后置通知不被执行,但是最终通知一定会被执行。  

      也就是说在最终通知中不受异常的影响。也就是说不论目标方法执行的过程中是否抛出异常,最终通知都将执行。

四、切入点表达式

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)
throws-pattern?)

   除了返回类型模式(上面代码片断中的ret-type-pattern),名字模式和参数模式以外, 所有的部分都是可选的。返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 你会使用的最频繁的返回类型模式是*,它代表了匹配任意的返回类型。 一个全限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。 你可以使用*通配符作为所有或者部分命名模式。 参数模式稍微有点复杂:()匹配了一个不接受任何参数的方法, 而(..)匹配了一个接受任意数量参数的方法(零或者更多)。 模式(*)匹配了一个接受一个任何类型的参数的方法。 模式(*,String)匹配了一个接受两个参数的方法,第一个可以是任意类型, 第二个则必须是String类型。更多的信息请参阅AspectJ编程指南中语言语义的部分。

  1.execution(public * *(..))

    任意的公有方法

  2.execution(* set*(..))

    任意以set开头的方法

  3.execution(* com.xyz.service.AccountService.*(..))

    com.xyz.service.AccountService接口或者类中的所有方法

  4.execution(* com.xyz.service.*.*(..))

    com.xyz.service包下的所有类的所有方法

  5.execution(* com.xyz.service..*.*(..))

    com.xyz.service包及子包下的所有类的所有方法

  6.execution(* com.xyz.service.*.*(String,*))

    com.xyz.service包及子包下的所有类中的第一个参数类型为String,第二个参数任意的所有方法。

【Java EE 学习 51】【Spring学习第三天】【cglib动态代理】【AOP和动态代理】【切入点表达式】的相关教程结束。