一、Spring的由来和简介

1.1、Spring的使命:简化Java开发

几个概念:
  • POJO:Plain Old Java Object,普通的Java对象。指只有属性、get、set等方法,不包含复杂逻辑的Java类。
  • JavaBean:一种可重用组件,是指符合JavaBean规范的类。
  • JavaBean规范主要有以下几条(仅从网络信息摘取过来,有待商榷)
  • 类必须是具体的和公共的,并且具有无参数的构造器。
  • 提供符合一致性设计模式的公共方法将内部域暴露成员属性,set和get方法获取
  • 属性名称符合这种模式,其他Java 类可以通过自省机制(反射机制)发现和操作这些JavaBean 的属性。
  • EJB:Enterprice JavaBean,是指符合Java企业级应用规范的类
  • 为了快速开发企业级应用,Java提出了EJB,但是太重量级,并不好用;Spring技术则提供了一种方案,仅基于POJO又能实现EJB或其它企业级规范JAVA对象才有的功能,使企业级开发变得更加轻量级,简单;另外Java也看到了Spring技术的优势和好处,也使用Spring技术的原理,改进了原来的EJB,使其更加简单好用;目前为止,Spring技术还是比EJB技术更加好用。
  • 另外,Spring还在不断进步中,移动开发、社交API集成、NoSQL数据库,云计算以及大数据等领域,Spring都一直创新,用更加高效简单的方式来提供这些领域的解决方案
  • Spring中把应用组件(类)也称为Bean或者JavaBean,但是Spring指的是任何POJO,所以文章中所有Bean或JavaBean不一定符合JavaBean规范,可能只是一个POJO
Spring的几个关键策略

为了简化Java开发,Spring使用了以下几个关键策略

  • 1、基于POJO的轻量级和最小侵入编程
  • 通常一个类应用Spring技术,不需要引用Spring;即使是最坏的情况,也只需要用到Spring注解,不会有Spring的代码出现在一个类中。从而保证应用中的类仍是POJO
  • 2、基于依赖注入和面向接口实现松耦合
  • DI:Dependency Injection 依赖注入

  在传统编程中,难免会有以下这种编程情景(多个类交互)



public class DamselRescuingKnight implements Knight {
  private RescueDamselQuest quest;
  public DamselRescuingKnight() {
    this.quest = new RescueDamselQuest();
  }
  public void embarkOnQuest() {
    quest.embark();
  }
}



  DamselRescuingKnight中,通过new 创建了一个RescueDamselQuest实例,2个类形成了紧耦合。

  其中的依赖关系是DamselRescuingKnight依赖RescueDamselQuest,而且限制了embarkOnQuest方法的实现

  而且,无法对DamselRescuingKnight进行单元测试,因为embarkOnQuest方法需要调用RescueDamselQuest的embark方法,而仅在这个方法内,并没有这个方法的实现

  耦合的两面性:必须的(复杂逻辑必然有多个类进行交互)、过于耦合将导致难以测试、复用、难以理解

  依赖注入:将对象的依赖关系交给第三方来进行创建和管理



public class BraveKnight implements Knight {
  private Quest quest;
  public BraveKnight(Quest quest) {
    this.quest = quest;
  }
  public void embarkOnQuest() {
    quest.embark();
  }
}



  上面使用了依赖注入的一种方式:构造器注入;而且传入的是一个Quest接口,对象和依赖对象的具体实现没有耦合关系,就形成了松耦合。

  而且,此情况下,可以使用mock(一种测试方式,具体自行学习)实现单元测试。

  Spring可以作为一个依赖注入容器,通过不同方式注入,也有相应的配置,装配策略,留到后面讲解。

  • 3、基于切面和惯例进行声明式编程
  • AOP:Aspect Oriented Programming,面向切面编程

  DI负责让互相协作的组件实现松耦合,而AOP则允许把遍布程序各处的功能分离出来形成可重用组件。

  可以把切面想象成很多组件上的一个外壳,借助AOP,可以使用各种功能层包裹核心业务层,以声明的方式灵活应用到系统中,核心应用中与这些功能层没有耦合,保证了POJO的简单性

  例如,你的代码中可能会出现以下情景



public class BraveKnight implements Knight {
    private Quest quest;
    private Minstrel minstrel;
    public BraveKnight(Quest quest, Minstrel minstrel) {
        this.quest = quest;
        this.minstrel = minstrel;
    }
    public void embarkOnQuest() {
        minstrel.singBeforeQuest();
        quest.embark();
        minstrel.singAfterQuest();
    }
}



  minstrel类可以看作一个日志功能,在某个其它类中,需要调用这个日志类来记录日志,导致这个功能代码与业务代码混淆在一起

  而Spring提供的AOP方案,可以通过配置文件等方式,将这个日志类的相关代码从这个业务类中去除,从而实现解耦;具体方式后面介绍

  • 4、通过切面和模板减少样板式代码

  例如:以往使用JDBC进行操作数据库时,每次操作,都有很多连接数据库,断开连接等代码和业务代码交织在一齐

  而Spring则提供了如jdbcTemplate等类,对这些样板式代码进行简化

1.2、使用Spring管理Bean

Spring框架中对象是由Spring创建和管理的,本节讨论Spring如何管理这些Bean

Spring提供一个或多个Spring容器,Spring容器负责创建、装配、配置、管理对象的整个生命周期

spring源码学习 github项目_json

Spring有多个容器实现,可以分为2种类型:
  • bean工厂:是最简单的容器,提供基本的DI支持(由于bean工厂对于大部分程序来说太低级,所以只讨论应用上下文)
  • 应用上下文:基于BeanFactory构建,提供应用框架级别的服务
使用应用上下文创建Spring容器:

Spring自带多种应用上下文:

  • AnnotationConfigApplicationContext:从一个或多个Java配置类加载Spring应用上下文
  • AnnotationConfigWebApplicationContext:从一个或多个Java配置类加载Spring Web应用上下文
  • ClassPathXmlApplicationContext:从类路径下的一个或多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类资源
  • FileSystemXmlapplicationcontext:从文件系统下的一个或多个XML配置文件中加载上下文定义
  • XmlWebApplicationContext:从Web应用下的以恶搞或多个XML配置文件加载上下文定义

创建Spring容器例子:ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/knight.xml");

通过context.getBean()方法即可获取容器内的Bean

bean生命周期:

普通Java应用程序中,bean的生命周期很简单,就是通过new实例化,就可以使用了,当bean不再被使用,就会被自动回收。

而Spring中的bean的生命周期就很复杂,而且非常重要,因为有时候对这个生命周期的扩展点进行自定义

spring源码学习 github项目_Java_02

spring源码学习 github项目_Java_03

1.3、Spring框架包含的内容

由前面的内容知道,Spring框架致力于通过DI、AOP和消除模板式代码来简化企业级JAVA开发

而在整个Spring框架范畴内,Spring不仅提供了多种简化开发的方式,还构建了一个在核心框架上的庞大生态圈,将Spring扩展到不同领域,如Web服务、REST、移动开发,NoSQL等

Spring框架中的模块:

如Spring4.0中,Spring框架发布版本中包含20个不同模块,每个模块有3个JAR文件(二进制类库、源码、JavaDoc)

Spring发布版本中lib目录下的JAR文件

spring源码学习 github项目_json_04

spring源码学习 github项目_spring源码学习 github项目_05

Spring Portfolio:

Spring Portfolio包含构建于核心Spring框架之上的框架和类库,概括地说,Spring Portfolio为每一个领域的Java开发都提供了Spring编程模型

  • Spring Web Flow:为基于流程的会话式Web应用提供支持,如购物车或向导系统
  • Spring Web Service:提供契约优先的Web Service模型,服务的实现都是为了满足服务的契约而编写的
  • Spring Security:为Spring应用提供声明式安全机制
  • Spring Integration:提供多种通用应用集成模式的声明式风格实现
  • Spring Batch:对数据进行大量操作
  • Spring Data:无论是关系型数据库还是NoSQL、图形数据库等数据模式,Spring Data都为这些持久化提供了一种简单的编程模型
  • Spring Social:一个社交网络扩展模块
  • Spring Mobile:支持移动Web应用开发
  • Spring for Android:旨在通过Spring框架为开发基于Android设备的本地应用提供某些简单的支持
  • Spring Boot:依赖自动配置技术,消除大部分Spring配置,减小Spring工程构建文件大小

二、装配Bean

2.1、装配Bean的可行方式

Spring框架支持以下3种主要装配Bean的方式:
  • 1、XML中进行显式配置
  • 2、Java中进行显式配置
  • 3、隐式的bean发现机制和自动装配

最佳实践:建议是尽可能使用自动配置,减少显式配置;当需要显式配置,推荐使用类型安全的JavaConfig;最后再使用XML配置

2.2、自动化装配bean

Spring从3个步骤完成自动化装配:
  • 创建组件(@Component):在类上面进行标注,告知Spring这个类需要创建Bean
  • 组件扫描(@ComponentScan):为Spring指定组件扫描范围,告知Spring从哪些地方发现Bean
  • 自动装配(@Autowiring):在属性、set方法、构造函数上指定,告知Spring这个属性或方法的参数,需要Spring来装配对应的Bean
@Component:

标明这个类是一个组建类,告知Spring要为这个类创建bean

也可以用@Named注解(Java依赖规范/Java Dependency Injection 提供)代替,但一般不推荐


public interface CompactDisc {
  void play();
}


@Component
public class SgtPeppers implements CompactDisc {
  private String title = "Sgt. Pepper's Lonely Hearts Club Band";  
  private String artist = "The Beatles";
  public void play() {
    System.out.println("Playing " + title + " by " + artist);
  }
}


可以为bean命名id(默认自动生成id,是类名首字母小写)


@Component("myName")


@ComponentScan:

Spring组件扫描默认是关闭的,可以通过Java设置或者是XML设置来开启

会搜索配置类所在的包以及子包

Java配置形式:


@Configuration
@ComponentScan
public class CDPlayerConfig { 
}


XML配置形式:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:c="http://www.springframework.org/schema/c"
  xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

  <context:component-scan base-package="soundsystem" />

</beans>


指定组件扫描的基础包,可以通过类名,或者包中的类或接口:


@ComponentScan("soundsystem")
@ComponentScan(basePackages = "soundsystem")
@ComponentScan(basePackages = { "soundsystem", "video" })
@ComponentScan(basePackages = { CDPlayer.class, MediaPlayer.class })


推荐在包中创建标识接口,这样在重构系统的时候,不会对这个配置造成影响

@Autowired:

可以用在类中任何方法内(包括构造方法、setter)

这样,调用这个方法的时候,Spring就会去查找适合方法参数的bean并装配到方法中

如果找不到,或者有多个符合,则会报错

可以通过@Autowired(required=false)使找不到时不报错,那么传入的参数值为null,需要自己手动处理代码

可以使用@Inject(Java依赖规范/Java Dependency Injection 提供)代替,但一般不推荐


public class CDPlayer implements MediaPlayer {
  private CompactDisc cd;
  @Autowired
  public CDPlayer(CompactDisc cd) {
    this.cd = cd;
  }
  public void play() {
    cd.play();
  }
}


