本文篇幅较长,建议合理利用右上角目录进行查看(若是没有目录请刷新)。前端
本文是对《SPRING实战第4版》的总结,你们也能够去仔细研读该书java
为了简化Java开发,Spring使用了如下几个关键策略web
一、基于POJO的轻量级和最小侵入编程正则表达式
二、基于依赖注入和面向接口实现松耦合redis
在传统编程中,不免会有如下这种编程情景(多个类交互)spring
public class DamselRescuingKnight implements Knight { private RescueDamselQuest quest; public DamselRescuingKnight() { this.quest = new RescueDamselQuest(); } public void embarkOnQuest() { quest.embark(); } }
DamselRescuingKnight中,经过new 建立了一个RescueDamselQuest实例,2个类造成了紧耦合。sql
其中的依赖关系是DamselRescuingKnight依赖RescueDamselQuest,并且限制了embarkOnQuest方法的实现数据库
并且,没法对DamselRescuingKnight进行单元测试,由于embarkOnQuest方法须要调用RescueDamselQuest的embark方法,而仅在这个方法内,并无这个方法的实现编程
耦合的两面性:必须的(复杂逻辑必然有多个类进行交互)、过于耦合将致使难以测试、复用、难以理解json
依赖注入:将对象的依赖关系交给第三方来进行建立和管理
public class BraveKnight implements Knight { private Quest quest; public BraveKnight(Quest quest) { this.quest = quest; } public void embarkOnQuest() { quest.embark(); } }
上面使用了依赖注入的一种方式:构造器注入;并且传入的是一个Quest接口,对象和依赖对象的具体实现没有耦合关系,就造成了松耦合。
并且,此状况下,可使用mock(一种测试方式,具体自行学习)实现单元测试。
Spring能够做为一个依赖注入容器,经过不一样方式注入,也有相应的配置,装配策略,留到后面讲解。
三、基于切面和惯例进行声明式编程
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方案,能够经过配置文件等方式,将这个日志类的相关代码从这个业务类中去除,从而实现解耦;具体方式后面介绍
四、经过切面和模板减小样板式代码
例如:以往使用JDBC进行操做数据库时,每次操做,都有不少链接数据库,断开链接等代码和业务代码交织在一齐
而Spring则提供了如jdbcTemplate等类,对这些样板式代码进行简化
Spring框架中对象是由Spring建立和管理的,本节讨论Spring如何管理这些Bean
Spring提供一个或多个Spring容器,Spring容器负责建立、装配、配置、管理对象的整个生命周期
Spring自带多种应用上下文:
建立Spring容器例子:ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/knight.xml");
经过context.getBean()方法便可获取容器内的Bean
普通Java应用程序中,bean的生命周期很简单,就是经过new实例化,就可使用了,当bean再也不被使用,就会被自动回收。
而Spring中的bean的生命周期就很复杂,并且很是重要,由于有时候对这个生命周期的扩展点进行自定义
由前面的内容知道,Spring框架致力于经过DI、AOP和消除模板式代码来简化企业级JAVA开发
而在整个Spring框架范畴内,Spring不只提供了多种简化开发的方式,还构建了一个在核心框架上的庞大生态圈,将Spring扩展到不一样领域,如Web服务、REST、移动开发,NoSQL等
如Spring4.0中,Spring框架发布版本中包含20个不一样模块,每一个模块有3个JAR文件(二进制类库、源码、JavaDoc)
Spring发布版本中lib目录下的JAR文件
Spring Portfolio包含构建于核心Spring框架之上的框架和类库,归纳地说,Spring Portfolio为每个领域的Java开发都提供了Spring编程模型
最佳实践:建议是尽量使用自动配置,减小显式配置;当须要显式配置,推荐使用类型安全的JavaConfig;最后再使用XML配置
标明这个类是一个组建类,告知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")
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 })
推荐在包中建立标识接口,这样在重构系统的时候,不会对这个配置形成影响
能够用在类中任何方法内(包括构造方法、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(); } }
例如,须要将第三方库的组件加载到你的应用中,此时没法给他的类上添加@Component和@Autowired注解,此时不能使用自动化装配了。
这种状况下,就必须使用显式装配的形式,能够选择Java代码装配或Xml装配
建议:显式配置是优先使用JavaConfig装配,由于他强大、类型安全且对重构友好;由于他和业务代码无关,应放到单独的包中
告诉Spring,这是一个Spring配置类,用来配置Spring应用上下文如何配置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); } }
Spring刚出现时,用XML描述配置是主要方式;如今有了强大的自动化配置和Java配置,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
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属性
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 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-模式:
首先,使用这个命名控件要在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-模式:
首先,使用这个命名控件要在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-命名控件的所有元素:
其中一个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 { }
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加载进来
<import resource="cd-config.xml" /> <bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />
<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" />
程序运行有多个环境,好比开发环境、测试环境、生产环境等。
在每一个环境中,不少东西有不一样的作法,如使用的数据库等。
为了不在切换运行环境是须要对程序进行大量修改,Spring提供了profile设置,使程序能对不一样环境作出对应的处理。
使用profile,可使一个部署单元(如war包)能够用于不一样的环境,内部的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>
bean根据profile的建立步骤:
spring.profiles.active,spring.profiles.default2个参数的设置方式:
具体写法,后面的例子会讲到
使用@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); } }
@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接口:
使用自动装配时,若是有多个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异常。
//使用@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出现没法选择的状况,致使错误
一、使用默认限定符:
@Autowired @Qualifier("iceCream") public void setDessert(Dessert dessert){ this.dessert = dessert; }
@Qualifier注解设置的参数是要注入的bean的ID,若是没有为bean设定ID,则为首字母小写的类名(也称这个bean的默认限定符)
二、为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; }
三、使用自定义的限定符注解:
假设已经有一个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; }
默认状况下,bean是单例的形式建立的,既整个应用程序使用的bean是同一个实例。
有些状况,若是想重用这个bean,结果这个bean被以前的操做污染了,会使程序发生错误。
Spring支持为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(){...}
一样,请求做用域也是有同样的问题,一样处理便可。
引用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>
以前讨论的依赖注入,主要关注将一个bean引用注入到另外一个bean的属性或构造器参数中。
依赖注入还有另一方面:将值注入到bean的属性或构造器参数中。
固然可使用以前说过的硬编码,可是咱们这里但愿这些值在运行时肯定。
Spring提供2种运行时求值方式:属性占位符(Property placeholder)、Spring表达式语言(SpEL)
使用@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()方法,进行检索属性。
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)
检查profile的激活状态
解析属性占位符“${...}”以及注解@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}" />
Spring 3引入了Spring表达式语言(Spring Expression Language,SpEL),可以强大和简洁地把值装配到bean属性和构造器参数中。
SpEL的一些特性:
2.一、表示字面值:
#{1}//整型 #{3.14159}//浮点数 #{9.87E4}//科学计数法 #{'Hello'}//String类型字面值 #{false}//Boolean类型
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.三、表达式中使用类型:
主要做用是调用类的静态方法和变量。使用T()运算符,得到Class对象。
#{T(java.lang.Math)}//获得一个class对象 #{T(java.lang.Math).PI}//类的常量 #{T(java.lang.Math).random()}//类的静态方法
2.四、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.五、计算正则表达式:
使用matches运算符,返回Boolean类型值
#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}//验证有效邮件
2.六、计算集合:
#{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.七、这里介绍的SpEL只是冰山一角,有须要的去查阅相关资料
为何须要面向切面编程(AOP)技术:
什么是面向切面编程(AOP)技术:
如上所述,切面能够帮助咱们模块化横切关注点。
通常若是咱们须要重用通用功能的话,常见的面向对象技术是继承或委托。
可是若是整个应用中都使用一样的基类,继承每每会致使一个脆弱的对象体系;
而使用委托可能须要对委托对象进行复杂的调用。
而切面提供了另外一种可选方案:在一个独立的地方定义通用功能,经过声明的方式定义此功能用何种方式在何处应用,而无需修改受影响的类。横切关注点能够被模块化特殊的类,这些类称为切面。
这样作有2个好处:
通知(Advice):
通知定义了切面是什么以及什么时候使用。除了描述切面要完成的工做,通知还解决了什么时候执行这个工做的问题。
Spring切面有5种类型的通知:
链接点(Join point):
链接点是咱们的程序能够应用通知的实际,是应用执行过程当中可以插入切面的一个点。
切面代码能够利用这些点插入到应用的正常流程之中,并添加新的行为。
切点(Poincut):
切点指切面所通知的链接点的范围。
一个切面不须要通知整个程序的链接点,经过切点,定义了切面所应用的范围。
通知定义了切面是“什么”以及“什么时候”执行,切点定义了“何处”须要执行切面。
切面(Aspect):
切面是通知和切点的结合,定义了须要作什么,在什么时候,何处完成该功能。
引入(Introduction):
引入容许咱们想现有的类添加新的方法或属性。
能够在不修改现有类的状况下,将新的方法和实例变量引入到该类中。
织入(Weaving):
织入是把切面应用到目标对象并建立新的代理对象的过程。
切面在指定的链接点被织入到目标对象中。
在目标对象的声明周期有几个点能够进行织入:
Spring支持4种类型的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
Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。
Spring仅支持AspectJ切点指示器的一个子集。
Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的。
其中,只有execution指示器是实际执行匹配的,其它指示器都是用来限制匹配的。
Spring中尝试使用AspectJ其它指示器时,会抛出IllegalArgument-Exception异常
假设有一个接口
public interface Performance{ public void perform(); }
使用切点表达式设置当perform()方法执行时触发通知的调用:
使用within()指示器限制仅匹配concert包
支持的关系操做符有且(&&),或(||),非(!)
若是用XML配置,由于&在XML中有特殊含义,因此可使用and,or,not来做为关系操做符
Spring引入了一个新的bean()指示器,经过bean的ID来限制bean
限制id为woodstock的bean才应用通知
限制id非woodstock的bean才应用通知
AspectJ5以前,编写AspectJ切面须要学习一种Java语言的扩展。
AspectJ5引入了使用注解来建立切面的关键特性,AspectJ面向注解的模型能够很是简便地经过注解把任意类转变为切面。
@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注解定义成一个可重用的节点,而后经过“方法名()”来进行引用
启用自动代理:
以上仅仅是定义了切面,要启动切面功能,须要启动切面的代理
@Configuration @EnableAspectJAutoProxy//启动AspectJ自动代理 @ComponentScan public class ConcertConfig{ @Bean Public Audience audience(){//声明Audience bean return new Audience(); } }
<?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。
@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对象来进行对目标方法的调用。
@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; } }
假设情景:有一个类,但愿让其以及其实例实现某个接口,但这个类是不能够修改的(如没有源码)。咱们能够经过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的组成部分
一、Value属性指定哪一种类型bean要引入该接口。本例中,指全部实现Performance的类型。(加号表示是Performance的全部子类型,而不是Performance自己。)
二、defaultImpl属性指定为引入功能提供实现的类。在这里咱们指定的是DefaultEncoreable提供实现。
三、@DeclareParents注解所标注的惊天属性知名了要引入的接口。在这里咱们引入的是Encoreable接口。
在基于HTTP协议的Web应用中,Spring MVC将用户请求在调度Servlet、处理器映射、控制器以及视图解析器之间移动,再将用户结果返回给用户。
咱们将介绍请求如何从客户端发起,通过Spring MVC中的组件,最终回到客户端。
请求:请求离开浏览器时,带有用户请求内容的信息。通常包含请求的URL、用户提交的表单信息等。
2.一、配置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.二、两个应用上下文之间的关系
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.三、启动Spring MVC
传统方法:使用XML配置Spring MVC组件
如今方法:使用Java配置Spring MVC组件
最简单的Spring MVC配置
@Configuration @EnableWebMvc public class WebConfig { }
这样就能启动Spring MVC,可是有如下问题:
如下配置便可解决上述问题
@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); } }
RootConfig的配置
@Configration @ComponentScan(basePackages={"spitter"}, excludeFilters={ @filter{type=FilterType.ANNOTATION, value=EnableWebMvc.class) }) public class RootConfig{ }
使用@ComponentScan注解,后期可使用不少非Web组件来完善RootConfig。
@RequestMapping注解:
声明这个控制器所要处理的请求
@Controller public class HomeController{ @RequestMapping(value="/", method=GET) public String home(){ return "home"; } }
1.一、声明控制器
2种方式让控制器类能被扫描称为组件:
1.二、指定处理请求路径
@RequestMapping(value="/",method=GET)
value表明要处理的请求路径,method属性指定所处理的HTTP方法
1.三、返回视图名称
return "home";
返回一个字符串,表明须要渲染的视图名称。DispatcherServlet会要求视图解析器将这个逻辑名称解析为实际的视图。
基于咱们在InternalResourceViewResolver的配置,视图名“home”将被解析为“/WEB-INF/views/home.jsp”路径的JSP。
控制器自己也是一个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请求并设置指望获得的视图名称。
对类使用@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"; } }
使用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()))); }
场景:咱们须要分页查找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()))); }
按上面讲解的写法:
@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)); }
使用路径传参只使用于少许参数的状况,若是须要传递大量数据,则须要使用表单。
Spring Security是一种基于Spring AOP和Servlet规范中filter实现的安全框架
Spring Security从2个角度解决安全问题
Spring Security分为11个模块
要使用Spring Security,至少要引入Core和Configuration2个模块
简化:使用一个特殊的filter:DelegatingFilterProxy,这个filter是一个代理类,会把工做交给Spring上下文中的javax.servlet.filter实现类
在实现了WebSecurityConfigurer的bean中使用注解@EnableWebSecurity
更简化:在扩展类WebSecurityConfigurerAdapter中使用注解@EnableWebSecurity;若是使用Spring MVC,则须要使用注解@EnableWebMvcSecurity
能够经过重写如下3个方法,对Spring Security的行为进行配置
Spring Security须要配置用户信息服务,代表哪些用户能够进行访问
待补充
待补充
Redis是一种基于key-value存储的数据库,Spring Data没有把Repository生成功能应用到Redis中,而是使用面向模版的数据访问的方式来支持Redis的访问。
Spring Data Redis经过提供一个链接工厂来建立模版
建立链接工厂:
为了链接和访问Redis,有不少Redis客户端,如Jedis、JRedis等。
Spring Data Redis为4种Redis客户端实现提供了链接工厂:
选择哪一个客户端实现取决于你,对于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)也是相同的,因此他们在配置方面是相同的操做。
获取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个模版:
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);
当某个条目保存到Redis key-value存储的时候,key和value都会使用Redis的序列化器进行序列化。
Spring Data Redis提供了多个这样的序列化器,包括:
这些序列化器都实现了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; }
一些变更不频繁或者不变更的数据,当每次获取的时候,都须要从数据库中提取或者计算,每次都须要消耗资源。
咱们能够把这些计算后的结果,在某个地方存放起来,当下次访问的时候直接返回,就能够避免了屡次的资源消耗,这就叫缓存技术。
@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做为缓存存储,是基于内存的,用在开发或者测试能够,可是在大型企业级应用程序就有其余更好的选择了。
Spring3.1内置五个缓存管理器实现:
Spring3.2引入了另一个缓存管理器实现
除了核心Spring框架,Spring Data提供了2个缓存管理器实现
咱们选择一个缓存管理器,而后在Spring应用上下文中,以bean的形式进行设置。使用不一样的缓存管理器会影响数据如何存储,可是不会影响Spring如何声明缓存。
例子——配置EhCache缓存管理器:
此例子暂时省略。。。
例子——配置Redis缓存管理器:
使用RedisCacheManager,它会与一个Redis服务器协做,并经过RedisTemplate存取条目
RedisCacheManager要求:
@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; }
设置好了链接工厂和缓存管理器,咱们就可使用注解来设置缓存了。
Spring的缓存抽象很大程度是基于切面构建的,启用了缓存,就会建立一个切面,触发Spring的缓存注解。
这些注解能够用在方法或类上,用在类上,则类的全部方法都会应用该缓存行为。
@Cacheable和@CachePut
最简单的状况下只须要使用value属性便可
@Cacheable("spittleCache") public Spittle findOne(long id){ return new Spittle ();//能够是具体的逻辑 }
缓存流程:
缓存注解也能够写在接口上,那么它的全部实现类都会应用这个缓存规则。
将值放到缓存中:
一个使用情景:咱们保存一个数据,而后前台刷新或者其余用户获取,咱们能够保存时直接更新缓存
@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);
@CacheEvict注解
调用方法时,会删除该缓存记录,
@CacheEvict("spittleCache") void remove(long spittleId);
有时候没法是注解,或者不想使用注解,可使用XML声明,这里暂不介绍。
REST的名称解释:
SOAP:简单对象访问协议(英文:Simple Object Access Protocol,简称SOAP)。
REST:表述性状态传递(英文:Representational State Transfer,简称REST)。
REST是比SOAP更简单的一个Web应用可选方案。
REST是一种面向资源的架构风格,强调描述应用程序的事物和名词。
简洁地说,REST就是将资源的状态,以最合适客户端或服务器的表述方式,在服务器与客户端之间转移。
REST与HTTP方法:
URL:REST中,资源经过URL定位和识别。虽然没有严格的URL格式定义,可是一个URL应该能识别资源,而不是简单的一个命令。由于REST的核心是资源,而不是行为。
行为:REST中也有行为,可是不是在URL中体现,通常经过HTTP行为来定义,例如CRUD
须要实现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); } }
内容协商
选择一个视图,可以将模型渲染为呈现给客户端的表述形式
这种方式通常不用
消息转换器
使用HTTP信息转换器
这是一种直接的方式,将控制器的数据转换为服务于客户端的表述形式。
当使用消息转换功能时, DispatcherServlet不用麻烦地将模型传递给视图中。
这里没有视图,甚至没有模型,只有控制器产生的数据,而后通过消息转换器,产生资源表述,传到客户端。
Spring中自带了各类各样的转换器,可以知足常见的对象转换为表述的需求。
例如,客户端经过请求的Accept头信息代表它能接受"application/json",而且Jackson JSON在类路径下,那么处理方法返回的对象将交给MappingJacksonHttpMessageConverter,由他转换为返回客户端的JSON表述形式。
或者,若是请求的头信息代表客户端想要"text/xml"格式,那么Jaxb2RootElementHttpMessageConverter将会为客户端产生XML表述形式。
除了其中5个外,其它都是自动注册的,不须要Spring配置;可是为了支持他们,须要把对应的库添加到类路径中。
在响应体中返回资源状态
正常状况,若是控制器方法返回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代表
为控制器默认设置消息转换
@RestController注解(Spring 4.0及以上)
使用@RestController代替@Controller标注控制器,Spring会为全部方法应用消息转换功能,咱们就不用每一个方法添加@ResponseBody,固然@RequestBody若是须要使用到,是不能省略的
@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提供了一下几种方式:
使用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.四、编写REST客户端
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); }