Spring实战--Bean的装配&切面

一、装配Bean

1.1 Spring配置的可选方案

Spring主要提供三种主要的装配机制:

  • 在XML中进行显式配置
  • 在java中进行显式配置
  • 隐式的bean发现机制和自动装配

1.2 自动化装配Bean

Spring从两个角度来实现自动化装配:

  • 组件扫描:Spring会自动发现应用上下文中所创建的bean
  • 自动装配:Spring会自动满足bean之间的依赖

这两种方法组合在一起就能发挥出强大的威力,他们可以将显示配置降低到最少

1.2.1 创建可被发现的bean

如下实例:

CompactDisc接口

package com.cf.springinaction;

public interface CompactDisc {

    void play();

}

SgtPeppers实现类

package com.cf.springinaction;

import org.springframework.stereotype.Component;

/**
 * component注解表明这个类会作为组件类,并告知Spring要为这个类创建bean
 * 使用此注解可以不用显式配置SgtPeppersBean
 */
@Component
public class SgtPeppers implements CompactDisc {

    private String title = "Sgt, Pepper's Lonely Hearts Club Sand";
    private String artist = "The Beatles";

    public void play() {
        System.out.println("Playing " + title + " by" + artist);
    }
}

配置类

package com.cf.springinaction;


import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * ComponentScan注解启动组件扫描,扫描与配置类相同的包以及下层的子包
 *
 * 如果使用xml来其中组件扫描则为
 * Spring实战--Bean的装配&切面_第1张图片
1.2.2 为组件扫描的bean命名

Spring应用上下文中所有的bean都会给定一个ID。上例,尽管没有明确地为SgtPeppers Bean设置ID,但Spring会根据类名为其指定一个ID。ID名为sgtPeppers,将类名的第一个字母变为小写

如果想为这个bean设置为不同的ID,就要将期望的ID作为值传递给@Component注解。

@Component("lonelyHeartsClub")

1.2.3 设置组件扫描的基础包

@ComponentScan无任何属性设置,则按默认规格,他会以配置类所在的包作为基础包(base package)来扫描组件。

当把配置类放在单独的包中,使其与其他的应用代码区分开来。那默认的基础包就不能满足要求了。需要在@ComponentScan的value属性中指明包的名称

@ComponentScan("包名")

或清晰的使用basePackages属性设置

@ComponentScan(basePackages="包名")

也可以设置多个包,将basePackages属性设置为要扫描包的一个数组即可:

@ComponentScan(basePackages={"包名1","包名1"})

还可以将其指定为包中所含的类或接口,这些类所在的包将会作为组件扫描的基础包

@ComponentScan(basePackageClasses = {CompactDisc.class,SgtPeppers.class})

可以考虑在包中创建一个用来进行扫描的空标记接口(marker interface)。这样,依然能够保持对重构友好的接口引用,但是可以避免引用任何实际的应用程序代码

1.2.4 注解实现自动装配

自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean需求的其他bean。

package com.cf.springinaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CDPlayer implements MediaPlayer {
  private CompactDisc cd;

  //在构造器上添加Autowired注解
  //表明Spring创建CDPlayer Bean时,会通过这个构造器来进行实例化并且会传入一个可设置CompactDisc类型的bean
  @Autowired
  public CDPlayer(CompactDisc cd) {
    this.cd = cd;
  }

  public void play() {
    cd.play();
  }
}

@Autowired注解可以用在类的任何方法上,Spring都会尝试满足方法参数上所声明的依赖。假如有且只有一个bean匹配依赖需求的话,那么这个bean将会被装配进来。

如果没有匹配的bean,那么在应用上下文创建的时候,Spring会抛出一个异常。

如果有多个bean都满足的化,Spring将会抛出一个异常,表明没有明确指定要选择哪个bean进行装配

1.3 通过Java代码装配bean

通过组件扫描和自动装配实现Spring的自动化配置是更为推荐的方式,但有时候自动化配置的方案行不通,因此需要明确配置Spring。

比如,将第三方库中的组件装配到应用中,这种情况下是没有办法在它的类上添加@Component和@Autowired注解的,因此不能使用自动化装配的方案。

在这种情况下,必须采用显示装配。有两种方案:Java和XML

1.3.1 创建配置类

通过JavaConfig显示装配Spring,其关键在于@Configuration注解,@Configuration表明这个类是一个配置类

在此移除CDPlayerConfig的@ComponentScan注解,这是bean就不会自动注入,因为组件扫描不会发现他们

