Spring注入(IOC)和AOP实例教程

1、Spring注入

1.1 准备

新建一个Maven项目,pom.xml文件如下:



    4.0.0

    com.lfqy.SpringIOC
    SpringIOC
    1.0-SNAPSHOT
    
        
        
        
            org.springframework
            spring-core
            4.1.4.RELEASE
        
        
        
        
            org.springframework
            spring-context
            4.1.4.RELEASE
        
        
        
            junit
            junit
            4.12
            test
        
    


在Module的src/main/java路径下,新建如下的两个类,作为准备:
School.java

package com.lfqy.SpringIOC.bean;

/**
 * Created by chengxia on 2019/7/31.
 */
public class School {
    private String name;

    public School() {
        System.out.println("执行School无参数构造函数");
    }

    public School(String name) {
        System.out.println("执行School带参数构造函数");
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "School{" +
                "name='" + name + '\'' +
                '}';
    }
}

Student.java

package com.lfqy.SpringIOC.bean;

/**
 * Created by chengxia on 2019/7/31.
 */
public class Student {
    private String name;
    private int age;
    private School school;

    public Student() {
        System.out.println("执行Student无参数构造函数ing");
    }

    public Student(String name, int age, School school) {

        System.out.println("执行Student有参数构造函数ing");
        this.name = name;
        this.age = age;
        this.school = school;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("执行Student的setName函数ing");
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        System.out.println("执行Student的setAge函数ing");
        this.age = age;
    }

    public School getSchool() {
        return school;
    }

    public void setSchool(School school) {
        System.out.println("执行Student的setSchool函数ing");
        this.school = school;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", school=" + school +
                '}';
    }
}

新建之后的目录结构如下:


Project Structure

1.2 基于xml的注入

1.2.1 设值注入

设值注入是指在bean定义的配置文件中,分别设置各个bean属性的值,以及bean之间的依赖关系。
在Module的resources目录下新建一个bean配置文件ApplicationContext.xml,内容如下:



    
        
    

    
        
        
        
    

注意:这里采用设值注入的方式,各个bean的定义类中,要有一个无参数的构造函数。否则,配置文件会提示标红“找不到构造函数”。
这时候,可以在Module的src\test\java目录中新建一个测试类com.lfqy.SpringIOC.bean.StudentTest,如下:

package com.lfqy.SpringIOC.bean;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import static org.junit.Assert.*;

/**
 * Created by chengxia on 2019/7/31.
 */
public class StudentTest {
    @Test
    public void test01() {
        // 获取Spring容器:加载Spring配置文件
        String resource = "ApplicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
        Student student = (Student) ac.getBean("myStudent");
        System.out.println(student);
    }
}

最后的目录结构如下:


Project Structure Finished

运行这个测试类,可以得到如下的输出:

执行School无参数构造函数
执行Student无参数构造函数ing
执行Student的setName函数ing
执行Student的setAge函数ing
执行Student的setSchool函数ing
Student{name='张三', age=23, school=School{name='清华大学'}}

Process finished with exit code 0

1.2.2 构造注入

构造注入是指通过在xml配置文件中配置bean类构造函数参数的值,来调用构造函数实例化bean。
修改ApplicationContext.xml文件如下:



    
        
    

    
        
        
        
    

运行同一个测试类,输出如下:

执行School无参数构造函数
执行Student有参数构造函数ing
Student{name='李四', age=24, school=School{name='清华大学'}}

Process finished with exit code 0

1.2.3 自动注入

对于域属性的注入,也可不在配置文件中显示的注入。可以通过为标签设置autowire属性值,为域属性进行隐式自动注入。根据自动注入判断标准的不同,可以分为两种:

  • byName:根据名称自动注入
  • byType:根据类型自动注入

1.2.3.1 byName

修改ApplicationContext.xml文件如下:



    
        
    

    
        
        
    

可以看出,对于myStudent这个bean的名为school的属性,在配置文件中并没有显示指定它的值。这时候,Spring会自动找到名称为school的bean,然后,赋值给它。这就是byName自动装配。
运行测试类,结果如下:

执行School无参数构造函数
执行Student无参数构造函数ing
执行Student的setName函数ing
执行Student的setAge函数ing
执行Student的setSchool函数ing
Student{name='李四', age=24, school=School{name='清华大学'}}

Process finished with exit code 0

1.2.3.2 byType

修改ApplicationContext.xml文件如下:



    
        
    

    
        
        
    

可以看出,对于myStudent这个bean的类型为School的属性,在配置文件中并没有显示指定它的值。这时候,Spring会自动找到类型为School的bean(这里是school),然后,赋值给它。这就是byType自动装配。
运行测试类,结果如下:

执行School无参数构造函数
执行Student无参数构造函数ing
执行Student的setName函数ing
执行Student的setAge函数ing
执行Student的setSchool函数ing
Student{name='李四', age=24, school=School{name='清华大学'}}

Process finished with exit code 0

1.3 基于注解的注入

