首页 > 代码库 > spring02

spring02

1. AOP面向切面编程的相关概念

1.1. 什么是AOP

AOP (Aspect Oriented Programing) 称为:面向切面编程,它是一种编程思想。

AOP采取横向抽取机制,取代了传统纵向继承体系重复性代码的编写方式(例如性能监视、事务管理、安全检查、缓存、日志记录等)。

【扩展了解】AOPOOP(面向对象编程(Object Oriented ProgrammingOOP,面向对象程序设计)是一种计算机编程架构),思想延续!

技术分享

什么是OCP:即开闭原则。

技术分享

AOP 思想:基于代理思想,对原来目标对象,创建代理对象,在不修改原对象代码情况下,通过代理对象,调用增强功能的代码,从而对原有业务方法进行增强!

切面:需要代理一些方法和增强代码。

 

1.2. AOP的应用场景

场景一: 记录日志

场景二: 监控方法运行时间 (监控性能)

场景三: 权限控制

场景四: 缓存优化(第一次调用查询数据库,将查询结果放入内存对象, 第二次调用, 直接从内存对象返回,不需要查询数据库

场景五: 事务管理(调用方法前开启事务,调用方法后提交或者回滚、关闭事务

 

 

1.3. Spring AOP编程两种方式

l Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类植入增强代码。

l AsPectJ是一个基于Java语言的AOP框架,Spring2.0开始,Spring AOP引入对Aspect的支持。

简单的说,Spring内部支持两套AOP编程的方案:

l Spring 1.2 开始支持AOP编程(传统SpringAOP 编程),编程非常复杂 ---- 更好学习Spring 内置传统AOP代码

l Spring 2.0 之后支持第三方 AOP框架(AspectJ),实现另一种AOP编程 -- 推荐

 

1.4. AOP编程相关术语

AOP思想编程的机制

技术分享

AOP的相关术语

Aspect(切面): 通知和切入点的结合,通知和切入点共同定义了关于切面的全部内容---它的功能、在何时和何地完成其功能

joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring,这些点指的是方法,因为spring只支持方法类型的连接点.

Pointcut(切入点):所谓切入点是指我们要对哪些joinpoint进行拦截的定义. 通知定义了切面的”什么”和”何时”,切入点就定义了”何地”.

Advice(通知):所谓通知是指拦截到joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)

Target(目标对象):代理的目标对象

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

Introduction(引入):在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field.

 

 

通过案例解释AOP相关概念

需求:UserDao中有5个方法,分别是save()update()delete()find()login();在访问UserDaosave()update()delete()方法之前,进行记录日志的操作。

技术分享

Aspect切面(类):增强代码AdvicewriteLog方法)和 切入点 Pointcutsaveupdatedelete) 的结合。换句话说:对哪些方法进行怎样的代码增强。

此处的切面是指生产代理对象的类,就是后面的生成代理对象的工厂类

 

2. AOP编程底层实现机制(了解)

AOP就是要对目标进行代理对象的创建,Spring AOP是基于动态代理的,基于两种动态代理机制:JDK动态代理和CGLIB动态代理

无论是传统的spring AOP编程还是Aspectj编程,底层都是这两种动态代理方式,既可以代理有接口的类,也可以代理普通类

 

面试题:动态代理和静态代理区别?

动态代理:在虚拟机内部,运行的时候,动态生成代理类(运行时生成,runtime生成) ,并不是真正存在的类, 一般格式:Proxy$$ Proxy$$Customer

静态代理:实际存在代理类 (例如:struts2 Action的代理类 ActionProxystruts2的拦截器)

技术分享

2.1. JDK动态代理

JDK动态代理,针对目标对象接口进行代理,动态生成接口的实现类!(必须有接口

【过程要点】:

1、 必须对接口生成代理

2、 采用Proxy类,通过newProxyInstance方法为目标创建代理对象。

该方法接收三个参数

  1)目标对象的类加载器

  2)目标对象实现的接口

  3)代理后的处理程序InvocationHandler

使用 Proxy类提供 newProxyInstance 方法对目标对象接口进行代理 

 技术分享

3、 实现InvocationHandler 接口中 invoke方法,在目标对象每个方法调用时,都会执行invoke

