美文网首页Java服务器端编程程序员@IT·互联网
Spring框架学习之注解配置与AOP思想

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

作者: Single_YAM | 来源:发表于2017-11-08 20:42 被阅读107次

         上篇我们介绍了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的方法也是类似的,此处不再赘述。总结不到之处,望指出!

    相关文章

      网友评论

        本文标题:Spring框架学习之注解配置与AOP思想

        本文链接:https://www.haomeiwen.com/subject/zatumxtx.html