使用注解注入,将不再需要在Spring配置文件中声明Bean实例。

1.3.1 配置文件修改

Spring中使用注解,需要在原有Spring运行环境基础上再做一些改变:
(1) 需要更换配置文件头,即添加相应的约束。


(2)需要在Spring配置文件中配置组件扫描器,用于在指定的基本包中扫描注解。


这里修改完成的ApplicationContext.xml文件如下:



    

1.3.2 简单注解介绍

1.3.2.1 @Component

需要在类上使用注解@Component,该注解的value属性用于指定该bean的id值。
另外,Spring还提供了3个功能基本和@Component等效的注解:

  • @Repository用于对DAO实现类进行注解。
  • @Service用于对Service实现类进行注解。
  • @Controller用于对Controller实现类进行注解。

之所以创建这三个功能与@Component等效的注解,是为了以后对其进行功能上的扩展,使它们不再等效。

1.3.2.2 @Scope

需要在类上使用注解@Scope,其value属性用于指定作用域。默认为singleton。

1.3.2.3 基本类型属性注入@Value

需要在属性上使用注解@Value,该注解的value属性用于指定要注入的值。使用该注解完成属性注入时,类中无需setter。当然,若属性有setter,则也可将其加到setter上。

1.3.2.4 按类型注入域属性@Autowired

需要在域属性上使用注解@Autowired,该注解默认使用按类型自动装配Bean的方式。使用该注解完成属性注入时,类中无需setter。当然,若属性有setter,则也可将其加到setter上。

1.3.3 基于注解注入的简单例子

上面的配置文件修改之后,可以通过修改bean的java代码来实现基于注解的注入。修改后的代码如下。
com.lfqy.SpringIOC.bean.School

package com.lfqy.SpringIOC.bean;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

/**
 * Created by chengxia on 2019/7/31.
 */
@Component("mySchool")
@Scope("singleton")
public class School {

    @Value("清华大学")
    private String name;

    @Override
    public String toString() {
        return "School{" +
                "name='" + name + '\'' +
                '}';
    }
}

com.lfqy.SpringIOC.bean.Student

package com.lfqy.SpringIOC.bean;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

/**
 * Created by chengxia on 2019/7/31.
 */
@Component("myStudent")
@Scope("singleton")
public class Student {
    @Value("张三")
    private String name;
    @Value("23")
    private int age;

    @Autowired  // 使用byType方式注入
    private School school;


    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", school=" + school +
                '}';
    }
}

运行前面的测试类,结果如下:

Student{name='张三', age=23, school=School{name='清华大学'}}

Process finished with exit code 0

1.3.4 按名称注入域属性@Autowired@Qualifier

需要在域属性上联合使用注解@Autowired@Qualifier@Qualifier的value属性用于指定要匹配的Bean的id值。同样类中无需setter,也可加到setter上。
@Autowired还有一个属性required,默认值为true,表示当匹配失败后,会终止程序
运行。若将其值设置为false,则匹配失败,将被忽略,未匹配的属性值为null。
下面是一个例子:

package com.lfqy.SpringIOC.bean;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

/**
 * Created by chengxia on 2019/7/31.
 */
@Component("myStudent")
@Scope("singleton")
public class Student {
    @Value("张三")
    private String name;
    @Value("23")
    private int age;

    @Autowired(required=false)
    @Qualifier("mySchool")//两个注解必须联合使用。使用byName方式注入
    private School school;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", school=" + school +
                '}';
    }
}

运行结果如下:

Student{name='张三', age=23, school=School{name='清华大学'}}

Process finished with exit code 0

1.3.5 域属性注解@Resource

Spring提供了对JSR-250规范中定义@Resource标准注解的支持。@Resource注解既可以按名称匹配Bean,也可以按类型匹配Bean。使用该注解,要求JDK必须是6及以上版本。

1.3.5.1 按类型注入域属性

@Resource注解若不带任何参数,则会按照类型进行Bean的匹配注入。

package com.lfqy.SpringIOC.bean;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * Created by chengxia on 2019/7/31.
 */
@Component("myStudent")
@Scope("singleton")
public class Student {
    @Value("张三")
    private String name;
    @Value("23")
    private int age;

    @Resource  //使用byType方式注入
    private School school;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", school=" + school +
                '}';
    }
}

运行测试类的结果如下:

Student{name='张三', age=23, school=School{name='清华大学'}}

Process finished with exit code 0

1.3.5.2 按名称注入域属性

@Resource注解指定其name属性,则name的值即为按照名称进行匹配的Bean的id。

package com.lfqy.SpringIOC.bean;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * Created by chengxia on 2019/7/31.
 */
@Component("myStudent")
@Scope("singleton")
public class Student {
    @Value("张三")
    private String name;
    @Value("23")
    private int age;

    @Resource(name="mySchool")  //使用byName方式注入
    private School school;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", school=" + school +
                '}';
    }
}

运行测试类的结果如下:

Student{name='张三', age=23, school=School{name='清华大学'}}