【示例】:需求:对目标对象中存在保存和查询的方法,在执行保存的方法的时候,记录日志

第一步:新建web工程

第二步:编写业务接口,接口中定义save()find()的方法。

//目标对象的接口(也是代理对象的目标接口)
public interface ICustomerService {
    //保存
    public void save();
    //查询
    public int find();
}

第三步:编写业务类,类要实现接口

//目标对象的类
public class CustomerServiceImpl implements ICustomerService{

    public void save() {
        System.out.println("客户保存了。。。。。");
    }

    public int find() {
        System.out.println("客户查询数量了。。。。。");
        return 100;
    }
}

第四步:使用JDK代理完成

代理工厂:(即切面)

有三种方案完成JDK动态代理:

方案一:在内部实现new InvocationHandler(),指定匿名类

//专门用来生成jdk的动态代理对象的-通用
public class JdkProxyFactory{
    //目标对象成员变量
    private Object target;
    //注入target目标对象
    public JdkProxyFactory(Object target) {
        this.target = target;
    }
    
    //返回的值是代理对象
    public Object getProxyObject(){
        //参数1:目标对象的类加载器
        //参数2:目标对象实现的接口
        //参数3:回调方法对象
        /**方案一:在内部实现new InvocationHandler(),指定匿名类*/
         return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler(){

            public Object invoke(Object proxy, Method method, Object[] args)    throws Throwable {
                //如果是保存的方法,执行记录日志操作(代理对象的增强代码)
                if(method.getName().equals("save")){
                    writeLog();
                }
                //执行目标对象原来的方法
                Object object = method.invoke(target, args); //调用目标对象的某个方法,并且返回方法的返回值
                return object;
            }
        });
    }
    
    //记录日志
    private static void writeLog(){
        System.out.println("增强代码:写日志了。。。");
    }
}

方案二:传递内部类的对象,指定内部类

//专门用来生成jdk的动态代理对象的-通用
public class JdkProxyFactory{
    //目标对象成员变量
    private Object target;
    //注入target
    public JdkProxyFactory(Object target) {
        this.target = target;
    }
    
    public Object getProxyObject(){
        //参数1:目标对象的类加载器
        //参数2:目标对象实现的接口
        //参数3:回调方法对象
        /**方案二:传递内部类的对象,指定内部类*/
    return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),new MyInvocationHandler());
    }
    
    //自己制定内部类:类的内部可以多次使用类型
    private class MyInvocationHandler implements InvocationHandler{

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //如果是保存的方法,执行记录日志操作
            if(method.getName().equals("save")){
                writeLog();
            }
            //目标对象原来的方法执行
            Object object = method.invoke(target, args);//调用目标对象的某个方法,并且返回目标对象方法的返回值
            return object;
        }
    }
    
    //记录日志
    private static void writeLog(){
        System.out.println("增强代码:写日志了。。。");
    }
}

方案三:直接使用调用类作为接口实现类,实现InvocationHandler接口

//专门用来生成jdk的动态代理对象的-通用
public class JdkProxyFactory implements InvocationHandler{
    //成员变量
    private Object target;
    //注入target
    public JdkProxyFactory(Object target) {
        this.target = target;
    }
    
    public Object getProxyObject(){
        //参数1:目标对象的类加载器
        //参数2:目标对象实现的接口
        //参数3:回调方法对象
        /**方案三:直接使用调用类作为接口实现类,实现InvocationHandler接口*/
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
    }
    
    //记录日志
    private static void writeLog(){
        System.out.println("增强代码:写日志了。。。");
    }

    //参数1:代理对象
    //参数2:目标的方法对象
    //参数3:目标的方法的参数
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //如果是保存的方法,执行记录日志操作
        if(method.getName().equals("save")){
            writeLog();
        }
        //目标对象原来的方法执行
        Object object = method.invoke(target, args);//调用目标对象的某个方法,并且返回目标对象
        return object;
    }
}