2.3、通过Java代码装配bean

例如,需要将第三方库的组件加载到你的应用中,此时无法给他的类上添加@Component和@Autowired注解,此时不能使用自动化装配了。

这种情况下,就必须使用显式装配的形式,可以选择Java代码装配或Xml装配

建议:显式配置是优先使用JavaConfig装配,因为他强大、类型安全且对重构友好;因为他和业务代码无关,应放到单独的包中

@Configuration:

告诉Spring,这是一个Spring配置类,用来配置Spring应用上下文如何配置bean

@Bean:

创建一个方法,用来产生类的实例,并告诉Spring,这个实例要注册为Spring应用上下文中的bean


@Configuration
public class CDPlayerConfig {
    @Bean
    public CompactDisc sgtPeppers() {
        return new SgtPeppers();
    }
    @Bean
    public CDPlayer cdPlayer1() {
        return new CDPlayer(sgtPeppers());
    }
    @Bean
    public CDPlayer cdPlayer2(CompactDisc compactDisc) {
        return new CDPlayer(compactDisc);
    }
}


  • 首先,使用new SgtPeppers()的实例,为CompactDisc类型创建一个bean
  • cdPlayer1中,通过直接调用SgtPeppers()方法获取实例,看似每次调用cdPlayer1,都会产生一个新的CompactDisc实例,但实际上,因为SgtPeppers()方法添加了@bean备注,Spring会拦截其调用,如果已创建bean则直接返回bean,不会重新创建。
  • cdPlayer2中,给这个方法添加了@Bean注解,Spring会在调用的时候为参数找到对应类型的实例,自动注入
  • 其中cdPlayer2的方式更符合实际代码的运行,建议使用这种方式,方便理解。

2.4、通过XML装配bean

Spring刚出现时,用XML描述配置是主要方式;现在有了强大的自动化配置和Java配置,XML配置不再是首选;但是以往的项目仍然存在大量的XML配置,所以有必要掌握这种方式。

创建XML配置规范:

Java配置需要@Configuration注解,而XML配置的配置规范如下


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context">

    <!-- 配置内容 -->

</beans>


<bean>:

用来声明一个简单的bean

spring检测到这个设置,会调用该类的默认构造器来创建实例,并注册为bean

使用字符串作为类名设置,会出现写错等问题;可以使用有Spring感知的IDE来确保XML配置的正确性,如Spring Tool Suite


<bean class="soundsystem.BlankDisc" />
<bean id="compactDisc" class="soundsystem.BlankDisc" />


如果不设置id,会自动给予id:class+#0,#1,#2...

为了减小XML文件的繁琐性,建议只对需要按名字引用的bean添加id属性

通过构造器注入初始化bean:

2种方式:使用<constructor-arg>元素、使用Spring3.0引入的c-命名空间

区别:<constructor-arg>元素冗长繁琐;但有些事情<constructor-arg>元素能做到,c-命名空间做不到

使用<constructor-arg>元素:

引用bean注入:


<bean id="compactDisc" class="soundsystem.SgtPeppers" />
      
<bean id="cdPlayer" class="soundsystem.CDPlayer">
  <constructor-arg ref="compactDisc" />
</bean>


  • 第一个bean:使用soundsystem.SgtPeppers的默认构造器创建实例作为bean,id为compactDisc
  • 第二个bean:把id为compactDisc的bean,传递到soundsystem.CDPlayer的带参数构造器中,使用这个bean作为参数,创建实例后作为bean,id为cdPlayer

用字面量注入:


<bean id="compactDisc" class="soundsystem.BlankDisc">
    <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
    <constructor-arg value="The Beatles" />
</bean>


装配集合:

传入空值:


<bean id="compactDisc" class="soundsystem.collections.BlankDisc">
    <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
    <constructor-arg value="The Beatles" />
    <constructor-arg><null/></constructor-arg>
</bean>


使用List传入字面量:


<bean id="compactDisc" class="soundsystem.collections.BlankDisc">
    <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
    <constructor-arg value="The Beatles" />
    <constructor-arg>
        <list>
            <value>Sgt. Pepper's Lonely Hearts Club Band</value>
            <value>With a Little Help from My Friends</value>
        </list>
    </constructor-arg>
</bean>


使用List传入引用bean:


<bean id="compactDisc" class="soundsystem.collections.BlankDisc">
    <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
    <constructor-arg value="The Beatles" />
    <constructor-arg>
        <list>
            <ref bean="cd1"/>
            <ref bean="cd2"/>
        </list>
    </constructor-arg>
</bean>


List可以替换成Set,区别是所创建的是List还是Set

使用c-命名空间:

声明c-模式:

首先,使用这个命名控件要在beans标签增加声明此模式:xmlns:c="http://www.springframework.org/schema/c"


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:c="http://www.springframework.org/schema/c"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context">

    <!-- 配置内容 -->

</beans>


引用bean注入:


<bean id="compactDisc" class="soundsystem.SgtPeppers" />

<bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />


spring源码学习 github项目_测试_06

其中,cd是指对应构造方法的参数名,按照这种写法,如果修改了构造方法,可能就会出错

替代方案:


<bean id="cdPlayer" class="soundsystem.CDPlayer" c:_0-ref="compactDisc" />

<bean id="cdPlayer" class="soundsystem.CDPlayer" c:_-ref="compactDisc" />


_0,_1:使用参数顺序来注入

_:如果只有一个参数,可以不用标示参数,用下划线代替

用字面量注入:


<bean id="compactDisc" class="soundsystem.BlankDisc" c:_0="Sgt. Pepper's Lonely Hearts Club Band" c:_1="The Beatles" />


装配集合:

c-的方式无法实现装配集合

使用属性注入

怎么选择构造器注入和属性注入?建议对强依赖使用构造器注入,对可选依赖使用属性注入。

假设有一个类CDPlayer:


public class CDPlayer implements MediaPlayer {
    private CompactDisc compactDisc;
    @Autowired
    public void setCompactDisc(CompactDisc compactDisc) {
        this.compactDisc = compactDisc;
    }
    public void play() {
        compactDisc.play();
    }
}


使用<property>元素:

引用bean注入:


<bean id="cdPlayer" class="soundsystem.properties.CDPlayer">
    <property name="compactDisc" ref="compactDisc" />
</bean>


代表通过setCompactDisc方法把id为compactDisc的bean注入到compactDisc属性中

用字面量注入以及装配集合:


<bean id="compactDisc" class="soundsystem.properties.BlankDisc">
    <property name="title" value="Sgt. Pepper's Lonely Hearts Club Band" />
    <property name="artist" value="The Beatles" />
    <property name="tracks">
        <list>
            <value>Sgt. Pepper's Lonely Hearts Club Band</value>
            <value>With a Little Help from My Friends</value>
        </list>
    </property>
</bean>


使用p-命名空间

声明p-模式:

首先,使用这个命名控件要在beans标签增加声明此模式:xmlns:p="http://www.springframework.org/schema/p"


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context">

    <!-- 配置内容 -->

</beans>


然后使用p-命名空间来装配属性

引用bean注入:


<bean id="cdPlayer" class="soundsystem.properties.CDPlayer" p:compactDisc-ref="compactDisc" />


spring源码学习 github项目_Java_07

用字面量注入以及装配集合:


<bean id="compactDisc" class="soundsystem.properties.BlankDisc" p:title="Sgt. Pepper's Lonely Hearts Club Band" p:artist="The Beatles">
    <property name="tracks">
        <list>
            <value>Sgt. Pepper's Lonely Hearts Club Band</value>
            <value>With a Little Help from My Friends</value>
        </list>
    </property>
</bean>


使用util-命名空间进行简化:

声明util-模式:

首先,使用这个命名控件要在beans标签增加声明此模式:xmlns:util="http://www.springframework.org/schema/util"和2个http


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/util 
    http://www.springframework.org/schema/util/spring-util.xsd
    http://www.springframework.org/schema/context">

    <!-- 配置内容 -->

</beans>


使用util:list简化:


<bean id="compactDisc" class="soundsystem.properties.BlankDisc"
    p:title="Sgt. Pepper's Lonely Hearts Club Band" 
    p:artist="The Beatles"
    p:tracks-ref="trackList" />

<util:list id="trackList">
    <value>Sgt. Pepper's Lonely Hearts Club Band</value>
    <value>With a Little Help from My Friends</value>
</util:list>

<bean id="cdPlayer" class="soundsystem.properties.CDPlayer"
    p:compactDisc-ref="compactDisc" />


util-命名控件的全部元素:

spring源码学习 github项目_测试_08

2.5、导入和混合配置

1、拆分JavaConfig:

其中一个JavaConfig:


@Configuration
public class CDConfig {
    @Bean
    public CompactDisc compactDisc() {
        return new SgtPeppers();
    }
}


使用@Import注解引入另外一个JavaConfig:


@Configuration
@Import(CDConfig.class)
public class CDPlayerConfig {
    @Bean
    public CDPlayer cdPlayer(CompactDisc compactDisc) {
        return new CDPlayer(compactDisc);
    }
}


或使用一个更高级别的JavaConfig引用2个JavaConfig


@Configuration
@Import({ CDPlayerConfig.class, CDConfig.class })
public class SoundSystemConfig {
}


2、JavaConfig中引用XML配置

1中把CDPlayer和CompactDisc分开了,假设出于某些原因,需要把CompactDisc用XML来配置


<bean id="compactDisc" class="soundsystem.BlankDisc"
    c:_0="Sgt. Pepper's Lonely Hearts Club Band" c:_1="The Beatles">
    <constructor-arg>
        <list>
            <value>Sgt. Pepper's Lonely Hearts Club Band</value>
            <value>With a Little Help from My Friends</value>
            <value>Lucy in the Sky with Diamonds</value>
            <value>Getting Better</value>
            <value>Fixing a Hole</value>
            <!-- ...other tracks omitted for brevity... -->
        </list>
    </constructor-arg>
</bean>


JavaConfig引用XML配置


@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath:cd-config.xml")
public class SoundSystemConfig {
}


这样,CDPlayer和BlankDisc都会作为bean被加载到Spring容器中;而CDPlayer添加了@Bean注解,所需参数CompactDisc也会把BlanDisc加载进来

3、拆分XML配置


<import resource="cd-config.xml" />
<bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />


4、XML配置中引用JavaConfig


<bean class="soundsystem.CDConfig" />
<bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />


推荐无论使用JavaConfig还是XML配置,都加入一个更高层次的配置文件,负责组合这些配置文件


<bean class="soundsystem.CDConfig" />
<import resource="cdplayer-config.xml" />


三、高级装配

3.1、环境与profile

程序运行有多个环境,比如开发环境、测试环境、生产环境等。

在每个环境中,很多东西有不同的做法,如使用的数据库等。

为了避免在切换运行环境是需要对程序进行大量修改,Spring提供了profile设置,使程序能对不同环境做出对应的处理。

使用profile,可以使一个部署单元(如war包)可以用于不同的环境,内部的bean会根据环境所需而创建。

配置profile bean:

Java配置profile bean:

使用@Profile注解指定bean属于哪个profile