Process finished with exit code 0

2、Spring AOP

2.1 AOP简介

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
From 百度百科《AOP》

AOP(Aspect Orient Programming),面向切面编程,是面向对象编程OOP的一种补充。面向对象编程是从静态角度考虑程序的结构,而面向切面编程是从动态角度考虑程序运行过程。
AOP底层,就是采用动态代理模式实现的。采用了两种代理:JDK的动态代理,与CGLIB的动态代理。
面向切面编程,就是将交叉业务逻辑封装成切面,利用AOP容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志等。
若不使用AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样,会使主业务逻辑变的混杂不清。
例如,转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但,它们的代码量所占比重能达到总代码量的一半甚至还多。它们的存在,不仅产生了大量的“冗余”代码,还大大干扰了主业务逻辑---转账。

2.2 AOP编程术语

2.2.1 切面(Aspect)

切面泛指交叉业务逻辑。前面例子中的事务处理、日志处理就可以理解为切面。常用的切面有通知与顾问。实际就是对主业务逻辑的一种增强。

2.2.2 织入(Weaving)

织入是指将切面代码插入到目标对象的过程。前面面向切面编程介绍中,动态代理中的invoke()方法完成的工作,就可以称为织入。

2.2.3 连接点(JoinPoint)

连接点指切面可以织入的位置。前面例子中IService中的doSome()与doOther()均为连接点。

2.2.4 切入点(Pointcut)

切入点指切面具体织入的位置。在StudentServiceImpl类中,若doSome()将被增强,而doOther()不被增强,则doSome()为切入点,而doOther()仅为连接点。被标记为final的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。

2.2.5 目标对象(Target)

目标对象指将要被增强的对象。即包含主业务逻辑的类的对象。上例中的StudentServiceImpl的对象若被增强,则该类称为目标类,该类对象称为目标对象。当然,不被增强,也就无所谓目标不目标了。

2.2.6 通知(Advice)

通知是切面的一种实现,可以完成简单织入功能(织入功能就是在这里完成的)。上例中的MyInvocationHandler就可以理解为是一种通知。换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。
切入点定义切入的位置,通知定义切入的时间。

2.2.7 顾问(Advisor)

顾问是切面的另一种实现,能够将通知以更为复杂的方式织入到目标对象中,是将通知包装为更复杂切面的装配器。 不仅指定了切入时间点,还可以指定具体的切入点

2.2.8 AOP代理(Proxy)

代理对象 = 目标对象 + 交叉业务逻辑

2.3 通知

通知(Advice),切面的一种实现,可以完成简单织入功能(织入功能就是在这里完成的)。

2.3.1 通知分类

常用通知有:前置通知、后置通知、环绕通知、异常处理通知。

2.3.1.1 前置通知MethodBeforeAdvice

定义前置通知,需要实现MethodBeforeAdvice接口。该接口中有一个方法before(),会在目标方法执行之前执行。
前置通知的特点:

  • 在目标方法执行之前先执行。
  • 不改变目标方法的执行流程,前置通知代码不能阻止目标方法执行。 不改变目标方法执行的结果。

2.3.1.2 后置通知AfterReturningAdvice

定义后置通知,需要实现接口AfterReturningAdvice。该接口中有一个方法afterReturning(),会在目标方法执行之后执行。
后置通知的特点:

  • 在目标方法执行之后执行。
  • 不改变目标方法的执行流程,后置通知代码不能阻止目标方法执行。不改变目标方法执行的结果。

2.3.1.3 环绕通知MethodInterceptor

定义环绕通知,需要实现MethodInterceptor接口。环绕通知,也叫方法拦截器,可以在目标方法调用之前及之后做处理,可以改变目标方法的返回值,也可以改变程序执行流程。注意,org.aopalliance.intercept.MethodInterceptor才是需要的包。

2.3.1.4 异常通知ThrowsAdvice

定义异常通知,需要实现ThrowsAdvice接口。该接口的主要作用是,在目标方法抛出异常后,根据异常的不同做出相应的处理。当该接口处理完异常后,会简单地将异常再次抛出给目标方法。
不过,这个接口较为特殊,从形式上看,该接口中没有必须要实现的方法。但这个接口却确实有必须要实现的方法afterThrowing()。这个方法重载了四种形式。由于使用时,一般只使用其中一种,若要都定义到接口中,则势必要使程序员在使用时必须要实现这四个方法。这是很麻烦的。所以就将该接口定义为了标识接口(没有方法的接口)。
这四个方法在打开ThrowsAdvice源码后,上侧的注释部分可以看到:


ThrowsAdvice

不过,在这四种形式中,常用的形式如下:public void afterThrowing(自定义的异常类 e) 这里的参数e为,与具体业务相关的用户自定义的异常类对象。容器会根据异常类型的不同,自动选择不同的该方法执行。这些方法的执行是在目标方法执行结束后执行的。
其它参数则与前面两个通知中方法的参数意义相同。

2.3.2 通知的用法步骤