1.3.2 声明简单的bean

给创建所需类型的实例添加@Bean注解,如下

@Configuration
public class CDPlayerConfig{

    @Bean
    public CompactDisc sgtPeppers(){
        return new SgtPeppers();
    }
}

@Bean注解会告诉Spring这个方法将会返回一个对象,该对象要注册为Spring应用上下文中的bean。方法体中包含了最终产生bean实例的逻辑

1.3.3 接触JavaConfig实现注入

需要声明有其他依赖的bean装配方法,如下:

@Bean
public CDPlayer cdPlayer(){
    return new CDPlayer(sgtPeppers());
}

默认情况下,Spring中的bean都是单例的,所以Spring会拦截对sgtPeppers()的调用并确保返回的是Spring所创建的bean。两个CDPlayer bean会得到相同的SgtPeppers实例

@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc){
    return new CDPlayer(compactDisc);
}

通过构造方法引入

1.4 通过XML装配bean

略。。。没什么好说的

二、高级装配

2.1 环境和profile

数据库配置、加密算法以及外部系统的集成是跨环境部署时会发生变化的几个典型例子。

2.1.1 配置profile bean

使用注解配置

@Profile("dev")

注解应用在类上。表示这个配置类中的bean只有在dev profile激活时才会创建。如果dev profile没有激活的化,那么带有@Bean注解的方法都会被忽略

@Profile("prod")

适用于生产环境配置,只要prod profile激活的时候,才会创建对应的bean

从spring3.2开始,也可以在方法级别上使用@Profile注解,与@Bean注解一同使用。

在XML中配置profile

使用

来配置

2.1.2 激活profile

激活profile需要依赖两个独立的属性:
Spring.profiles.active和spring.profiles.default

Spring.profiles.active:确定哪个profile是激活的
spring.profiles.default:如果没有设置spring.profiles.active属性的话,此值就会用来确定哪个profile是激活的

如果两个的值军没有设置,就没有激活的profile,只会创建那些没有定义在profile中的bean

使用profile进行测试

使用@ActiveProfile(“dev”)注解

2.2 条件化bean

当一个bean或多个只有在应用的类路径下包含特定的库时才创建。或者我们希望某个bean只有当另外某个特定的bean也声明了之后才会创建。也可能在要求只有某个特定的环境变量设置之后,才会创建某个bean

可以使用@Conditional注解,可以用在@Bean注解的方法上。如果给定的条件计算为true,就会创建这个bean

如下条件

package com.cf.springconditionaldemo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MagicConfig {


    /**
     * MagicExistsCondition.class条件为真时创建MagicBean
     * @return
     */
    @Bean
    @Conditional(MagicExistsCondition.class)
    public MagicBean magicBean(){
        return new MagicBean();
    }

}

条件类

package com.cf.springconditionaldemo;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class MagicExistsCondition implements Condition {

    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        Environment env = conditionContext.getEnvironment();
        //检查是否存在magic环境属性
        return env.containsProperty("magic");
    }
}

当属性存在即满足条件,matches()方法就会返回true,则@Conditional注解上引用MagicExistsCondition的bean就会被创建

2.3 自动处理装配的歧义性

自动装配可以减少装配应用组件时所需要的显式装配的数量,但是仅有一个bean匹配所需的结果,自动装配才是有效的。如果不仅有一个bean能匹配结果的话,这种歧义性会阻碍Spring自动装配属性、构造器参数或方法参数

Dessert接口

package com.cf.dessert;

public interface Dessert {

    void display();

}

接口实现类

package com.cf.dessert;



 public class Order {
    
    
        public static void main(String[] args) {
    
        }
    
    }

package com.cf.dessert;

import org.springframework.stereotype.Component;

@Component
public class Cake implements Dessert {
    public void display() {
        System.out.println("this is a cake");
    }
}

package com.cf.dessert;

import org.springframework.stereotype.Component;

@Component
public class Cookies implements Dessert {
    public void display() {
        System.out.println("this is cookies");
    }
}

订单配置类

 package com.cf.dessert;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackageClasses = Dessert.class)
public class Order {

    private Dessert dessert;

    public Order(Dessert dessert) {
            this.dessert = dessert;
    }

    public void showOrder(){
        dessert.display();
    }
}

此时已经抛出了异常

Spring实战--Bean的装配&切面_第2张图片
2.3.1 标示首选的bean

声明bean时,可以通过将其中一个可选的bean设置为首选bean来避免自动装配时的歧义性。