针对类,表明这个类下所有的bean都在对应profile激活时才创建:


@Configuration
@Profile("dev")
public class DataSourceConfig {
  @Bean(destroyMethod = "shutdown")
  public DataSource embeddedDataSource() {
    return new EmbeddedDatabaseBuilder().build();
  }
}


针对方法,表明只有注解的方法才会根据profile来创建bean


@Configuration
public class DataSourceConfig {
  @Bean(destroyMethod = "shutdown")
  @Profile("dev")
  public DataSource embeddedDataSource() {
    return new EmbeddedDatabaseBuilder().build();
  }
  @Bean
  @Profile("prod")
  public DataSource jndiDataSource() {
    JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();return (DataSource) jndiObjectFactoryBean.getObject();
  }
}


 XML配置profile:

设置整个XML文件属于某个profile, 每一个环境创建一个XML文件,把这些XML文件都放在部署环境中,根据环境自动调用


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="
    http://www.springframework.org/schema/jee
    http://www.springframework.org/schema/jee/spring-jee.xsd
    http://www.springframework.org/schema/jdbc
    http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd"
    profile="dev">
    
    <jdbc:embedded-database id="dataSource" type="H2">
      <jdbc:script location="classpath:schema.sql" />
      <jdbc:script location="classpath:test-data.sql" />
    </jdbc:embedded-database>
    
</beans>


 也可以通过嵌套<beans>元素,在同一个XML文件下创建不同profile bean


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="
    http://www.springframework.org/schema/jee
    http://www.springframework.org/schema/jee/spring-jee.xsd
    http://www.springframework.org/schema/jdbc
    http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

  <beans profile="dev">
    <jdbc:embedded-database id="dataSource" type="H2">
      <jdbc:script location="classpath:schema.sql" />
      <jdbc:script location="classpath:test-data.sql" />
    </jdbc:embedded-database>
  </beans>
  
  <beans profile="prod">
    <jee:jndi-lookup id="dataSource"
      lazy-init="true"
      jndi-name="jdbc/myDatabase"
      resource-ref="true"
      proxy-interface="javax.sql.DataSource" />
  </beans>
</beans>


激活profile:

bean根据profile的创建步骤:

  • 1、没有指定profile的bean任何时候都会创建
  • 2、根据spring.profiles.active属性,找到当前激活的profile
  • 3、如果active属性没有设置,则找spring.profiles.default属性,找到默认的profile
  • 4、如果2个值都没有设置,则不会创建任何定义在profile里面的bean

spring.profiles.active,spring.profiles.default2个参数的设置方式:

  • 1、作为DispatcherServlet的初始化参数;
  • 2、作为Web应用的上下文参数;
  • 3、作为JNDI条目;
  • 4、作为环境变量;
  • 5、作为JVM的系统属性;
  • 6、在集成测试类上,使用@ActiveProfiles注解设置

具体写法,后面的例子会讲到

使用profile进行测试:

使用@ActiveProfiles注解:


@RunWith(SpringJUnit4ClassRunner.class)
  @ContextConfiguration(classes=DataSourceConfig.class)
  @ActiveProfiles("prod")
  public static class ProductionDataSourceTest {
    @Autowired
    private DataSource dataSource;
    
    @Test
    public void shouldBeEmbeddedDatasource() {
      // should be null, because there isn't a datasource configured in JNDI
      assertNull(dataSource);
    }
  }


3.2、条件化Bean

使用@Conditional注解:

@Conditional注解中给定一个类,这个类实现Condition接口,其中实现的matches方法的值代表改bean是否生成。


@Configuration
public class MagicConfig {

  @Bean
  @Conditional(MagicExistsCondition.class)
  public MagicBean magicBean() {
    return new MagicBean();
  }
  
}


public class MagicExistsCondition implements Condition {

  @Override
  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    Environment env = context.getEnvironment();
    return env.containsProperty("magic");
  }
  
}


ConditionContext接口:

spring源码学习 github项目_web.xml_09

AnnotatedTypeMetadata接口:

spring源码学习 github项目_web.xml_10

3.3、处理自动装配的歧义性

出现歧义的情景:

使用自动装配时,如果有多个bean能匹配上,会产生错误

例如:


//某方法
@Autowired
public void setDessert(Dessert dessert){
    this.dessert = dessert;
}

//有3个类实现了该接口
@Component
public class Cake implements Dessert{...}
@Component
public class Cookies implements Dessert{...}
@Component
public class IceCream implements Dessert{...}


Spring无法从3者中做出选择,抛出NoUniqueBeanDefinitionException异常。

使用@Primary注解标注首选bean:


//使用@Component配置bean时,可以使用@Primary注解
@Component
@Primary
public class Cake implements Dessert{...}

//使用@Bean配置bean时,可以使用@Primary注解
@Bean
@Primary
public Dessert iceCream{
    return new IceCream();
}


<!-- 使用XML配置时,设置primary属性 -->
<bean id="iceCream" class="com.desserteater.IceCream" primary="true" />


 使用@Primary时,也会出现多个匹配的bean都标注了primary属性,同样会让Spring出现无法选择的情况,导致错误

 使用@Qualifier注解限定装配的bean:

1、使用默认限定符:


@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
    this.dessert = dessert;
}


@Qualifier注解设置的参数是要注入的bean的ID,如果没有为bean设定ID,则为首字母小写的类名(也称这个bean的默认限定符)

2、为bean设置自定义限定符:


@Component
@Qualifier("soft")
public class Cake implements Dessert{...}

@Bean
@Qualifier("cold")
public Dessert iceCream{
    return new IceCream();
}


在bean上使用@Qualifier注解,表示为bean设置自定义的限定符。那么自动装载时,就可以使用自定义的限定符进行限定


@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert){
    this.dessert = dessert;
}


3、使用自定义的限定符注解:

假设已经有一个bean使用了限定符cold,结果另外一个bean也需要使用限定符cold,这样也出现了多个匹配的bean也会报错。

解决这个问题的思路是,再为这2个bean增加限定符,继续细化;但是@Qualifier注解并不支持重复注解,不能在一个bean上使用多个@Qualifier注解。

为了解决这个问题,可以使用自定义的限定符注解:


//代替@Qualifier("cold")
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold{}

//代替@Qualifier("creamy")
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy{}


 这样,以下代码,自动装配式使用了2个自定义限定符注解,也可以找到唯一匹配的bean。


@Bean
@Cold
@Creamy
public Dessert iceCream{
    return new IceCream();
}

@Bean
@Cold
public Dessert ice{
    return new Ice();
}

@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert){
    this.dessert = dessert;
}


3.4、bean的作用域

默认情况下,bean是单例的形式创建的,既整个应用程序使用的bean是同一个实例。

有些情况,如果想重用这个bean,结果这个bean被之前的操作污染了,会使程序发生错误。

Spring支持为bean设置作用域,提供了以下几种作用域:

  • 单例(Singleton):整个应用中,只创建bean的一个实例。
  • 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
  • 会话(Session):Web应用中,为每个会话创建一个bean实例。
  • 请求(Request):Web应用中,为每个请求创建一个bean实例。
//可以使用组件扫描时,声明作用域;作用域可以使用ConfigurableBeanFactory表示
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Cake implements Dessert{...}

//可以在Java配置Bean时,声明作用域;作用域也可以使用字符串表示,不过建议使用ConfigurableBeanFactory更不容易出错
@Bean
@Scope("prototype")
public Dessert iceCream{
    return new IceCream();
}


<!-- 使用XML配置时,设置bean作用域 -->
<bean id="iceCream" class="com.desserteater.IceCream" scope="prototype" />


使用会话和请求作用域

 某些情景下,某个bean(例如Web应用中的购物车bean),不应该是对于程序单例的,而是对于每一个用户有对应一个bean。

这种情况下,适合使用会话作用域的bean


@Component
@Scope(
    value=WebApplicationContext.SCOPE_SESSION,
    proxyMode=ScopedProxyMode.INTERFACES)
public ShopingCart cart(){...}


  • WebApplicationContext:SCOPE_SESSION属性表明Spring为每一个Web会话创建一个实例
  • ScopedProxyMode:解决会话作用域的bean注入到单例bean的问题。当会话作用域的bean注入到单例bean的时候,Spring改为注入一个bean代理,当单例bean需要调用会话作用域的bean时,该代理才针对当前会话,找到对应的bean,进行调用。
  • ScopedProxyMode.INTERFACES:当该bean是一个接口的时候,设置值为INTERFACES,表示该代理要实现该接口,并讲调用委托给实现的bean。
  • ScopedProxyMode.TARGET_CLASS:当该bean是一个类的时候,设置值为TARGET_CLASS,因为SPring无法创建基于接口的代理,需要使用CGLib来生成基于类的代理,通过生成目标类扩展的方式来进行代理。

同样,请求作用域也是有一样的问题,同样处理即可。

在XML中声明作用域代理

引用aop命名空间


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
      http://www.springframework.org/schema/aop
      http://www.springframework.org/schema/aop/spring-beans.xsd
      http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context">

    <!-- 配置内容 -->

</beans>


使用aop命名控件声明作用域代理:

<aop:scoped-proxy />是和@Scope注解的proxyMode属性相同的XML元素

它告诉Spring为bean创建一个作用域代理,默认状态下,会使用CGLib创建目标类代理。


<bean id="cart"
    class="com.myapp.ShoppingCart"
    scope="session">
    <aop:scoped-proxy />
</bean>


设置为生成基于接口的代理


<bean id="cart"
    class="com.myapp.ShoppingCart"
    scope="session">
    <aop:scoped-proxy proxy-target-class="false" />
</bean>


3.5、运行时值注入

之前讨论的依赖注入,主要关注将一个bean引用注入到另一个bean的属性或构造器参数中。

依赖注入还有另外一方面:将值注入到bean的属性或构造器参数中。

当然可以使用之前说过的硬编码,但是我们这里希望这些值在运行时确定。

Spring提供2种运行时求值方式:属性占位符(Property placeholder)、Spring表达式语言(SpEL)

1、注入外部的值

使用@PropertySource注解和Environment:


@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
public class EnvironmentConfig {

  @Autowired
  Environment env;
  
  @Bean
  public BlankDisc blankDisc() {
    return new BlankDisc(
        env.getProperty("disc.title"),
        env.getProperty("disc.artist"));
  }
  
}


通过@PropertySource注解引用一个名为app.properties的文件,文件内容如下


disc.title=Sgt. Peppers Lonely Hearts Club Band
disc.artist=The Beatles


然后通过Spring的Environment,使用getProperty()方法,进行检索属性。

深入Spring的Environment:

getProperty()方法的重载


String getProperty(String key)
String getProperty(String key, String defaultValue)
T getProperty(String key, class<T> type)
T getProperty(String key, class<T> type,T defaultValue)


  • getProperty()方法如果没有找到对应的属性,会返回null值
  • getRequiredProperty()方法,如果没有找到属性,会抛出IllegalStateException异常
  • containsProperty()方法:检查某个属性是否存在
  • getPropertyAsClass()方法:将属性解析为类