对于通知的定义、配置与使用,主要分为以下几步:

  • 定义目标类
    定义目标类,就是定义之前的普通Bean类,也就是即将被增强的Bean类。
  • 定义通知类
    通知类是指,实现了相应通知类型接口的类。当前,实现了这些接口,就要实现这些接口中的方法,而这些方法的执行,则是根据不同类型的通知,其执行时机不同:前置通知,在目标方法执行之前执行;后置通知,在目标方法执行之后执行;环绕通知,在目标方法执行之前与之后均执行;异常处理通知,在目标方法执行过程中,若发生指定异常,则执行通知中的方法。
  • 注册目标类
    即在Spring配置文件中注册目标对象Bean。
  • 注册通知切面
    即在Spring配置文件中注册定义的通知对象Bean。
  • 注册代理工厂Bean类对象ProxyFactoryBean
    常用的代理使用的是ProxyFactoryBean类。代理对象的配置,是与JDK的Proxy代理参数是一致的,都需要指定三部分:目标类,接口,切面。
  • 客户端访问动态代理对象
    客户端访问的是动态代理对象,而非原目标对象。因为代理对象可以将交叉业务逻辑按照通知类型,动态的织入到目标对象的执行中。

2.3.3 通知相关的例子

2.3.3.1 前置通知

在前面的工程中,新建如下包、类和配置文件:


Project Structure

代码如下:
com.lfqy.SpringAOP.business.IStudentService

package com.lfqy.SpringAOP.business;

/**
 * Created by chengxia on 2019/8/1.
 */
// 主业务接口
public interface IStudentService {
    void doSome();
    String doOther();
}

com.lfqy.SpringAOP.business.StudentServiceImpl

package com.lfqy.SpringAOP.business;

/**
 * Created by chengxia on 2019/8/1.
 */
// 目标类
public class StudentServiceImpl implements IStudentService {
    // 目标方法
    public void doSome() {
        System.out.println("执行主业务逻辑doSome()");
    }

    // 目标方法
    public String doOther() {
        System.out.println("执行主业务逻辑doOther()");
        return "other";
    }

    // 由于业务接口中没有该方法,所以该方法不是目标方法
    public void doThird() {
        System.out.println("doThird Method executed");
    }

}

com.lfqy.SpringAOP.pointcut.MyMethodBeforeAdvice

package com.lfqy.SpringAOP.pointcut;

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

/**
 * Created by chengxia on 2019/8/1.
 */
// 前置通知
public class MyMethodBeforeAdvice implements MethodBeforeAdvice {

    // 目标方法执行之前执行该方法
    public void before(Method method, Object[] args, Object target)
            throws Throwable {
        System.out.println("output from pointcut: 执行前置通知ing");
    }

}

ApplicationContextAOP.xml



    
    

    
    

    
    
        
        
        
    

com.lfqy.SpringAOP.business.StudentServiceImplTest

package com.lfqy.SpringAOP.business;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import static org.junit.Assert.*;

/**
 * Created by chengxia on 2019/8/1.
 */
public class StudentServiceImplTest {
    @Test
    public void test01() {
        // 获取Spring容器:加载Spring配置文件
        String resource = "ApplicationContextAOP.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
        IStudentService service = (IStudentService) ac.getBean("myServiceProxy");
        service.doSome();
        System.out.println("return from doOther():" + service.doOther());
        //service.doThird();//从容器中获取的是代理对象,而非目标对象。所以,这里只包含接口中定义的方法,doThird方法只定义在目标类中,不在接口中,所以不能执行
    }
}

执行这个单元测试方法,输出如下:

output from pointcut: 执行前置通知ing
执行主业务逻辑doSome()
output from pointcut: 执行前置通知ing
执行主业务逻辑doOther()
return from doOther():other

Process finished with exit code 0

2.3.3.2 后置通知

首先,定义一个后置通知类。
com.lfqy.SpringAOP.pointcut.MyAfterReturningAdvice

package com.lfqy.SpringAOP.pointcut;

import org.springframework.aop.AfterReturningAdvice;

import java.lang.reflect.Method;

/**
 * Created by chengxia on 2019/8/1.
 */
// 后置通知
public class MyAfterReturningAdvice implements AfterReturningAdvice {

    // returnValue:目标方法的返回值
    // method:目标方法
    // args:目标方法的参数列表
    // target:目标对象

    // 目标方法执行之后执行该方法
    public void afterReturning(Object returnValue, Method method,
                               Object[] args, Object target) throws Throwable {
        System.out.println("目标方法执行之后,目标方法的返回值 = " + returnValue);

        // 后置通知不能修改目标方法的返回值
        /*if (returnValue != null) {
            returnValue = ((String) returnValue).toUpperCase();
        }*/
    }
}

修改Spring配置文件。
ApplicationContextAOP.xml



    
    

    
    

    
    
        
        
    

运行和前面同样的测试类,输出如下:

执行主业务逻辑doSome()
目标方法执行之后,目标方法的返回值 = null
执行主业务逻辑doOther()
目标方法执行之后,目标方法的返回值 = other
return from doOther():other

Process finished with exit code 0

2.3.3.3 环绕通知

首先,定义一个环绕通知类。
com.lfqy.SpringAOP.pointcut.MyAroundAdvice

package com.lfqy.SpringAOP.pointcut;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

/**
 * Created by chengxia on 2019/8/1.
 */

// 环绕通知:可以修改目标方法的返回结果
public class MyAroundAdvice implements MethodInterceptor {

    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("环绕通知:目标方法执行之前");
        // 执行目标方法
        Object result = invocation.proceed();
        System.out.println("环绕通知:目标方法执行之后");
        if(result != null){
            result = ((String)result).toUpperCase();
        }
        return result;
    }

}

修改Spring配置文件。
ApplicationContextAOP.xml



    
    

    
    

    
    
        
        
    

运行前面的测试用例,输出如下:

环绕通知:目标方法执行之前
执行主业务逻辑doSome()
环绕通知:目标方法执行之后
环绕通知:目标方法执行之前
执行主业务逻辑doOther()
环绕通知:目标方法执行之后
return from doOther():OTHER

Process finished with exit code 0

2.3.3.4 异常通知

这个例子中,是模仿了一个用户登陆时用户名和密码验证。当用户名或者密码不正确时,会抛出用户名或者密码有误异常,会执行异常通知的逻辑,执行完成之后,才会执行异常捕获的代码。
首先,定义如下异常相关的类:
com.lfqy.SpringAOP.exception.UserException

package com.lfqy.SpringAOP.exception;

/**
 * Created by chengxia on 2019/8/1.
 */
public class UserException extends Exception {

    public UserException() {
        super();
    }
    public UserException(String message) {
        super(message);
    }

}

com.lfqy.SpringAOP.exception.UsernameException

package com.lfqy.SpringAOP.exception;

/**
 * Created by chengxia on 2019/8/1.
 */
public class UsernameException extends UserException {

    public UsernameException() {
        super();
    }

    public UsernameException(String message) {
        super(message);
    }

}

com.lfqy.SpringAOP.exception.PasswordException

package com.lfqy.SpringAOP.exception;

/**
 * Created by chengxia on 2019/8/1.
 */
public class PasswordException extends UserException {

    public PasswordException() {
        super();
    }

    public PasswordException(String message) {
        super(message);
    }

}

然后,定义服务接口和服务的实现类:
com.lfqy.SpringAOP.business.ILogInAuthService

package com.lfqy.SpringAOP.business;

import com.lfqy.SpringAOP.exception.UserException;

/**
 * Created by chengxia on 2019/8/1.
 */
public interface ILogInAuthService {
    boolean check(String username, String password) throws UserException;
}

com.lfqy.SpringAOP.business.LogInAuthServiceImpl

package com.lfqy.SpringAOP.business;

import com.lfqy.SpringAOP.exception.PasswordException;
import com.lfqy.SpringAOP.exception.UserException;
import com.lfqy.SpringAOP.exception.UsernameException;

/**
 * Created by chengxia on 2019/8/1.
 */
public class LogInAuthServiceImpl implements ILogInAuthService {
    // 目标方法
    public boolean check(String username, String password) throws UserException {
        System.out.println("执行用户名密码认证ing");
        if(!"error_name".equals(username)){
            throw new UsernameException("用户名不对了!!!!!!!");
        }
        if(!"error_password".equals(password)){
            throw new PasswordException("密码不对了!!!!!!!");
        }
        return true;
    }
}

在定义一个异常通知类:
com.lfqy.SpringAOP.pointcut.MyThrowsAdvice

package com.lfqy.SpringAOP.pointcut;

import com.lfqy.SpringAOP.exception.PasswordException;
import com.lfqy.SpringAOP.exception.UsernameException;
import org.springframework.aop.ThrowsAdvice;

/**
 * Created by chengxia on 2019/8/1.
 */
public class MyThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(UsernameException ex){
        System.out.println("用户名异常:" + ex.getMessage());
    }

    public void afterThrowing(PasswordException ex){
        System.out.println("密码异常:" + ex.getMessage());
    }
}

在xml配置文件中添加服务和切面定义:
ApplicationContextAOP.xml



    
    

    
    

    
    
        
        
    

    

    
    

    
    
        
        
    

最后,新建一个测试类:
com.lfqy.SpringAOP.business.LogInAuthServiceImplTest

package com.lfqy.SpringAOP.business;

import com.lfqy.SpringAOP.exception.UserException;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * Created by chengxia on 2019/8/1.
 */
