Spring之旅 Spring是一个开源框架,Spring根本使命:简化Java开发。
为了简化java开发的复杂性,Spring使用如下4种策略:
1、基于POJO(Plain Old Java Object 简单Java对象 )的轻量级和最小侵入性编程。
2、通过依赖注入和面向接口实现松耦合。
3、基于切面和惯例进行声明式编程。
4、通过切面和模板减少样板式代码。
这里以一个骑士出征的例子来解释一些相关概念
首先定义一个骑士接口,其中有一个embarkOnQuest方法。
1 2 3 4 5 6 7 package hoo.knights;public interface Knight { void embarkOnQuest () ; }
然后再定义一个远征行动接口。
1 2 3 4 5 package hoo.knights;public interface Quest { void embark () ; }
下一步定义一个拯救少女行动类
1 2 3 4 5 6 7 8 9 package hoo.knights;public class RescueDamselQuest implements Quest { @Override public void embark () { System.out.println("Embarking on a quest to rescue the damsel" ); } }
再定义一个屠杀巨龙行动类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package hoo.knights;import java.io.PrintStream;public class SlayDragonQuest implements Quest { private PrintStream printStream; public SlayDragonQuest (PrintStream printStream) { this .printStream = printStream; } @Override public void embark () { printStream.println("Embarking on quest to slay the dragon" ); } }
接下来定义一个英勇的骑士类实现骑士接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 javapackage hoo.knights; public class BraveKnight implements Knight { private Quest quest; public BraveKnight (Quest quest) { this .quest = quest; } @Override public void embarkOnQuest () { quest.embark(); } }
到这里有人可能就写成下面这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package hoo.knights;public class BraveKnight implements Knight { private Quest quest; public BraveKnight () { quest = new RescueDamselQuest(); } @Override public void embarkOnQuest () { quest.embark(); } }
但是有一种情况:骑士不仅仅可以去杀龙还可以去拯救少女这里将它写死就不太合适,采用传递一个Quest对象,只要实现了Quest类的接口,任何类都可以传进来,骑士就可以去做很多行动,不是只执行一个任务。
这是依赖注入(DI)的一种方式:构造器注入
同样SlayDragonQuest类中也有依赖注入:将一个PrintStream对象注入到SlayDragonQuest中。
骑士出征之前和之后需要法师的吟唱,骑士类拥有法师类对象似乎不是一个好的选择,法师不应该被骑士拥有,有的骑士也不想法师吟唱这里先定义一个法师类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package hoo.knights;import java.io.PrintStream;public class Minstrel { private PrintStream printStream; public Minstrel (PrintStream printStream) { this .printStream = printStream; } public void singBeforeQuest () { printStream.println("Fa la la,the knight is so brave!" ); } public void singAfterQuest () { printStream.println("Tee hee hee , the brave knight did embark on a quest" ); } }
法师的吟唱是骑士的出征非关键因素,这里利用AOP来实现法师吟唱模块化。
有多个Quest对象可以被注入到BraveKnight中,到底选择哪一个来注入呢??
这里利用XML来正确装配这些对象。
装配的方式有:自动装配、XML显式装配、Java显式装配。
先定义knight.xml来解决骑士到底要做什么的问题。
1 2 3 4 5 6 7 8 9 10 11 <?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" > <bean id="knight" class ="hoo.knights.BraveKnight" > <constructor-arg ref="quest" /> </bean> <bean id="quest" class ="hoo.knights.SlayDragonQuest" > <constructor-arg value="#{T(System).out}" /> </bean> </beans>
使用 声明一个bean 其中id为自己指定的名字 class为bean的类
元素为构造器参数 ref为依赖的bean,第一个constructor元素就决定了将一个id为quest的bean注入到id为knight的对象中。第二个constructor元素中 value代表将一个System.out值传入构造器。
第一个constructor元素就决定了骑士去杀龙而不是拯救少女。
等等,法师吟唱在哪???
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd" > <bean id="knight" class ="hoo.knights.BraveKnight" > <constructor-arg ref="quest" /> </bean> <bean id="quest" class ="hoo.knights.SlayDragonQuest" > <constructor-arg value="#{T(System).out}" /> </bean> <bean id="minstrel" class ="hoo.knights.Minstrel" > <constructor-arg value="#{T(System).out}" /> </bean> <aop:config> <aop:aspect ref="minstrel" > <aop:pointcut id="embark" expression="execution(* *.embarkOnQuest(..))" /> <aop:before pointcut-ref="embark" method="singBeforeQuest" /> <aop:after pointcut-ref="embark" method="singAfterQuest" /> </aop:aspect> </aop:config> </beans>
<aop:aspect ref=”minstrel”>这一句将法师类声明为一个切面(AOP)。
pointcut定义了一个切入点,before决定在切入点之前做什么,after决定在切入点之后做什么。
来到验证阶段,看看骑士到底干了啥事?
1 2 3 4 5 6 7 8 9 10 11 12 package hoo.knights;import org.springframework.context.support.ClassPathXmlApplicationContext;public class KnightMain { public static void main (String[] args) throws Exception { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("knight.xml" ); Knight knight = context.getBean(Knight.class ) ; knight.embarkOnQuest(); context.close(); } }
ClassPathXmlApplicationContext 从xml文件中读取bean装配信息。
注释的第一行决定了法师要出来吟唱,通过控制台可以看到法师出来吟唱了,并且骑士执行的是杀龙的行动。
法师吟唱
没有注释的第二行决定法师不出来吟唱,执行的也是杀龙任务,也可以将xml里面的配置改为拯救少女,可能这个骑士他不喜欢拯救少女吧。
法师不吟唱
这个例子 初步展示了DI、AOP的思想。
装配Bean Bean – 一种表达实体和信息的规范,便于封装重用。
Bean有以下特点:
1、所有属性为private 2、提供默认构造方法 3、提供Setter和Getter 4、实现Serializable接口
Spring装配Bean三种方式: XML显式配置、Java显式配置、自动装配。
一、自动装配
Spring通过以下两个角度来实现自动化装配
1、组件扫描:Spring会自动发现应用上下文中所创建的bean
2、自动装配:Spring自动满足bean之间的依赖
接下来以一个CD播放器的例子来演示自动装配
首先定义媒体播放器和CD两个接口
1 2 3 4 5 6 7 8 9 10 package soundsystem;public interface MediaPlayer { void play () ; } package soundsystem;public interface CompactDisc { void play () ; }
定义CD播放器类实现媒体播放器接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package soundsystem;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;@Component public class CDPlayer implements MediaPlayer { private CompactDisc cd; @Autowired public CDPlayer (CompactDisc cd) { this .cd = cd; } @Override public void play () { cd.play(); } }
@Component注解表明该类会作为组件类,并告知Spring为这个类创建bean
@Autowired注解表明当Spring创建CDPlayer bean 的时候,会通过这个构造器来进行实例化并且会传入一个可设置给CompactDisc类型的bean。
@Autowired注解可以用在类的任何方法上,还可以用于成员变量上。@Inject源于Java依赖注入规范,在大多场景下可与@Autowired互换。
然后定义一个SgtPeppers类,它实现了CompactDisc接口,也是一个组件类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package soundsystem;import org.springframework.stereotype.Component;@Component public class SgtPeppers implements CompactDisc { private String title = "Sgt. Pepper's Lonely Hearts Club Band" ; private String artist = "The Beatles" ; @Override public void play () { System.out.println("Playing " +title+" by " +artist); } }
最后定义一个CDPlayer配置类
1 2 3 4 5 6 7 8 9 10 package soundsystem;import org.springframework.context.annotation.*;@Configuration @ComponentScan public class CDPlayerConfig {}
@Configuration代表这是一个配置类
@ComponentScan表示启用组件扫描
组件扫描可以在一个CD播放器一张CD的时候正常工作,但是在有两张CD的时候就不能正常工作了,因为它不知道应该播放哪一张CD,这里我们只有一张披头士的CD,关于多张CD的情况留到后面处理。
我们也可以用xml来实现组件扫描
1 2 3 4 5 6 7 8 9 10 11 12 <?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 >
<context:component-scan base-package=”soundsystem” />表示会在soundsystem这个包内找合适的bean注入到CDplayer
现在定义CDPlayerTest类测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package soundsystem;import static org.junit.Assert.*;import org.junit.Rule;import org.junit.Test;import org.junit.contrib.java.lang.system.StandardOutputStreamLog;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;@RunWith (SpringJUnit4ClassRunner.class ) @ContextConfiguration (classes = CDPlayerConfig.class ) public class CDPlayerTest { @Rule public final StandardOutputStreamLog log = new StandardOutputStreamLog(); @Autowired private MediaPlayer player; @Autowired private CompactDisc cd; @Test public void cdShouldNotNull () { assertNotNull(cd); } @Test public void play () { player.play(); assertEquals("Playing Sgt. Pepper's Lonely Hearts Club Band by The Beatles\n" ,log.getLog()); } }
@ContextConfiguration(classes = CDPlayerConfig.class)会使用CDPlayerConfig这个类来配置应用上下文。
下面试试使用XML配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package soundsystem;import static org.junit.Assert.*;import org.junit.Rule;import org.junit.Test;import org.junit.contrib.java.lang.system.StandardOutputStreamLog;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;@RunWith (SpringJUnit4ClassRunner.class ) @ContextConfiguration (locations = "classpath:soundsystem.xml" )public class CDPlayerXMLConfigTest { @Rule public final StandardOutputStreamLog log = new StandardOutputStreamLog(); @Autowired private MediaPlayer player; @Autowired private CompactDisc cd; @Test public void cdShouldNotNull () { assertNotNull(cd); } @Test public void play () { player.play(); assertEquals("Playing Sgt. Pepper's Lonely Hearts Club Band by The Beatles\n" ,log.getLog()); } }
@ContextConfiguration(locations =”classpath:soundsystem.xml”)会使用soundsystem.xml来配置应用上下文。
二、Java显式配置
媒体播放器和CD接口保持不变
接下来看看CD播放器类与自动装配有什么不同之处
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package soundsystem;import org.springframework.beans.factory.annotation.Autowired;public class CDPlayer implements MediaPlayer { private CompactDisc cd; @Autowired public CDPlayer (CompactDisc cd) { this .cd = cd; } public void play () { cd.play(); } }
与自动装配比较可以发现类声明上面少了@Component注解 我们没有将CDPlayer声明为一个组件类
SgtPeppers类也没有@Component注解
重点来看看CDplayerConfig类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package soundsystem;import org.springframework.context.annotation.*;@Configuration public class CDPlayerConfig { @Bean public CompactDisc compactDisc () { return new SgtPeppers(); } @Bean public CDPlayer cdPlayer (CompactDisc cd) { return new CDPlayer(cd); } }
这里没有@ComponentScan注解然后在配置类中自己定义了两个bean从第一个bean可以看到如果需要注入CompactDisc bean的时候注入的是SgtPeppers类对象,需要注入CDPlayer bean 时注入的时候注入的是CdPlayer对象。
总结一下:Java显式配置与自动装配主要区别在配置类上,Java配置需要显示地声明bean,自动装配配置类有@ComponentScan注解,它会自动扫描带有@Component注解的bean来将其注入到合适的地方。
三、XML显式配置
1 2 3 4 5 6 7 8 9 <?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" > <bean id ="compactDisc" class ="soundsystem.SgtPeppers" /> <bean id ="cdPlayer" class ="soundsystem.CDPlayer" c:cd-ref ="compactDisc" /> <bean id ="compactDisc" class ="soundsystem.SgtPeppers" /> </beans >
上述的一行XML配置和上面的一段CDConfig Java配置起到的作用是一样的。
XML中参数的声明是由标签来实现,想要注入一个集合可以使用标签包裹集合内所有项
使用标签包裹集合内的一项。
<bean id=”compactDisc”
class=”soundsystem.BlankDisc”
c:_0=”Sgt. Pepper’s Lonely Hearts Club Band”
c:_1=”The Beatles”>
c:_0代表构造器的第一个参数,c:_1代表构造器的第二个参数。
四、混合配置
1 2 3 4 5 6 7 8 9 10 package soundsystem;import org.springframework.context.annotation.*;@Configuration @Import (CDPlayerConfig.class ) @ImportResource("classpath:cd-config.xml") public class SoundSystemConfig {}
加粗的两个注解从一个Java配置类和一个XML文档加载应用上下文。
Java高级装配 一、环境与profile
开发软件的时候需要将一个应用从一个环境迁移到另一个环境,不同的环境数据库配置、加密算法以及外部系统的集成可能会变化。
Spring为解决环境问题引入了bean profile功能,要使用profile首先要将不同的bean定义或整理到一个或多个profile之中,在应用部署到每个环境时要确保对应的profile处于激活状态。
下面以一个数据库配置为例
在开发环境我们使用H2数据库,在生产环境我们使用JNDI从容器中获取数据库配置
我们可以使用Java或XML配置profile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.myapp;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Profile;import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;import org.springframework.jndi.JndiObjectFactoryBean;import javax.sql.DataSource;@Configuration public class DataSourceConfig { @Bean @Profile ("dev" ) public DataSource embeddedDataSource () { return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2) .addScript("classpath:schema.sql" ).addScript("classpath:test-data.sql" ).build(); } @Bean @Profile ("prod" ) public DataSource jndiDataSource () { JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean(); jndiObjectFactoryBean.setJndiName("jndi/myDS" ); jndiObjectFactoryBean.setResourceRef(true ); jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class ) ; return (DataSource)jndiObjectFactoryBean.getObject(); } }
与一般的配置相似,多加了一个profile,参数是环境名称。
下面是XML配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?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="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>
接下来写一个测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import com.myapp.DataSourceConfig;import org.junit.Test;import static org.junit.Assert.*;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.jdbc.core.RowMapper;import org.springframework.test.context.ActiveProfiles;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import javax.sql.DataSource;import java.sql.ResultSet;import java.sql.SQLException;import java.util.List;public class DataSourceConfigTest { @RunWith (SpringJUnit4ClassRunner.class ) @ContextConfiguration (classes = DataSourceConfig.class ) @ActiveProfiles("dev") public static class DevDataSourceTest { @Autowired private DataSource dataSource; @Test public void shouldBeEmbeddedDataSource () { assertNotNull(dataSource); JdbcTemplate jdbc = new JdbcTemplate(dataSource); List<String> results = jdbc.query("select id , name from Things" , new RowMapper<String>() { @Override public String mapRow (ResultSet rs, int rowNum) throws SQLException { return rs.getLong("id" )+":" +rs.getString("name" ); } }); assertEquals(1 ,results.size()); assertEquals("1:A" ,results.get(0 )); } }
可以看到我们使用Java配置上下文,也可改为使用XML注解,@ActiveProfiles(“dev”)代表激活开发环境profile。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RunWith (SpringJUnit4ClassRunner.class ) @ContextConfiguration("classpath:datasource-config.xml") @ActiveProfiles ("prod" ) public static class ProductionDataSourceTest_XMLConfig { @Autowired (required = false ) private DataSource dataSource; @Test public void shouldBeEmbeddedDatasource () { assertNull(dataSource); } } }
可以看到我们使用Java配置上下文,也可改为使用XML注解,@ActiveProfiles(“prod”)代表激活生产环境profile。
二、条件化的Bean
假设你希望一个或多个bean只有在特定的类路径下包含特定的库时才创建,或者我们希望某个bean只有当另外一个特定的bean也声明了之后才会创建。我们还有可能要求某个特定的环境变量设置之后才会创建某个bean。
我们先定义一个MagicBean
1 2 3 4 5 package com.hubuma.restfun;public class MagicBean {}
然后是它的配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.hubuma.restfun;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Conditional;import org.springframework.context.annotation.Configuration;@Configuration public class MagicConfig { @Bean @Conditional (MagicExistsCondition.class ) public MagicBean magicBean () { return new MagicBean(); } }
@Conditional会通过Condition接口进行条件对比,从而实现条件化创建bean,设置给@Conditional的类可以是任意实现Condition接口的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.hubuma.restfun;import org.springframework.context.annotation.*;import org.springframework.core.env.Environment;import org.springframework.core.type.AnnotatedTypeMetadata;public class MagicExistsCondition implements Condition { @Override public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { Environment env = context.getEnvironment(); return env.containsProperty("magic" ); } }
matches方法表示需要检查环境中是否存在名为magic 的环境属性,若存在则创建bean。
三、处理自动装配的歧义性
自动装配再仅有一个bean匹配所需要的结果时才有效。
假设我们使用@Autowired注解标注了getDessert()方法
1 2 3 4 @Autowired public void setDessert (Dessert dessert) { this .dessert = dessert; }
Dessert是一个接口,并且有三个类实现了这个接口。
1 2 3 4 5 6 @Component public class Cake imlements Dessert {...}@Component public class Cookies implements Dessert {...}@Component public class IceCream implements Dessert {...}
在组件扫描的时候,能够发现他们并创建为Spring应用上下文里的bean,当Spring尝试装配setDessert()中的Dessert参数时,它并没有唯一的、无歧义的可选值,Spring此时会抛出NoUniqueBeanDefinitionException。
我们可以使用@Primary注解来将一个可选的bean设为首选项。
1 2 3 @Component @Primary public class IceCream implements Dessert {...}
解决自动装配歧义性问题,限定符是一个更强大的机制。
1 2 3 4 5 @Autowired @Qualifier ("iceCream" )public void setDessert (Dessert dessert) { this .dessert = dessert; }
指定注入setDessert方法的是IceCream 类的实例。@Qualifier注解所设置的参数就是想要注入bean的ID,所有使用@Component注解的声明的类都会创建为bean,并且bean的ID为首字母变为小写的类名。
我们可以设置自己的限定符
1 2 3 4 5 6 7 8 9 @Component @Qualifier ("cold" )public class IceCream implements Dessert {...}@Autowired @Qualifier ("cold" )public void setDessert (Dessert dessert) { this .dessert = dessert; }
这样注入setDessert的就是IceCream类的实例了。
当有两个cold限定符的时候一个cold限定符就不够用了,但是Java不允许在同一个条目上重复出现相同类型的多个注解我们不能使用两条@Qualifier注解来实现限定,此时我们只能自己声明注解然后进行限定。
1 2 3 4 5 6 7 8 9 10 11 @Target ({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})@Retention (RetentionPolicy.RUNTIME)@Qualifier public @interface Cold {} @Target ({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})@Retention (RetentionPolicy.RUNTIME)@Qualifier public @interface Creamy {}
在方法或类上使用@Cold和@Creamy注解即可限定。
四、bean的作用域
Spring定义了多种作用域,可以基于这些作用域创建bean,包括:
单例(Singleton)在整个应用中只创建bean的一个实例。
原型(ProtoType)每次注入或通过Spring应用上下文获取的时候都会创建一个新的bean实例。
会话(Session)在Web应用中,为每个会话创建一个bean实例。
请求(Request)在Web应用中,为每个请求创建一个bean实例。
默认创建是以单例模式创建的。
1 2 3 @Component public class UniqueThing {}
以原型模式创建
1 2 3 4 @Component @Scope (ConfigurableBeanFactory.SCOPE_PROTOTYPE)public class NotePad {}
五、运行时值注入
当讨论依赖注入时我们通常讨论的是将一个bean注入到另一个bean的属性或构造器中,但bean装配的另一个方面是将一个值注入到bean的属性或构造器参数中。
定义一个BlankDisc类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.soundsystem;public class BlankDisc { private final String title; private final String artist; public BlankDisc (String title, String artist) { this .title = title; this .artist = artist; } public String getTitle () { return title; } public String getArtist () { return artist; } }
app.properties文件内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 disc.title=Sgt. Peppers Lonely Hearts Club Band disc.artist=The Beatles package com.soundsystem;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.PropertySource;import org.springframework.core.env.Environment;@Configuration @PropertySource ("app.properties" )public class EnvironmentConfig { @Autowired Environment env; @Bean public BlankDisc blankDisc () { return new BlankDisc(env.getProperty("disc.title" ),env.getProperty("disc.artist" )); } }
app.properties文件会被加载到Spring的Environment中然后可以从中检索属性。
值注入还可以有默认值。
1 2 3 4 5 @Bean public BlankDiscblankDisc () { return new BlankDisc(env.getProperty("disc.title" ,"Rattle and Hum" ), env.getProperty("disc.artist" ,"U2" ); }
使用SpEL表达式来进行装配
1 2 3 4 5 public BlankDisc (@Value("#systemProperties['disc.title']" ) String title, @Value ("#systemProperties['disc.artist']" ) String artist) { this .title = title; this .artist = artist; }
SpEL表示浮点值 #{3.1.4159}
表示String#{‘Hello’}
表示boolean类型#{false}
SpEL能做的一件基础事情就是通过ID引用其他bean
#{sgtPeppers}
#{sgtPeppers.artist}
我们还可以调用bean上的方法
#{artistSelector.selectArtist()}
这里只介绍了SpEL的部分知识,具体的看手册。
面向切面的Spring 我们知道如何使用依赖注入(DI)管理和配置我们的应用对象,DI有助于应用对象之间的解耦,而AOP可以实现横切关注点与他们所影响的对象之间的解耦。切面能帮助我们模块化横切关注点,横切关注点可以被描述为影响应用多处的功能。
定义AOP术语,描述切面的术语有通知,切点和连接点。
通知定义了切面是什么以及切面何时开始使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?
Spring切面可以应用5种类型的通知:
前置通知:在目标方法被调用之前调用通知功能;
后置通知:在目标方法完成之后调用通知功能;
返回通知:在目标方法成功执行之后调用通知功能;
异常通知:在目标方法抛出异常后调用的通知功能;
环绕通知:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
我们的应用可能有数千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时,抛出异常时,甚至修改一个字段时,切面代码可以利用这些点插入到应用正常的流程之中,并添加新的行为。
如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式来匹配通知所要织入的一个或多个连接点。
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容–它是什么,在何时和何处完成其功能。
织入是把切面应用到目标对象并创建新的代理对象的过程。
我们先定义一个Performance接口
1 2 3 4 package concert;public interface Performance { public void perform () ; }
再定义一个它的实现类
1 2 3 4 5 6 7 8 9 package concert;public class ConcertPerformance implements Performance { @Override public void perform () { System.out.println("Performing..." ); } }
重点是这里的Audience类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package concert;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;@Aspect public class Audience { @Pointcut (value = "execution(* concert.Performance.perform(..))" ) public void performance () {} @Before ("performance()" ) public void silenceCellPhone () { System.out.println("silencing cellPhone" ); } @After ("performance()" ) public void afterPerformance () { System.out.println("after performance" ); } @AfterThrowing ("performance()" ) public void demanRefund () { System.out.println("demanding a refund" ); } @Around ("performance()" ) public void watchPerformance (ProceedingJoinPoint joinPoint) { try { System.out.println("around before performance..." ); joinPoint.proceed(); System.out.println("around after performance..." ); }catch (Throwable t) { t.printStackTrace(); } } }
@Aspect声明这是一个Aspect注解驱动的切面。
@Poincut定义了一个切点,execution(* concert.Performance.perform(..))代表perform这个方法执行时将会声明为一个切点
“ * ” 代表任意返回值类型,“ .. ”代表任意参数类型。也可以写成 * *.perform(..)代表任意类的perform方法。
@Before,@After,@AfterThrowing,分别是前置,后置,异常通知。
@Around声明一个环绕通知
这还不够,我们需要一个配置类,来启用自动代理功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package concert;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration @EnableAspectJAutoProxy @ComponentScan (basePackages = {"concert" })public class ConcertConfig { @Bean public Performance performance () { return new ConcertPerformance(); } @Bean public Audience audience () { return new Audience(); } }
@EnableAspectJAutoProxy注解启动了自动代理功能,这里我们声明了两个Bean。
接下来测试一下,AOPTest类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package concert;import concert.ConcertConfig;import concert.Performance;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;@RunWith (SpringJUnit4ClassRunner.class ) @ContextConfiguration (classes = {ConcertConfig.class }) public class AOPTest { @Autowired private Performance performance; @Test public void testAop () { performance.perform(); } }
performance对象会自动注入到AOPTest类中。
切面运行结果
这里详细写一下IDEA 配置AspectJ的步骤:
1、启用IDEA AspectJ plugin File—Settings–Plugins,搜索栏内搜索AspectJ,启用支持,注意这里只有Ultimate版才会有。
2、添加依赖
build.gradle添加
compile”org.springframework:spring-context:4.0.7.RELEASE”
compile”org.springframework:spring-test:4.0.7.RELEASE”
compile group:’org.aspectj’,name:’aspectjtools’,version:’1.8.9’
testCompile”org.springframework:spring-test:4.0.7.RELEASE”
implementation’junit:junit:4.12’
Spring+JDBC征服数据库 Spring将数据访问过程中固定的和可变的部分明确划分为两个不同的类,模板(template) 和回调(callback) 。模板管理过程中固定的部分,回调处理自定义的数据访问代码。
Spring的模板类处理数据访问的固定部分–事务控制、管理资源以及处理异常。同时,应用程序相关的数据访问–语句、绑定参数以及整理结果集在回调的实现中处理。
以Spittr这个应用来演示JDBC。
我们先定义Spitter和Spittle两个实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package spittr.domain;public class Spitter { private Long id; private String username; private String password; private String fullname; private String email; private boolean updateByEmail; public Spitter (Long id, String username, String password, String fullname, String email, boolean updateByEmail) { this .id = id; this .username = username; this .password = password; this .fullname = fullname; this .email = email; this .updateByEmail = updateByEmail; } public Long getId () { return id; } public String getUsername () { return username; } public String getPassword () { return password; } public String getFullname () { return fullname; } public String getEmail () { return email; } public boolean isUpdateByEmail () { return updateByEmail; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package spittr.domain;import java.util.Date;public class Spittle { private final Long id; private final Spitter spitter; private final String message; private final Date postedDate; public Spittle (Long id, Spitter spitter, String message, Date postedDate) { this .id = id; this .spitter = spitter; this .message = message; this .postedDate = postedDate; } public Long getId () { return id; } public Spitter getSpitter () { return spitter; } public String getMessage () { return message; } public Date getPostedDate () { return postedDate; } }
然后定义SpittleRepository、SpitterRepository两个接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package spittr.db;import spittr.domain.Spittle;import java.util.List;public interface SpittleRepository { long count () ; List<Spittle> findRecent () ; List<Spittle> findRecent (int count) ; Spittle findOne (long id) ; Spittle save (Spittle spittle) ; List<Spittle> findBySpitterId (long id) ; void delete (long id) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package spittr.db;import spittr.domain.Spitter;import java.util.List;public interface SpitterRepository { long count () ; Spitter save (Spitter spitter) ; Spitter findOne (long id) ; Spitter findByUsername (String username) ; List<Spitter> findAll () ; }
Spring的JDBC框架承担了资源管理和异常处理的工作,从而简化了JDBC代码,让我们只需编写从数据库读写数据的必要代码。
Spring为JDBC提供了三个模板供选择:
JdbcTemplate:最基本的Spring JDBC 模板,支持简单的JDBC数据库访问功能以及基于索引参数的查询。
NamedParameterJdbcTemplate:使用该模板类进行查询时可以将值以命名参数的形式绑定到SQL中,而不是简单的使用索引参数。
对于大多数JDBC任务来说,JdbcTemplate就是最好的方案。
我们定义JdbcSpitterRepository实现SpitterRepository接口,定义JdbcSpittleRepository实现SpittleRepository接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 package spittr.db.jdbc;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.jdbc.core.RowMapper;import org.springframework.jdbc.core.simple.SimpleJdbcInsert;import spittr.db.SpitterRepository;import spittr.domain.Spitter;import java.sql.ResultSet;import java.sql.SQLException;import java.util.HashMap;import java.util.List;import java.util.Map;public class JdbcSpitterRepository implements SpitterRepository { private JdbcTemplate jdbcTemplate; private static final String INSERT_SPITTER = "insert into Spitter(username,password,fullname,email,updateByEmail) values(?,?,?,?,?) " ; private static final String SELECT_SPITTER = "select id,username,password,fullname,email,updateByEmail from Spitter " ; public JdbcSpitterRepository (JdbcTemplate jdbcTemplate) { this .jdbcTemplate = jdbcTemplate; } @Override public Spitter save (Spitter spitter) { Long id = spitter.getId(); if (id == null ){ long spitterId = insertSpitterAndReturnId(spitter); return new Spitter(spitterId,spitter.getUsername(),spitter.getPassword(),spitter.getFullname(),spitter.getEmail(),spitter.isUpdateByEmail()); } else { jdbcTemplate.update("update Spitter set username=?, password=?, fullname=?, email=?, updateByEmail=? where id =? " , spitter.getUsername(), spitter.getPassword(), spitter.getFullname(), spitter.getEmail(), spitter.isUpdateByEmail(), id); } return spitter; } private long insertSpitterAndReturnId (Spitter spitter) { SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate).withTableName("Spitter" ); jdbcInsert.setGeneratedKeyName("id" ); Map<String, Object>args = new HashMap<>(); args.put("username" ,spitter.getUsername()); args.put("password" ,spitter.getPassword()); args.put("fullname" ,spitter.getFullname()); args.put("email" ,spitter.getEmail()); args.put("updateByEmail" ,spitter.isUpdateByEmail()); long spitterId = jdbcInsert.executeAndReturnKey(args).longValue(); return spitterId; } public void insertSpitter (Spitter spitter) { jdbcTemplate.update(INSERT_SPITTER, spitter.getUsername(), spitter.getPassword(), spitter.getFullname(), spitter.getEmail(), spitter.isUpdateByEmail()); } @Override public Spitter findOne (long id) { return jdbcTemplate.queryForObject(SELECT_SPITTER+"where id = ?" ,new SpitterRowMapper(),id); } @Override public List<Spitter> findAll () { return jdbcTemplate.query(SELECT_SPITTER+"order by id" ,new SpitterRowMapper()); } @Override public long count () { return jdbcTemplate.queryForLong("select count(id) from spitter" ); } @Override public Spitter findByUsername (String username) { return jdbcTemplate.queryForObject(SELECT_SPITTER+"where username = ?" ,new SpitterRowMapper(),username); } private static final class SpitterRowMapper implements RowMapper <Spitter > { @Override public Spitter mapRow (ResultSet rs, int rowNum) throws SQLException { return new Spitter(rs.getLong("id" ),rs.getString("username" ),rs.getString("password" ), rs.getString("fullname" ),rs.getString("email" ),rs.getBoolean("updateByEmail" )); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 package spittr.db.jdbc;import org.springframework.dao.DataAccessException;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.jdbc.core.RowMapper;import org.springframework.jdbc.core.simple.SimpleJdbcInsert;import spittr.db.SpittleRepository;import spittr.domain.Spitter;import spittr.domain.Spittle;import java.sql.ResultSet;import java.sql.SQLException;import java.util.HashMap;import java.util.List;import java.util.Map;public class JdbcSpittleRepository implements SpittleRepository { private static final String SELECT_SPITTLE = "select sp.id, s.id as spitterId, s.username, s.password, s.fullname, s.email, s.updateByEmail, sp.message, sp.postedTime from Spitter s, Spittle sp where s.id = sp.spitter" ; private static final String SELECT_SPITTLE_BY_ID = SELECT_SPITTLE+" and sp.id=?" ; private static final String SELECT_SPITTLE_BY_SPITTER_ID = SELECT_SPITTLE+" and s.id=? order by sp.postedTime desc" ; private static final String SELECT_RECENT_SPITTLES = SELECT_SPITTLE+" order by sp.postedTime desc limit ?" ; private JdbcTemplate jdbcTemplate; public JdbcSpittleRepository (JdbcTemplate jdbcTemplate) { this .jdbcTemplate = jdbcTemplate; } @Override public long count () { return jdbcTemplate.queryForLong("select count(id) from Spittle" ); } @Override public Spittle findOne (long id) { try { return jdbcTemplate.queryForObject(SELECT_SPITTLE_BY_ID,new SpittleRowMapper(),id); } catch (DataAccessException e) { return null ; } } @Override public Spittle save (Spittle spittle) { long spittleId = insertSpittleAndReturnId(spittle); return new Spittle(spittleId,spittle.getSpitter(),spittle.getMessage(),spittle.getPostedDate()); } @Override public List<Spittle> findBySpitterId (long id) { return jdbcTemplate.query(SELECT_SPITTLE_BY_SPITTER_ID,new SpittleRowMapper(),id); } @Override public List<Spittle> findRecent () { return findRecent(10 ); } @Override public List<Spittle> findRecent (int count) { return jdbcTemplate.query(SELECT_RECENT_SPITTLES,new SpittleRowMapper(),count); } @Override public void delete (long id) { jdbcTemplate.update("delete from Spittle where id = ?" ,id); } private long insertSpittleAndReturnId (Spittle spittle) { SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate).withTableName("Spittle" ); jdbcInsert.setGeneratedKeyName("id" ); Map<String,Object> args = new HashMap<>(); args.put("spitter" ,spittle.getSpitter().getId()); args.put("message" ,spittle.getMessage()); args.put("postedTime" ,spittle.getPostedDate()); long spittleId = jdbcInsert.executeAndReturnKey(args).longValue(); return spittleId; } private static final class SpittleRowMapper implements RowMapper <Spittle > { @Override public Spittle mapRow (ResultSet rs, int rowNum) throws SQLException { return new Spittle(rs.getLong("id" ), new Spitter(rs.getLong("spitterId" ),rs.getString("username" ), rs.getString("password" ),rs.getString("fullname" ), rs.getString("email" ),rs.getBoolean("updateByEmail" )), rs.getString("message" ),rs.getTimestamp("postedTime" )); } } }
可以看到Spring中JDBC将获取连接,捕获异常都封装在JdbcTemplate中让我们更关注数据操作代码。对于增删改查四个方面,JdbcTemplate都为我们封装了一系列的函数。
SimpleJdbcInsert 可以简化我们的插入操作,指定一个数据库中的表名,然后传入一个Map,Key为数据表中的表项名,Value为插入的值,并可以指定一个返回的值。
通过update方法来实现删改。
查询基本类型例如Long等有对应的queryForLong方法等,若要将查询结果处理成为对象或对象集合,我们需要实现RowMapper接口实现将查询结果的一行映射为对象。
其实以上的代码我们并不能运行,因为我们还没有指定数据库数据源我们写一个JdbcConfig配置类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package spittr.jdbc;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;import org.springframework.transaction.PlatformTransactionManager;import spittr.db.jdbc.JdbcSpitterRepository;import spittr.db.jdbc.JdbcSpittleRepository;import javax.sql.DataSource;@Configuration public class JdbcConfig { @Bean public DataSource dataSource () { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScripts("classpath:spittr/db/jdbc/schema.sql" ,"classpath:spittr/db/jdbc/test-data.sql" ) .build(); } @Bean public JdbcTemplate jdbcTemplate (DataSource dataSource) { return new JdbcTemplate(dataSource); } @Bean public JdbcSpitterRepository spitterRepository (JdbcTemplate jdbcTemplate) { return new JdbcSpitterRepository(jdbcTemplate); } @Bean public JdbcSpittleRepository spittleRepository (JdbcTemplate jdbcTemplate) { return new JdbcSpittleRepository(jdbcTemplate); } @Bean public PlatformTransactionManager transactionManager (DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
我们使用EmbeddedDatabaseBuilder来新建一个嵌入式数据库,这里我们选择H2,addScripts方法可以执行我们写好的SQL脚本。
将数据源注入到JdbcTemplate就可以使用它了,是不是很方便??
我们还可以使用DriverManagerDataSource来配置基于JDBC驱动的数据源。
我们定义了dataSource、jdbcTemplate、spitterRepository、spittleRepository和transactionManager五个bean,dataSource bean 用来注入到jdbcTemplate bean中,jdbcTemplate bean 用来注入到用到它的repository实现中。spitterRepository和spittleRepository bean 用来注入到使用到它们的类。transactionManager bean 用来实现 事务管理,这里我们没有用到它。
下面是测试类的一部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package spittr.jdbc;import static org.junit.Assert.*;import java.util.List;import org.junit.BeforeClass;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import org.springframework.transaction.annotation.Transactional;import spittr.db.jdbc.JdbcSpitterRepository;import spittr.domain.Spitter;@RunWith (SpringJUnit4ClassRunner.class ) @ContextConfiguration (classes =JdbcConfig.class ) public class JdbcSpitterRepositoryTest { @Autowired JdbcSpitterRepository spitterRepository; @Test public void count () { assertEquals(4 , spitterRepository.count()); } @Test @Transactional public void findAll () { List<Spitter> spitters = spitterRepository.findAll(); assertEquals(4 , spitters.size()); assertSpitter(0 , spitters.get(0 )); assertSpitter(1 , spitters.get(1 )); assertSpitter(2 , spitters.get(2 )); assertSpitter(3 , spitters.get(3 )); } }
我们使用JdbcConfig类配置应用上下文,自动装配相应的repository实现。
这里需要注意的是@Transactional注解当它置于方法前时表示该方法使用事务处理,当它置于类前时表示该类的所有方法都使用事务处理。
使用对象-关系映射持久化数据 在数据持久化的世界中,JDBC就像自行车,对于份内的工作它能完成的很好。随着应用程序越来越复杂,对持久化的要求也越来越复杂。我们需要将对象的属性映射到数据库的列上,并且需要自动生成语句和查询,这样我们就能从无休止的问号字符串中解脱出来。
一、Hibernate
还是以Spittr应用为例来具体阐述Hibernate的使用。
我们定义Spitter、Spittle两个实体类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package spittr.domain;import javax.persistence.Column;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;@Entity public class Spitter { private Spitter () {} @Id @GeneratedValue (strategy=GenerationType.IDENTITY) private Long id; @Column (name="username" ) private String username; @Column (name="password" ) private String password; @Column (name="fullname" ) private String fullName; @Column (name="email" ) private String email; @Column (name="updateByEmail" ) private boolean updateByEmail; public Spitter (Long id, String username, String password, String fullName, String email, boolean updateByEmail) { this .id = id; this .username = username; this .password = password; this .fullName = fullName; this .email = email; this .updateByEmail = updateByEmail; } public Long getId () { return id; } public String getUsername () { return username; } public String getPassword () { return password; } public String getFullName () { return fullName; } public String getEmail () { return email; } public boolean isUpdateByEmail () { return updateByEmail; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package spittr.domain;import java.util.Date;import javax.persistence.Column;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.persistence.JoinColumn;import javax.persistence.ManyToOne;@Entity public class Spittle { private Spittle () {} @Id @GeneratedValue (strategy=GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn (name="spitter" ) private Spitter spitter; @Column private String message; @Column private Date postedTime; public Spittle (Long id, Spitter spitter, String message, Date postedTime) { this .id = id; this .spitter = spitter; this .message = message; this .postedTime = postedTime; } public Long getId () { return this .id; } public String getMessage () { return this .message; } public Date getPostedTime () { return this .postedTime; } public Spitter getSpitter () { return this .spitter; } }
@Entity注解表示这是一个实体类,@Id注解代表这是数据表的primary key ,@GeneratedValue代表这是自动生成的列,
@Column注解将数据表的一列与类的属性绑定,不指定name属性则代表和变量名相同,@ManyToOne注解代表会有多个Spittle参照同一个Spitter, @JoinColumn注解代表参考spitter列。
这样就实现了类属性和数据表项的绑定。
同样我们定义SpitterRepository、SpittleRepository接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package spittr.db;import java.util.List;import spittr.domain.Spitter;public interface SpitterRepository { long count () ; Spitter save (Spitter spitter) ; Spitter findOne (long id) ; Spitter findByUsername (String username) ; List<Spitter> findAll () ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package spittr.db;import java.util.List;import spittr.domain.Spittle;public interface SpittleRepository { long count () ; List<Spittle> findRecent () ; List<Spittle> findRecent (int count) ; Spittle findOne (long id) ; Spittle save (Spittle spittle) ; List<Spittle> findBySpitterId (long spitterId) ; void delete (long id) ; }
我们先看看如何配置Hibernate 下面是RepositoryTestConfig.java。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package spittr.db.hibernate4;import java.io.IOException;import java.util.Properties;import javax.inject.Inject;import javax.sql.DataSource;import org.hibernate.SessionFactory;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;import org.springframework.orm.hibernate4.HibernateTransactionManager;import org.springframework.orm.hibernate4.LocalSessionFactoryBean;import org.springframework.transaction.PlatformTransactionManager;import org.springframework.transaction.annotation.EnableTransactionManagement;import org.springframework.transaction.annotation.TransactionManagementConfigurer;@Configuration @EnableTransactionManagement @ComponentScan public class RepositoryTestConfig implements TransactionManagementConfigurer { @Inject private SessionFactory sessionFactory; @Bean public DataSource dataSource () { EmbeddedDatabaseBuilder edb = new EmbeddedDatabaseBuilder(); edb.setType(EmbeddedDatabaseType.H2); edb.addScript("spittr/db/hibernate4/schema.sql" ); edb.addScript("spittr/db/hibernate4/test-data.sql" ); EmbeddedDatabase embeddedDatabase = edb.build(); return embeddedDatabase; } public PlatformTransactionManager annotationDrivenTransactionManager () { System.out.println(sessionFactory); HibernateTransactionManager transactionManager = new HibernateTransactionManager(); transactionManager.setSessionFactory(sessionFactory); return transactionManager; } @Bean public SessionFactory sessionFactoryBean () { try { LocalSessionFactoryBean lsfb = new LocalSessionFactoryBean(); lsfb.setDataSource(dataSource()); lsfb.setPackagesToScan("spittr.domain" ); Properties props = new Properties(); props.setProperty("dialect" , "org.hibernate.dialect.H2Dialect" ); lsfb.setHibernateProperties(props); lsfb.afterPropertiesSet(); SessionFactory object = lsfb.getObject(); return object; } catch (IOException e) { return null ; } } }
这里我们声明它是一个配置类并且启用了组件扫描和事务管理。
@Inject和@Autowired基本相同,同样dataBase bean为数据源配置,annotationDrivenTransactionManager bean 为事务处理,
使用Hibernate所需要的主要接口时org.hibernate.Session。Session接口提供了基本的数据访问功能,如保存、更新、删除以及从数据库加载对象的功能。通过Hibernate的Session接口,应用程序的Repository能够满足所有的持久化需求。
SessionFactory主要负责Hibernate Session的打开,关闭以及管理。
dataSource和hibernateProperties属性声明了从哪里获取数据库连接以及要使用哪一种数据库。这里不再列出Hibernate配置文件,使用packageToScan属性告诉Spring扫描一个或多个包以查找域类,这些类表明要使用Hibernate进行持久化,这些类可以使用的注解包括JPA的@Entity或@MappedSuperclass以及Hibernate的Entity。
了解完Hibernate的配置之后我们开始编写HibernateSpitterRepository和HibernateSpittleRepository他们分别实现SpitterRepository接口和SpittleRepository接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 package spittr.db.hibernate4;import java.io.Serializable;import java.util.List;import javax.inject.Inject;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.criterion.Restrictions;import org.springframework.stereotype.Repository;import spittr.db.SpitterRepository;import spittr.domain.Spitter;@Repository public class HibernateSpitterRepository implements SpitterRepository { private SessionFactory sessionFactory; @Inject public HibernateSpitterRepository (SessionFactory sessionFactory) { this .sessionFactory = sessionFactory; } private Session currentSession () { return sessionFactory.getCurrentSession(); } public long count () { return findAll().size(); } public Spitter save (Spitter spitter) { Serializable id = currentSession().save(spitter); return new Spitter((Long) id, spitter.getUsername(), spitter.getPassword(), spitter.getFullName(), spitter.getEmail(), spitter.isUpdateByEmail()); } public Spitter findOne (long id) { return (Spitter) currentSession().get(Spitter.class , id ) ; } public Spitter findByUsername (String username) { return (Spitter) currentSession() .createCriteria(Spitter.class ) .add(Restrictions.eq("username", username)) .list().get(0 ); } public List<Spitter> findAll () { return (List<Spitter>) currentSession() .createCriteria(Spitter.class ).list () ; } } package spittr.db.hibernate4;import java.io.Serializable;import java.util.List;import javax.inject.Inject;import org.hibernate.Criteria;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.criterion.Order;import org.hibernate.criterion.Restrictions;import org.springframework.stereotype.Repository;import spittr.db.SpittleRepository;import spittr.domain.Spittle;@Repository public class HibernateSpittleRepository implements SpittleRepository { private SessionFactory sessionFactory; @Inject public HibernateSpittleRepository (SessionFactory sessionFactory) { this .sessionFactory = sessionFactory; } private Session currentSession () { return sessionFactory.getCurrentSession(); } public long count () { return findAll().size(); } public List<Spittle> findRecent () { return findRecent(10 ); } public List<Spittle> findRecent (int count) { return (List<Spittle>) spittleCriteria() .setMaxResults(count) .list(); } public Spittle findOne (long id) { return (Spittle) currentSession().get(Spittle.class , id ) ; } public Spittle save (Spittle spittle) { Serializable id = currentSession().save(spittle); return new Spittle( (Long) id, spittle.getSpitter(), spittle.getMessage(), spittle.getPostedTime()); } public List<Spittle> findBySpitterId (long spitterId) { return spittleCriteria() .add(Restrictions.eq("spitter.id" , spitterId)) .list(); } public void delete (long id) { currentSession().delete(findOne(id)); } public List<Spittle> findAll () { return (List<Spittle>) spittleCriteria().list(); } private Criteria spittleCriteria () { return currentSession() .createCriteria(Spittle.class ) .addOrder(Order.desc("postedTime")); } }
我们将一个SessionFactory注入到HibernateSpitterRepository和HibernateSpittleRepository的sessionFactory属性中,在currentSession()方法中我们使用这个sessionFactory来获取当前事务的Session。
我们使用了@Repository注解,它能够被组件扫描扫描到,不必显示声明,它还会捕获平台相关的异常,然后使用Spring统一非检查型异常重新抛出。
数据类同数据库的表存在对应关系,使用Hibernate操作数据类时,Hibernate会将之转换为对数据库中对应表的操作。
org.hibernate.Criteria接口表示特定持久类的一个查询。Session是 Criteria实例的工厂。currentSession()
.createCriteria(Spittle.class)表示Spittle类的一个查询。参见:http://www.baike.com/wiki/criteria
session.save()方法返回一个生成的id,该id为Serializable类型。
下面是测试的一部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package spittr.db.hibernate4;import static org.junit.Assert.*;import java.util.List;import org.junit.BeforeClass;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import org.springframework.transaction.annotation.Transactional;import spittr.db.SpitterRepository;import spittr.domain.Spitter;@RunWith (SpringJUnit4ClassRunner.class ) @ContextConfiguration (classes = RepositoryTestConfig.class ) public class SpitterRepositoryTest { @Autowired SpitterRepository spitterRepository; @Test @Transactional public void count () { assertEquals(4 , spitterRepository.count()); } @Test @Transactional public void findAll () { List<Spitter> spitters = spitterRepository.findAll(); assertEquals(4 , spitters.size()); assertSpitter(0 , spitters.get(0 )); assertSpitter(1 , spitters.get(1 )); assertSpitter(2 , spitters.get(2 )); assertSpitter(3 , spitters.get(3 )); } }
二、JPA-Hibernate
我们尝试开发基于JPA的Repository。Java持久化API(Java Persistence API,JPA)诞生在EJB2实体Bean的废墟之上,并成为下一代Java持久化标准。JPA是基于POJO的持久化机制。
以Spittr应用为例,我们首先看看JpaConfig类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package spittr.db.jpa;import javax.inject.Inject;import javax.persistence.EntityManagerFactory;import javax.sql.DataSource;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;import org.springframework.orm.jpa.JpaTransactionManager;import org.springframework.orm.jpa.JpaVendorAdapter;import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;import org.springframework.orm.jpa.vendor.Database;import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;import org.springframework.transaction.PlatformTransactionManager;import org.springframework.transaction.annotation.EnableTransactionManagement;import org.springframework.transaction.annotation.TransactionManagementConfigurer;@Configuration @ComponentScan public class JpaConfig { @Bean public DataSource dataSource () { EmbeddedDatabaseBuilder edb = new EmbeddedDatabaseBuilder(); edb.setType(EmbeddedDatabaseType.H2); edb.addScript("spittr/db/jpa/schema.sql" ); edb.addScript("spittr/db/jpa/test-data.sql" ); EmbeddedDatabase embeddedDatabase = edb.build(); return embeddedDatabase; } @Bean public LocalContainerEntityManagerFactoryBean emf (DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) { LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean(); emf.setDataSource(dataSource); emf.setPersistenceUnitName("spittr" ); emf.setJpaVendorAdapter(jpaVendorAdapter); emf.setPackagesToScan("spittr.domain" ); return emf; } @Bean public JpaVendorAdapter jpaVendorAdapter () { HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter(); adapter.setDatabase(Database.H2); adapter.setShowSql(true ); adapter.setGenerateDdl(false ); adapter.setDatabasePlatform("org.hibernate.dialect.H2Dialect" ); return adapter; } @Configuration @EnableTransactionManagement public static class TransactionConfig implements TransactionManagementConfigurer { @Inject private EntityManagerFactory emf; public PlatformTransactionManager annotationDrivenTransactionManager () { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(emf); return transactionManager; } } }
基于JPA的应用程序需要使用EntityManagerFactory的实现类来获得EntityManager实例。JPA定义了应用程序管理类型和容器管理类型的实体管理器,这两种实体管理器实现了同一个EntityManager接口。关键的区别不在于EntityManager本身,而是在于EntityManager的创建和管理方式。顾名思义,前者由应用程序创建和管理,后者由Java EE创建和管理。
JpaVendorAdapter 属性用于指明所使用的是哪一个厂商的JPA实现。
使用LocalContainerEntityManagerFactoryBean来配置容器管理类型的JPA。
接下来编写基于JPA的Repository:JpaSpitterRepository、JpaSpittleRepository
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package spittr.db.jpa;import java.util.List;import javax.persistence.EntityManager;import javax.persistence.PersistenceContext;import org.springframework.stereotype.Repository;import spittr.db.SpitterRepository;import spittr.domain.Spitter;@Repository public class JpaSpitterRepository implements SpitterRepository { @PersistenceContext private EntityManager entityManager; public long count () { return findAll().size(); } public Spitter save (Spitter spitter) { entityManager.persist(spitter); return spitter; } public Spitter findOne (long id) { return entityManager.find(Spitter.class , id ) ; } public Spitter findByUsername (String username) { return (Spitter) entityManager.createQuery("select s from Spitter s where s.username=?" ).setParameter(1 , username).getSingleResult(); } public List<Spitter> findAll () { return (List<Spitter>) entityManager.createQuery("select s from Spitter s" ).getResultList(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package spittr.db.jpa;import java.util.List;import javax.persistence.EntityManager;import javax.persistence.PersistenceContext;import org.springframework.stereotype.Repository;import spittr.db.SpittleRepository;import spittr.domain.Spittle;@Repository public class JpaSpittleRepository implements SpittleRepository { @PersistenceContext private EntityManager entityManager; public long count () { return findAll().size(); } public List<Spittle> findRecent () { return findRecent(10 ); } public List<Spittle> findRecent (int count) { return (List<Spittle>) entityManager.createQuery("select s from Spittle s order by s.postedTime desc" ) .setMaxResults(count) .getResultList(); } public Spittle findOne (long id) { return entityManager.find(Spittle.class , id ) ; } public Spittle save (Spittle spittle) { entityManager.persist(spittle); return spittle; } public List<Spittle> findBySpitterId (long spitterId) { return (List<Spittle>) entityManager.createQuery("select s from Spittle s, Spitter sp where s.spitter = sp and sp.id=? order by s.postedTime desc" ) .setParameter(1 , spitterId) .getResultList(); } public void delete (long id) { entityManager.remove(findOne(id)); } public List<Spittle> findAll () { return (List<Spittle>) entityManager.createQuery("select s from Spittle s" ).getResultList(); } }
由于EntityManager并不是线程安全的,一般并不适合注入到Repository这样共享的单例Bean中,我们使用@PersistanceContext注解解决这个问题。@PersistanceContext并没有注入一个真正的EntityManager,而是给了它一个EntityManager的代理。真正的EntityManager是与当前事务相关联的那一个,如果不存在,就会创建一个新的。@Transactional表明这个Repository中的持久化方法是在事务上下文中执行的。
以下是测试类部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package spittr.db.jpa;import static org.junit.Assert.*;import java.util.List;import org.junit.BeforeClass;import org.junit.Ignore;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import org.springframework.transaction.annotation.Transactional;import spittr.db.SpitterRepository;import spittr.domain.Spitter;@RunWith (SpringJUnit4ClassRunner.class ) @ContextConfiguration (classes =JpaConfig.class ) public class SpitterRepositoryTest { @Autowired SpitterRepository spitterRepository; @Test @Transactional public void count () { assertEquals(4 , spitterRepository.count()); } @Test @Transactional public void findAll () { List<Spitter> spitters = spitterRepository.findAll(); assertEquals(4 , spitters.size()); assertSpitter(0 , spitters.get(0 )); assertSpitter(1 , spitters.get(1 )); assertSpitter(2 , spitters.get(2 )); assertSpitter(3 , spitters.get(3 )); } }
三、借助Spring Data实现自动化的JPA Repository
尽管JPA-Hibernate代码已经很简单,但依然会有直接与EntityManager交互来查询数据库。我们借助Spring Data,以接口定义的方式创建Repository。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package spittr.db;import java.util.List;import org.springframework.data.jpa.repository.JpaRepository;import spittr.domain.Spitter;public interface SpitterRepository extends JpaRepository <Spitter , Long >, SpitterSweeper { Spitter findByUsername (String username) ; List<Spitter> findByUsernameOrFullNameLike (String username, String fullName) ; } package spittr.db;import java.util.List;import org.springframework.data.jpa.repository.JpaRepository;import spittr.domain.Spittle;public interface SpittleRepository extends JpaRepository <Spittle , Long >, SpittleRepositoryCustom { List<Spittle> findBySpitterId (long spitterId) ; }
这里我们并不需要实现findBySpittleId方法,findByUsername方法,findByUsernameOrFullNameLike方法,方法签名已经告诉Spring Data JPA足够的信息来创建这个方法的实现了。
编写Spring Data JPA Repository 的关键在于要从一组接口中选一个进行扩展,这里SpittleRepository扩展了Spring Data JPA 的JpaRepository。通过这种方式,JpaRepository进行了参数化,所以它就能知道这是一个用来持久化Spitter对象的Repository,并且Spitter对象的ID类型为Long。
接下来我们看看自定义查询方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package spittr.db;public interface SpitterSweeper { int eliteSweep () ; } package spittr.db;import javax.persistence.EntityManager;import javax.persistence.PersistenceContext;public class SpitterRepositoryImpl implements SpitterSweeper { @PersistenceContext private EntityManager em; public int eliteSweep () { String update = "UPDATE Spitter spitter " + "SET spitter.status = 'Elite' " + "WHERE spitter.status = 'Newbie' " + "AND spitter.id IN (" + "SELECT s FROM Spitter s WHERE (" + " SELECT COUNT(spittles) FROM s.spittles spittles) > 10000" + ")" ; return em.createQuery(update).executeUpdate(); } }
复杂查询可以自己编写。
配置Spring Data JPA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package spittr.db;import javax.sql.DataSource;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.jpa.repository.config.EnableJpaRepositories;import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;import org.springframework.orm.jpa.JpaTransactionManager;import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;import org.springframework.orm.jpa.vendor.Database;import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;@Configuration @EnableJpaRepositories ("spitter.db" )public class SpringDataJpaConfig { @Bean public DataSource dataSource () { return new EmbeddedDatabaseBuilder() .addScript("classpath:/com/habuma/spitter/db/jpa/schema.sql" ) .addScript("classpath:/com/habuma/spitter/db/jpa/test-data.sql" ) .build(); } @Bean public JpaTransactionManager transactionManager () { return new JpaTransactionManager(); } @Bean public HibernateJpaVendorAdapter jpaVendorAdapter () { HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter(); adapter.setDatabase(Database.H2); adapter.setShowSql(false ); adapter.setGenerateDdl(true ); return adapter; } @Bean public Object emf () { LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean(); emf.setDataSource(dataSource()); emf.setPersistenceUnitName("spitter" ); emf.setJpaVendorAdapter(jpaVendorAdapter()); return emf; } }
@EnableJpaRepositories(“spitter.db”)会扫描spittr.db包查找扩展自Spring Data JPA Repository接口的所有接口,如果发现了扩展自Repository的接口,他会自动(在应用启动的时候)生成这个接口的实现。
使用NoSQL数据库 一、使用MongoDB持久化文档数据
有一些数据的最佳表现形式是文档,也就是说,不要把这些数据分散到多个表、节点或实体中,将这些信息收集到一个非规范化(也就是文档)的结构中更有意义。如果数据之间有明显的关联关系,文档数据库就不太适合了。
我们在一个购物订单系统中学习MongoDB。接下来我们要配置Spring Data MongoDB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package orders.config;import com.mongodb.Mongo;import com.mongodb.MongoClient;import org.springframework.context.annotation.Configuration;import org.springframework.data.mongodb.config.AbstractMongoConfiguration;import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;@Configuration @EnableMongoRepositories (basePackages = "orders.db" )public class MongoConfig extends AbstractMongoConfiguration { @Override public Mongo mongo () throws Exception { return new MongoClient(); } @Override protected String getDatabaseName () { return "OrdersDB" ; } }
通过@EnableMongoRepository注解启用Spring Data 的自动化MongoDB repository生成功能,我们让配置类扩展AbstractMongoConfiguration并重载mongo()方法,getDatabaseName()方法,mongo()方法会直接返回一个MongoClient实例而不是声明MongoFactory bean 和MongoTemplate bean。使用MongoClient更加简单。这里需要一个运行在本地的MongoDB 服务器,MongoClient监听的默认的端口27017,如果需要更改端口在MongoClient构造方法中指定端口即可。
下面是Order类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package orders;import org.springframework.data.annotation.Id;import org.springframework.data.mongodb.core.mapping.Document;import org.springframework.data.mongodb.core.mapping.Field;import java.util.Collection;import java.util.LinkedHashSet;@Document public class Order { @Id private String id; @Field ("customer" ) private String customer; private String type; private Collection<Item> items = new LinkedHashSet<>(); public String getId () { return id; } public String getCustomer () { return customer; } public void setCustomer (String customer) { this .customer = customer; } public String getType () { return type; } public void setType (String type) { this .type = type; } public Collection<Item> getItems () { return items; } public void setItems (Collection<Item> items) { this .items = items; } }
我们可以看到Order类使用了@Document注解,这样它就能借助MongoTemplate或自动生成的Repository进行持久化,其id属性上使用了@Id注解用来指定它作为文档的ID,除此之外,customer属性上使用了@Field注解,这样在文档持久化的时候customer属性会映射为customer的域。同时items属性它指的是订单中具体条目的集合,在传统的关系型数据库中,这些条目会在另一个数据表中,通过外键进行引用,items域上可能还会有使用JPA的@OneToMany注解。
下面是Item类,Item类本身没有任何注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package orders;public class Item { private Long id; private Order order; private String product; private double price; private int quantity; public Long getId () { return id; } public Order getOrder () { return order; } public String getProduct () { return product; } public double getPrice () { return price; } public int getQuantity () { return quantity; } public void setProduct (String product) { this .product = product; } public void setPrice (double price) { this .price = price; } public void setQuantity (int quantity) { this .quantity = quantity; } }
我们定义OrderRepository扩展MongoRepository,它和Spring Data JPA一样,扩展了Repository的接口将会在运行时自动生成实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 package orders.db;import orders.Order;import org.springframework.data.mongodb.repository.MongoRepository;import org.springframework.data.mongodb.repository.Query;import java.util.List;public interface OrderRepository extends MongoRepository <Order ,String > { List<Order> findByCustomer (String customer) ; List<Order> findByCustomerLike (String customer) ; List<Order> findByCustomerAndType (String customer,String type) ; List<Order> getByType (String type) ; @Query ("{customer:'Chuck Wagon'}" ) List<Order> findChucksOrders () ; } 下面是测试类 package orders;import static org.junit.Assert.*;import java.util.Arrays;import java.util.List;import orders.config.MongoConfig;import orders.db.OrderRepository;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.mongodb.core.MongoOperations;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;@ContextConfiguration (classes=MongoConfig.class ) @RunWith (SpringJUnit4ClassRunner .class ) public class MongoDbTest { @Autowired private OrderRepository orderRepository; @Autowired MongoOperations mongoOps; @Before public void cleanup () { orderRepository.deleteAll(); } @Test public void testMongoRepository () { assertEquals(0 , orderRepository.count()); Order order = createAnOrder(); Order savedOrder = orderRepository.save(order); assertEquals(1 , orderRepository.count()); Order foundOrder = orderRepository.findOne(savedOrder.getId()); assertEquals("Chuck Wagon" , foundOrder.getCustomer()); assertEquals(2 , foundOrder.getItems().size()); List<Order> chucksOrders = orderRepository.findByCustomer("Chuck Wagon" ); assertEquals(1 , chucksOrders.size()); assertEquals("Chuck Wagon" , chucksOrders.get(0 ).getCustomer()); assertEquals(2 , chucksOrders.get(0 ).getItems().size()); List<Order> chuckLikeOrders = orderRepository.findByCustomerLike("Chuck" ); assertEquals(1 , chuckLikeOrders.size()); assertEquals("Chuck Wagon" , chuckLikeOrders.get(0 ).getCustomer()); assertEquals(2 , chuckLikeOrders.get(0 ).getItems().size()); List<Order> chucksWebOrders = orderRepository.findByCustomerAndType("Chuck Wagon" , "WEB" ); assertEquals(1 , chucksWebOrders.size()); assertEquals("Chuck Wagon" , chucksWebOrders.get(0 ).getCustomer()); assertEquals(2 , chucksWebOrders.get(0 ).getItems().size()); List<Order> chucksPhoneOrders = orderRepository.findByCustomerAndType("Chuck Wagon" , "PHONE" ); assertEquals(0 , chucksPhoneOrders.size()); List<Order> chucksOrders2 = orderRepository.findChucksOrders(); assertEquals(1 , chucksOrders2.size()); assertEquals("Chuck Wagon" , chucksOrders2.get(0 ).getCustomer()); assertEquals(2 , chucksOrders2.get(0 ).getItems().size()); orderRepository.delete(savedOrder.getId()); assertEquals(0 , orderRepository.count()); } private Order createAnOrder () { Order order = new Order(); order.setCustomer("Chuck Wagon" ); order.setType("WEB" ); Item item1 = new Item(); item1.setProduct("Spring in Action" ); item1.setQuantity(2 ); item1.setPrice(29.99 ); Item item2 = new Item(); item2.setProduct("Module Java" ); item2.setQuantity(31 ); item2.setPrice(29.95 ); order.setItems(Arrays.asList(item1, item2)); return order; } }
二、使用Neo4j操作位图数据
文档型数据库会将数据存储到粗粒度的文档中,图数据库会将数据存储到多个细粒度的节点中,这些节点之间通过关系建立关联。Spring Data Neo4j提供了将Java对象映射到节点和关联关系的注解、面向模板的Neo4j访问方式以及Repository’实现的自动化生成功能。
配置Spring Data Neo4j的关键在于声明GraphDatabaseService bean 和启用Neo4j Repository自动生成功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package orders.config;import org.neo4j.graphdb.GraphDatabaseService;import org.neo4j.graphdb.factory.GraphDatabaseFactory;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.neo4j.config.EnableNeo4jRepositories;import org.springframework.data.neo4j.config.Neo4jConfiguration;@Configuration @EnableNeo 4jRepositories(basePackages = "orders.db" )public class Neo4jConfig extends Neo4jConfiguration { public Neo4jConfig () { setBasePackage("orders" ); } @Bean (destroyMethod="shutdown" ) public GraphDatabaseService graphDatabaseService () { return new GraphDatabaseFactory() .newEmbeddedDatabase("/tmp/graphdb" ); } }
@EnableNeo4jRepositories注解能够让Spring Data Neo4j自动生成 Neo4j的Repository实现。定义GraphDatabaseService bean使用GraphDatabaseFactory 来创建嵌入式的Neo4j数据库。
Neo4j定义了两种类型的实体:节点和关联关系。节点反映了应用中的事物,而关联关系定义了这些事物是如何联系在一起的。
Order、Item是两个节点他们之间的关系是Order has items
下面我们为Order添加注解,使其成为图数据库中的一个节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package orders;import java.util.Collection;import java.util.LinkedHashSet;import java.util.Set;import org.springframework.data.neo4j.annotation.GraphId;import org.springframework.data.neo4j.annotation.NodeEntity;import org.springframework.data.neo4j.annotation.RelatedTo;@NodeEntity public class Order { @GraphId private Long id; private String customer; private String type; @RelatedTo (type="HAS_ITEMS" ) private Set<Item> items = new LinkedHashSet<Item>(); public String getCustomer () { return customer; } public void setCustomer (String customer) { this .customer = customer; } public String getType () { return type; } public void setType (String type) { this .type = type; } public Collection<Item> getItems () { return items; } public void setItems (Set<Item> items) { this .items = items; } public Long getId () { return id; } }
@NodeEntity注解代表这是一个节点,@GraphId注解代表这是一个图Id,@RelatedTo(type=”HAS_ITEMS”)注解代表Order与一个Item的Set存在关系。
Item也是图数据库中的节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package orders;import org.springframework.data.neo4j.annotation.GraphId;import org.springframework.data.neo4j.annotation.NodeEntity;@NodeEntity public class Item { @GraphId private Long id; private Order order; private String product; private double price; private int quantity; public Order getOrder () { return order; } public String getProduct () { return product; } public void setProduct (String product) { this .product = product; } public double getPrice () { return price; } public void setPrice (double price) { this .price = price; } public int getQuantity () { return quantity; } public void setQuantity (int quantity) { this .quantity = quantity; } public Long getId () { return id; } }
同样我们需要创建自动化的Neo4j Repository
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 package orders.db;import java.util.List;import orders.Order;import org.springframework.data.neo4j.repository.GraphRepository;public interface OrderRepository extends GraphRepository <Order > { List<Order> findByCustomer (String customer) ; List<Order> findByCustomerLike (String customer) ; List<Order> findByCustomerAndType (String customer, String type) ; List<Order> getByType (String type) ; } 下面是测试类 package orders;import static org.junit.Assert.*;import java.util.Arrays;import java.util.HashSet;import java.util.List;import orders.config.Neo4jConfig;import orders.db.OrderRepository;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;@ContextConfiguration (classes=Neo4jConfig.class ) @RunWith (SpringJUnit4ClassRunner .class ) public class Neo4jTest { @Autowired private OrderRepository orderRepository; @Before public void cleanup () { orderRepository.deleteAll(); } @Test public void testNeo4jRepository () { assertEquals(0 , orderRepository.count()); Order order = createAnOrder(); Order savedOrder = orderRepository.save(order); assertEquals(1 , orderRepository.count()); Order foundOrder = orderRepository.findOne(savedOrder.getId()); assertEquals("Chuck Wagon" , foundOrder.getCustomer()); assertEquals(2 , foundOrder.getItems().size()); List<Order> chucksOrders = orderRepository.findByCustomer("Chuck Wagon" ); assertEquals(1 , chucksOrders.size()); assertEquals("Chuck Wagon" , chucksOrders.get(0 ).getCustomer()); assertEquals(2 , chucksOrders.get(0 ).getItems().size()); List<Order> chuckLikeOrders = orderRepository.findByCustomerLike("Chuck.*" ); assertEquals(1 , chuckLikeOrders.size()); assertEquals("Chuck Wagon" , chuckLikeOrders.get(0 ).getCustomer()); assertEquals(2 , chuckLikeOrders.get(0 ).getItems().size()); List<Order> chucksWebOrders = orderRepository.findByCustomerAndType("Chuck Wagon" , "WEB" ); assertEquals(1 , chucksWebOrders.size()); assertEquals("Chuck Wagon" , chucksWebOrders.get(0 ).getCustomer()); assertEquals(2 , chucksWebOrders.get(0 ).getItems().size()); List<Order> chucksPhoneOrders = orderRepository.findByCustomerAndType("Chuck Wagon" , "PHONE" ); assertEquals(0 , chucksPhoneOrders.size()); orderRepository.delete(savedOrder.getId()); assertEquals(0 , orderRepository.count()); } private Order createAnOrder () { Order order = new Order(); order.setCustomer("Chuck Wagon" ); order.setType("WEB" ); Item item1 = new Item(); item1.setProduct("Spring in Action" ); item1.setQuantity(2 ); item1.setPrice(29.99 ); Item item2 = new Item(); item2.setProduct("Module Java" ); item2.setQuantity(31 ); item2.setPrice(29.95 ); order.setItems(new HashSet<Item>(Arrays.asList(item1, item2))); return order; } }
三、使用Redis操作键值对数据
Redis是一种特殊类型的数据库,它被称之为key-value存储,key-value存储与哈希Map有很大的相似性,可以把它理解为持久化的哈希Map。 连接Redis
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package cart;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;@Configuration public class RedisConfig { @Bean public RedisConnectionFactory redisCF () { return new JedisConnectionFactory(); } @Bean public RedisTemplate<String, Product> redisTemplate (RedisConnectionFactory cf) { RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>(); redis.setConnectionFactory(cf); return redis; } }
我们配置RedisConnectionFactory bean 通过默认构造器创建的连接工厂会向localhost上的6379端口创建连接。顾名思义,Redis连接工厂会生成到Redis key-value存储的连接,我们使用RedisTemplate简化数据访问,能够让我们持久化各种类型的key和value。 将RedisConnectionFactory bean 注入到 RedisTemlate中,RedisTemplate使用两个类型进行参数化,第一个是key的类型,第二个是value的类型。
下面是Product类,实现了Serializable接口,因为Redis存储对象时需要将其序列化,取出时需要反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package cart;import java.io.Serializable;public class Product implements Serializable { private static final long serialVersionUID = 1L ; private String sku; private String name; private float price; public String getSku () { return sku; } public void setSku (String sku) { this .sku = sku; } public String getName () { return name; } public void setName (String name) { this .name = name; } public float getPrice () { return price; } public void setPrice (float price) { this .price = price; } }
下面是测试类的一部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package cart;import static org.junit.Assert.*;import java.util.List;import java.util.Set;import org.junit.After;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.BoundListOperations;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;@RunWith (SpringJUnit4ClassRunner.class ) @ContextConfiguration (classes =RedisConfig.class ) public class CartTest { @Autowired private RedisConnectionFactory cf; @Autowired private RedisTemplate<String, Product> redis; @After public void cleanUp () { redis.delete("9781617291203" ); redis.delete("cart" ); redis.delete("cart1" ); redis.delete("cart2" ); } @Test public void workingWithSimpleValues () { Product product = new Product(); product.setSku("9781617291203" ); product.setName("Spring in Action" ); product.setPrice(39.99f ); redis.opsForValue().set(product.getSku(), product); Product found = redis.opsForValue().get(product.getSku()); assertEquals(product.getSku(), found.getSku()); assertEquals(product.getName(), found.getName()); assertEquals(product.getPrice(), found.getPrice(), 0.005 ); } }
缓存数据 缓存(Caching)可以存储经常会用到的信息,这样每次需要的时候,这些信息是立即可用的。尽管Spring本身没有实现缓存解决方案,但它对缓存功能提供了声明式的支持,能够与多种流行的缓存实现进行集成。
在往bean上添加缓存注解之前,必须要启用Spring对注解驱动缓存的支持。如果我们使用Java配置的话,可以在其中的一个配置类上加上@EnableCaching,这样的话就能启用注解驱动的缓存。
我们以Spittr 应用的Repository JDBC实现来演示缓存的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package spittr.config;import net.sf.ehcache.CacheManager;import org.springframework.cache.annotation.EnableCaching;import org.springframework.cache.ehcache.EhCacheCacheManager;import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.ClassPathResource;@Configuration @EnableCaching public class CachingConfig { @Bean public EhCacheCacheManager cacheManager (CacheManager cm) { return new EhCacheCacheManager(cm); } @Bean public EhCacheManagerFactoryBean ehcache () { EhCacheManagerFactoryBean ehCacheFactoryBean = new EhCacheManagerFactoryBean(); ehCacheFactoryBean.setConfigLocation(new ClassPathResource("spittr/cache/ehcache.xml" )); return ehCacheFactoryBean; } }
cacheManager()方法创建了一个EhCacheManager的实例,这是通过传入EhCache CacheManager实例实现的。稍微有些诡异的注入会让人感到迷惑,这是因为Spring和EhCache都定义了CacheManager类型。需要明确的是,EhCache的CacheManager要被注入到Spring的EhCacheManager之中。
我们使用EhCache的CacheManager来进行注入,所以也必须声明一个CacheManager bean。为了对其进行简化,Spring提供了EhCacheManagerFactoryBean 来生成EnCache 的CacheManager。方法ehcache()会创建并返回一个EhCacheManagerFactoryBean实例。因为它是一个工厂bean,所以注册在Spring应用上下文的并不是EhCacheManagerFactoryBean,而是CacheManager的一个实例。
下面是 ehcache.xml。
1 2 3 4 5 6 <ehcache > <cache name ="spittleCache" maxBytesLocalHeap ="50m" timeToLiveSeconds ="100" > </cache > </ehcache >
我们使用注解为SpittleRepository声明缓存规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package spittr.db;import java.util.List;import org.springframework.cache.annotation.CacheEvict;import org.springframework.cache.annotation.CachePut;import org.springframework.cache.annotation.Cacheable;import spittr.domain.Spittle;public interface SpittleRepository { long count () ; @Cacheable ("spittleCache" ) List<Spittle> findRecent () ; List<Spittle> findRecent (int count) ; @Cacheable ("spittleCache" ) Spittle findOne (long id) ; @CachePut (value="spittleCache" , key="#result.id" ) Spittle save (Spittle spittle) ; @Cacheable ("spittleCache" ) List<Spittle> findBySpitterId (long spitterId) ; @CacheEvict (value="spittleCache" ,condition="" ) void delete (long id) ; }
Spring提供了四个注解来声明缓存规则 @Cacheable 表明Spring在调用方法之前,首先应该在缓存中查找方法的返回值,如果这个值找到,就会返回缓存的值,否则的话,这个方法就会被调用,返回值会房贷缓存之中。 @CachePut 表明Spring应该将方法的返回值放入缓存之中,在方法的调用前并不会检查缓存,方法始终都会被调用。 @CacheEvict 表明Spring应该在缓存中清除一个或多个条目。 @Caching 这是一个分组的注解,能够同时应用多个其他的缓存注解。 将注解放在方法前即可启用缓存。
借助Spring Boot 简化 Spring 开发 Spring Boot 是令人兴奋的新项目,它提供了四个主要的特性,能够改变开发Spring应用程序的方式。
1、Spring Boot Starter: 他将常用的依赖分组进行了整合,将其合并到一个依赖中,这样就可以一次性添加到项目的Maven或Gradle构建中。
2、自动配置: Spring Boot 的自动配置特性利用了Spring 4对条件化配置的支持,合理地推测应用所需的bean 并自动化配置它们。
3、命令行接口: Spring 的 CLI 发挥了Groovy 编程语言的优势,并结合自动配置进一步简化 Spring 应用的开发。
4、Actuator: 它为Spring Boot 应用添加了一定的管理特性。
我们要从头开始编写一个新的 Spring 应用。这是一个 Web 项目,所以需要 Spring MVC 。同时还要有 REST API 将资源暴露为 JSON ,所以在构建中需要包含Jackson JSON 库
首先让我们从添加依赖开始,新建一个 Gradle 项目,在 build.gradle dependency 添加以下内容:
1 2 3 4 compile("org.springframework.boot:spring-boot-starter-web:1.1.4.RELEASE" ) compile("org.springframework.boot:spring-boot-starter-jdbc:1.1.4.RELEASE" ) compile("org.thymeleaf:thymeleaf-spring4:2.1.2.RELEASE" ) compile("com.h2database:h2:1.3.174" )
Spring Boot 的 Starter 减少了构建中的依赖列表的长度,而 Spring Boot 的自动配置功能则缩减了 Spring 配置的数量。它在实现时,会考虑应用中的其他因素并推断你所需要的 Spring 配置。
Spring Boot Starter 也会触发自动配置。例如在 Spring Boot 应用中,如果我们想使用 Spring MVC 的话,所需要做的仅仅是将 Web Starter 作为依赖放入到构建中。将 Web Starter 放入到构建中以后,它会自动添加 Spring MVC 依赖。如果 Spring Boot 的 Web 自动配置探测到 Spring MVC 位于类路径下,它将会自动配置支持 Spring MVC 的多个 bean,包括视图解析器、资源处理器、以及消息转换器等等。
Spring Boot CLI 充分利用了Spring Boot Starter 和自动配置的魔力,并添加了一些 Groovy 的功能,它简化了 Spring 的开发流程,通过 CLI ,我们能够运行一个或多个 Groovy 脚本,并查看它是如何运行的。在应用的运行过程中,CLI 能够自动导入 Spring 类型并解析依赖。
Spring Boot Actuator 为 Spring Boot 项目带来了很多有用的特性,包括:
1、管理端点;
2、合理的异常处理以及默认的“/error”映射端点;
3、获取应用信息的“Info”端点;
4、当启用Spring Security 时,会有一个审计事件框架。
这里我们开发一个 Contacts 应用。
ContactController 为 Contacts 应用处理基本的 Web 请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package contacts;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import java.util.List;import java.util.Map;@Controller @RequestMapping ("/" )public class ContactController { private ContactRepository contactRepo; @Autowired public ContactController (ContactRepository contactRepo) { this .contactRepo = contactRepo; } @RequestMapping (method = RequestMethod.GET) public String home (Map<String,Object> model) { List<Contact> contacts = contactRepo.findAll(); model.put("contacts" ,contacts); return "home" ; } @RequestMapping (method = RequestMethod.POST) public String submit (Contact contact) { contactRepo.save(contact); return "redirect:/" ; } }
home()方法使用注入的 ContactRepository 来获取所有 Contact 对象的列表,并将它们放到模型当中,然后把请求转交给home视图。这个视图将会展现联系人的列表以及添加新的Contact 表单。submit()方法将会处理表单提交的POST请求,保存Contact,并重定向到首页。
Contact 是一个简单的POJO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package contacts;public class Contact { private Long id; private String firstName; private String lastName; private String phoneNumber; private String emailAddress; public Long getId () { return id; } public void setId (Long id) { this .id = id; } public String getFirstName () { return firstName; } public void setFirstName (String firstName) { this .firstName = firstName; } public String getLastName () { return lastName; } public void setLastName (String lastName) { this .lastName = lastName; } public String getPhoneNumber () { return phoneNumber; } public void setPhoneNumber (String phoneNumber) { this .phoneNumber = phoneNumber; } public String getEmailAddress () { return emailAddress; } public void setEmailAddress (String emailAddress) { this .emailAddress = emailAddress; } }
按照传统的方式,Java Web 应用会使用JSP作为视图层的技术。但是,Thymeleaf 的原生模板比 JSP 更加便于使用,而且它能够让我们以 HTML 的形式编写模板。鉴于此,我们会使用Thymeleaf 来定义 Contacts 应用的home视图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <!DOCTYPE html > <html xmlns:th ="http://www.thymeleaf.org" > <head > <title > Spring Boot Contacts</title > <link rel ="stylesheet" th:href ="@{/style.css}" /> </head > <body > <h2 > Spring Boot Contacts</h2 > <form method ="POST" > <label for ="firstName" > First Name:</label > <input type ="text" name ="firstName" > </input > <br /> <label for ="lastName" > Last Name:</label > <input type ="text" name ="lastName" > </input > <br /> <label for ="phoneNumber" > Phone #:</label > <input type ="text" name ="phoneNumber" > </input > <br /> <label for ="emailAddress" > Email:</label > <input type ="text" name ="emailAddress" > </input > <br /> <input type ="submit" > </input > </form > <ul th:each ="contact : ${contacts}" > <li > <span th:text ="${contact.firstName}" > First</span > <span th:text ="${contact.lastName}" > Last</span > : <span th:text ="${contact.phoneNumber}" > phoneNumber</span > , <span th:text ="${contact.emailAddress}" > emailAddress</span > </li > </ul > </body > </html >
只要我们将 Thymeleaf 添加到项目的类路径下,就启用了 Spring Boot 的自动配置。当应用运行时,Spring Boot 将会探测到类路径中的Thymeleaf,然后会自动配置视图解析器,模板解析器以及模板引擎,这些都是在 Spring MVC 中使用 Thymeleaf所需要的。因此,在我们的应用中,不需要使用显示 Spring 配置的方式来定义 Thymeleaf。
ContactController 中 home() 方法返回的逻辑视图名为 home ,因此模板文件名为 home.html,自动配置的模板解析器会在指定的目录下查找Thymeleaf模板,这个目录也就是相对根类目录下的templates目录下 ,所以在Maven或Gradle项目中,我们需要将home.html放到“src / main / resources / templates” 中。这个模板中使用了style.css文件,我们倾向于将静态资源放入“src / main / resources / public”目录下。
style.css
1 2 3 4 5 6 7 8 9 10 body { background-color : #eeeeee ; font-family : sans-serif; } label { display : inline-block; width : 120px ; text-align : right; }
持久化数据
ContactRepository 能够从数据库中存取 Contact
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package contacts;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.jdbc.core.RowMapper;import org.springframework.stereotype.Repository;import java.sql.ResultSet;import java.sql.SQLException;import java.util.List;@Repository public class ContactRepository { private JdbcTemplate jdbc; @Autowired public ContactRepository (JdbcTemplate jdbc) { this .jdbc = jdbc; } public List<Contact> findAll () { return jdbc.query("select id, firstName, lastName, phoneNumber, emailAddress from contacts order by lastName" , new RowMapper<Contact>() { @Override public Contact mapRow (ResultSet rs, int rowNum) throws SQLException { Contact c = new Contact(); c.setId(rs.getLong(1 )); c.setFirstName(rs.getString(2 )); c.setLastName(rs.getString(3 )); c.setPhoneNumber(rs.getString(4 )); c.setEmailAddress(rs.getString(5 )); return c; } }); } public void save (Contact contact) { jdbc.update("insert into contacts(firstName,lastName,phoneNumber,emailAddress) values(?,?,?,?)" , contact.getFirstName(),contact.getLastName(),contact.getPhoneNumber(),contact.getEmailAddress()); } }
ContactRepository使用了@Repository注解,在组件扫描的时候他会被发现并创建为 Spring 应用上下文中的 bean。
JdbcTemplate 呢?我们难道不用再Spring应用上下文中声明JdbcTemplate bean 吗?为了声明它, 我们是不是需要声明一个H2 DataSource?
一切都不需要!!当 Spring Boot 嗅探到 Spring 的 JDBC 模块和H2 在类路径下的时候,自动配置就会发挥作用,将会自动配置 JdbcTemplate bean 和 H2 DataSource bean。
我们需要自己创建contacts表的模式,Spring Boot 可不会知道 contacts 表会是什么样子。
我们将一个名为schema.sql的脚本文件放入类路径根下,也就是Maven或Gradle项目目录下的”src/main/resources“目录下。
1 2 3 4 5 6 7 create table contacts ( id identity, firstName varchar(30 ) not null , lastName varchar (50 ) not null , phoneNumber varchar (13 ) , emailAddress varchar (30 ) ) ;
我们需要一个特定的类来启动Spring Boot 项目。
1 2 3 4 5 6 7 8 9 10 11 12 13 package contacts;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.context.annotation.ComponentScan;@ComponentScan @EnableAutoConfiguration public class Application { public static void main (String[] args) { SpringApplication.run(Application.class ,args ) ; } }
点击运行,打开浏览器输入localhost:8080就能进入Contacts应用。