package com.cf.dessert;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Primary
@Component
public class IceCream implements Dessert {
    public void display() {
        System.out.println("Do you want IceCream?");
    }
}

在需要自动装配的bean上加上@Primary注解

Order.class正常

package com.cf.dessert;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Order {

    @Autowired
    private Dessert dessert;

    public Order(Dessert dessert) {
            this.dessert = dessert;
    }

    public void showOrder(){
        dessert.display();
    }
}

测试类

package com.cf.dessert;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Config.class)
public class OrderShow {

    @Autowired
    private Order order;

    @Test
    public void test(){
        order.showOrder();
    }

}

成功自动注入
Spring实战--Bean的装配&切面_第3张图片
2.3.2限制自动装配的bean

使用@Primary注解将可选方案限定到唯一一个无歧义的选项中。但只能标记一个优选的可选方案。首选bean超过一个时,没有其他的方法进一步缩小可选范围。

使用@Qualifier注解是使用限定符的主要方式,在注入时指定想要注入进去的是哪个bean

package com.cf.dessert;

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

@Component
public class Order {

    @Autowired
    @Qualifier(value = "iceCream")
    private Dessert dessert;

    public void setDessert(Dessert dessert){
        this.dessert = dessert;
    }

    public void showOrder(){
        dessert.display();
    }
}

测试依然OK。。。

但这样的问题就在于setDessert()方法上所指定的限定符与要注入的bean名称是紧耦合的。对类名称的任意改动都会导致限定符失效

所以可以为bean设置自己的限定符,而不是依赖bean ID做为限定符。

Spring实战--Bean的装配&切面_第4张图片
Spring实战--Bean的装配&切面_第5张图片
测试OK。。。。

如果想使用多个限定符,因为java不允许在同一条目上重复出现相同类型的多个注解。可以使用创建自定义的限定符注解

cold注解

package com.cf.dessert;