public class LogInAuthServiceImplTest {
    @Test
    public void test01() {
        // 获取Spring容器:加载Spring配置文件
        String resource = "ApplicationContextAOP.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
        ILogInAuthService service = (ILogInAuthService) ac.getBean("myServiceProxy1");
        try {
            service.check("yupaopao", "error_password");
        } catch (UserException e) {
            // TODO Auto-generated catch block
            //e.printStackTrace();
            System.out.println("test01出现了异常");
        }
    }
    @Test
    public void test02() {
        // 获取Spring容器:加载Spring配置文件
        String resource = "ApplicationContextAOP.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
        ILogInAuthService service = (ILogInAuthService) ac.getBean("myServiceProxy1");
        try {
            service.check("error_name", "111");
        } catch (UserException e) {
            // TODO Auto-generated catch block
            //e.printStackTrace();
            System.out.println("test02出现了异常");
        }
    }

}

该测试类的运行结果如下:

八月 01, 2019 6:17:13 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@198e2867: startup date [Thu Aug 01 18:17:13 CST 2019]; root of context hierarchy
八月 01, 2019 6:17:14 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [ApplicationContextAOP.xml]
执行用户名密码认证ing
用户名异常:用户名不对了!!!!!!!
test01出现了异常
八月 01, 2019 6:17:14 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@2f8f5f62: startup date [Thu Aug 01 18:17:14 CST 2019]; root of context hierarchy
八月 01, 2019 6:17:14 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [ApplicationContextAOP.xml]
执行用户名密码认证ing
密码异常:密码不对了!!!!!!!
test02出现了异常

Process finished with exit code 0

到目前为止,上面的例子都创建完,目录结构如下图:


Project Structure Finished

2.4 AspectJ

对于AOP这种编程思想,很多框架都进行了实现。Spring就是其中之一,可以完成面向切面编程。然而,AspectJ也实现了AOP的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring又将AspectJ的对于AOP的实现也引入到了自己的框架中。
在Spring中使用AOP开发时,一般使用AspectJ的实现方式。

AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。
From 百度百科《AspectJ》

2.4.1 AspectJ的通知类型

AspectJ中常用的通知有五种类型:

  • 前置通知
  • 后置通知
  • 环绕通知
  • 异常通知
  • 最终通知
    无论程序执行是否正常,该通知都会执行。类似于try..catch中finally代码块。

2.4.2 AspectJ的切入点表达式execution()

AspectJ除了提供了六种通知外,还定义了专门的表达式用于指定切入点。表达式的原型是:


Prototype

切入点表达式要匹配的对象就是目标方法的方法名。所以,execution表达式中明显就是方法的签名。注意,表达式中加[ ]的部分表示可省略部分,各部分间用空格分开。在其中可以使用以下符号:


Syntax Comment

举例:
execution(public * *(..))
指定切入点为:任意公共方法。
execution(* set *(..))
指定切入点为:任何一个以“set”开始的方法。
execution(* com.xyz.service.*.*(..))
指定切入点为:定义在service包里的任意类的任意方法。
execution(* com.xyz.service..*.*(..))
指定切入点为:定义在service包或者子包里的任意类的任意方法。..出现在类名中时,后面必须跟*,表示包、子包下的所有类。
execution(* *.service.*.*(..))
指定只有一级包下的serivce子包下所有类(接口)中的所有方法为切入点
execution(* *..service.*.*(..))
指定所有包下的serivce子包下所有类(接口)中的所有方法为切入点
execution(* *.ISomeService.*(..))
指定只有一级包下的ISomeService 接口中的任意方法为切入点。
execution(* *..ISomeService.*(..))
指定所有包下的ISomeService 接口中的任意方法为切入点。

2.4.3 AspectJ的使用依赖

在使用AspectJ时,除了引入Spring-coreSpring-context等Spring相关的依赖,还需要引入Aspect相关的依赖aspectjweaver。同时,在Spring框架中使用AspectJ还需要引入spring-aspects。这些依赖的pom标签如下:




    org.springframework
    spring-core
    4.1.4.RELEASE




    org.springframework
    spring-context
    4.1.4.RELEASE




    org.springframework
    spring-aspects
    5.1.8.RELEASE




    org.aspectj
    aspectjweaver
    1.7.4

引入这些依赖之后,同时要注意在Spring的xml配置文件中要引入aop约束。如下:


2.4.4 AspectJ的使用步骤

(1) 定义业务接口与实现类
IStudentService.java

package com.lfqy.SpringAspect.business;

/**
 * Created by chengxia on 2019/8/1.
 */
// 主业务接口
public interface IStudentService {
    void doSome();
    String doOther();
    void doThird();
}

StudentServiceImpl.java

package com.lfqy.SpringAspect.business;

/**
 * Created by chengxia on 2019/8/1.
 */
// 目标类
public class StudentServiceImpl implements IStudentService {
    // 目标方法
    public void doSome() {
        System.out.println("执行主业务逻辑doSome()");
    }

    // 目标方法
    public String doOther() {
        System.out.println("执行主业务逻辑doOther()");
        return "other";
    }

    // 由于业务接口中没有该方法,所以该方法不是目标方法
    public void doThird() {
        System.out.println("doThird Method executed");
    }

}