检查profile的激活状态

  • String[] getActiveProfiles():返回激活profile名称的数组;
  • String[] getDefaultProfiles():返回默认profile名称的数组;
  • boolean acceptsProfiles(String... profiles):如果environment支持给定profile的话,返回true

解析属性占位符“${...}”以及注解@Value:

使用Environment检索属性很方便,Spring同时也提供了占位符装配属性的方法

a、配置PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean(Spring3.1开始推荐用这个) ,这个bean可以基于Spring Environment以及其属性源来解析占位符

Java配置


@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
    return new PropertySourcesPlaceholderConfigurer();
}


XML配置,引用Spring context命名空间中的<context:property-placeholder />即可自动生成PropertySourcesPlaceholderConfigurer bean


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
      http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-beans.xsd">

    <context:property-placeholder />

</beans>


b、注入

Java注入


public BlandKisc(
    @Value("${disc.title}") String title,
    @Value("${disc.artist}") String artist){
    this.title = title;
    this.artist = artist;
}


XML注入


<bean id="sgtPeppers"
      class="soundsystem.BlandDisc"
      c:_title="${disc.title}"
      c:_artist="${disc.artist}" />


2、使用Spring表达式语言进行装配

Spring 3引入了Spring表达式语言(Spring Expression Language,SpEL),能够强大和简洁地把值装配到bean属性和构造器参数中。

SpEL的一些特性:

  • 使用bean的ID引用bean;
  • 调用方法和访问对象的属性;
  • 对值进行算术、关系和逻辑运算;
  • 正则表达式匹配;
  • 集合操作;

2.1、表示字面值:


#{1}//整型
#{3.14159}//浮点数
#{9.87E4}//科学计数法
#{'Hello'}//String类型字面值
#{false}//Boolean类型


2.2、引用bean、属性和方法:


#{sgtPeppers}//引用ID为sgtPeppers的bean
#{sgtPeppers.artist}//引用bean的属性
#{sgtPeppers.selectArtist()}//调用bean的方法
#{sgtPeppers.selectArtist().toUpperCase()}//调用bean的方法的值的方法,例如方法值是String类型,则可以调用String对象的toUpperCase方法
#{sgtPeppers.selectArtist()?.toUpperCase()}//类型安全的运算符“?.”,当值为null时返回null,否则猜执行toUpperCase方法


2.3、表达式中使用类型:

主要作用是调用类的静态方法和变量。使用T()运算符,获得Class对象。


#{T(java.lang.Math)}//得到一个class对象
#{T(java.lang.Math).PI}//类的常量
#{T(java.lang.Math).random()}//类的静态方法


2.4、SpEL运算符:

用于在SpEL表达式上做运算

spring源码学习 github项目_spring源码学习 github项目_11


#{2 * T(java.lang.Math).PI * circle.radius}//计算周长
#{T(java.lang.Math).PI * circle.radius ^ 2}//计算面积
#{disc.title + ' by ' + disc.artist}//拼接字符串
#{counter.total == 100}//比较运算符 结果为布尔值
#{counter.total eq 100}//比较运算符 结果为布尔值
#{scoreboard.score ? 1000 ? "Winner!" : "Loser"}//三元运算符
#{disc.title ?: 'Rattle and Hum'}//Elvis运算符(Elvis是猫王的名字,?:符号像猫王的头发),判断是否为null,是null则给默认值


2.5、计算正则表达式:

使用matches运算符,返回Boolean类型值


#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}//验证有效邮件


 2.6、计算集合:


#{jukebox.songs[4].title}//songs集合第5个元素的title属性
#{'this is a test'[3]}//String中的一个字符
#{jukebox.songs.?[artist eq 'Jay']}//使用查询运算符(.?[])过滤子集
#{jukebox.songs.^[artist eq 'Jay']}//使用第一个匹配符(.^[])找到第一个匹配的项
#{jukebox.songs.$[artist eq 'Jay']}//使用最后一个匹配符(.$[])找到最后一个匹配的项
#{jukebox.songs.![title]}//投影运算符(.![])选择属性投影到一个新的集合


2.7、这里介绍的SpEL只是冰山一角,有需要的去查阅相关资料

四、面向切面的Spring

4.1、什么是面向切面编程

什么是AOP:

为什么需要面向切面编程(AOP)技术:

  • 在软件开发中,有一些需求需要散步在应用中的多处,称为横切关注点。
  • 例如希望每一次操作,都记录下日志;当然我们可以在每一次操作都加上记录日志的代码,但是这样变得十分复杂和繁琐。
  • 面向切面编程(AOP)的目的就是把这些横切关注点和业务逻辑相分离。
  • 依赖注入(DI)实现了应用对象之间的解耦;而面向切面编程(AOP)实现了横切关注点和它们影响的对象之间的解耦。

什么是面向切面编程(AOP)技术:

如上所述,切面可以帮助我们模块化横切关注点。

一般如果我们需要重用通用功能的话,常见的面向对象技术是继承或委托。

但是如果整个应用中都使用同样的基类,继承往往会导致一个脆弱的对象体系;

而使用委托可能需要对委托对象进行复杂的调用。

而切面提供了另一种可选方案:在一个独立的地方定义通用功能,通过声明的方式定义此功能用何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化特殊的类,这些类称为切面。

这样做有2个好处:

  • a、每个关注点集中在一个地方,而不是分散到多出代码中;
  • b、服务模块更简洁,因为它们只关注核心代码,次要代码被转移到切面中。

spring源码学习 github项目_web.xml_12

AOP术语:

spring源码学习 github项目_web.xml_13

通知(Advice):

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

Spring切面有5种类型的通知:

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

连接点(Join point):

连接点是我们的程序可以应用通知的实际,是应用执行过程中能够插入切面的一个点。

切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

切点(Poincut):

切点指切面所通知的连接点的范围。

一个切面不需要通知整个程序的连接点,通过切点,定义了切面所应用的范围。

通知定义了切面是“什么”以及“何时”执行,切点定义了“何处”需要执行切面。

切面(Aspect):

 切面是通知和切点的结合,定义了需要做什么,在何时,何处完成该功能。

引入(Introduction):

引入允许我们想现有的类添加新的方法或属性。

可以在不修改现有类的情况下,将新的方法和实例变量引入到该类中。

织入(Weaving):

织入是把切面应用到目标对象并创建新的代理对象的过程。

切面在指定的连接点被织入到目标对象中。

在目标对象的声明周期有几个点可以进行织入:

  • a、编译期:切面在目标类编译时被织入。这种方式需要特殊的编译期。AspectJ的织入编译器就是用这种方式织入切面。
  • b、类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。
  •       AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
  • c、运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会以目标对象动态创建一个代理对象。Spring AOP就是以这种方式织入切面的。
Spring对AOP的支持:
  • 不是所有的AOP框架都相同,比如在连接点模型上可能有强弱之分,有些允许在字段修饰符级别应用通知,而另一些只支持方法调用相关的连接点;织入切面的方式和时机也不同。
  • 但是,AOP框架的基本功能,就是创建切点来定义切面所织入的连接点。
  • 本书关注Spring AOP,同时Spring和AspectJ项目之间有大量协作,Spring对AOP支持有很多方面借鉴了AspectJ项目

Spring支持4种类型的AOP支持:

  • a、基于代理的经典Spring AOP
  • 经典Spring AOP过于笨重和复杂,不作介绍。
  • b、纯POJO切面
  • 借助Spring的aop命名空间,可以将纯POJO转换为切面。
  • 实际上这些POJO知识提供了满足切点条件时所调用的方法。
  • 但这种技术需要XML配置。
  • c、@AspectJ注解驱动的切面
  • 提供以注解驱动的AOP。本质上还是Spring基于代理的AOP,但是编程模型修改为和AspectJ注解一致。
  • 这种方式好处在于不用使用XML。
  • d、注入式AspectJ切面(适用于Spring各版本)
  • 如果AOP需求超过了简单的方法调用(如构造器或属性拦截),则需要使用AspectJ来实现切面。
  • 前3种都是Spring AOP实现的变体,基于动态代理,所以Spring对AOP的支持局限于方法拦截

Spring通知是Java编写的:

Spring所创建的通知是标准的Java类编写的,我们可以使用Java开发IDE来开发切面。

而且定义通知所应用的切点通常会用注解或Spring XML编写,Java开发者都非常熟悉。

AspectJ与之相反,AspectJ最初是以Java语言扩展的方式实现的。

优点是通过特有的AOP语言,我们可以获得更强大和细粒度的控制,以及更丰富的AOP工具集。

缺点是需要额外学习新的工具和语法。

Spring在运行时通知对象:

spring源码学习 github项目_web.xml_14

通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。

代理类封装目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。

代理拦截到方法调用时,会在调用目标bean方法之前执行切面逻辑。

知道应用需要被代理的bean时,Spring才会创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。

因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。

Spring只支持方法级别的连接点:

因为Spring是基于动态代理,所以只支持方法连接点;但也足以满足绝大部分需求。

如果需要拦截字段和构造器,可以使用AspectJ和JBoss

4.2、通过切点来选择连接点

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

Spring仅支持AspectJ切点指示器的一个子集。

Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的。

下面是Spring AOP所支持的AspectJ切点指示器:

spring源码学习 github项目_测试_15

其中,只有execution指示器是实际执行匹配的,其它指示器都是用来限制匹配的。

Spring中尝试使用AspectJ其它指示器时,会抛出IllegalArgument-Exception异常

编写切点:

假设有一个接口


public interface Performance{
    public void perform();
}


 使用切点表达式设置当perform()方法执行时触发通知的调用:

spring源码学习 github项目_测试_16

使用within()指示器限制仅匹配concert包

spring源码学习 github项目_Java_17

支持的关系操作符有且(&&),或(||),非(!)

如果用XML配置,因为&在XML中有特殊含义,所以可以使用and,or,not来作为关系操作符

切点中选择bean:

Spring引入了一个新的bean()指示器,通过bean的ID来限制bean

 限制id为woodstock的bean才应用通知

 

spring源码学习 github项目_web.xml_18


 限制id非woodstock的bean才应用通知

spring源码学习 github项目_spring源码学习 github项目_19

4.3、使用注解创建切面

AspectJ5之前,编写AspectJ切面需要学习一种Java语言的扩展。

AspectJ5引入了使用注解来创建切面的关键特性,AspectJ面向注解的模型可以非常简便地通过注解把任意类转变为切面。

1、定义切面:


@Aspect
public class Audience{
    //表演前
    @Before("execution(** concert.Performance.perform(..))")
    public void silenceCellPhones(){
        System.out.println("Silencing cell phones");
    }
    //表演前
    @Before("execution(** concert.Performance.perform(..))")
    public void takeSeats(){
        System.out.println("Taking seats");
    }
    //表演后
    @AfterReturning("execution(** concert.Performance.perform(..))")
    public void applause(){
        System.out.println("Clap!!");
    }
    //表演失败后
    @AfterThrowing("execution(** concert.Performance.perform(..))")
    public void demandRefund(){
        System.out.println("Refund!!");
    }

}


@Aspect注解表示Audience不仅是一个POJO,还是一个切面。

@Before,@AfterReturning等注解,用来声明通知方法。

spring源码学习 github项目_测试_20

