一、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有多个容器实现,可以分为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的生命周期就很复杂,而且非常重要,因为有时候对这个生命周期的扩展点进行自定义
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 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" />
其中,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" />
用字面量注入以及装配集合:
<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-命名控件的全部元素:
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接口:
AnnotatedTypeMetadata接口:
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表达式上做运算
#{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、服务模块更简洁,因为它们只关注核心代码,次要代码被转移到切面中。
AOP术语:
通知(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在运行期把切面织入到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切点指示器:
其中,只有execution指示器是实际执行匹配的,其它指示器都是用来限制匹配的。
Spring中尝试使用AspectJ其它指示器时,会抛出IllegalArgument-Exception异常
编写切点:
假设有一个接口
public interface Performance{
public void perform();
}
使用切点表达式设置当perform()方法执行时触发通知的调用:
使用within()指示器限制仅匹配concert包
支持的关系操作符有且(&&),或(||),非(!)
如果用XML配置,因为&在XML中有特殊含义,所以可以使用and,or,not来作为关系操作符
切点中选择bean:
Spring引入了一个新的bean()指示器,通过bean的ID来限制bean
限制id为woodstock的bean才应用通知
限制id非woodstock的bean才应用通知
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等注解,用来声明通知方法。
使用@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;
}
}
4、通过注解引入新功能
假设情景:有一个类,希望让其以及其实例实现某个接口,但这个类是不可以修改的(如没有源码)。我们可以通过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 Web------------------------】
五、构建Spring Web应用程序
5.1、Spring MVC起步
在基于HTTP协议的Web应用中,Spring MVC将用户请求在调度Servlet、处理器映射、控制器以及视图解析器之间移动,再将用户结果返回给用户。
我们将介绍请求如何从客户端发起,经过Spring MVC中的组件,最终回到客户端。
1、跟踪Spring MVC的请求
请求:请求离开浏览器时,带有用户请求内容的信息。一般包含请求的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上下文之中。
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())));
}
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 Security,至少要引入Core和Configuration2个模块
9.1、Spring Security简介
开发方式的演进:
- Spring Security基于Servlet Filter,在web.xml或webApplicationInitializer中配置多个filter
简化:使用一个特殊的filter:DelegatingFilterProxy,这个filter是一个代理类,会把工作交给Spring上下文中的javax.servlet.filter实现类
- Spring 3.2带来了新的Java配置方式
在实现了WebSecurityConfigurer的bean中使用注解@EnableWebSecurity
更简化:在扩展类WebSecurityConfigurerAdapter中使用注解@EnableWebSecurity;如果使用Spring MVC,则需要使用注解@EnableWebMvcSecurity
Spring Security的配置
可以通过重写以下3个方法,对Spring Security的行为进行配置
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存取数据
//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的缓存注解。
这些注解可以用在方法或类上,用在类上,则类的所有方法都会应用该缓存行为。
13.2.1、填充缓存
@Cacheable和@CachePut
最简单的情况下只需要使用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表达式解决这个问题
@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);
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表述形式。
除了其中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文档,写入到相应体中。
@ 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);
}