import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = {ElementType.CONSTRUCTOR, ElementType.TYPE, ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {
}

Creamy注解

package com.cf.dessert;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = {ElementType.CONSTRUCTOR, ElementType.TYPE, ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy {
}

Spring实战--Bean的装配&切面_第6张图片
测试OK。。。
Spring实战--Bean的装配&切面_第7张图片
2.4 bean的作用域

默认情况下,Spring应用上下文中所有bean都是作为单例形式创建的

Spring定义了多种作用域:

  • 单例(Singleton):在整个应用中,只创建bean的一个实例
  • 原型(Prototype) : 每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例
  • 会话(Session):在web应用中,为每个会话创建一个bean实例
  • 请求(request):在web应用中,为每个请求创建一个bean实例

对于易变的类型,单例并不合适。可以使用@Scope注解来选择其他的作用域

2.4.1 使用会话和请求作用域

实例化在会话范围和请求范围共享bean,如电子商务应用中实现购物车

2.5 运行时注入

Spring提供了两种在运行时求值的方式:

  • 属性占位符
  • Spring表达式语言(SpEL)

2.5.1 注入外部的值

最简单的方式是声明属性源并通过Spring的Environment来检索属性

有一部分先略

三、面向切面的Spring

3.1 什么是面向切面编程

切面可以帮助我们模块化横切关注点。横切点可以被描述为影响应用多处的功能。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。
这样做有两个好处:

  1. 每个关注点都集中在一个地方,而不是分散到多处代码中
  2. 服务模块更简洁,因为他们只包含主要关注点(核心功能)的代码,次要关注点的代码被转移到切面中

3.1.1 定义AOP术语
Spring实战--Bean的装配&切面_第8张图片
常用术语有通知(advice)、切点(pointcut)、连接点(join point)

通知(Advice): 切面的工作被称为通知。通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。

通知的五种类型:

  • 前置通知(Before):在目标方法调用之前调用通知功能
  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
  • 返回通知(After-returning):在目标方法成功执行之后调用通知
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知
  • 环绕通知(Around):通知包裹了被通知的方法,在被痛殴之的方法调用之前和调用之后执行自定义的行为

连接点(Join point):在应用执行过程中能够插入切面的一个点,这个点可是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程中,并添加新的行为

切点(Poincut):切点相当于定义了何处,切点的定义会匹配通知所要织入的一个或多个连接点。

切面(Aspect):切面就通知和切点的节后。通知和切点共同定义了切面的全部内容–它是什么,在何时和何处完成功能

引入(Introduction):引入允许我们向现有的类添加新方法或属性,从而可以在无需修改这些现有类的情况下,让它们具有新的行为和状态

织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:

  • 编译期:切面在目标类编译时就被织入。这种方式需要特殊的编译期。AspectJ的织入编译期就是以这种方式织入切面的
  • 类加载器:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器,他可以在目标类被引入应用之前增强该目标类的字节码
  • 运行期:切面在应用运行的某个时刻被织入。AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的

3.1.2 Spring对AOP的支持

Spring提供了4种类型的AOP支持:

  • 基于代理的经典SpringAOP
  • 纯POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面(适用于Spring各版本)

前三种都是SpringAOP实现的辩题,Spring AOP构建在动态代理基础上,因此,Spring对AOP的支持局限于方法拦截。

如果AOP需求超过了简单的方法调用(如构造器或属性拦截),那么需要考虑使用AspectJ来实现切面。即第4中类型

3.2 通过切点来选择连接点

切点用于准确定位应该在什么地方应用切面的通知。通知和切点是切面的最基本元素。

SpringAOP中,要使用AspectJ的切点表达式语言来定义切点。

3.2.1 编写切点

使用execution()指示器

Spring实战--Bean的装配&切面_第9张图片
3.2.2 在切点中选择bean
在这里插入图片描述
限定bean的ID为woodstock

3.3 使用注解创建切面

AspectJ面向注解的模型可以非常简编地通过少量注解把任意类转变为切面

3.3.1 定义切面

package com.cf.aopdemo;

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

@Aspect
public class Audience {

    @Pointcut("execution(* com.cf.aopdemo.Performance.perform(..))")
    public void performance(){}

    //表演之前
    @Before("performance()")
    public void sileneCellPhones(){
        System.out.println("Silencing cell phone");
    }

    //表演之前
    @Before("performance()")
    public void takeSets(){
        System.out.println("Taking sets");
    }

    //表演之后
    @AfterReturning("performance()")
    public void applause(){
        System.out.println("CLAP CLAP CLAP!!!");
    }

    //表演失败之后
    @AfterReturning("performance()")
    public void demandRefund(){
        System.out.println("Demanding a refund");
    }

}

使用@Pointcut注解在一个@AspectJ切面内定义可重用的切点。
然后就可以在切点表达式中使用performance()了。否则每个地方都会使用那个更长的表达式。

接口实现类

package com.cf.aopdemo;

import org.springframework.stereotype.Component;

@Component
public class PerformanceStart implements Performance {
    public void perform() {
        System.out.println("表演开始");
    }
}

配置类

package com.cf.aopdemo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
//启动AspectJ自动代理
public class ConcertConfig {

    //声明audience bean
    @Bean
    public Audience audience(){
        return new Audience();
    }
}

测试类:

package com.cf.aopdemo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ConcertConfig.class)
public class PerformanceTest {

    @Autowired
    private Audience audience;

    @Autowired
    private Performance performance;

    @Test
    public void testAOP(){
        performance.perform();
    }
}

测试结果:
Spring实战--Bean的装配&切面_第10张图片
3.3.2 环绕通知

相当于一个通知方法中同时编写前置通知和后置通知

package com.cf.aopdemo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
public class Audience {

    @Pointcut("execution(* com.cf.aopdemo.Performance.perform(..))")
    public void performance(){}

    //环绕通知,在动作前后开始
    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint){
        System.out.println("Silencing cell phone");
        System.out.println("Taking seats");
        try{
            joinPoint.proceed();
            System.out.println("CLAP CLAP CLAP ....");
        }catch (Throwable e){
            System.out.println("Demanding a refund");
        }
    }
}

结果:

Spring实战--Bean的装配&切面_第11张图片
环绕通知必须接受ProceedingJoinPoint作为参数,因为需要在通知中通过他来调用被通知的方法。通知方法中可以做任何的事情,当需要把控制权交给被通知的方法时,需要调用ProccedingJoinPoint的proceed()方法

如果不调用proceed()方法,通知实际上会阻塞对被通知方法的调用

3.3.3 处理通知中的参数
实例如下:

后面再补吧,。。。

3.4 在XML中声明切面

略。。。xml没什么好说的

3.5 注入AspectJ切面

SpringAOP能够满足很多应用的切面需求,但是与AspectJ相比,SpringAOP是一个功能比较弱的AOP解决方案。AspectJ提供了SpringAOP所不能支持的许多类型的切点

暂时这样吧。。。后面详细完善下切面的例子。。

参考Spring实战

你可能感兴趣的:(JAVA框架,Spring,IoC,AOP)