第五步:使用SpringTest.java进行测试

  //目标:使用动态代理,对原来的方法进行功能增强,而无需更改原来的代码。
    //JDK动态代理:基于接口的(对象的类型,必须实现接口!)
    @Test
    public void testJdkProxy(){
        //target(创建目标对象)
        ICustomerService target = new CustomerServiceImpl();
        //实例化注入目标对象(创建工厂对象,且注入目标对象)
        JdkProxyFactory jdkProxyFactory = new JdkProxyFactory(target);
        //获取代理对象Proxy: 代理对象是接口的实现类,所有可以用接口接收
        ICustomerService proxy = (ICustomerService)jdkProxyFactory.getProxyObject();
        //调用目标对象的方法
        proxy.save();
        System.out.println("————————————————————————————————————————");
        proxy.find();
    }

第六步:在控制台查看输出结果

技术分享

从结果上看出:在保存方法的前面,输入了日志增强。

最后,使用断点查看JDK代理,生成的代理对象

 技术分享

说明
Interface ICustomerService{
    //目标接口
}
Class CustomerServiceImpl implements ICustomerService{
    //目标类实现接口
}

JDK代理对接口代理
Class $Proxy2 implements ICustomerService{
  //JDK代理类是目标接口的实现类
  ICustomerService customerService = new CustomerServiceImpl(); 
  public void save() {
      writeLog()
      customerService.save();
  }

  public int find() {
      int returnValue =http://www.mamicode.com/ customerService.find();
      return returnValue;
  }

  //记录日志
  private static void writeLog(){
      System.out.println("增强代码:写日志了。。。");
  }
}

注意:

JDK动态代理的缺点:只能面向接口代理,不能直接对目标类进行代理,如果没有接口,则不能使用JDK代理。

代理对象对目标对象的接口进行代理,也实现了接口,可以重写接口的方法,

而且代理对象还可以获取目标对象,然后重写接口中的方法时既可以做增强操作,又可以调用目标对象的方法;

 

2.2. Cglib动态代理

Cglib的引入为了解决类的直接代理问题(生成代理子类),不需要接口也可以代理

什么是cglib

CGLIB(Code Generation Library)是一个开源项目!是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。

 技术分享

该代理方式需要相应的jar包,但不需要导入。因为Spring core包已经包含cglib ,而且同时包含了cglib 依赖的asm的包(动态字节码的操作类库)

【示例】:需求:对目标对象中存在保存和查询的方法,在执行保存的方法的时候,记录日志

第一步:将spring的核心jar导入进来

第二步:编写业务类,创建类ProductService.java,类不需要实现接口

//没有接口的类
public class ProductService {
    public void save() {
        System.out.println("商品保存了。。。。。");
    }

    public int find() {
        System.out.println("商品查询数量了。。。。。");
        return 99;
    }
}

第三步:使用cglib代理,创建类CglibProxyFactory.java

注意:需要实现MethodInterceptor接口,才可以使用cglib代理方式

底层的两种代理方式没有前置后置等方式,只是取决于intercept方法中增强方法在目标对象方法中的位置而已

//cglib动态代理工厂:用来生成cglib代理对象
public class CglibProxyFactory implements MethodInterceptor{
    //声明一个代理对象引用
    private Object target;
    //注入代理对象
    public CglibProxyFactory(Object target) {
        this.target = target;
    }
    
    //获取代理对象
    public Object getProxyObject(){
        //1.代理对象生成器(工厂思想)
        Enhancer enhancer = new Enhancer();
        //2.在增强器上设置两个属性
        //2.1.设置要生成代理对象的目标对象:代理对象是目标对象的子类
        enhancer.setSuperclass(target.getClass());
        //2.2.设置回调方法
        enhancer.setCallback(this);
        //3.创建并返回代理对象
        return enhancer.create();
    }

    
    //回调方法(代理对象的方法)
    //参数1:代理对象
    //参数2:目标对象的方法对象
    //参数3:目标对象的方法的参数的值
    //参数4:代理对象的方法对象
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        //如果是保存的方法,执行记录日志操作
        if(method.getName().equals("save")){
            writeLog();
        }
        //目标对象原来的方法执行
        Object object = method.invoke(target, args); //调用目标对象的某个方法,并且返回方法的返回值对象
        return object;
    }
    
    //写日志的增强功能
    //Advice通知、增强
    //记录日志
    private static void writeLog(){
        System.out.println("增强代码:写日志了。。。");
    }
}