使用@Pointcut注解定义可重用切点:


@Aspect
public class Audience{
    //可重用切点
    @Pointcut("excution(** concert.performance.perform(..))")
    public void performance(){}
    //表演前
    @Before("performance()")
    public void silenceCellPhones(){
        System.out.println("Silencing cell phones");
    }
    //表演前
    @Before("performance()")
    public void takeSeats(){
        System.out.println("Taking seats");
}


使用一个空的方法,作为标记,使用@Pointcut注解定义成一个可重用的节点,然后通过“方法名()”来进行引用

启用自动代理:

以上仅仅是定义了切面,要启动切面功能,需要启动切面的代理

Java配置方法:


@Configuration
@EnableAspectJAutoProxy//启动AspectJ自动代理
@ComponentScan
public class ConcertConfig{
    @Bean
    Public Audience audience(){//声明Audience bean
        return new Audience();
    }
}


 XML配置方法(使用aop命名空间)


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
      http://www.springframework.org/schema/aop
      http://www.springframework.org/schema/aop/spring-aop.xsd
      http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-beans.xsd">

    <context:conponent-scan base-package="concert" />

    <!-- 启用AspectJ自动代理 -->
    <aop:aspectj-autoproxy />
    
    <!-- 声明Audience bean -->
    <bean class="concert.Audience" />
    
</beans>


代理的作用:

使用上述2种方法,会给Concert bean创建一个代理,Audence类中的通知方法会在perform()调用前后执行。

注意!Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的知道,切面本质上还是Spring基于代理的切面,仅局限于代理方法的调用。

如果想要使用AspectJ的所有能力,必须运行时使用AspectJ并且不依赖Spring。

2、创建环绕通知


@Aspect
public class Audience{
    //可重用切点
    @Pointcut("excution(** concert.performance.perform(..))")
    public void performance(){}

    //环绕通知方法
    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint jp){
        try{
            System.out.println("Silencing cell phones");
            System.out.println("Taking seats");
            jp.proceed();
            System.out.println("CLAP!!");
        }catch(Throwable e){
            System.out.println("refund!!");
        }
    }
}


 可以将前置和后置通知写在一个方法中,并使用ProceedingJoinPoint对象来进行对目标方法的调用。

3、处理通知中的参数


@Aspect
public class TrackCounter{
    private Map<Interger,Integer> trackCounts = new HashMap<Integer,Integer>();

    @Pointcut(
        "execution(* soundsystem.CompactDisc.playTrack(int))" +
        "&& args(trackNumber)")
    public  void trackPlayed(int trackNumber){}

    @Before("trackPlayed(trackNumber)")
    public void countTrrack(int trackNumber){
        int currentCount = getPlayCount(trackNumber);
        trackCounts.put(trackNumber,currentCount + 1);
    }

    public int getPlayCount(int trackNumber){
        return trackCounts.containsKey(trackNumber)
                ? trackCounts.get(trackNumber) : 0;
    }
}


spring源码学习 github项目_Java_21

4、通过注解引入新功能

spring源码学习 github项目_json_22

假设情景:有一个类,希望让其以及其实例实现某个接口,但这个类是不可以修改的(如没有源码)。我们可以通过AOP为这个类引入新的方法,实现该接口。

例如

我们有一个类concert.Performance

希望能通过AOP实现接口


public interface Encoreable{
    void performEncore();
}


切面


@Aspect
public class EncoreableIntroducer{
    @DeclareParents(value="concert.performance+",defaultImpl=DefaultEncoreable.class)
    public static Encoreable encoreable;
}


通过声明一个切面,使用@DeclareParents注解,讲Encoreable接口引入到Performance bean中。

@DeclareParents的组成部分

1、Value属性指定哪种类型bean要引入该接口。本例中,指所有实现Performance的类型。(加号表示是Performance的所有子类型,而不是Performance本身。)

2、defaultImpl属性指定为引入功能提供实现的类。在这里我们指定的是DefaultEncoreable提供实现。

3、@DeclareParents注解所标注的惊天属性知名了要引入的接口。在这里我们引入的是Encoreable接口。

spring源码学习 github项目_spring源码学习 github项目_23

【------------------------Spring Web------------------------】

五、构建Spring Web应用程序

5.1、Spring MVC起步

在基于HTTP协议的Web应用中,Spring MVC将用户请求在调度Servlet、处理器映射、控制器以及视图解析器之间移动,再将用户结果返回给用户。

我们将介绍请求如何从客户端发起,经过Spring MVC中的组件,最终回到客户端。

1、跟踪Spring MVC的请求

spring源码学习 github项目_web.xml_24

请求:请求离开浏览器时,带有用户请求内容的信息。一般包含请求的URL、用户提交的表单信息等。

  • 1、前端控制器:前端控制器是一种常用的Web应用程序模式。服务器使用一个单例的Servlet(Spring的DispatcherServlet)作为前端控制器,将请求委托给应用程序的其它组件来执行实际的处理。
  • 2、查询映射并分发请求:DispatcherServlet的任务是将请求发送给Spring MVC控制器。DispatcherServlet收到请求后,根据请求携带的URL信息,查询处理器映射,确定请求对应的控制器,并发送给控制器。
  • 3、控制器处理请求:控制器得到请求后,会获取请求中用户提交的信息,并等待控制器处理这些信息。
  • 4、模型和视图名:控制器处理完数据后,会产生一些信息(称为模型),为了显示,这些模型需要发送给一个视图。控制器把模型数据打包,并标示出用于渲染输出的视图名,然后将请求、模型、视图逻辑名发送回给DispatcherServlet
  • 5、视图解析器:DispatcherServlet根据逻辑视图名找到真正的视图实现。
  • 6、视图渲染:根据找到的视图实现,并将模型数据交付给视图实现,即可将数据渲染输入。
  • 7、传递到客户端:这个输出会通过响应对象传递给客户端。
2、搭建Spring MVC

2.1、配置DispatcherServlet:

传统方式:配置在web.xml中,这个文件放到应用的WAR包中。

现在方法:使用Java将DispatcherServlet配置到Servlet容器中。


public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
  
  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] { RootConfig.class };
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { WebConfig.class };//指定配置类
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };//将DispatcherServlet映射到“/”
  }

}


扩展AbstractAnnotationConfigDispatcherServletInitializer的任意类都会自动配置DipatcherServlet和Spring应用上下文,Spring的应用上下文会位于应用程序的Servlet上下文之中。

spring源码学习 github项目_spring源码学习 github项目_25

getServletMappings()方法将一个或多个路径映射到DispatcherServlet上。本例中“/”,代表是应用的默认Servlet,会处理进入应用的所有请求。

要理解另外2个方法,需要先理解DispatcherServlet和一个Servlet监听器(ContextLoaderListener)的关系。

2.2、两个应用上下文之间的关系

DispatcherServlet启动时,创建Spring应用上下文,加载配置文件或配置类中声明的bean。

getServletConfigClasses()方法要求DispatcherServlet加载应用上下文时,使用定义在WebConfig配置类中的bean。

ContextLoaderListener会创建另外一个应用上下文

我们希望DispatcherServlet记载包含Web组件的bean,如控制器、视图解析器以及处理器映射;而ContextLoaderListener要加载应用中其它bean,通常是驱动应用后端的中间层和数据层组件。

AbstractAnnotationConfigDispatcherServletInitializer 会同时创建DispatcherServlet和ContextLoaderListener。

getServletConfigClasses()方法返回的带有@Configuration注解的类会用来定义DispatcherServlet应用上下文的bean。

getRootConfigClasses()方法返回的带有@Configuration注解的类将会用来配置ContextLoaderListener创建的应用上下文中的bean。

Servlet3.0之前的服务器,只能支持web.xml的配置方式;Servlet3.0及以后版本才支持AbstractAnnotationConfigDispatcherServletInitializer 的配置方式。

2.3、启动Spring MVC

传统方法:使用XML配置Spring MVC组件

现在方法:使用Java配置Spring MVC组件

最简单的Spring MVC配置


@Configuration
@EnableWebMvc
public class WebConfig {
}


这样就能启动Spring MVC,但是有以下问题:

  • 1、没有配置视图解析器,Spring使用默认的BeanNameViewResolver,这个视图解析器会查找ID与视图名称匹配的bean,并且查找的bean要实现View接口,以这样的方式解析视图。
  • 2、没有启动组件扫描。导致Spring只能找到显式声明在配置类中的控制器。
  • 3、DispatcherServlet会映射为默认的Servlet,所以会处理所有的请求,包括对静态资源的请求,如图片和样式表(一般不希望这样)。

以下配置即可解决上述问题


@Configuration
@EnableWebMvc//启用Spring MVC
@ComponentScan("spittr.web")//启用组件扫描
public class WebConfig extends WebMvcConfigurerAdapter {
  @Bean
  public ViewResolver viewResolver() {//配置JSP视图解释器
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    return resolver;
  }
  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();
  }
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {//配置静态资源处理
    // TODO Auto-generated method stub
    super.addResourceHandlers(registry);
  }
}


  • 1、@ComponentScan注解,会扫描spittr.web包查找组件;通常我们编写的控制器会使用@Controller注解,使其成文组件扫描时的houxuanbean。所以我们不需要在配置类显式声明任何控制器。
  • 2、ViewResolver bean。后面会详细讨论,这个的作用是查找JSP文件。例如home的视图会被解析为/WEB-INF/views/home.jsp。
  • 3、新的WebConfig类扩展了WebMvcConfigurerAdapter并重写了其configureDefaultServletHandling()方法。通过调用enable()方法,要求DispatcherServlet将对静态资源的请求转发到Servlet容器中默认的Servlet上,而不是使用DispatcherServlet本身来处理此类请求。

RootConfig的配置


@Configration
@ComponentScan(basePackages={"spitter"},
    excludeFilters={
        @filter{type=FilterType.ANNOTATION, value=EnableWebMvc.class)
    })
public class RootConfig{
}


 使用@ComponentScan注解,后期可以使用很多非Web组件来完善RootConfig。

5.2、编写基本的控制器

1、控制器的基本形式

@RequestMapping注解:

声明这个控制器所要处理的请求


@Controller
public class HomeController{
    @RequestMapping(value="/", method=GET)
    public String home(){
        return "home";
    }
}


1.1、声明控制器

2种方式让控制器类能被扫描称为组件:

  • a、在类上使用@Controller声明这是一个控制器
  • b、在类上使用@Component声明这是一个组件,并且类名使用Controller作为结束

1.2、指定处理请求路径

@RequestMapping(value="/",method=GET)

value代表要处理的请求路径,method属性指定所处理的HTTP方法

1.3、返回视图名称

return "home";

返回一个字符串,代表需要渲染的视图名称。DispatcherServlet会要求视图解析器将这个逻辑名称解析为实际的视图。

基于我们在InternalResourceViewResolver的配置,视图名“home”将被解析为“/WEB-INF/views/home.jsp”路径的JSP。

2、测试控制器

控制器本身也是一个POJO,可用普通POJO的测试方法测试,但是没有太大的意义。


Public class HomeControllerTest{
    @Test
    public void testHomePage() throws Exception{
        HomeController controller = new HomeController();
        assertEquals("home", controller.home());
    }
}