(2) 定义切面
首先,定义切面POJO类该类为一个POJO类,将作为切面出现。其中定义了若干普通方法,将作为不同的通知方法。
然后,在切面pojo上添加@Aspect注解指定当前POJO类将作为切面。
最后,在POJO类的普通方法上添加通知注解。切面类是用于定义增强代码的,即用于定义增强目标类中目标方法的增强方法。这些增强方法使用不同的“通知”注解,会在不同的时间点完成织入。当然,对于增强代码,还要通过execution表达式指定具体应用的目标类与目标方法,即切入点。
效果如下:
MyAspect.java

package com.lfqy.SpringAspect.aspects;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

/**
 * Created by chengxia on 2019/8/2.
 */
@Aspect   // 表明当前POJO类是一个切面
public class MyAspect {

    // 前置通知
    @Before("execution(* *..IStudentService.doSome(..))")
    public void myBefore(){
        System.out.println("执行前置通知方法");
    }

    // 后置通知
    @AfterReturning("execution(* *..IStudentService.doOther(..))")
    public void myAfterReturning(){
        System.out.println("执行后置通知");
    }
}

(3) 配置Spring的xml配置文件
首先,注册目标对象与POJO切面类,就是将前面定义的目标对象和切面类定义为bean。
然后,注册AspectJ的自动代理。在定义好切面Aspect后,需要通知Spring容器,让容器生成目标类 + 切面的代理对象。这个代理是由容器自动生成的。只需要在Spring配置文件中注册一个基于aspectj的自动代理生成器,其就会自动扫描到@Aspect注解,并按通知类型与切入点,将其织入,并生成代理。开启方法就是在xml文件中,添加:



的底层是由AnnotationAwareAspectJAutoProxyCreator实现的。从其类名就可看出,是基于AspectJ的注解适配自动代理生成器。
其工作原理是,通过扫描找到@Aspect定义的切面类,再由切面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。

2.4.5 AspectJ使用示例

在IDEA中新建groupid为com.lfqy.SpringAspect的项目,pom.xml文件如下:



    4.0.0

    com.lfqy.SpringAspect
    SpringAspect
    1.0-SNAPSHOT
    
        
        
        
            org.springframework
            spring-core
            4.1.4.RELEASE
        
        
        
        
            org.springframework
            spring-context
            4.1.4.RELEASE
        

        
        
            org.springframework
            spring-aspects
            5.1.8.RELEASE
        

        
        
            org.aspectj
            aspectjweaver
            1.7.4
        

        
        
            junit
            junit
            4.12
            test
        
    


添加前面AspectJ使用说明里面写好的服务类和切面类。然后,配置Spring的xml配置文件如下:
ApplicationContextAOP.xml



    
    
    
    
    
    

创建一个测试类:
com.lfqy.SpringAspect.business.StudentServiceImplTest

package com.lfqy.SpringAspect.business;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import static org.junit.Assert.*;

/**
 * Created by chengxia on 2019/8/2.
 */
public class StudentServiceImplTest {
    @Test
    public void test01() {
        // 获取Spring容器:加载Spring配置文件
        String resource = "ApplicationContextAOP.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
        IStudentService service = (IStudentService) ac.getBean("myService");
        service.doSome();
        System.out.println(service.doOther());
        service.doThird();
    }
}

这个Maven项目的工程结构如下:


Maven Project Structure

运行该测试类,输出如下:

执行前置通知方法
执行主业务逻辑doSome()
执行主业务逻辑doOther()
执行后置通知
other
doThird Method executed

Process finished with exit code 0

2.4.6 AspectJ通知类型详解

2.4.6.1 @Before前置通知

在目标方法执行之前执行。

// 前置通知
@Before("execution(* *..IStudentService.doSome(..))")
public void myBefore(){
    System.out.println("执行前置通知方法");
}

2.4.6.2 @AfterReturning后置通知,注解有returning属性

在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目标方法的返回值。该注解的returning属性就是用于指定接收方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含JoinPoint参数外,还可以包含用于接收返回值的变量。该变量最好为Object类型,因为目标方法的返回值可能是任何类型。

// 后置通知
@AfterReturning("execution(* *..IStudentService.doOther(..))")
public void myAfterReturning(){
    System.out.println("执行后置通知");
}

// 后置通知,带返回值
@AfterReturning(value="execution(* *..IStudentService.doOther(..))", returning="result")
public void myAfterReturning2(Object result){
    System.out.println("执行后置通知,目标方法返回值 = " + result);
}

2.4.6.3 @Around环绕通知,增强方法有ProceedingJoinPoint参数

在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object类型。并且方法可以包含一个ProceedingJoinPoint类型的参数。接口ProceedingJoinPoint其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。

// 环绕通知
@Around("execution(* *..IStudentService.doOther(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
    System.out.println("环绕通知:目标方法执行前");
    // 执行目标方法
    Object result = pjp.proceed();
    System.out.println("环绕通知:目标方法执行后");
    if(result != null){
        result = ((String)result).toUpperCase();
    }
    return result;
}

