Spring框架学习之注解配置与AOP思想

     上篇我们介绍了Spring中有关高级依赖关系配置的内容,也可以调用任意方法的返回值作为属性注入的值,它解决了Spring配置文件的动态性不足的缺点。而本篇,我们将介绍Spring的又一大核心思想,AOP,也就是面向切面编程。这是对面向对象编程的一个扩展,即便问世不长,但是已经成为当下最流行的编程思想之一。本篇主要涉及以下内容:

  • Spring中的后置处理器
  • "零配置"实现Bean的配置
  • Spring AOP

一、后置处理器
为了实现良好的扩展性,Spring允许我们扩展它的IOC容器,它提供了两种后置处理器来支持我们对容器进行扩展。

  • Bean后置处理器:该处理器会对容器中的bean进行增强
  • 容器后置处理器:该处理器针对容器,对容器进行额外增强

1、Bean后置处理器
Bean后置处理器需要继承接口BeanPostProcessor,并实现它的如下两个方法:

  • public Object postProcessBeforeInitialization(Object o, String s):在当前bean实例初始化属性注入的之前回调
  • public Object postProcessAfterInitialization(Object o, String s):在当前bean实例初始化属性注入之后回调

当整个容器在加载的时候,会扫描整个容器中的bean,如果发现有bean实现了接口BeanPostProcessor,那么就将该bean注册为Bean后置处理器,一旦其他bean实例化完成之后,将逐个调用后置处理器进行bean实例的增强。

在这两个方法中,传入了同样的两个参数,第一个参数表示即将被后处理的bean实例,第二个参数是该实例在容器中的 id属性。postProcessBeforeInitialization方法指明在容器创建实例之后,但是在实际初始化属性之前回调,此处传过来的bean实例是一个完整的实例,它包含还未实际初始化的属性的值信息,该方法的返回值就是最终保存在容器中的实例。

postProcessAfterInitialization方法是类似的,它会在容器初始化属性结束后回调,在该方法中,我们也可以修改该bean的信息,最终返回的bean将作为容器中真实存在的bean,对应于传入的该bean的引用,相当于修改了该bean实例。

我们简单看个例子:

//定义两个类,并定义两个属性name和age
//容器中配置两个bean实例
<bean id="programmer" class="Test_spring.Programmer" p:name="single" p:age="22" />

<bean id="coder" class="Test_spring.Coder" p:name="walker" p:age="21" />
//定义Bean后置处理器
public class MyBeanProcess implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object o, String s) throws BeansException {
        //属性初始化之前
        if(o instanceof Programmer){
            Programmer programmer = (Programmer) o;
            programmer.setAge(50);
        }else if(o instanceof Coder) {
            Coder coder = (Coder) o;
            coder.setAge(60);
        }
        return o;
    }
    @Override
    public Object postProcessAfterInitialization(Object o, String s) throws BeansException {
        //属性初始化之后
        if(o instanceof Programmer){
            Programmer programmer = (Programmer) o;
            System.out.println(programmer.getName() + "," + programmer.getAge());
        }else if(o instanceof Coder) {
            Coder coder = (Coder) o;
            System.out.println(coder.getName() + "," + coder.getAge());
        }
        return o;
    }
}
//将该Bean后置处理器实例配置在容器中
<bean class="Test_spring.MyBeanProcess" />

我们看最终的输出结果:

这里写图片描述

程序很简单,在实际初始化属性之前,我们分别修改两个bean实例的age属性,又在属性初始化结束时,打印了他们的信息。Bean的后处理器一般就这么用,当然此处的例子有点大材小用了,根据实际情况适时选择使用即可。

2、容器后置处理器
Bean后置处理器负责增强处理所有的bean实例,而容器后置处理器则只负责处理容器本身。容器后置处理器必须实现接口 BeanFactoryPostProcessor,并实现其一个方法:

void postProcessBeanFactory(ConfigurableListableBeanFactory var1)