这个测试只是测试home()方法的返回值,没有站在SpringMVC控制器的角度进行测试。

Spring 3.2开始可以按照控制器的方式来测试控制器。

Spring 3.2开始包含一种mock Spring MVC并针对控制器执行HTTP请求的机制,这样测试控制器就不用启动Web服务器和Web浏览器了。


Public class HomeControllerTest{
    @Test
    public void testHomePage() throws Exception{
        HomeController controller = new HomeController();
        MockMvc mockMvc = standaloneSetup(controller).build();//搭建MockMvc
        mockMvc.perform(get("/"))//对"/"执行GET请求
                      .andExpect(view().name("home"));//预期得到home视图
    }
}


先传递一个HomeController实例到standaloneSetup()并调用build()来构建MockMvc实例,然后用这个实例来执行针对“/”的GET请求并设置期望得到的视图名称。

3、定义类级别的请求处理

对类使用@RequestMapping注解,那么这个注解会应用到所有处理器方法中。


@Controller
@RequestMapping("/")
public class HomeController{
    @RequestMapping( method=GET)
    public String home(){
        return "home";
    }
}


路径还可以是一个数组,下面例子代表home()方法可以映射到对“/”和“homepage”的GET请求。


@Controller
@RequestMapping({"/", "/homepage"})
public class HomeController{
    @RequestMapping( method=GET)
    public String home(){
        return "home";
    }
}


4、传递模型数据到视图中

使用Model传递数据


@RequestMapping(method=RequestMethod.GET)
public String spittles(Model model){
    model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE,20));
    return "spittles";
}


通过Model参数,可以将控制器里面的值,传递到视图中,渲染到客户端。

Model实际上是一个Map(Key-Value对集合)

使用addAttribute并且不指定key的时候,Model会根据类型自动生成key,例如上面是一个List<Spittle>,那么key值就是spittleList

最后控制器返回视图逻辑名,标明需要渲染的视图。

改用Map传递数据

如果不想使用Spring类型,把Model改成Map类型也是可以的


@RequestMapping(method=RequestMethod.GET)
public String spittles(Map model){
    model.put("spittleList",spittleRepository.findSpittles(Long.MAX_VALUE,20));
    return "spittles";
}


直接返回数据


@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(){
    return spittleRepository.findSpittles(Long.MAX_VALUE,20);
}


这种写法,没有返回视图名称,也没有显式设定模型。

模型:当处理器方法直接返回对象或集合时,这个值会放进模型中,模型的key由类型推断出来。

视图:而视图的逻辑名称会根据请求路径推断得出,如/spittles的GET请求,逻辑视图名称就是spittles(去掉开头斜线)。

视图的渲染

无论使用哪种方法,结果是一样的:

在控制器中,将数据定义为模型,并发送到指定的视图,根据视图的逻辑名称,按照我们配置的InternalResourceViewResolver视图解析器,找到对应的视图文件(如"/WEB-INF/views/spittles.jsp")。

当视图是JSP的时候,模型数据会作为请求属性放到请求(request)之中。

因此,在jsp文件中可以使用JSTL(JavaServer Pages Standard Tag Library)的<c:forEach>标签进行渲染。

测试控制器视图名以及传递的模型数据


@Test
public void houldShowRecentSpittles() throws Exception {
List<Spittle> expectedSpittles = createSpittleList(20);
SpittleRepository mockRepository = mock(SpittleRepository.class);//使用mock,利用接口创建一个实现,并创建一个实例对象
when(mockRepository.findSpittles(Long.MAX_VALUE, 20))
    .thenReturn(expectedSpittles);//调用mock实现,创建20个Spittle对象

SpittleController controller = new SpittleController(mockRepository);
MockMvc mockMvc = standaloneSetup(controller)//搭建MockMvc
    .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
    .build();

mockMvc.perform(get("/spittles"))//对/spittles发起GET请求
   .andExpect(view().name("spittles"))//断言视图名为spittles
   .andExpect(model().attributeExists("spittleList"))
   .andExpect(model().attribute("spittleList", 
              hasItems(expectedSpittles.toArray())));
}

 

spring源码学习 github项目_Java_26

5.3、接受请求的输入

1、处理查询参数

场景:我们需要分页查找Spittle列表,希望传入2个参数:上一页最后项的id、每页的数据数量,从而找到下一页应该读取的数据。

获取参数

使用@RequestParam注解获取参数


@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(
        @RequestParam("max") long max,
        @RequestParam("count") int count){
    return spittleRepository.findSpittles(max,count);
}


给定默认值


private static final String MAX_LONG_AS_STRING = Long.toString(Long.MAX_VALUE);
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(
        @RequestParam(value="max",defaultValue=MAX_LONG_AS_STRING) long max,
        @RequestParam(value="count",defaultValue="20") int count){
    return spittleRepository.findSpittles(max,count);
}


注意,查询参数都是String类型,所以需要把Long.MAX_VALUE转换为String类型才能制定默认值。

当绑定到方法的参数时,才会转换为对应的参数的类型。

测试方法


@Test
public void shouldShowPagedSpittles() throws Exception {
    List<Spittle> expectedSpittles = createSpittleList(50);
    SpittleRepository mockRepository = mock(SpittleRepository.class);
    when(mockRepository.findSpittles(238900, 50)).thenReturn(expectedSpittles);//预期的max和count参数

    SpittleController controller = new SpittleController(mockRepository);
    MockMvc mockMvc = standaloneSetup(controller)
            .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp")).build();

    mockMvc.perform(get("/spittles?max=238900&count=50")).andExpect(view().name("spittles"))//传入max和count参数
            .andExpect(model().attributeExists("spittleList"))
            .andExpect(model().attribute("spittleList", hasItems(expectedSpittles.toArray())));
}


2、通过路径接收参数

按上面讲解的写法:


@RequestMapping(value = "/show", method = RequestMethod.GET)
public String spittle(@RequestParam("spittleId") long spittleId, Model model) {
    model.addAttribute(spittleRepository.findOne(spittleId));
    return "spittle";
}


这个控制器,可以处理请求是这样的:"/spittle/show/?spittle_id=12345"

但是现在流行面向资源的方式,我们查找一个spittle的行为,相当于获取一个spittle资源,更希望URL的方式是:对"/spittle/12345"这样的URL进行GET请求。

使用@RequestMapping的占位符与@PathVariable注解


@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
public String spittle(@PathVariable("spittleId") long spittleId, Model model) {
    model.addAttribute(spittleRepository.findOne(spittleId));
    return "spittle";
}


这样在"/spittle/"后面的参数将会传递到spittleId变量中

如果变量名和占位符名称相同,可以省掉@PathVariable中的变量


@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
public String spittle(@PathVariable("spittleId") long spittleId, Model model) {
    model.addAttribute(spittleRepository.findOne(spittleId));
    return "spittle";
}


测试方法


@Test
public void testSpittle() throws Exception {
    Spittle expectedSpittle = new Spittle("Hello", new Date());
    SpittleRepository mockRepository = mock(SpittleRepository.class);
    when(mockRepository.findOne(12345)).thenReturn(expectedSpittle);

    SpittleController controller = new SpittleController(mockRepository);
    MockMvc mockMvc = standaloneSetup(controller).build();

    mockMvc.perform(get("/spittles/12345")).andExpect(view().name("spittle"))//通过路径请求资源
            .andExpect(model().attributeExists("spittle")).andExpect(model().attribute("spittle", expectedSpittle));
}


使用路径传参只使用于少量参数的情况,如果需要传递大量数据,则需要使用表单。

5.4、处理表单(待补充)

【暂略】六、渲染Web视图

【暂略】七、Spring MVC的高级技术

【暂略】八、使用Spring Web Flow

九、Spring Security保护Web应用

Spring Security是一种基于Spring AOP和Servlet规范中filter实现的安全框架

  • 提供声明式安全保护
  • 提供了完整的安全解决方案

Spring Security从2个角度解决安全问题

  • 使用Servlet规范中的filter保护Web请求并限制URL级别的访问(本章)
  • 使用Spring的AOP,借助对象代理和使用通知,确保有适当权限的用户才能访问受保护的方法(14章)

Spring Security分为11个模块

spring源码学习 github项目_json_27

要使用Spring Security,至少要引入Core和Configuration2个模块

9.1、Spring Security简介

开发方式的演进:
  • Spring Security基于Servlet Filter,在web.xml或webApplicationInitializer中配置多个filter

简化:使用一个特殊的filter:DelegatingFilterProxy,这个filter是一个代理类,会把工作交给Spring上下文中的javax.servlet.filter实现类

 

spring源码学习 github项目_Java_28

  • Spring 3.2带来了新的Java配置方式

在实现了WebSecurityConfigurer的bean中使用注解@EnableWebSecurity

更简化:在扩展类WebSecurityConfigurerAdapter中使用注解@EnableWebSecurity;如果使用Spring MVC,则需要使用注解@EnableWebMvcSecurity

Spring Security的配置

可以通过重写以下3个方法,对Spring Security的行为进行配置

spring源码学习 github项目_Java_29

9.2、配置用户信息服务

Spring Security需要配置用户信息服务,表明哪些用户可以进行访问

有以下几种方式,暂时先不详细介绍
  • 基于内存设置
  • 基于数据库表设置
  • 基于LDAP设置
  • 配置自定义用户服务,实现UserDetailsService接口

9.3、拦截请求

 

 

 

 

【---------------------后端中的Spring----------------------】

【暂略】十、通过Spring和JDBC征服数据库

【暂略】十一、使用对象-关系映射持久化数据库

十二、Spring中使用NoSQL数据库

12.1、使用MongoDB持久化文档数据

待补充

12.2、使用Noe4j操作图数据

待补充

12.3、使用Redis操作key-value数据

Redis是一种基于key-value存储的数据库,Spring Data没有把Repository生成功能应用到Redis中,而是使用面向模版的数据访问的方式来支持Redis的访问。

Spring Data Redis通过提供一个连接工厂来创建模版

12.3.1、连接到Redis

创建连接工厂:

  为了连接和访问Redis,有很多Redis客户端,如Jedis、JRedis等。

  Spring Data Redis为4种Redis客户端实现提供了连接工厂:

  • JedisConnectionFactory
  • JredisConnectionFactory
  • LettuceConnectionFactory
  • SrpConnectionFactory

  选择哪个客户端实现取决于你,对于Spring Data Redis,这些连接工厂的适用性是一样的

  我们使用一个bean来创建这个工厂


@Bean
public RedisConnectionFactory redisCF() {
    JedisConnectionFactory cf = new JedisConnectionFactory();
    cf.setHostName("redis-server");
    cf.setPort(7379);
    cf.setPassword("123456");
    return cf;
}


  对于不同的客户端实现,他的工厂的方法(如setHostName)也是相同的,所以他们在配置方面是相同的操作。

12.3.2、Redis模版RedisTemplate

获取RedisTemplate

  其中一种访问Redis的方式是,从连接工厂中获取连接RedisConnection,使用字节码存取数据(不推荐):