2.4.6.4 @AfterThrowing异常通知,注解中有throwing属性

在目标方法抛出异常后执行。该注解的throwing属性用于指定所发生的异常类对象。当然,被注解为异常通知的方法可以包含一个参数Throwable,参数名称为throwing指定的名称,表示发生的异常对象。

// 异常通知
@AfterThrowing(value="execution(* *..IStudentService.doThird(..))", throwing="ex")
public void afterThrowing(Exception ex){
    System.out.println("执行异常通知,ex = " + ex.getMessage());
}

2.4.6.5 @After最终通知

无论目标方法是否抛出异常,该增强均会被执行。

// 最终通知
@After("execution(* *..IStudentService.doThird(..))")
public void after(){
    System.out.println("执行最终通知");
}

2.4.6.6 @Pointcut定义切入点

当较多的通知增强方法使用相同的execution切入点表达式时,编写、维护均较为麻烦。AspectJ提供了@Pointcut注解,用于定义execution切入点表达式。
其用法是,将@Pointcut注解在一个方法之上,以后所有的executeion的value属性值均可使用该方法名作为切入点。代表的就是@Pointcut定义的切入点。这个使用@Pointcut注解的方法一般使用private的标识方法,即没有实际作用的方法。

@After("doThirdPointcut()")
public void after2(){
    System.out.println("执行最终通知2");
}

// 定义一个名称为doThirdPointcut()的切入点
@Pointcut("execution(* *..IStudentService.doThird(..))")
private void doThirdPointcut(){}

2.4.7 AspectJ基于XML的AOP实现

AspectJ除了提供了基于注解的AOP的实现外,还提供了以XML方式的实现。切面就是一个POJO类,而用于增强的方法就是普通的方法。通过配置文件,将切面中的功能增强织入到了目标类的目标方法中。

2.4.7.1 实现步骤

这里以前面的例子为基础写一个基于xml配置文件的AspectJ切面实现。
(1) 定义业务接口与实现类
这里复用前面的类。
(2) 定义切面POJO类
该类为一个POJO类,将作为切面出现。其中定义了若干普通方法,将作为不同的通知方法。
com.lfqy.SpringAspect.aspects.MyAspectXML

package com.lfqy.SpringAspect.aspects;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * Created by chengxia on 2019/8/5.
 */
// 定义切面:该类为POJO
public class MyAspectXML {
    // 前置通知
    public void myBefore(){
        System.out.println("执行前置通知方法myBefore");
    }
    // 后置通知
    public void myAfterReturning(){
        System.out.println("执行后置通知方法myAfterReturning");
    }
    // 后置通知
    public void myAfterReturning2(Object result){
        System.out.println("执行后置通知方法myAfterReturning2,目标方法返回值 = " + result);
    }
    // 环绕通知
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("环绕通知:目标方法执行前输出");
        // 执行目标方法
        Object result = pjp.proceed();
        System.out.println("环绕通知:目标方法执行后输出");
        if(result != null){
            result = ((String)result).toUpperCase();
        }
        return result;
    }
    // 异常通知
    public void afterThrowing(Exception ex){
        System.out.println("执行异常通知方法afterThrowing,ex = " + ex.getMessage());
    }
    // 最终通知
    public void after(){
        System.out.println("执行最终通知方法after");
    }

}

(3) 配置xml配置文件
ApplicationContextAOPXML.xml



    
    
    
    
    

    
    
    
        
        
        
        
        
        
            
            
            
            
            
            
        
    

配置文件中,除了要定义目标类与切面的Bean外,最主要的是在中进行aop的配置。而该标签的底层,会根据其子标签的配置,生成自动代理。 通过其子标签定义切入点,该标签有两个属性,id与expression。分别用于指定该切入点的名称及切入点的值。expression的值为execution表达式。

新建一个测试类:
com.lfqy.SpringAspect.business.StudentServiceImplXMLTest

package com.lfqy.SpringAspect.business;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * Created by chengxia on 2019/8/2.
 */
public class StudentServiceImplXMLTest {
    @Test
    public void test01() {
        // 获取Spring容器:加载Spring配置文件
        String resource = "ApplicationContextAOPXML.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
        IStudentService service = (IStudentService) ac.getBean("myService");
        service.doSome();
        System.out.println(service.doOther());
        service.doThird();
    }
}

运行该测试类,输出如下:

执行前置通知方法myBefore
执行主业务逻辑doSome()
环绕通知:目标方法执行前输出
执行主业务逻辑doOther()
环绕通知:目标方法执行后输出
执行后置通知方法myAfterReturning2,目标方法返回值 = OTHER
执行后置通知方法myAfterReturning
OTHER
doThird Method executed
执行最终通知方法after

Process finished with exit code 0

2.4.7.2 测试项目的目录结构

完成上面的例子之后,测试项目的目录结构如下:


Last Project Structure

你可能感兴趣的:(Spring注入(IOC)和AOP实例教程)