第四步:测试代码,使用SpringTest.java进行测试

//cglib动态代理:可以基于类(无需实现接口)生成代理对象
    @Test
    public void testCglibProxy(){
        //target目标:
        ProductService target = new ProductService();
        //weave织入,生成proxy代理对象
        //代理工厂对象,注入目标
        CglibProxyFactory cglibProxyFactory = new CglibProxyFactory(target);
        //获取proxy:思考:对象的类型
        //代理对象,其实是目标对象类型的子类型
        ProductService proxy=(ProductService) cglibProxyFactory.getProxyObject();
        //调用代理对象的方法
        proxy.save();
        System.out.println("————————————————————————————————————————");
        proxy.find();
    }

第五步:控制台输出结果

技术分享

最后,使用断点查看cglib代理,生成的代理对象

技术分享

说明
Class ProductService{
    //目标类
}

Cglib对类代理
Class ProductService$$EnhancerByCGLIB$$df9980d0 extends ProductService{
  //CGLIB代理类是目标类的子类
  ProductService productService= new ProductService(); 
  public void save() {
      writeLog();
      productService.save();
  }

  public int find() {
      int returnValue =http://www.mamicode.com/ productService.find();
      return returnValue;
  }

  //记录日志
  private static void writeLog(){
      System.out.println("增强代码:写日志了。。。");
  }
}

2.3.代理知识小结

区别

l Jdk代理:基于接口的代理,一定是基于接口,会生成目标对象的接口的子对象。

l Cglib代理:基于类的代理,不需要基于接口,会生成目标对象的子对象。

jdk代理对象实质上是目标对象接口的实现类

Cglib代理对象实质上是目标对象的子类

代理知识总结:

l spring在运行期,生成动态代理对象,不需要特殊的编译器.

l spring有两种代理方式:

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

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

使用该方式时需要注意:

    1.对接口创建代理优于对类创建代理,因为会产生更加松耦合的系统,所以spring默认是使用JDK代理

      对类代理是让遗留系统或无法实现接口的第三方类库同样可以得到通知,这种方式应该是备用方案。

    2.标记为final的方法不能够被通知。spring是为目标类产生子类。任何需要被通知的方法都被复写,将通知织入。final方法是不允许重写的。

3.spring只支持方法连接点:不提供属性接入点,spring的观点是属性拦截破坏了封装。

  面向对象的概念是对象自己处理工作,其他对象只能通过方法调用的得到的结果。

提示:

l Spring AOP 优先对接口进行代理 (使用Jdk动态代理)

如果目标对象没有实现任何接口,才会对类进行代理 (使用cglib动态代理)

3. 传统Spring AOP编程案例(略) 

4. AspectJ切面编程(xml方式)

传统Spring1.2 AOP编程,确定目标--- 编写通知(通知需要实现一个接口)

    ---- 配置ProxyFactoryBean生成代理对象,配置非常复杂,

spring2.0 支持AspectJ 语法,简化AOP开发。

开发方法还是三步:

(1)确定目标对象(bean

(2)编写通知,对目标对象增强(advice

(3)配置切入点(pointcut)、切面(aspect

4.1.  AspectJ 提供Advice类型

普通的pojo即可。(不需要实现接口)

AspectJ提供不同的通知类型:

l Before 前置通知,相当于BeforeAdvice(接口)

l AfterReturning 后置通知,相当于AfterReturningAdvice(接口)

l Around 环绕通知,相当于MethodInterceptor(接口)

l AfterThrowing抛出通知,相当于ThrowAdvice(接口)

l After 最终final通知,不管是否异常,该通知都会执行

l DeclareParents 引介通知,相当于IntroductionInterceptor (不要求掌握)

相比传统Spring AOP通知类型多了After最终通知(类似 finally )。

实现步骤:

第一步:确定目标对象,即确定bean对象

第二advice通知(编写)

第三步:配置切面(包括切入点),让切入点关联通知

第一步:确定目标对象,即确定bean对象:src下,创建applicationContext-aspectj.xml

   <!-- 1.确定了要增强的target对象 -->
    <!-- 对于spring来说,目标对象:就是bean对象 -->
    <!-- 基于接口类 -->
    <bean id="customerService" class="cn.spring.a_proxy.CustomerServiceImpl"/>
    <!-- 基于一般类 -->
    <bean id="productService" class="cn.spring.a_proxy.ProductService"/>

第二步:编写Before 前置通知Advice增强 :创建类:MyAspect.java

编写MyAspect.java

//aspectj的advice通知增强类,无需实现任何接口
public class MyAspect {
    //前置通知
    //普通的方法。方法名随便,但也不能太随便,一会要配置
    public void firstbefore(){
        System.out.println("------------第一个前置通知执行了。。。");
    }
}

将前置通知配置到spring的容器中

<!-- 2.配置advice通知增强 -->
<bean id="myAspectAdvice" class="cn.spring.c_aspectjaop.MyAspect"/>

4.2.  配置切入点和切面(让切入点关联通知)

第三步:配置切面(包括切入点),让切入点关联通知

核心配置文件applicationContext-aspectj.xml中添加:

<!-- 2.配置advice通知 -->
    <bean id="myAspectAdvice" class="cn..spring.c_aspectjaop.MyAspect"/>
    
<!-- 3:配置aop -->
    <aop:config>
        <!-- 切入点:拦截哪些bean的方法 -->
            <aop:pointcut expression="bean(*Service)" id="myPointcut"/>
        <!--
            切面:要对哪些方法进行怎样的增强  
            aop:aspect:aspejctj的方式!
            ref:配置通知(通知的id值)
        -->
            <aop:aspect ref="myAspectAdvice">
            <!-- 第一个前置通知 :在访问目标对象方法之前,先执行通知的方法;目的是让通知关联切入点
                method:advice类中的方法名
                pointcut-ref="myPointcut":注入切入点(切入点的id值)
            -->
            <aop:before method="firstbefore" pointcut-ref="myPointcut"/>
        </aop:aspect>
    </aop:config>

使用SpringTest测试代码:

//springjunit集成测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:applicationContext-aspectj.xml")
public class SpringTest {
    //注入要测试bean
    @Autowired
    private ICustomerService customerService;
    @Autowired
    private ProductService productService;
    
    //测试
    @Test
    public void test(){
        //基于接口
        customerService.save();
        customerService.find();
        //基于类的
        productService.save();
        productService.find();
    }
}

和传统的aop配置相比,更灵活,advice不需要实现接口,简单的pojo就可以了;一个通知可以增强多个连接点,一个连接点可以被多次增强。

【扩展优化】:

1.将切入点放入aspectj标签里面写,同时配置多个通知方法

<!-- 2.配置advice通知增强 -->
    <bean id="myAspectAdvice" class="cn.spring.c_aspectjaop.MyAspect"/>
    
    <!-- 3:配置aop -->
    <aop:config>
        <!--
            切面:要对哪些方法进行怎样的增强  
            aop:aspect:aspejctj的方式!
            ref:配置通知
        -->
        <aop:aspect ref="myAspectAdvice">
            <aop:pointcut expression="bean(*Service)" id="myPointcut"/>
            <!-- 第一个前置通知 :在访问目标对象方法之前,先执行通知的方法;目的是让通知关联切入点
                method:advice类中的方法名,
                pointcut-ref="myPointcut":注入切入点                            
            -->
            <aop:before method="firstbefore" pointcut-ref="myPointcut"/>
            <aop:before method="firstbefore2" pointcut-ref="myPointcut"/>
        </aop:aspect>
    </aop:config>

2.配置多个通知方法:

//aspectj的advice通知增强类,无需实现任何接口
public class MyAspect {
    
    //前置通知
    //普通的方法。方法名随便,但也不能太随便,一会要配置
    public void firstbefore(){
        System.out.println("------------第一个前置通知执行了。。。");
    }
    
    public void firstbefore2(){
        System.out.println("------------第二个前置通知执行了222。。。");
    }
}

3.执行结果:表示在执行目标对象方法之前执行

技术分享

4.3.   分析各种通知应用(五种通知略)

5. @Aspectj注解配置切面编程

 

spring02