RedisConnectionFactory cf = ...;
RedisConnection conn = cf.getConnection();
conn.set("greeting".getBytes(),"Hello World".getBytes());
byte[] greetingBytes = conn.get("greeting".getBytes());
String greeting = new String(greetingBytes);


  Spring Date Redis提供了基于模版的较高等级的数据访问方案,其中提供了2个模版:

  • RedisTemplate:直接持久化各种类型的key和value,而不是只局限于字节数组
  • StringRedisTemplate:扩展了RedisTemplate,只关注String类型
RedisConnectionFactory cf = ..;
RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>();
redis.setConnectionFactory(cf);

RedisConnectionFactory cf = ..;
StringRedisTemplate redis = new StringRedisTemplate(cf);

//设置为bean
@Bean
public RedisTemplate<String, Product> redisTemplate(RedisConnectionFactory cf) {
    RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>();
    redis.setConnectionFactory(cf);
    return redis;
}

@Bean
public StringRedisTemplate redisTemplate(RedisConnectionFactory cf) {
    return new StringRedisTemplate(cf);
}


使用RedisTemplate存取数据

  

spring源码学习 github项目_json_30


//redis是一个RedisTemplate<String,Product>类型的bean

//【使用简单的值】
//通过product的sku进行存储和读取product对象
redis.opsForValue().set(product.getSku(),product);
Product product = redis.opsForValue().get("123456");

//【使用List类型的值】
//在List类型条目尾部/头部添加一个值,如果没有cart这个key的列表,则会创建一个
redis.opsForList().rightPush("cart",product);
redis.opsForList().leftPush("cart",product);
//弹出一个元素,会移除该元素
Product product = redis.opsForList().rightPop("cart");
Product product = redis.opsForList().leftPop("cart");
//只是想获取值
//获取索引2到12的元素;如果超出范围,则只返回范围内的;如果范围内没有,则返回空值
List<Product> products = redis.opsForList().range("cart",2,12);

//在Set上执行操作
//添加一个元素
redis.opsForSet().add("cart", product);
//求差异、交集、并集
Set<Product> diff = redis.opsForSet().difference("cart1", "cart2");
Set<Product> union = redis.opsForSet().union("cart1", "cart2");
Set<Product> isect = redis.opsForSet().intersect("cart1", "cart2");
//移除元素
redis.opsForSet().remove(product);
//随机元素
Product random = redis.opsForSet().randomMember("cart");

//绑定到某个key上,相当于创建一个变量,引用这个变量时都是针对这个key来进行的
BoundListOperations<String, Product> cart = redis.boundListOps("cart");
Product popped = cart.rightPop(); 
cart.rightPush(product);
cart.rightPush(product2);
cart.rightPush(product3);


12.3.3、使用key和value的序列化器

当某个条目保存到Redis key-value存储的时候,key和value都会使用Redis的序列化器进行序列化。

Spring Data Redis提供了多个这样的序列化器,包括:

  • GenericToStringSerializer:使用Spring转换服务进行序列化
  • JacksonJsonRedisSerializer:使用Jackson1,讲对象序列化为JSON
  • Jackson2JsonRedisSerializer:使用Jackson2,讲对象序列化为JSON
  • JdkSerializationRedisSerializer:使用Java序列化
  • OxmSerializer:使用Spring O/X映射的编排器和解排器实现序列化,用于XML薛丽华
  • StringRedisSerializer:序列化String类型的key和value

这些序列化器都实现了RedisSerializer接口,如果其中没有符合需求的序列化器,可以自行创建。

RedisTemplate默认使用JdkSerializationRedisSerializer,那么key和value都会通过Java进行序列化。

StringRedisTemplate默认使用StringRedisSerializer,实际就是实现String和byte数组之间的转化。

例子:

   使用RedisTemplate,key是String类型,我们希望使用StringRedisSerializer进行序列化;value是Product类型,我们希望使用JacksonJsonRedisSerializer序列化为JSON


@Bean
public void RedisTemplate<String,Product> redisTemplate(RedisConnectionFactory cf) {
    RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>();
    redis.setConnectionFactory(cf);
    redis.setKeySerializer(new StringRedisSerializer());
    redis.setValueSerializer(new Jackson2JsonRedisSerializer<Product>(Product.class));
    return redis;
}


十三、使用Spring缓存技术

什么叫缓存:

  一些变动不频繁或者不变动的数据,当每次获取的时候,都需要从数据库中提取或者计算,每次都需要消耗资源。

  我们可以把这些计算后的结果,在某个地方存放起来,当下次访问的时候直接返回,就可以避免了多次的资源消耗,这就叫缓存技术。

13.1、启用缓存支持

13.1.1、2种方式启用Spring对注解驱动缓存的支持:
  • 在一个配置类上使用@EnableCaching注解
  • 使用XML进行配置

@EnableCaching注解方式:


@Configuration
@EnableCaching
public class CachingConfig {
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager();
    }
}


XML方式:

  使用Spring cache命名空间中的<cache:annotation-driven>元素启动注解驱动的缓存


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:cache="http://www.springframework.org/schema/cache"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache
        http://www.springframework.org/schema/cache/spring-cache.xsd">
    
    <!-- 启用缓存-->
    <cache:annotation-driven />

    <!-- 声明缓存管理器-->
    <bean id="cacheManager" class="org.springframework.cache.concurrent.ConcurrentMapCacheManager" />

<beans>


 代码解析:

  2种方式本质上是一样的,都创建一个切面并触发Spring缓存注解的切点

  根据所使用的注解和缓存状态,切面会从缓存中获取数据,并将数据添加到缓存中或者从缓存删除某个值。

  其中不仅启用了注解驱动的缓存,还声明了一个缓存管理器(cache manager)的bean。

  缓存管理器是SPring缓存抽象的狠心,可以和不同的缓存实现进行集成。

  2个例子都是用ConcurrentHashMap作为缓存存储,是基于内存的,用在开发或者测试可以,但是在大型企业级应用程序就有其他更好的选择了。

13.1.2、 配置缓存管理器

Spring3.1内置五个缓存管理器实现:

  • SimpleCacheManager
  • NoOpCacheManager
  • ConcurrentMapCacheManager
  • CompositeCacheManager
  • EhCacheCacheManager

Spring3.2引入了另外一个缓存管理器实现

  • 这个管理器可以用于基于JCache(JSR-107)的缓存提供商之中

除了核心Spring框架,Spring Data提供了2个缓存管理器实现

  • RedisCacheManager(来自于Spring Data Redis项目)
  • GemfireCacheManager(来自于Spring Data GemFire项目)

我们选择一个缓存管理器,然后在Spring应用上下文中,以bean的形式进行设置。使用不同的缓存管理器会影响数据如何存储,但是不会影响Spring如何声明缓存。

例子——配置EhCache缓存管理器:

  此例子暂时省略。。。

例子——配置Redis缓存管理器

  使用RedisCacheManager,它会与一个Redis服务器协作,并通过RedisTemplate存取条目

  RedisCacheManager要求:

  • 一个RedisConnectFactory实现类的bean
  • 一个RedisTemplate的bean
@Configuration
@EnableCaching
public class CachingConfig {

    //Redis缓存管理器bean
    @Bean
    public CacheManager cacheManager(RedisTemplate redisTemplate) {
        return new RedisCacheManager(redisTemplate);
    }

    //Redis连接工厂bean
    @Bean
    public JedisConnectionFactory redisConnectionFactory() {
        JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
        jedisConnectionFactory.afterPropertiesSet();
        return jedisConnectionFactory;
    }

    //RedisTemplate bean
    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisCF) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>();
        redisTemplate.setConnectionFactory(redisCF);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}


例子——使用多个缓存管理器

使用Spring的CompositeCacheManager

以下例子创建一个CompositeCacheManager的bean,里面配置了多个缓存管理器,会按顺序迭代查找这些缓存管理器里面是否存在值


@Bean
public CacheManager cacheManager(net.sf.ehcache.CacheManager cm,javax.cache.CacheManager jcm){
    CompositeCacheManager cacheManager = new CompositeCacheManager();
    List<CacheManager> managers = new ArrayList<CacheManager>();
    managers.add(new JCacheCacheManager(jcm));
    managers.add(new EhcacheCacheManager(cm));
    manager.add(new RedisCacheManager(reisTemplate()));
    cacheManager.setCacheManagers(managers);
    return cacheManager;
}


13.2、为方法添加注解以支持缓存

设置好了连接工厂和缓存管理器,我们就可以使用注解来设置缓存了。

Spring的缓存抽象很大程度是基于切面构建的,启用了缓存,就会创建一个切面,触发Spring的缓存注解。

这些注解可以用在方法或类上,用在类上,则类的所有方法都会应用该缓存行为。

spring源码学习 github项目_测试_31

13.2.1、填充缓存

@Cacheable和@CachePut

spring源码学习 github项目_测试_32

最简单的情况下只需要使用value属性即可


@Cacheable("spittleCache")
public Spittle findOne(long id){
    return new Spittle ();//可以是具体的逻辑
}


缓存流程:

  • 调用findOne方法
  • 缓存切面拦截
  • 缓存切面在缓存中名为spittleCache的缓存数据中,key为id的值
  • 如果有值,则直接返回缓存值,不再调用方法。
  • 如果无值,则调用方法,并将结果放到缓存中。

缓存注解也可以写在接口上,那么它的所有实现类都会应用这个缓存规则。

将值放到缓存中:

一个使用情景:我们保存一个数据,然后前台刷新或者其他用户获取,我们可以保存时直接更新缓存


@CachePut("spittleCache")
Spittle save(Spittle spittle);


自定义缓存key:

上面例子中,默认把Spittle参数作为缓存的key,这样并不合适,我们希望用Spittle的ID作为key值

但是Spittle未保存情况下,又未有ID,这样我们可以通过SpEL表达式解决这个问题

spring源码学习 github项目_spring源码学习 github项目_33


@CachePut(value="spittleCache",key="#result.id")
Spittle save(Spittle spittle);


条件化缓存:

使用unless和condition属性,接受一个SpEL表达式

unless:如果为false,调用方法时依然会在缓存中查找,只是阻止结果放进缓存;

condition:如果是false,则禁用整个缓存,包括方法调用前的缓存查找和方法调用后把结果放进缓存都被禁用


@Cacheable(value="spittleCache" 
unless="#result.message.contains('NoCache')")
Spittle findOne(long id);


@Cacheable(value="spittleCache" 
unless="#result.message.contains('NoCache')" 
condition="#id >= 10")
Spittle findOne(long id);


13.2.2、移除缓存条目

@CacheEvict注解

调用方法时,会删除该缓存记录,


@CacheEvict("spittleCache")
void remove(long spittleId);

 

spring源码学习 github项目_web.xml_34

13.3、使用XML声明缓存

有时候无法是注解,或者不想使用注解,可以使用XML声明,这里暂不介绍。

【暂略】十四、保护方法应用

【------------------------Spring集成------------------------】

【暂略】十五、使用远程服务

十六、使用Spring MVC创建REST API

16.1、REST介绍

1、什么是REST

REST的名称解释:

SOAP:简单对象访问协议(英文:Simple Object Access Protocol,简称SOAP)。