通过实现该方法体,我们可以做到对Spring容器进行扩展,通常来说,我们也很少自己去扩展Spring容器,毕竟难度有点大。我们会使用Spring为我们内置的容器后处理器,例如:属性占位符配置器。

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <list>
            <!--多个属性文件都可以一起进行读取-->
            <value>db.properties</value>
        </list>
    </property>
</bean>

<bean id="Dbcon" class="Test_spring.DbCon"
      p:driverClass="${jdbc.driverClassName}"
      p:conUrl="${jdbc.url}"
      p:userName="${jdbc.username}"
      p:pwd="${jdbc.password}"
/>

属性文件:

//属性文件名:db.properties
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///test
jdbc.username=root
jdbc.password=123456

上述代码中,我们使用Spring为我们提供的一个属性占位配置器,PropertyPlaceholderConfigurer。在实例化容器中的bean实例之前,容器会调用PropertyPlaceholderConfigurer容器后置处理器读取指定的Properties文件并保存在Spring配置信息中。

于是,我们可以使用${....}来获取被加载属性文件中的内容。

二、注解配置Bean实例
一直以来,我们都是使用的XML形式来配置我们的bean实例,但在潮流的推动下,大部分的Java框架也开始倾向于使用简单的注解来配置我们的bean实例。Spring也已经完全支持注解配置了。那么本小节就将学习下使用注解对bean实例的配置。

1、标识Bean类
在XML中,一个bean元素代表一个bean实例,它的class属性指定了它的类型,id指定了它的名称。而在我们注解中,使用以下几种注解来标识Bean类:

  • @Component:标识一个普通Bean
  • @Controller:标识一个控制器组件
  • @Service:标识一个业务组件
  • @Repository:标识一个DAO组件

我们这里主要使用注解@Component来标识Bean类,其他的三种类型的注解在整合第三方框架的时候再做详细介绍。例如:

@Component("teacher")
public class Teacher {
    private String name;
    private int age;
    //省略setter方法
}

上述代码等效于以下的XML配置:

<bean id="teacher" class="Test_spring.Teacher" />

当然,如果想要Spring的注解生效,还需要在XML中配置扫描器:

<!--配置注解扫描器-->
<context:component-scan base-package="Test_spring" />

base-package属性告诉Spring容器,应该从哪个包开始扫描所有被Spring注解修饰的类。这样我们就可以在容器外通过getBean获取到该实例了。

Teacher teacher = (Teacher) context.getBean("teacher");

2、指定Bean实例的作用域
XML中指定bean作用域通常使用scope属性来指定,例如:

<bean id="coder" class="Test_spring.Coder" scope="singleton" />

在Spring注解中,指定bean实例的作用域相对简单很多。例如:

@Scope("singleton")
@Component(value = "teacher")
public class Teacher {
    ........
}

直接使用@Scope注解进行作用域指定即可。

3、配置属性依赖
Spring中,我们使用注解@Resources来给属性注入依赖。例如:

//容器中配置coder的bean实例
<bean id="coder" class="Test_spring.Coder" p:name="single" p:age="22"/>
@Component(value = "teacher")
public class Teacher {
    private String name;
    private int age;
    private Coder coder;

    @Resource(name = "coder")
    public void setCoder(Coder coder) {
        this.coder = coder;
    }
    //省略其他setter方法
}

@Resource的name属性指向的容器中已经配置的bean实例id,它实现了和ref一样的语义,需要特别注意的是,@Resource注解是修饰在setter方法上的,而非直接修饰实例属性。

4、管理Bean实例的生命周期
在XML中,我们使用init-method和destory-method来管理bean的两个特殊时间点。当然,使用注解的话会清晰很多。例如:

@Component(value = "teacher")
public class Teacher {
    private String name;
    private int age;
    //省略setter方法

    @PostConstruct
    public void init(){
        System.out.println("initialize all properties...");
    }