REST:表述性状态传递(英文:Representational State Transfer,简称REST)。

REST是比SOAP更简单的一个Web应用可选方案。

REST是一种面向资源的架构风格,强调描述应用程序的事物和名词。

  • Representational :表述性,REST资源可以使用各种不同的形式进行表述,如XML,JSON,HTML;
  • State:状态,使用REST的时候,我们关注的是资源的状态,而不是行为;
  • Transfer:转移,REST的资源,通过某种形式的表述,在应用之间传递转移。

简洁地说,REST就是将资源的状态,以最合适客户端或服务器的表述方式,在服务器与客户端之间转移。

REST与HTTP方法:

URL:REST中,资源通过URL定位和识别。虽然没有严格的URL格式定义,但是一个URL应该能识别资源,而不是简单的一个命令。因为REST的核心是资源,而不是行为。

行为:REST中也有行为,但是不是在URL中体现,一般通过HTTP行为来定义,例如CRUD

  • Creat:POST
  • Read:GET
  • Update:PUT/PATCH
  • Delete:Delete
2、Spring对REST的支持
  • a、控制器支持所有HTTP方法,包含POST/GET/PUT/DELETE,Spring3.2及以上版本还包含PATCH。
  • b、@PathVariable注解使控制器可以处理参数化URL
  • c、Spring的视图和视图解析器,资源可以以多种方式表述,包括将模型数据渲染成XML/JSON/Atom/RSS的View实现。
  • d、可以使用ContentNegotiatingViewResolver来选择客户端最适合的表述。
  • e、使用@Response注解的各种HttpMethodConverter实现,能够替换基于视图的渲染方式。
  • f、使用@Response注解的各种HttpMethodConverter可以将传入的HTTP数据转化为控制器处理方法的Java对象。
  • g、借助RestTemplate,Spring应用能够方便地使用REST资源。

16.2、创建REST端点

需要实现RESTful功能的Spring MVC控制器


@Controller
@RequestMapping("/spittle")
public class SpittleApiController {
    private static final String MAX_LONG_AS_STRING = "9223372036854775807";
    private SpittleRepository spittleRepository;
    @Autowired
    public SpittleApiController(SpittleRepository spittleRepository) {
        this.spittleRepository = spittleRepository;
    }
    @RequestMapping(method = RequestMethod.GET)
    public List<Spittle> spittles(@RequestParam(value = "max", defaultValue = MAX_LONG_AS_STRING) long max,
            @RequestParam(value = "count", defaultValue = "20") int count) {
        return spittleRepository.findSpittles(max, count);
    }
}


1、Spring提供了2种将Java表述形式转换为发给客户端的表述形式的方式

内容协商

选择一个视图,能够将模型渲染为呈现给客户端的表述形式

这种方式一般不用

消息转换器

使用HTTP信息转换器

这是一种直接的方式,将控制器的数据转换为服务于客户端的表述形式。

当使用消息转换功能时, DispatcherServlet不用麻烦地将模型传递给视图中。

这里没有视图,甚至没有模型,只有控制器产生的数据,然后经过消息转换器,产生资源表述,传到客户端。

Spring中自带了各种各样的转换器,能够满足常见的对象转换为表述的需求。

例如,客户端通过请求的Accept头信息表明它能接受"application/json",并且Jackson JSON在类路径下,那么处理方法返回的对象将交给MappingJacksonHttpMessageConverter,由他转换为返回客户端的JSON表述形式。

或者,如果请求的头信息表明客户端想要"text/xml"格式,那么Jaxb2RootElementHttpMessageConverter将会为客户端产生XML表述形式。

spring源码学习 github项目_web.xml_35

除了其中5个外,其它都是自动注册的,不需要Spring配置;但是为了支持他们,需要把对应的库添加到类路径中。

2、消息转换器的具体用法

在响应体中返回资源状态

正常情况,如果控制器方法返回Java对象,这个对象会放到模型中,并在视图中渲染。

为了使用消息转换功能,我们需要告诉Spring跳过正常的模型/视图流程,并使用消息转换器。

最简单的方式:使用@ResponseBody


@RequestMapping(method = RequestMethod.GET, produces = "application/json")
public @ResponseBody List<Spittle> spittles(@RequestParam(value = "max", defaultValue = MAX_LONG_AS_STRING) long max,
        @RequestParam(value = "count", defaultValue = "20") int count) {
    return spittleRepository.findSpittles(max, count);
}


@ResponseBody会告诉Spring,我们要将返回的对象作为资源发送给客户端,并转换为客户端要求的表述形式。

DispatcherServlet会根据请求中Accept头部信息,找到对应的消息转换器,然后把Java对象转换为客户端需要的表述形式。

例如客户端请求的Accept头部信息表明它接收"application/json",且Jackson JSON库位于应用的类路径下,那么将选择MappingJacksonHttpMessageConverter或MappingJackson2HttpMessageConverter(取决于类路径下是哪个版本的Jackson)作为消息转换器,并将Java对象转换为JSON文档,写入到相应体中。

spring源码学习 github项目_spring源码学习 github项目_36

@ RequestMapping中produces属性,代表该控制器只处理预期输出为JSON的请求,也就是Accept头信息包含"application/json"的请求。

其它类型请求,即使URL匹配且为GET请求,也不会被处理。

这样的请求会被其它的方法进行处理(有适当方法的情况下),或者返回HTTP 406(Not Acceptable)响应。

请求体中接收资源状态

上面只讨论了如何将一个REST资源转换为客户端所需要的表述,这里讨论如何将客户端发送过来的资源状态表述转换为JAVA对象。

使用@RequestBody注解


@RequestMapping(method = RequestMethod.POST, consumes = "application/json")
public @ResponseBody Spittle saveSpittle(@RequestBody Spittle spittle) {
    return spittleRepository.save(spittle);
}


@RequestBody表明

  • a、这个控制器方法,只能处理/spittles(定义在类级别上了)的POST请求,而且请求中预期要包含一个Spittle的资源表述
  • b、Spring会根据请求的Content-Type头信息,查找对应的消息转换器,把客户端发送的资源的表述形式(JSON,或HTMl)转化为Java对象

为控制器默认设置消息转换

@RestController注解(Spring 4.0及以上)

使用@RestController代替@Controller标注控制器,Spring会为所有方法应用消息转换功能,我们就不用每个方法添加@ResponseBody,当然@RequestBody如果需要使用到,是不能省略的

16.3、提供资源之外的其它内容

1、发送错误信息到客户端


@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public @ResponseBody Spittle spittleById(@PathVariable Long id) {
    return spittleRepository.findOne(id);
}


上述方法是查找一个Spittle对象。

假设找不到,则返回null,这时候,返回给客户端的HTTP状态码是200(OK),代表事情正常运行,但是数据是空的。

而我们希望这种情况下或许可以使状态码未404(Not Found),这样客户端就知道发生了什么错误了。

要实现这种功能,Spring提供了一下几种方式:

  • a、使用@ResponseStatus注解指定状态码
  • b、控制器方法可以返回ResponseEntity对象,改对象能够包含更多响应相关元数据
  • c、异常处理器能够应对错误场景,这样处理器方法就能关注于正常的状况

使用ResponseEntity

使用ResponseEntity对象替代@ResponseBody


@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ResponseEntity<Spittle> spittleById(@PathVariable Long id) {
    return spittleRepository.findOne(id);
    HttpStatus status = spittle != null ? HttpStatus.OK : HttpStatus.NOT_FOUND;
    return new ResponseEntity<Spittle>(spittle, status);
}


使用ResponseEntity,可以指定HTTP状态码,而且本身也包含@ResponseBody的定义,相当于使用了@ResponseBody。

接下来,我们希望如果找不到Spittle对象时,返回错误信息

可以创建一个Error类,然后使用泛型,在找不到Spittle对象时,返回一个Error对象


@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ResponseEntity<?> spittleById(@PathVariable Long id) {
    return spittleRepository.findOne(id);
    if (spittle == null){
        Error error = new Error(4, "Spittle [" + id + "] not found");
        return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
    }
    return new ResponseEntity<Spittle>(spittle, HttpStatus.OK);
}


但是,这种写法貌似使代码变得有点复杂,我们可以考虑使用错误处理器。

处理错误

步骤1:创建一个异常类


public class SpittleNotFoundException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    private long spittleId;
    public SpittleNotFoundException(long spittleId) {
        this.spittleId = spittleId;
    }
    public long getSpittleId() {
        return spittleId;
    }
}


步骤2:在控制器下创建一个错误处理器的处理方法


@ExceptionHandler(SpittleNotFoundException.class)
public ResopnseEntity<Error> spittleNotFound(SpittleNotFoundException e) {
    long spittleId = e.getSpittleId();
    Error error =  new Error(4, "Spittle [" + spittleId + "] not found");
    return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
}


@ExceptionHandler注解加到控制器方法中,可以处理对应的异常。

如果请求A发生异常,被这个方法捕获到该异常, 那么请求的返回值就是这个异常处理器的返回值。

步骤3:原来的业务控制器方法可以得到简化


@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ResponseEntity<Spittle> spittleById(@PathVariable Long id) {
    return spittleRepository.findOne(id);
    if (spittle == null) { throw new SpittleNotFoundException(id); }
    return new ResponseEntity<Spittle>(spittle, HttpStatus.OK);
}


又因为此时任何时候,这个控制方法都有数据返回,所以HTTP状态码始终是200,所以可以不使用ResponseEntity,二使用@ResponseBody;如果控制器上面还使用了@RestController,我们又可以把@ResponseBody省掉, 最后得到代码


@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public Spittle spittleById(@PathVariable Long id) {
    return spittleRepository.findOne(id);
    if (spittle == null) { throw new SpittleNotFoundException(id); }
    return Spittle;
}


步骤4:同理,可以简化一下错误处理器


@ExceptionHandler(SpittleNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Error spittleNotFound(SpittleNotFoundException e) {
    long spittleId = e.getSpittleId();
    return new Error(4, "Spittle [" + spittleId + "] not found");
}


在简化过程中,我们都希望避免使用ResponseEntity,因为他会导致代码看上来很复杂。

但是有的情况必须使用ResponseEntity才能实现某些功能。

在响应中设置头部信息  

要使用到ResponseEntity,比如说我新建一个Spittle后,系统通过HTTP的Location头部信息,返回这个Spittle的URL资源地址。

这里暂时不讨论

【暂略】16.4、编写REST客户端

【暂略】十七、Spring消息

【暂略】十八、使用WebSocket和STMOP实现消息功能

【暂略】十九、使用Spring发送Email

【暂略】二十、使用JMX管理Spring Bean

【暂略】二十一、使用Spring Boot简化Spring开发

--

实例

实例01:@ResponseBody使用实体接收数据,发送实体内没有实体时,报错

SpringMVC中默认的序列化器只支持子集传入,可以注入bean,使用fastJson作为MVC使用的序列化和反序列化器


@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
    FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
    FastJsonConfig fastJsonConfig = new FastJsonConfig();
    fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
    fastConverter.setFastJsonConfig(fastJsonConfig);
    HttpMessageConverter<?> converter = fastConverter;
    return new HttpMessageConverters(converter);
}