    @PreDestroy
    public void destory(){
        System.out.println("destory this bean ....");
    }
}

我们使注解@PostConstruct标识初始化属性之后回调的方法,使用注解@PreDestroy指定在bean实例销毁之前回调的方法。

常用的注解基本上就是这几个,还有一些例如@Autowire、@Qualifier用于指定依赖的自动装配和精准自动装配的注解,由于本身使用场景有限,具体用到的时候,可以再次学习。

三、Spring AOP
AOP(Aspect Orient Programming),面向切面编程。它作为面向对象编程思维的一种延伸,逐渐成为了一种成熟的编程思维。AspectJ是当下对AOP思想实现情况中最优秀的框架,它提供了强大的AOP功能,有着自己的编译器和织入器。目前而言,Spring有着自己的AOP实现,底层是基于动态代理,但是也逐渐向AspectJ的规范靠近,并且在整个Spring AOP中使用的注解全部依赖AspectJ的注解,也就是说一旦哪天Spring 底层修改了AOP的实现,采用AspectJ做实现的话,并不影响上层注解的使用。既然是基于动态代理的,那我们先简单回顾下动态代理的内容,详细的内容在以前的文章中已经做过介绍,读者可以返回去查看。

//定义一个接口类型
public interface Person {
    void programming();
}
//提供一个该接口的实现类
public class Programmer implements Person{
    private String name;

    public Programmer(String name){
        this.name = name;
    }
    @Override
    public void programming(){
        System.out.println("my name is :" + this.name);
    }
}
//自定义一个处理器
public class MyHander implements InvocationHandler {

    private Object target;

    public MyHander(Object o){
        this.target = o;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("i am programming");
        method.invoke(target,args);
        System.out.println("programming finished");
        return null;
    }
}
//生成动态代理
Programmer programmer = new Programmer("single");
MyHander hander = new MyHander(programmer);
Class proxy =       Proxy.getProxyClass(Programmer.class.getClassLoader(),new Class[]{Person.class});
Constructor constructor  = proxy.getConstructor(new Class[]{InvocationHandler.class});
Person person = (Person) constructor.newInstance(hander);
person.programming();

程序首先创建一个Programmer 的实例,这也是我们即将代理的实例对象。然后定义了一个处理器并将当前代理对象实例传入,接着通过getProxyClass传入代理类的类加载器以及该类所有的接口即可,该方法将负责默认实现所有接口中的方法并返回一个代理类型,最后我们通过反射创建一个代理类的实例并完成方法的调用。

程序输出结果如下:

这里写图片描述

环绕着programming方法的前后,我们打印了日志信息。这是一个典型的基于jdk的动态代理的实现。

而我们的AOP大致也就是做这样类似的事情,在一个方法调用之前或者之后增加一些额外的处理,具体的我们先看看几个有关AOP的概念:

  • 切面:为目标对象增加的每一个模块叫做一个切面
  • 连接点:程序执行过程中确切的点,方法调用前,方法调用后或者异常出现点
  • 通知:切面中的每一个方法叫做一个通知或者一次增强处理
  • 切入点:可以插入增强处理的连接点叫做切入点

根据通知的类型不同,我们大致可以分为以下几类:

  • before增强通知
  • after增强通知
  • around增强通知
  • AfterReturning增强通知
  • AfterThrowing增强通知

1、before增强通知处理
我们说过,Spring AOP完全依赖的AspectJ的注解,所以在使用AOP之前,我们需要引入相应的jar包:

  • com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
  • com.springsource.org.aopalliance-1.0.0.jar
  • spring-aspects-4.0.0.RELEASE.jar
  • spring-aop-4.0.0.RELEASE.jar

before增强通知处理是在目标方法执行之前,进行额外加强。例如:

//定义一个普通bean实例
@Component("runner")
public class Runner {
    private String name ="single";
    //省略setter方法
    public void run(){
        System.out.println("my name is:"+ this.name+" and i am running");
    }
}
@Aspect
@Component
public class MyAspect {

    @Before("execution(* Test_SpringAop.*.*(..))")
    public void prepareForRun(){
        System.out.println("before running, you need to do some excercises!");
    }
}

创建一个普通的Java类,并通过@Aspect将其申明为一个切面,@Component将其注册到容器中。

//在容器中增加对aspectj注解的支持
<aop:aspectj-autoproxy/>

于是,我们向容器索取runner实例,并调用它的run方法,结果如下:

这里写图片描述

显然,我们从容器中获取的runner的bean实例已经不再是普通的bean了,而是它的一个代理对象。

再回顾下整个过程,
当容器加载的时候,扫描所有配置容器中的bean,一旦发现有被注解@Aspect修饰的,则注册它为一个切面,扫描其中的方法或者叫通知,根据配置在这些方法之前的注解类型判断当前切面需要操作的目标对象集。例如,我们上述使用@Before注解指定该切面对Test_SpringAop包下的所有方法进行加强处理,怎么处理呢?Before指定在该调用之前,调用当前的通知方法(prepareForRun),其实也就是生成一个动态代理的过程。于是容器中的bean都是被加强后代理实例。

整个过程对目标对象中的方法未做一点污染,神不知,鬼不觉的增强了指定实例的指定方法。这就是AOP的核心思想,也是它的魅力所在。

这里写图片描述

@Before类型的通知类比于我们的动态代理,InvocationHandler处理器中invoke方法中,method.invoke是调用目标的原方法,而@Before则指明在调用该目标方法之前,插入我们指定的方法。

2、after增强通知处理
和before类似,after是后置处理,它指定在目标方法调用之后插入我们指定的方法,由于与before极其类似,此处不再赘述。

3、afterReturning增强通知处理
afterReturning相较于after来说,它更倾向于处理有返回值的目标方法,就是说通过使用afterReturning,我们可以获取到刚刚执行结束的方法的返回值。例如:

//修改runner的run方法,让他返回name
public String run(){
    System.out.println("my name is:"+ this.name+" and i am running");
    return this.name;
}
//为切面类增加两个通知增强器
@Before("execution(* Test_SpringAop.*.*(..))")
public void prepareForRun(){
    System.out.println("before running, you need to do some excercises!");
}
    
@AfterReturning(returning = "result",value = "execution(* Test_SpringAop.*.*(..))")
public void afterForRun(Object result){
    System.out.println("runner : "+ result + " is running");
}

先看结果:

这里写图片描述

AfterReturning指定的afterForRun(Object result)方法将在目标方法调用之后被调用,注解中的returning 属性的值指向目标方法的返回值,于是我们可以在afterForRun方法中获取原方法的返回值。

4、afterThrowing增强通知处理
afterThrowing主要用于处理目标方法中未被处理的异常。例如:

//目标方法中有一个未检查异常
public void doMath(){
    int result = 12/0;
}
//切面中定义异常处理通知
@AfterThrowing(throwing = "ex",value = "execution(* Test_SpringAop.*.*(..))")
public void catchEx(Throwable ex){
    System.out.println("目标方法出现异常:" + ex.getMessage());
}

当我们外部调用doMath方法的时候,该未处理的异常信息将会被捕获并输出。

这里写图片描述

显然,这种增强器虽然能捕获到该异常,但是并不能对它做任何处理,程序依然抛出异常信息。它和try catch不同,被catch住的异常如果不手动抛出的话,程序是可以正常结束的。

5、around增强通知处理
Around类型的增强处理功能比较全,近乎是Before和AfterReturning两者合起来的功能,但是它也有额外的增强处理,它可以控制目标方法是否被执行,选择给目标方法传入什么形参等等。权限比较大,但是要求线程安全,所以一般建议尽量减少使用around的次数。看个例子:

@Around("execution(* Test_SpringAop.*.*(..))")
public void useAround(ProceedingJoinPoint point) throws Throwable {
    System.out.println("i meet single");
    point.proceed(new Object[]{"single"});
    System.out.println("we leave ....");
}

被@Around修饰增强处理方法中,必须传入一个ProceedingJoinPoint 类型的参数,该参数代表了目标方法,调用它的proceed方法可以控制执行该目标方法,并传入参数。

所以说,Around其实模拟的是我们整个动态代理的过程,在这里我们可以选择调用proceed方法与否来控制是否调用目标方法,也可以选择传入参数与否来控制是否覆盖原方法的参数值。

ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
Runner runner = (Runner)context.getBean("runner");
runner.sayHello("walker");

我们在main函数中调用容器为我们生成的代理对象,调用他的sayhello方法并传入参数Walker,但是实际输出结果如下:

这里写图片描述

实际上整个目标方法的调用与否,参数修改与否就完全交到切面中完成了,这样的程序灵活性很高。

以上简单介绍了增强处理的几种不同类型,除此之外,Spring还允许我们在每次增强方法调用时获取到连接点的信息。通过向方法传入JoinPoint类型的形参即可对目标方法的相关信息进行获取。该类型中有以下几个方法:

  • Object[] getArgs();:获取目标方法的所有参数
  • Signature getSignature();:获取目标方法的签名
  • Object getTarget();:获取目标对象
@Before("execution(* Test_SpringAop.*.*(..))")
public void prepareForRun(JoinPoint point){
    System.out.println(point.getSignature());
    Object[] args = point.getArgs();
    Object obj = point.getTarget();
}

在某些情况下,这些信息还是很有作用的。

最后我们了解下,切入点表达式,也就是上述代码中一直使用的:

"execution(* Test_SpringAop.*.*(..))"

这个叫做切入点表达式,用于定位具体的切入点。完整的AspectJ中具有大量的切入点指示符,但是Spring AOP只支持其中的部分。我们主要看其中最重要也是最常用的:execution。该指示符使用时最标准的格式为:

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

其中:

  • modifiers-pattern:指定方法的修饰符,可以省略
  • ret-type-pattern:指定方法的返回值类型,"*"表示匹配所有的返回值类型
  • declaring-type-pattern:指定方法所属的类,可以省略
  • name-pattern:方法名称,"*"表示匹配所有的方法
  • param-pattern:该方法的所有形参列表,"*"表示一个任意类型的参数,".."表示零个或者多个任意类型的参数。
  • throws-pattern:指定方法声明的异常类型

接下来我们解析下我们上述一直在使用的切入点表达式:

"execution(* Test_SpringAop.*.*(..))"
* 匹配任意的返回值类型
Test_SpringAop.* 匹配Test_SpringAop包下的任意类
Test_SpringAop.*.*(..) 匹配了Test_SpringAop包下的任意类的任意方法,并且该方法具有零个或多个任意类型的参数

当然,我们也可以具体到某个方法:

@Before("execution(public void Test_SpringAop.Runner.run(String,int))")

至此,有关Spring中AOP的基本知识已经介绍完了,使用XML配置Spring AOP的方法也是类似的,此处不再赘述。总结不到之处,望指出!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,724评论 6 342
  • 什么是Spring Spring是一个开源的Java EE开发框架。Spring框架的核心功能可以应用在任何Jav...
    jemmm阅读 16,438评论 1 133
  • 01 “小丁,给我一个文案,钻展的。”设计同事在工作QQ上说。 “哦,好的,稍等。”小丁迅速开动脑筋思索起来。 “...
    安悦微阅读 1,348评论 9 37
  • 最近南宁失恋小哥的一段视频火了! 翻译:蓝瘦(难受),香菇(想哭)。本来今天高高兴兴,你为什么要说这种话?难受,想...
    心理师静怡阅读 330评论 2 4