greycode 2020-05-03T05:55:49+00:00 kejinlu@gmail.com Spring 快速上手 2016-01-24T00:00:00+00:00 greycode http://greycode.github.io//2016/01/spring-fast Spring 快速上手

1. 前言

本文是 Spring in Action, 4/e 的读书笔记,涵盖Spring 4.x新增的内容,并结合Spring的文档与源码做了一些注解。 因为是篇实战性文章,将不对Spring中具体技术的实现/原理作太多描述。

快速上手 ”之意是适合有经验的Spring开发人员作快速参考之用。

初学者不建议阅读。

2. Spring Core

2.1. Bean的装配

2.1.1. 装配方式

Spring 提供了三种主要的Bean装配方式:

  • 显示地在XML中配置

  • 显示地在Java中配置

  • 隐式地通过Bean的自动装配机制配置

三种方式的使用依个人喜好,但应尽可能使用自动化配置,减少显示的配置方式的使用。如果必须使用显示配置 的情况(比如要配置的Bean的源码并非由你维护),使用 类型安全(type-safe)、重构方便的 JavaConfig 要好于XML。 当然,在XML方式提供了非常方便的标签(或namespace)而JavaConfig中却没有等价实现的情况,可以使用XML。

2.1.2. Bean的自动装配

Spring对Bean的自动装配主要通过 “Component Scanning”“Autowiring” 来完成,其实分别指 @ComponentScan@Autowired 注解。

给类加上 @Component 注解就可以创建一个能被组件扫描机制(Component Scanning)发现的Bean:

package org.calvin.springfast;
import org.springframework.stereotype.Component;

@Component                                                           (1)
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);
  }
}
1 这种情况下该bean的ID就是首字母小写的类名,也可以通过使用 @Component("sgtPeppers") 显示赋一个ID

不过组件扫描默认是不开启的,需要在Spring中配置一下:

package org.calvin.springfast;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan                                                       (1)
public class CDPlayerConfig {
}
1 @ComponentScan 用于配置Spring的组件扫描功能

如上不加额外配置的情况,@ComponentScan 默认只扫描当前配置类所在package及其子package下加了 @Component 注解的类。 等价的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"
  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="org.calvin.springfast" />
</beans>

如果需要扫描其他的package,可以如下配置:

// 直接使用ComponentScan的value属性
@Configuration
@ComponentScan("org.calvin.springfast.other")
public class CDPlayerConfig { }

// 或者显示地配置basePackages
@Configuration
@ComponentScan(basePackages="org.calvin.springfast.other")
public class CDPlayerConfig { }

// 对于需要配置多个package的情况
@Configuration
@ComponentScan(basePackages={"org.calvin.springfast.one", "org.calvin.springfast.two"})
public class CDPlayerConfig { }

// 或者使用更加类型安全的方式配置
@Configuration
@ComponentScan(basePackageClasses={CDPlayer.class, DVDPlayer.class})
public class CDPlayerConfig { }

对于Bean中依赖的其他Bean,可以使用 @Autowired 装配,@Autowired可以加到构造函数、Setter方法或类变量之上, 如果没有找到匹配的Bean,Spring 会抛出异常,当然可以通过 @Autowired(required=false) 来避免, 但这么做@Autowired注解的依赖可能为null,需要注意检查:

@Autowired(required=false)
public CDPlayer(CompactDisc cd) {
  this.cd = cd;
}

2.1.3. 在Java中配置Bean

自动配置虽然很方便,不过在有些场景下,比如说要装配一个来自第三方库的组件,显然不可能或不适合在其源码上加上@Component和@Autowired, 这种情况适合使用显示地配置方式。

JavaConfig 的方式因为类型安全、重构友好、更加强大,相对XML是更好的选择。注意不要在JavaConfig的配置代码中掺入业务逻辑相关的代码。

使用 @Configuration 注解就可以创建一个JavaConfig的配置类,正如之前示例代码中那样,而要在JavaConfig配置类中创建Bean可以如下:

@Bean                                                                (1)
public CompactDisc sgtPeppers() {                                    (2)
  return new SgtPeppers();
}
1 @Bean 注解告诉Spring该方法所返回的对象应该在Spring 的ApplicationContext中注册为Bean。
2 该Bean的ID默认为该方法的名称,也可以使用 @Bean(name="yourBeanName") 为Bean显示设置ID。

在JavaConfig方式下注入依赖直接调用创建Bean的方法即可:

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

@Bean
public CDPlayer anotherCDPlayer() {
  return new CDPlayer(sgtPeppers());
}
1 因为sgtPeppers方法添加了 @Bean 注解,所有对该方法的调用都被Spring拦截并确保返回同一个Bean对象。

除了使用方法注入,还可以使用参数的方式:

@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {                  (1)
  return new CDPlayer(compactDisc);
}
// ..或者
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
  CDPlayer cdPlayer = new CDPlayer(compactDisc);
  cdPlayer.setCompactDisc(compactDisc);
  return cdPlayer;
}
1 CompactDisc会被Spring自动注入为之前声明的Bean

2.1.4. 在XML中配置Bean

XML 已经不是首选的配置方式,不过在某些特殊场景下(比如遗留代码等)还是会用到。

在XML中配置一个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"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context">
  <!-- configuration details go here -->

  <!--
    BlankDisc 的构造函数为 :
       public BlankDisc(String title, String artist, List<String> tracks) { }
     -->
  <bean class="org.calvin.springfast.BlankDisc">                     (1)
    <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
    <constructor-arg ref="compactDisc" />
    <constructor-arg>
      <list>
      <value>Sgt. Pepper's Lonely Hearts Club Band</value>
      <value>With a Little Help from My Friends</value>
      <ref bean="sgtPeppers" />
      <!-- ...other tracks omitted for brevity... -->
      </list>
    </constructor-arg>
    <property name="compactDisc" ref="compactDisc" />
    <property name="artist" value="The Beatles" />
    <property name="tracks">
      <set>
        <value>Getting Better</value>
        <value>Fixing a Hole</value>
        <!-- ...other tracks omitted for brevity... -->
      </set>
    </property>
  </bean>
</beans>
1 该bean没有显示设置ID,所以其ID由Spring设置为 org.calvin.springfast.SgtPeppers#0

这一个示例展示了如何配置一个Bean及其相关的依赖,包括构造函数和属性的注入。

2.1.5. 混合方式配置

XML和JavaConfig各有各的适用场景,也可能会存在两种方式同时使用的情况,这样要么在JavaConfig的配置类中引入XML配置文件, 要么在XML配置文件中引入JavaConfig的配置类,无论哪种方式,其实都是将配置好的Bean在Spring容器中注册。

JavaConfig中通过 @Import@ImportResource 注解来分别引入其他的配置类和配置文件。

例如存在:

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

那么可以在另外一个配置类中这样引入:

@Configuration
@Import(CDConfig.class)
public class CDPlayerConfig {
  @Bean
  public CDPlayer cdPlayer(CompactDisc compactDisc) {                (1)
    return new CDPlayer(compactDisc);
  }
}
1 因为已经使用@Import引入CDConfig配置类,所以这里的compactDisc将注入Bean,不会为空。

多个配置类的情况:

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

需要引入XML配置文件的情况:

@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath:cd-config.xml")                           (1)
public class SoundSystemConfig {
}
1 引入cd-config.xml后,其中定义的Bean可以注入到CDPlayerConfig中

我们知道在XML中引入其他XML配置文件使用如 <import resource="cd-config.xml" /> , 而如果要在XML中引入JavaConfig的配置类,只需在XML中将该配置类配置为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: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 class="org.calvin.springfast.CDConfig" />                    (1)
  <import resource="cdplayer-config.xml" />                          (2)
</beans>
1 引入JavaConfig配置类
2 引入另外一个XML配置文件

2.2. 高级用法

2.2.1. 环境相关的配置

Spring支持配置与环境相关的Bean,也就是在某些环境下这些Bean可用,而在其他环境下不可用, 比如与数据源相关的Bean,开发环境和生产环境所使用的完全不同的情况。

在Java配置方式下使用的是 @Profile 注解,该注解在Spring 3.1 只能放在类上,3.2之后可以跟 @Bean 注解一起放到方法上:

// @Profile 加到配置类上,则在该配置类中配置的所有的Bean,只在profile激活时才创建
@Configuration
@Profile("prod")
public class ProductionProfileConfig {
  @Bean
  public DataSource dataSource() {
    JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
    jndiObjectFactoryBean.setJndiName("jdbc/myDS");
    jndiObjectFactoryBean.setResourceRef(true);
    jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
    return (DataSource) jndiObjectFactoryBean.getObject();
  }
}

// @Profile 配合 @Bean 加到方法上,相关方法生成的Bean只在其所属Profile激活时加载
@Configuration
public class DataSourceConfig {

  @Bean(destroyMethod="shutdown")
  @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("jdbc/myDS");
    jndiObjectFactoryBean.setResourceRef(true);
    jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
    return (DataSource) jndiObjectFactoryBean.getObject();
  }
}

在XML中配置只需要使用 <beans profile="dev">..</beans> 将相关代码包括起来:

<?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">
    <jdbc:script location="classpath:schema.sql" />
    <jdbc:script location="classpath:test-data.sql" />
    </jdbc:embedded-database>
  </beans>

  <beans profile="qa">
    <bean id="dataSource"
      class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"
      p:url="jdbc:h2:tcp://dbserver/~/test"
      p:driverClassName="org.h2.Driver"
      p:username="sa"    p:password="password"
      p:initialSize="20" p:maxActive="30" />
  </beans>

  <beans profile="prod">
    <jee:jndi-lookup id="dataSource" jndi-name="jdbc/myDatabase"
      resource-ref="true" proxy-interface="javax.sql.DataSource" />
  </beans>

</beans>

Spring通过两个属性 spring.profiles.activespring.profiles.default 来决定应该激活的profile, spring.profiles.active 的值优先于 spring.profiles.default ,如果两者都没有赋值,则不激活任何profile, 也就是只有非profile中的Bean才会被创建。

可以通过许多途径设置这两个属性
  • 作为 DispatcherServlet 的初始化参数

  • 作为web应用的上下文参数

  • 作为JNDI记录

  • 作为环境变量

  • 作为JVM的系统属性

比如在 web.xml 中:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" >
...
<context-param>
  <param-name>spring.profiles.default</param-name>                   (1)
  <param-value>dev</param-value>
</context-param>

<servlet>
  <servlet-name>appServlet</servlet-name>
  <servlet-class>
    org.springframework.web.servlet.DispatcherServlet
  </servlet-class>
  <init-param>
    <param-name>spring.profiles.default</param-name>                 (2)
    <param-value>dev</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>
...
1 作为web应用的上下文参数
2 作为DispatcherServlet的初始化参数

2.2.2. 有条件约束的bean

Spring 4 新增的 @Conditional 注解可用来添加到@Bean注解的方法上,并在特定条件满足时才创建该Bean。 @Conditional注解只接受实现了Conditional接口的类:

public interface Condition {
  boolean matches(ConditionContext ctxt,  AnnotatedTypeMetadata metadata);
}
// ... 只有在属性magic不为空时满足条件
public class MagicExistsCondition implements Condition {
  public boolean matches( ConditionContext context, AnnotatedTypeMetadata metadata) {
    Environment env = context.getEnvironment();
    return env.containsProperty("magic");
  }
}
//..
@Bean
@Conditional(MagicExistsCondition.class)                             (1)
public MagicBean magicBean() {
  return new MagicBean();
}
1 MagicBean只有在属性magic不为空时才会创建

2.2.3. 解决Bean自动装配时的歧义性

我们知道Bean的自动装配是根据类型匹配的,如果是一个接口就可能就会存在多个实现类的情况, 这种情况下Bean的自动装配会失效并抛出异常 NoUniqueBeanDefinitionException

如果使用 @Autowired 会匹配到多个Bean,可以使用 @Primary 注解来消除歧义。@Primary注解的Bean总是被优先匹配。

// 加入存在如下需要自动装配实现了Dessert接口的Bean
@Autowired
public void setDessert(Dessert dessert) {
  this.dessert = dessert;
}

// @Primary 可以跟 @Component 一起使用
@Component
@Primary
public class IceCream implements Dessert { ... }

// 当然也可以跟 @Bean 方法一起
@Bean
@Primary
public Dessert iceCream() {
  return new IceCream();
}

如果使用XML:

<bean id="iceCream" class="com.desserteater.IceCream" primary="true" />

@Primary的问题是如果存在多个添加了该注解的Bean,那么在匹配时还是会存在歧义性。

可以使用 @Qualifier 配合 @Autowired 可以自动装配一个唯一的Bean, @Qualifier 注解的参数为Bean的唯一限定符,通常也就是Bean的ID或者是标注在Bean上的@Qualifier的参数。

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

// 以下为可能匹配到的情况

@Bean
public Dessert cold() { ... }

@Bean
@Qualifier("cold")
public Dessert iceCream() { ... }

@Component
@Qualifier("cold")
public class IceCream implements Dessert { ... }

除了@Qualifier和@Autowired的组合,还可以直接使用 @Resource(name="beanId")

2.2.4. Bean的生命周期

默认情况下Spring创建的Bean是单例模式,在整个应用生命周期中不管被使用、被注入多少次,始终是同一个实例。 Spring也支持其他生命周期范围的Bean:

  • Singleton ,整个应用中只有一个实例

  • Prototype ,每次在被注入或者从Spring应用上下文(application context)中获取时使用新创建的实例

  • Session ,在Web应用中使用,在每个会话范围内只有唯一的实例

  • Request ,在Web应用中使用,在每个请求范围内只有唯一的实例

可以使用 @Scope 注解配合 @Component@Bean 来配置Bean的生命周期:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)                      (1)
public class Notepad { ... }
// 或者
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad() {
  return new Notepad();
}
1 也可以使用 @Scope("prototype") ,使用SCOPE_PROTOTYPE常量可以减少可能的拼写错误

在XML中:

<bean id="notepad" class="com.myapp.Notepad" scope="prototype" />

在Web应用中对于需要在请求或会话中保存状态的Bean来说,全局的单例模式不适用,比如说一个购物车Bean, 一个生命周期为会话范围的Bean会更合适。

@Component
@Scope(
  value=WebApplicationContext.SCOPE_SESSION,                         (1)
  proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() { ... }
1 常量 SCOPE_SESSION 是从 WebApplicationContext 中获取的

这样Spring对于每个会话都会创建一个ShoppingCart的实例, 注意 @Scope 的 proxyMode 属性, 将一个会话范围的Bean注入到另一个单例模式的Bean中时,注入到单例Bean中的其实是Spring生成的一个代理, 该代理暴露会话范围Bean一样的方法,只是将具体的请求转发给实际的会话Bean实例。

Scoped-proxies
Figure 1. 会话/请求范围的Bean采用代理注入的方式

如上代码中如果ShoppingCart是接口那么proxyMode应为 ScopedProxyMode.INTERFACES , 如果是具体的类则必须为 ScopedProxyMode.TARGET_CLASS (这种情况下代理类是通过 CGLIB 生成的)。

在XML下等效的配置为:

<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
  <aop:scoped-proxy />                                               (1)
  <!-- <aop:scoped-proxy proxy-target-class="false" />  -->          (2)
</bean>
1 等效于@Scope中的proxyMode属性,默认是采用CGLIB生成基于目标类的代理
2 proxy-target-class 设为false则采用基于接口生成的代理

2.2.5. 运行时注入value

对在运行时值的注入,Spring提供了两种方式:

  • 属性(Property)占位符

  • SpEL 表达式

对于属性文件,最简单的使用方式就是先加载,然后再从 Environment 中获取其中属性的值:

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

  @Autowired
  Environment env;

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

// .. app.properties 的内容如下
disc.title=Sgt. Peppers Lonely Hearts Club Band
disc.artist=The Beatles
1 使用 @PropertySource 将指定位置的属性文件数据加载到 Environment

Environment 中提供了许多方便的方法用来获取属性文件中的值:

// 属性不存在时使用默认值
env.getProperty("disc.artist", "U2")
// 将属性的值转为特定的类型
int connectionCount =
  env.getProperty("db.connection.count", Integer.class, 30);
// 属性不存在时会抛出异常 IllegalStateException
env.getRequiredProperty("disc.title");
// 检查属性是否存在
boolean titleExists = env.containsProperty("disc.title");

除了Environment,另一种使用属性的方式就是用属性占位符(${ …​ })在运行时动态注入, XML方式下直接用就可以:

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

但在Java中需要使用 @Value 注解:

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

但要属性占位符生效,需要配置一个 PropertyPlaceholderConfigurer 或者 PropertySourcesPlaceholderConfigurer 的Bean,Spring 3.1之后的最好使用后者 ,因为可以从Environment中解析属性值(从属性文件中加载的)。

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

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"
  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:property-placeholder />

</beans>

2.3. Spring AOP

2.3.1. 什么是面向切面编程

面向切面编程中的一些概念
  • ADVICE (通知),通知定义了切面是什么( what )和什么时候使用( when ), Spring中可以使用五种类型的通知:

    • Before —— 在方法调被用前调用通知

    • After —— 在方法完成后调用通知,无论方法是否成功

    • After-returning —— 在方法成功执行后调用通知

    • After-throwing —— 在方法抛出异常后调用通知

    • Around —— 包裹被通知的方法,可以在方法调用前后执行自定义的行为

  • JOIN POINTS (连接点),在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时、 抛出异常时、甚至修改一个字段时

  • POINTCUTS (切点),切点定义了切面的“何处”( where ),切点会匹配通知所需要织入的一个或多个连接点

  • ASPECTS (切面),切面是通知和切点的结合——它是什么,在何时何处完成其功能

  • INTRODUCTIONS (引入),引入允许我们向已有的类添加新属性或方法

  • WEAVING (织入),织入是将切面应用到目标对象来创建新的代理对象的过程。目标对象的生命周期中有多个点可以织入:

    • 编译时——切面在目标类编译时织入,需要使用特殊的编译器比如AspectJ的织入编译器。

    • 类加载时——切面在目标类加载到JVM时织入,需要使用特殊的类加载器比如AspectJ 5中的 LTW。

    • 运行时——切面在应用运行的某个时间织入,AOP容器会为目标对象动态创建一个代理,Spring AOP就是这种方式

Spring支持4种类型的AOP

  1. Spring中经典的基于代理的AOP

  2. 纯POJO的切面

  3. @AspectJ注解驱动的切面

  4. 注入式AspectJ切面

前三种是Spring 自身对AOP实现的变体,Spring AOP是基于动态代理构建的,因此只限于对方法的拦截。

注意@AspectJ注解并不是基于AspectJ的,底层还是Spring基于代理的实现,只不过是Spring从AspectJ借鉴而来, 从而提供与AspectJ一致的编程模型。

2.3.2. 使用pointcuts来选择切入点

在Spring AOP中,需要使用AspectJ的切点表达式语言来定义切点。Spring只支持AspectJ切点指示器(pointcut designator) 的一个子集:

AspectJ 指示器 描述 通配符

args()

限定匹配的连接点为执行中的方法,且方法参数为指定的类型(动态切入)

@args()

限定匹配的连接点为执行中的方法,且方法参数的注解为指定的注解类型

execution()

匹配的连接点为执行中的方法

this()

限定匹配的连接点为AOP代理类所代理Bean的引用为指定类型的类

target()

限定匹配的连接点为当前目标对象(被代理对象)类型中的执行方法(只匹配类型,不匹配接口)

@target()

限定匹配的连接点为当前目标对象类型的执行方法,其中目标对象持有指定的注解

within()

限定匹配的连接点为指定类型的对象中的执行方法

@within()

限定匹配的连接点为带有指定注解的类型的对象中的执行方法(注解在类型上)

@annotation

限定匹配带有指定注解的当前执行方法(注解在方法上)

execution()指示器是唯一的执行匹配,其他的都是限定匹配。 所以execution()是编写切点时主要的指示器,其他的用来做限定。

假如存在:

package springfast;
public interface Performance {
  public void perform();
}

那么拦截 Performance 接口 perform 方法的 pointcut 表达式如下:

Scoped-proxies
Figure 2. 使用pointcut表达式拦截Performance的perform方法

返回值处的星号(*)和参数列表处的双点号(..)表示任意的返回值和参数。

如果想限定拦截的范围,比如当且仅当如上方法在包 org.calvin.springfast 中调用时:

execution(* springfast.Performance.perform(..))
        && within(org.calvin.springfast.*))                          (1)
1 这里用 && 来表示 and 的关系,类似的可以使用 || 表示 or , ! 表示 not 。 因为&&在XML中有特殊含义,所以可使用 and 代替 &&,其他也类似。

除了上面表格中的指示器,Spring 还引入了新的 bean() 指示器。 bean() 使用 Bean ID或Bean名称作为参数来限制切点只匹配特定的bean:

execution(* springfast.Performance.perform())
          and bean('woodstock')
更多关于pointcut表达式的描述可以参考 这里

2.3.3. 使用注解创建切面

在Spring中使用注解可以很容易地创建一个切面:

package org.calvin.springfast;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect                                                              (1)
public class Audience {

  @Before("execution(** springfast.Performance.perform(..))")           (2)
  public void silenceCellPhones() {
    System.out.println("Silencing cell phones");
  }

  @Before("execution(** springfast.Performance.perform(..))")
  public void takeSeats() {
    System.out.println("Taking seats");
  }

  @AfterReturning("execution(** springfast.Performance.perform(..))")
  public void applause() {
    System.out.println("CLAP CLAP CLAP!!!");
  }

  @AfterThrowing("execution(** springfast.Performance.perform(..))")
  public void demandRefund() {
    System.out.println("Demanding a refund");
  }

}
1 @Aspect 注解将该POJO类变为一个切面
2 @Before 注解的方法为一个Advice,在perform方法之前执行

上面方法中添加的通知注解指明该方法调用的时机,AspectJ一共定义了五种通知注解:

注解 通知

@After

(添加该注解的方法)在被拦截方法执行完毕或抛出异常后调用

@AfterReturning

在被拦截方法执行完毕调用

@AfterThrowing

在被拦截方法抛出异常后调用

@Around

包裹被拦截方法的执行

@Before

在被拦截方法执行之前执行

上个示例代码中所有通知注解中的pointcut表达式都是一样的,为了减少重复,可以在@Aspect注解的切面中使用 @Pointcut

...

@Aspect
public class Audience {

  @Pointcut("execution(** springfast.Performance.perform(..))")
  public void performance() {}                                       (1)

  @Before("performance()")
  public void silenceCellPhones() {
    System.out.println("Silencing cell phones");
  }

  @Before("performance()")
  public void takeSeats() {
    System.out.println("Taking seats");
  }

  ...

}
1 @Pointcut 注解的方法只是作为标记来引用,不需要定义任何操作

仅使用上面的注解并不能让切面生效,若使用JavaConfig,需要在配置类上添加 @EnableAspectJAutoProxy 注解开启 自动代理(auto-proxying)

@Configuration
@EnableAspectJAutoProxy                                              (1)
@ComponentScan
public class ConcertConfig {

  @Bean
  public Audience audience() {
    return new Audience();                                           (2)
  }

}
1 启用自动代理功能
2 将该切面声明为Bean

在XML配置环境中,等效的配置项为 <aop:aspectj-autoproxy> :

<?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"              (1)
  xsi:schemaLocation="http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd         (1)
    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="org.calvin.springfast" />

  <aop:aspectj-autoproxy />

  <bean class="org.calvin.springfast.Audience" />

</beans>
1 注意添加Spring的aop命名空间

自动代理机制会根据 @Aspect 注解的Bean中的通知方法,为其pointcut表达式匹配到的Bean创建代理。

Around通知是最强的通知类型,类似于before和after通知的结合:

@Aspect
public class Audience {

  @Pointcut("execution(** springfast.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();                                                  (1)
      System.out.println("CLAP CLAP CLAP!!!");
    } catch (Throwable e) {
      System.out.println("Demanding a refund");
    }
  }

}
1 注意参数ProceedingJoinPoint,ProceedingJoinPoint.proceed() 将调用被通知的方法。 如果没有调用,相当于忽略掉被通知的方法。

如果要在通知方法中获取被通知方法的参数,需要在pointcut表达式中配合使用 args :

@Aspect
public class TrackCounter {

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

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

}
1 args 的参数跟pointcut标记方法的参数一致,表示任何传给被通知方法playTrack的int参数也同样传给通知方法countTrack。

AOP中的 引入(introduction) 能给已有的Bean添加新方法。切面是实现了与所包装Bean相同接口的代理, 引入就是除了实现这些接口,还发布了新的接口,当新接口的方法被调用时,代理将调用委托给实现该接口的其他对象, 有点像一个Bean的实现被拆分到多个类中一样,例如:

public interface Encoreable {
  void performEncore();
}

// 该切面将Encoreable引入到所有Performance接口的实现中
@Aspect
public class EncoreableIntroducer {

  @DeclareParents(value="springfast.Performance+",                   (1)
                  defaultImpl=DefaultEncoreable.class)               (2)
  public static Encoreable encoreable;                               (3)

}
1 @DeclareParents 注解的 value 属性使用pointcut表达式指定将被引入新接口的beans
2 defaultImpl 指定所引入的新接口的实现类
3 添加了 @DeclareParents 注解的静态变量指明了将被引入的新接口 Encoreable

2.3.4. 在XML中创建切面

在XML中创建切面的方式可以避免对类的修改(不用添加注解)。 Spring 的 aop 命名空间中提供了许多配置元素:

AOP配置元素 描述

<aop:advisor>

定义AOP通知器

<aop:after>

定义后置通知

<aop:after-returning>

定义返回通知

<aop:after-throwing>

定义异常通知

<aop:around>

定义环绕通知

<aop:before>

定义前置通知

<aop:aspect>

定义切面

<aop:aspectj-autoproxy>

启用@AspectJ注解驱动的切面

<aop:config>

顶层的配置元素,大多的<aop:*>元素必须包含在该元素内

<aop:declare-parents>

为被通知对象提供额外接口和实现

<aop:pointcut>

定义切点

与之前注解方式配置的切面等效的XML配置如下:

<aop:config>

  <!-- 1.带各种通知的切面 -->
  <aop:aspect ref="audience">                                                   (1)
    <aop:pointcut
      id="performance"
      expression="execution(** springfast.Performance.perform(..))" />

    <aop:before pointcut-ref="performance" method="silenceCellPhones"/>
    <aop:before pointcut-ref="performance" method="takeSeats"/>
    <aop:after-returning  pointcut-ref="performance" method="applause"/>
    <aop:after-throwing
       pointcut="execution(** springfast.Performance.perform(..))"
       method="demandRefund"/>

    <aop:around
       pointcut-ref="performance"
       method="watchPerformance"/>                                              (2)
  </aop:aspect>

  <!-- 2. 往通知方法中传入参数 -->
  <aop:aspect ref="trackCounter">                                               (3)
    <aop:pointcut id="trackPlayed"
      expression= "execution(* soundsystem.CompactDisc.playTrack(int))
      and args(trackNumber)" />                                                 (4)

    <aop:before pointcut-ref="trackPlayed" method="countTrack"/>
  </aop:aspect>

  <!-- 3. 使用切面引入新接口(或功能) -->
  <aop:aspect>
    <aop:declare-parents
      types-matching="springfast.Performance+"
      implement-interface="springfast.Encoreable"
      default-impl="springfast.DefaultEncoreable"                               (5)
      />
  </aop:aspect>
</aop:config>
1 audience为前例中去掉所有注解的纯POJO类的bean,并去掉了 @Pointcut 注解的标记方法
2 watchPerformance的实现跟之前一样,参数为 ProceedingJoinPoint, 并调用 ProceedingJoinPoint.proceed(),只不过去掉了 @Around 注解
3 trackCounter为前例中去掉所有注解的纯POJO类的bean,并去掉了 @Pointcut 注解的标记方法
4 在XML中需要使用 and 代替 &&
5 如果需要委托的接口实现类是Bean的话可以使用 delegate-ref="encoreableDelegate" 代替 default-impl 属性

2.3.5. 使用AspectJ

AspectJ提供了Spring AOP所不支持的许多类型的切点。AspectJ切面与Spring 相互独立, 但如果AspectJ切面在执行通知时依赖于其他的类,虽然可以在切面内部实例化这些类, 但最好的方式还是使用Spring将依赖注入。

比如存在以下切面:

public aspect CriticAspect {
  public CriticAspect() {}

  pointcut performance() : execution(* perform(..));

  afterReturning() : performance() {
    System.out.println(criticismEngine.getCriticism());                         (1)
  }

  private CriticismEngine criticismEngine;
  public void setCriticismEngine(CriticismEngine criticismEngine) {             (2)
    this.criticismEngine = criticismEngine;
  }

}
1 切面在执行时依赖于criticismEngine的实现
2 通过Spring将依赖的CriticismEngine实现注入

因为需要使用Spring的依赖注入,需要在配置文件中将切面声明为Bean:

<bean class="com.springinaction.springidol.CriticAspect"
    factory-method="aspectOf">                                                  (1)
  <property name="criticismEngine" ref="criticismEngine" />
</bean>
1 CriticAspect切面的实例是由AspectJ创建而不是Spring,为了能将依赖注入到该切面实例中, 需要使用AspectJ的静态方法aspectOf(单例方法)来获取该切面的一个引用。

3. Spring Web

3.1. 创建Spring Web应用

3.1.1. Spring MVC 起步

一个请求在Spring MVC中首先会到达 DispatcherServlet , DispatcherServlet 是 Spring MVC 的 前端控制器(front controller,常见的web应用模式) ,这个单例的servlet负责将请求委托给应用的其他组件来执行实际的处理。

请求在Spring MVC中的处理流程如下:

Request-Life-cycle
Figure 3. Spring MVC中请求的处理流程

以前Servlet一般都是在web.xml中配置的(当然现在也可以),但根据Servlet 3规范以及从Spring 3.1开始, 我们还可以在Java中配置DispatcherServlet:

package org.calvin.springfast.config;
import org.springframework.web.servlet.support.
    AbstractAnnotationConfigDispatcherServletInitializer;

public class SpittrWebAppInitializer
    extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };                                     (1)
  }

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

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { WebConfig.class };                       (2)
  }
}
1 将DispatcherServlet映射到路径 “/”,可以指定多个路径
2 指定配置类

所有继承 AbstractAnnotationConfigDispatcherServletInitializer 的类将被自动识别并用于 配置 DispatcherServlet 以及在servlet上下文(context)中设置Spring 应用上下文(application context)。

解读AbstractAnnotationConfigDispatcherServletInitializer

在 Servlet 3.0 环境中,容器会查找classpath下所有实现了 javax.servlet.ServletContainerInitializer 接口的类, 并使用这些类来配置Servlet 容器。

Spring 中提供了一个实现该接口的类 SpringServletContainerInitializer ,这个类添加了 @HandlesTypes(WebApplicationInitializer.class) 这样的注解,因此Servlet 3.0+的容器将会根据 @HandlesTypes 注解的参数扫描所有 WebApplicationInitializer 接口的实现类, SpringServletContainerInitializer 再将后续的配置委托给这些类,Spring 3.2提供了一个方便的类实现了 WebApplicationInitializer 接口, 即 AbstractAnnotationConfigDispatcherServletInitializer

因此任何继承该类的子类在Servlet 3.0 环境中将被容器自动发现并用于配置servlet上下文。

注意上面代码中的两个配置类。Spring Web应用中通常会有两个Spring上下文,一个是由DispatcherServlet创建的子上下文, 主要用于创建和加载一些Web组件如控制器(controller)、视图解析器(view resolver)以及处理器映射(handler mapping); 另外一个是由ContextLoaderListener创建的根上下文,主要用于创建和加载一些中间层和数据层的Bean。

因为 AbstractAnnotationConfigDispatcherServletInitializer 已经在底层创建了 DispatcherServlet 和 ContextLoaderListener , getServletConfigClasses()getRootConfigClasses() 则分别对应用于配置其中的子上下文和根上下文的配置类。

在Java中配置的方式只适用于支持Servlet 3.0以上的环境,否则还是得在web.xml中配置DispatcherServlet。

在配置类中添加 @EnableWebMvc 注解就可以启用Spring MVC,不过仅使用这个注解只会用到 WebMvcConfigurationSupport 中的默认设置, 通常应该实现 WebMvcConfigurer 接口或直接继承 WebMvcConfigurerAdapter 类并重写相关方法来定制配置项:

@Configuration
@EnableWebMvc                                                                   (1)
@ComponentScan("org.calvin.springfast.web")                                     (2)
public class WebConfig extends WebMvcConfigurerAdapter {

  @Bean
  public ViewResolver viewResolver() {
    InternalResourceViewResolver resolver = new InternalResourceViewResolver(); (3)
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    resolver.setExposeContextBeansAsAttributes(true);
    return resolver;
  }

  @Override
  public void configureDefaultServletHandling(
      DefaultServletHandlerConfigurer configurer) {
    configurer.enable();                                                        (4)
  }

}

// 另外 RootConfig 的代码如下,仅作参考
@Configuration
@ComponentScan(basePackages={"org.calvin.springfast"},
    excludeFilters={
      @Filter(type=FilterType.ANNOTATION, value=EnableWebMvc.class)
    })
public class RootConfig {
}
1 启用Spring MVC
2 启用组件扫描
3 配置JSP视图解析器
4 配置对静态资源(如图片、css文件)的处理,将对这些资源的请求转接给容器的servlet

如果WebMvcConfigurer接口提供的配置项不能满足特殊的定制需求,可以考虑去掉配置类的注解 @EnableWebMvc ,而直接继承 WebMvcConfigurationSupportDelegatingWebMvcConfiguration

3.1.2. 编写简单的控制器(Controller)

在Spring MVC中,控制器其实就是一些类,这些类的方法添加了 @RequestMapping 注解以声明方法要处理的请求。

举几个简单的例子:

// 最简单的控制器,映射到路径 “/”
@Controller
public class HomeController {
  @RequestMapping(value="/", method=GET)
  public String home() {
    return "home";
  }
}

// 将RequestMapping添加到类上,类的所有方法的映射路径将继承此路径
@Controller
@RequestMapping("/")
public class HomeController {
  @RequestMapping(method=GET)
  public String home() {
    return "home";
  }
}

// 也可以在RequestMapping中映射多个路径
@Controller
@RequestMapping({"/", "/homepage"})
public class HomeController {
...
}

3.1.3. 处理请求参数

有三种方法可以向控制器的处理方法传入参数:

  • 查询(query)参数

  • 表单(form)参数

  • 路径(path)变量

使用查询参数

如果要处理请求中的查询参数,如 /spittles?max=238900&count=50 中的 maxcount , 需要使用 @RequestParam 注解:

// 类级的RequestMapping为 /spittles
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,                    (1)
    @RequestParam(value="count", defaultValue="20") int count) {
  return spittleRepository.findSpittles(max, count);
}
1 defaultValue 表示max参数为空时的默认值,因为查询参数都是String,所以只接受String类型, 这个不是必选项
使用路径变量

如果要处理请求路径中的一部分,如 /spittles/12345 中的 12345传给后台,需要使用 @PathVariable 注解:

// 类级的RequestMapping为 /spittles
@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
  public String spittle(
    @PathVariable("spittleId") long spittleId, Model model) {                   (1)
  model.addAttribute(spittleRepository.findOne(spittleId));
  return "spittle";
}
1 如果参数的名字和占位符的相同可以省略PathVariable的value值

3.1.4. 处理表单

处理表单提交的一个示例如下:

@Controller
@RequestMapping("/spitter")
public class SpitterController {

  @RequestMapping(value="/register", method=POST)
  public String processRegistration(Spitter spitter) {               (1)
    spitterRepository.save(spitter);
    return "redirect:/spitter/" + spitter.getUsername();             (2)
  }
}
1 Spitter 的属性值会自动匹配到同名的请求参数
2 InternalResourceViewResolver 对带有 redirect: 前缀的视图名会自动解析为重定向请求, 类似的还有 forward: 前缀会被解析为服务端转发。

Spring提供了对 Java Validation API(JSR-303) 的支持,在Spring MVC中不需要做额外的配置, 只要在项目的classpath下存在该API的实现如Hibernate Validator就可以使用。

Java Validation API中有许多用于约束属性值的注解:

注解 描述

@AssertFalse

被注解元素必须为 Boolean 类型值为 false

@AssertTrue

被注解元素必须为 Boolean 类型值为 true

@DecimalMax

被注解元素必须为数值类型且值小于等于给定的BigDecimal字符串

@DecimalMin

被注解元素必须为数值类型且值大于等于给定的BigDecimal字符串

@Digits

被注解元素必须为数值类型且值为指定的数

@Future

被注解元素必须为将来的某一个时间(date)

@Past

被注解元素必须为过去的某一个时间(date)

@Max

被注解元素必须为数值类型且值小于等于给定的值

@Min

被注解元素必须为数值类型且值大于等于给定的值

@NotNull

被注解元素的值必须必须不能为null

@Null

被注解元素的值必须必须为null

@Pattern

被注解元素的值必须匹配给定的正则表达式

@Size

被注解元素的值必须为String,集合(collection)或数组,其大小在指定的范围内

以上只是部分约束注解,一个示例:

public class Spitter {
  private Long id;

  @NotNull
  @Size(min=5, max=16)
  private String username;

  @NotNull
  @Size(min=5, max=25)
  private String password;

  @NotNull
  @Size(min=2, max=30)
  private String firstName;

  @NotNull
  @Size(min=2, max=30)
  private String lastName;                                           (1)
  ...
}
// ...
@RequestMapping(value="/register", method=POST)
public String processRegistration(
  @Valid Spitter spitter,                                            (2)
  Errors errors) {                                                   (3)
  if (errors.hasErrors()) {
    return "registerForm";
  }
  spitterRepository.save(spitter);
  return "redirect:/spitter/" + spitter.getUsername();
}
1 lastName 不能为null,字符长度在2~30之间
2 @Valid 指示Spring对注解的参数验证是否满足约束
3 如果某个约束验证失败可以从 Errors 中查到, Errors 必须紧跟在添加了 @Valid 注解的参数后面

3.2. 展示Web视图

3.2.1. 视图的解析

Spring MVC中的视图解析器是为了将处理请求的逻辑与视图的展现解耦。 Spring MVC定义了 ViewResolverView 接口:

public interface ViewResolver {
  View resolveViewName(String viewName, Locale locale) throws Exception;
}

public interface View {
  String getContentType();
  void render(Map<String, ?> model,
              HttpServletRequest request,
              HttpServletResponse response) throws Exception;
}

这两个接口可用于实现自定义的视图解析过程。

Spring已经提供许多开箱即用的视图解析器:

视图解析器 描述

BeanNameViewResolver

在Spring上下文中查找bean id与逻辑视图名匹配的View的实现

ContentNegotiatingViewResolver

根据客户端的请求内容类型将视图的请求委托给能产生该类型内容的视图解析器

FreeMarkerViewResolver

查找FreeMarker模版

InternalResourceViewResolver

查找应用内的资源(通常是JSP)

JasperReportsViewResolver

查找JasperReports报表文件

ResourceBundleViewResolver

根据属性文件(property file)查找视图View的实现

TilesViewResolver

查找Tiles模板文件

UrlBasedViewResolver

直接使用视图名查找物理位置所在的视图

VelocityLayoutViewResolver

使用多个Velocity模板组合为页面

VelocityViewResolver

解析为Velocity模板的视图

XmlViewResolver

从XML文件中查找View的实现,类似于BeanNameViewResolver

XsltViewResolver

解析为XSLT的视图

3.2.2. 创建JSP视图

InternalResourceViewResolver 使用 前缀(prefix)后缀(suffix) 并组合上逻辑视图名来 定位物理位置上的视图资源。

比如要匹配到 /WEB-INF/views/ 路径下的JSP文件,在Java中可以这么配置:

@Bean
public ViewResolver viewResolver() {
  InternalResourceViewResolver resolver = new InternalResourceViewResolver();
  resolver.setPrefix("/WEB-INF/views/");
  resolver.setSuffix(".jsp");
  return resolver;
}

XML中等效的配置为:

<bean id="viewResolver"
      class="org.springframework.web.servlet.view.InternalResourceViewResolver"
      p:prefix="/WEB-INF/views/"
      p:suffix=".jsp" />

这样当使用视图名比如 home 时,实际返回的会是 /WEB-INF/views/home.jsp

3.3. Spring MVC的高级用法

3.3.1. 备选的Spring MVC配置方式

在之前使用 AbstractAnnotationConfigDispatcherServletInitializer 配置时只重载了3个必要的方法, 还可以重载其他方法以提供更多配置项。比如 customizeRegistration() ,这个方法是在 DispatcherServlet 被注册到servlet容器后调用,参数为 ServletRegistration.Dynamic ,比如:

@Override
protected void customizeRegistration(Dynamic registration) {
  registration.setMultipartConfig(
    new MultipartConfigElement("/tmp/spittr/uploads"));
}

ServletRegistration.Dynamic 中可配置的项包括 setLoadOnStartup 设置加载优先级, setInitParameter 设置初始化参数, setMultipartConfig 配置对Servlet 3.0 mutipart的支持。

AbstractAnnotationConfigDispatcherServletInitializer 只创建了 DispatcherServletContextLoaderListener ,如果要添加其他的servlet、filter和listener就得定义自己的 初始化器类(initializer),最简单的方式就是实现 WebApplicationInitializer 接口 :

package com.myapp.config;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
import com.myapp.MyServlet;
import com.myapp.MyFilter;

public class MyServletInitializer implements WebApplicationInitializer {
  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    Dynamic myServlet =
              servletContext.addServlet("myServlet", MyServlet.class);          (1)
    myServlet.addMapping("/custom/**");

    javax.servlet.FilterRegistration.Dynamic filter =
              servletContext.addFilter("myFilter", MyFilter.class);             (1)
    filter.addMappingForUrlPatterns(null, false, "/custom/*");
  }
}
1 注册servle和filter

这种方式适合自定义servlet和filter的映射和配置,如果是直接将filter映射到 DispatcherServlet , 还有种可选的方式是重写 AbstractAnnotationConfigDispatcherServletInitializer 中的 getServletFilters

@Override
protected Filter[] getServletFilters() {
  return new Filter[] { new MyFilter() };                            (1)
}
1 该方法返回的所有filter都将自动映射到 DispatcherServlet

以上的所有在java中配置servlet、filter和listener的方式只适用于部署到servlet 3.0环境的应用, 如果不喜欢这种配置方式或是不得不在servlet低于3.0的环境下部署应用,可以使用传统的 web.xml 配置方式:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
  xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
      http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/spring/root-context.xml</param-value>                 (1)
  </context-param>

  <listener>
    <listener-class>
      org.springframework.web.context.ContextLoaderListener                     (2)
    </listener-class>
  </listener>

  <servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>
      org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      /WEB-INF/spring/appServlet/servlet-context.xml                            (3)
    </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>appServlet</servlet-name>                                     (4)
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>
1 配置Spring根上下文的路径,多个xml文件可以使用 , 分隔
2 注册ContextLoaderListener
3 配置DispatcherServlet中的Spring子上下文的路径
4 将DispatcherServlet映射到路径 “/”

这个web.xml加载Spring的配置文件也是XML,对于使用JavaConfig配置Spirng的方式, 需要修改 DispatcherServletContextLoaderListenercontextClass 参数:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
  xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
      http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

  <context-param>
    <param-name>contextClass</param-name>
    <param-value>
      org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    </param-value>
  </context-param>

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>com.habuma.spitter.config.RootConfig</param-value>             (1)
  </context-param>

  <listener>
    <listener-class>
      org.springframework.web.context.ContextLoaderListener
    </listener-class>
  </listener>

  <servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>
      org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
      <param-name>contextClass</param-name>
      <param-value>
        org.springframework.web.context.support.AnnotationConfigWebApplicationContext
      </param-value>
    </init-param>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>
        com.habuma.spitter.config.WebConfigConfig                               (2)
      </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>appServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>
1 Spring根上下文配置类
2 DispatcherServlet的Spring上下文配置类

3.3.2. 处理multipart表单数据

配置对mutipart的支持

multipart表单将表单数据拆分成多个独立的部分,每个部分对应一个表单域(field)而且可以有自己的类型,比如:

------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="firstName"

Charles
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="lastName"

Xavier
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="email"

charles@xmen.com
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="username"

professorx
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="password"

letmein01
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="profilePicture"; filename="me.jpg"
Content-Type: image/jpeg

[[ Binary image data goes here ]]
------WebKitFormBoundaryqgkaBn8IHJCuNmiW--

DispatcherServlet 不会解析multipart请求中的数据,而是委托给 MultipartResolver 接口解析其中的内容。 从Spring 3.1起,该接口就有两个开箱即用的实现可供选择:

  • CommonsMultipartResolver ,使用 Jakarta Commons FileUpload 解析mutipart请求

  • StandardServletMultipartResolver ,依靠 Servlet 3.0 对mutipart请求的支持。 这个应该首选,利用底层容器的支持而不需要添加其他依赖项。

StandardServletMultipartResolver 的使用非常简单,只需要将其声明为Bean即可:

@Bean
public MultipartResolver multipartResolver() throws IOException {
  return new StandardServletMultipartResolver();                     (1)
}
1 这里不能配置文件上传路径、文件大小限制等参数

但只是这样不会使其生效,必须在配置 DispatcherServlet 的时候同时配置mutipart的细节如上传文件的路径(路径是必须的)才可以使用。

如果使用 AbstractAnnotationConfigDispatcherServletInitializer 就重写方法 customizeRegistration

@Override
protected void customizeRegistration(Dynamic registration) {
  registration.setMultipartConfig(
      new MultipartConfigElement("/tmp/spittr/uploads"));
}

MultipartConfigElement 的构造函数还可以接受其他参数配置对文件或请求的限制:

  • 每个上传文件的最大值(字节),默认没有限制

  • 整个mutipart请求的最大值(字节),因为可以上传多个文件,这个限制很有意义,默认没有限制

  • 上传文件的最大缓冲值,超过该值后将被写入磁盘,默认没有限制,也就是直接写入磁盘。

如果在 web.xml 中配置 DispatcherServlet 的方式需要使用 <multipart-config> :

<servlet>
  <servlet-name>appServlet</servlet-name>
  <servlet-class>
    org.springframework.web.servlet.DispatcherServlet
  </servlet-class>
  <load-on-startup>1</load-on-startup>
  <multipart-config>
    <location>/tmp/spittr/uploads</location>
    <max-file-size>2097152</max-file-size>
    <max-request-size>4194304</max-request-size>
  </multipart-config>
</servlet>

非Servlet 3.0 环境可以使用 CommonsMultipartResolver ,不同的是,上传路径不是必须配置项, 默认的上传路径为容器的临时文件夹,而且可以直接在声明Bean的时候把各种参数配置好:

@Bean
public MultipartResolver multipartResolver() throws IOException {
  CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
  multipartResolver.setUploadTempDir(
      new FileSystemResource("/tmp/spittr/uploads"));
  multipartResolver.setMaxUploadSize(2097152);
  multipartResolver.setMaxInMemorySize(0);
  return multipartResolver;
}
处理mutipart请求

在Controller方法中处理mutipart请求上传的文件最常用的是在参数上添加 @RequestPart 注解, 参数的一种方式是使用 byte 数组:

@RequestMapping(value="/register", method=POST)
public String processRegistration(
    @RequestPart("profilePicture") byte[] profilePicture,
    @Valid Spitter spitter,
    Errors errors) {
      ...
}

这种方式的缺点是无法得知上传文件的类型、文件名等信息。另一种方式是使用 MultipartFile , 可以参考下该接口的代码:

public interface MultipartFile {
  String getName();
  String getOriginalFilename();
  String getContentType();
  boolean isEmpty();
  long getSize();
  byte[] getBytes() throws IOException;
  InputStream getInputStream() throws IOException;
  void transferTo(File dest) throws IOException;
}

如果是在Servlet 3.0 环境下,还有一个备选就是 javax.servlet.http.PartPartMultipartFile 许多地方都是一样的:

// Part 的接口
public interface Part {
  public InputStream getInputStream() throws IOException;
  public String getContentType();
  public String getName();
  public String getSubmittedFileName();
  public long getSize();
  public void write(String fileName) throws IOException;
  public void delete() throws IOException;
  public String getHeader(String name);
  public Collection<String> getHeaders(String name);
  public Collection<String> getHeaderNames();
}

// 使用 Part
@RequestMapping(value="/register", method=POST)
public String processRegistration(
    @RequestPart("profilePicture") Part profilePicture,
    @Valid Spitter spitter,
    Errors errors) {
      profilePicture.write("/data/spittr/" +
        profilePicture.getOriginalFilename());
      ...
}

3.3.3. 处理异常

Spring提供了一些能将异常转为响应(responses)的方式:

  • 特定的Spring异常会被自动转为HTTP状态码

  • 给异常添加 @ResponseStatus 注解可以将其映射到某个HTTP状态码

  • 在方式添加 @ExceptionHandler 注解处理异常

Spring自带的异常与HTTP状态码的映射为:

Spring异常 HTTP状态码

BindException

400 - Bad Request

ConversionNotSupportedException

500 - Internal Server Error

HttpMediaTypeNotAcceptableException

406 - Not Acceptable

HttpMediaTypeNotSupportedException

415 - Unsupported Media Type

HttpMessageNotReadableException

400 - Bad Request

HttpMessageNotWritableException

500 - Internal Server Error

HttpRequestMethodNotSupportedException

405 - Method Not Allowed

MethodArgumentNotValidException

400 - Bad Request

MissingServletRequestParameterException

400 - Bad Request

MissingServletRequestPartException

400 - Bad Request

NoSuchRequestHandlingMethodException

404 - Not Found

TypeMismatchException

400 - Bad Request

以上是在 DispatcherServlet 或是执行验证时可能抛出的异常,而应用内的异常默认为 500 - Internal Server Error , 使用 @ResponseStatus 可以自定义应用异常的HTTP状态码:

// 使用 @ResponseStatus自定义异常的映射
package org.calvin.springfast.web;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value=HttpStatus.NOT_FOUND,
                reason="Spittle Not Found")
public class SpittleNotFoundException extends RuntimeException {
}

// ...SpittleNotFoundException被抛出时,返回 404 - Spittle Not Found
@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String spittle(
    @PathVariable("spittleId") long spittleId,
    Model model) {
  Spittle spittle = spittleRepository.findOne(spittleId);
  if (spittle == null) {
    throw new SpittleNotFoundException();
  }
  model.addAttribute(spittle);
  return "spittle";
}

相比异常出现时返回状态码,可能更希望像处理正常的请求那样返回一个错误页面的视图名, 或是自定义的responsebody,虽然可以在处理请求的方法中直接捕获异常,然后再如此处理, 但会使代码显得臃肿。

可以在controller的异常处理方法上添加 @ExceptionHandler 注解来拦截指定异常:

@ExceptionHandler(DuplicateSpittleException.class)                   (1)
public String handleDuplicateSpittle() {
  return "error/duplicate";                                          (2)
}
1 指定该方法捕获的异常类型
2 返回逻辑视图名,跟正常的请求处理方法一样

该方法只会捕获当前controller中的 任意 请求处理方法抛出的 DuplicateSpittleException 。 如果要捕获 任意 controller中的 任意 请求处理方法抛出的异常,需要配合 通知型控制器

3.3.4. 通知型控制器(Advising controllers)

Spring 3.2 新增了一种 控制器通知 用于拦截所有的控制器。一个通知型控制器就是添加了 @ControllerAdvice 并包含至少以下一种类型方法的类:

  • @ExceptionHandler 注解的方法

  • @InitBinder 注解的方法

  • @ModelAttribute 注解的方法

以上在通知型控制器的方法中将被用于应用所有的控制器中添加了 @RequestMapping 的方法。 @ControllerAdvice 注解本身添加了 @Component 注解,因此添加该注解的类会被组件扫描发现。

一个最实用的场景就是使用 @ControllerAdvice@ExceptionHandler 在一个类中处理所有控制器可能抛出的异常:

@ControllerAdvice
public class AppWideExceptionHandler {
  @ExceptionHandler(DuplicateSpittleException.class)
  public String duplicateSpittleHandler() {
    return "error/duplicate";
  }
}

3.3.5. 在重定向请求中携带数据

在处理完 POST 请求后建议做一次重定向,这样可以避免客户端在刷新或点击后退按钮时重复提交。

我们在前面见过 redirect: 的用法。当一个控制器方法返回以 redirect: 开头的字符串,这个字符串不会被用来查找视图, 而是直接作为浏览器重定向到的路径(返回302状态码,并设置HTTP头的location字段为重定向地址)。

重定向实际上是两个请求,那么怎样在后续请求中继续使用之前请求中的数据呢?有两种途径可以解决:

  • 使用URL模板,并将数据作为路径变量( path variable )或是请求参数( query parameter

  • 使用 flash 属性 传递数据

使用URL模板

使用路径变量和请求参数的方式看上去实现很简单,把参数作为字符串拼接起来就可以了。 但在URL和SQL中使用字符串拼接并不安全。

Spring中提供了URL模板的方式:

@RequestMapping(value="/register", method=POST)
public String processRegistration(Spitter spitter, Model model) {
  spitterRepository.save(spitter);
  model.addAttribute("username", spitter.getUsername());             (1)
  model.addAttribute("spitterId", spitter.getId());                  (2)
  return "redirect:/spitter/{username}";                             (3)
}
1 必须在 Model 中设置占位符 username 的值
2 spitterId 并没有对应的占位符,因此将作为请求参数拼接到路径后如 /spitter/habuma?spitterId=42
3 username 中所有不安全(或非法)的字符将被转义

这种传数据的方式简单直接,但只对简单的字符串和数值类型友好,复杂的数据就不方便了。

使用 flash 属性

这种方式适用于传递复杂的对象数据。Spring负责将这些数据临时保存到session中,并在重定向后清理掉。

Spring 3.1 中新增了一个 Model 的子接口 RedirectAttributes 用于设置 flash 属性, 比如可以使用 addFlashAttribute()

// 第一个请求
@RequestMapping(value="/register", method=POST)
public String processRegistration(Spitter spitterA, RedirectAttributes model) {
  spitterRepository.save(spitterA);
  model.addAttribute("username", spitterA.getUsername());
  model.addFlashAttribute("spitter", spitterA);                       (1)
  return "redirect:/spitter/{username}";
}

// 被重定向到的请求
@RequestMapping(value="/spitter/{username}", method=GET)
public String showSpitterProfile(@PathVariable String username, Model model) {
  if (!model.containsAttribute("spitter")) {
    model.addAttribute(
    spitterRepository.findByUsername(username));
  }
  return "profile";
}
1 或者也可以使用 model.addFlashAttribute(spitterA); ,这样将根据类型推导出key的值为 spitter

第一个请求处理结束后, Model 中的 flash 属性被存入到session中。 而在被重定向到的请求处理之前,所有的 flash 属性从session中取出(然后清理掉)并放入该请求中的 Model 里。

4. Spring Backend

4.1. 使用Spring JDBC玩转数据库

4.1.1. Spring的数据访问思想

了解spring数据访问层的异常体系

我们知道在使用JDBC的时候必须得捕获 SQLException ,但从这个异常并不能获知错误的细节或如何处理。 该异常可能由以下情况导致:

  • 应用无法连接到数据库

  • 正在执行的查询语句存在语法错误

  • 查询中使用的表或列名不存在

  • 执行 insertupdate 时违背数据库的约束条件

  • …​

对异常的处理既要能捕获到具体的细节,又不能太依赖框架的实现(比如Hibernate的异常体系), 这正是 Spring 的异常所解决的问题。

Table 1. JDBC的异常体系 vs Spring 的异常体系
JDBC 异常类 Spring 异常类

BatchUpdateException

DataTruncation

SQLException

SQLWarning

BadSqlGrammarException

CannotAcquireLockException

CannotSerializeTransactionException

CannotGetJdbcConnectionException

CleanupFailureDataAccessException

ConcurrencyFailureException

DataAccessException

DataAccessResourceFailureException

DataIntegrityViolationException

DataRetrievalFailureException

DataSourceLookupApiUsageException

DeadlockLoserDataAccessException

DuplicateKeyException

EmptyResultDataAccessException

IncorrectResultSizeDataAccessException

IncorrectUpdateSemanticsDataAccessException

InvalidDataAccessApiUsageException

InvalidDataAccessResourceUsageException

InvalidResultSetAccessException

JdbcUpdateAffectedIncorrectNumberOfRowsException

LobRetrievalFailureException

NonTransientDataAccessResourceException

OptimisticLockingFailureException

PermissionDeniedDataAccessException

PessimisticLockingFailureException

QueryTimeoutException

RecoverableDataAccessException

SQLWarningException

SqlXmlFeatureNotImplementedException

TransientDataAccessException

TransientDataAccessResourceException

TypeMismatchDataAccessException

UncategorizedDataAccessException

UncategorizedSQLException

Spring 的异常类都继承于 DataAccessException ,而 DataAccessException 是非检查异常( unchecked exception )。 Spring的立场是:多数异常应该抛出而不是被捕获,Spring将捕获处理具体的异常与否交由开发者来决定。

对数据的访问模版化

Spring自带的一些数据访问模版类:

模板类( org.springframework.* `) 用途

jca.cci.core.CciTemplate

JCA CCI 连接

jdbc.core.JdbcTemplate

JDBC连接

jdbc.core.namedparam.NamedParameterJdbcTemplate

JDBC连接,支持命名参数

jdbc.core.simple.SimpleJdbcTemplate

JDBC连接,使用Java 5简化(自Spring 3.1已废弃)

orm.hibernate3.HibernateTemplate

Hibernate 3.x+ sessions

orm.ibatis.SqlMapClientTemplate

iBATIS SqlMap clients

orm.jdo.JdoTemplate

JDO

orm.jpa.JpaTemplate

JPA实体管理器

4.1.2. 配置数据源

使用JNDI数据源

使用Spring jee 命名空间中的 <jee:jndi-lookup> 元素可以获取JNDI中的任意对象——包括数据源——并将其作为Bean以供使用。

在XML中配置为:

<jee:jndi-lookup id="dataSource"
    jndi-name="/jdbc/SpitterDS"                                      (1)
    resource-ref="true" />                                           (2)
1 用于从JNDI中查找的名称
2 若应用运行在java应用服务器中,将 resource-ref 设为 true 会在 jndi-name 中添加 java:comp/env/ 前缀

在Java中配置需要使用 JndiObjectFactoryBean 从JNDI中获取数据源:

@Bean
public JndiObjectFactoryBean dataSource() {
  JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
  jndiObjectFB.setJndiName("jdbc/SpittrDS");
  jndiObjectFB.setResourceRef(true);
  jndiObjectFB.setProxyInterface(javax.sql.DataSource.class);
  return jndiObjectFB;
}
使用池化(pooled)数据源

现今有很多开源的数据源连接池库,其中大多数在Spring中的配置都类似于Spring自带的 DriverManagerDataSourceSingleConnectionDataSource 。比如 DBCP 的 BasicDataSource

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
      p:driverClassName="org.h2.Driver"
      p:url="jdbc:h2:tcp://localhost/~/spitter"
      p:username="sa"
      p:password=""
      p:initialSize="5"
      p:maxActive="10" />

或是在Java中:

@Bean
public BasicDataSource dataSource() {
  BasicDataSource ds = new BasicDataSource();
  ds.setDriverClassName("org.h2.Driver");
  ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
  ds.setUsername("sa");
  ds.setPassword("");
  ds.setInitialSize(5);
  ds.setMaxActive(10);
  return ds;
}

更多的属性配置项建议阅读相关连接池库的文档。

使用基于JDBC驱动的数据源

Spring中提供了三种基于驱动的数据源:

  • DriverManagerDataSource ——每次请求时返回一个 连接,该连接是非池化的。

  • SimpleDriverDataSource ——类似于 DriverManagerDataSource ,不过它直接使用JDBC驱动以避免在特定环境下类加载的问题。

  • SingleConnectionDataSource ——每次请求时返回 同一个 连接。

这些配置起来跟DBCP的数据源一样简单,比如在Java中配置 DriverManagerDataSource

@Bean
public DataSource dataSource() {
  DriverManagerDataSource ds = new DriverManagerDataSource();
  ds.setDriverClassName("org.h2.Driver");
  ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
  ds.setUsername("sa");
  ds.setPassword("");
  return ds;
}

等效的XML配置为:

<bean id="dataSource"
      class="org.springframework.jdbc.datasource.DriverManagerDataSource"
      p:driverClassName="org.h2.Driver"
      p:url="jdbc:h2:tcp://localhost/~/spitter"
      p:username="sa"
      p:password="" />

这类数据源适用于小型应用或开发环境,不要在其他情况下使用。

使用嵌入式数据源

嵌入式数据库非常适合测试或开发环境:不用连接到外部的数据库服务器,每次重启应用后会被重置。

如下是使用Spring jdbc 命名空间来配置一个预加载测试数据的H2数据库的示例:

<?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:c="http://www.springframework.org/schema/c"
    xsi:schemaLocation="http://www.springframework.org/schema/jdbc
      http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
      http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd">

  <jdbc:embeddeddatabase id="dataSource" type="H2">                             (1)
     <jdbc:script location="com/habuma/spitter/db/jdbc/schema.sql"/>
     <jdbc:script location="com/habuma/spitter/db/jdbc/test-data.sql"/>
  </jdbc:embedded-database>

</beans>
1 type 还可以设为 DERBY 来嵌入 Apache Derby 数据库;通过 id 可获取/注入该嵌入式数据源

在Java中的配置:

@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:schema.sql")
            .addScript("classpath:test-data.sql")
            .build();
}
使用profile选择数据源

使用Spring的profile非常适合于在不同环境下切换不同的数据源。

比如在Java配置中:

package org.calvin.springfast.config;
import org.apache.commons.dbcp.BasicDataSource;
import javax.sql.DataSource;
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;

@Configuration
public class DataSourceConfiguration {

  @Profile("development")
  @Bean
  public DataSource embeddedDataSource() {
    return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:schema.sql")
                .addScript("classpath:test-data.sql")
                .build();
  }

  @Profile("qa")
  @Bean
  public DataSource Data() {
    BasicDataSource ds = new BasicDataSource();
    ds.setDriverClassName("org.h2.Driver");
    ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
    ds.setUsername("sa");
    ds.setPassword("");
    ds.setInitialSize(5);
    ds.setMaxActive(10);
    return ds;
  }

  @Profile("production")
  @Bean
    public DataSource dataSource() {
    JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
    jndiObjectFactoryBean.setJndiName("jdbc/SpittrDS");
    jndiObjectFactoryBean.setResourceRef(true);
    jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
    return (DataSource) jndiObjectFactoryBean.getObject();
  }
}

等效的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/jdbc
        http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
        http://www.springframework.org/schema/jee
        http://www.springframework.org/schema/jee/spring-jee-3.1.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

  <beans profile="development">
    <jdbc:embeddeddatabase id="dataSource" type="H2">
      <jdbc:script location="com/hab uma/spitter/db/jdbc/schema.sql"/>
      <jdbc:script location="com/habum a/spitter/db/jdbc/test-data.sql"/>
    </jdbc:embeddeddatabase>
  </beans>

  <beans profile="qa">
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
          p:driverClassName="org.h2.Driver"
          p:url="jdbc:h2:tcp://localhost/~/spitter"
          p:username="sa" p:password=""
          p:initialSize="5" p:maxActive="10"/>
  </beans>

  <beans profile="production">
    <jee:jndi-lookup id="dataSource"
          jndi-name="/jdbc/SpitterDS"
          resource-ref="true"/>
  </beans>
</beans>

4.1.3. 使用 Spring JDBC

经考验证明比较好的存储数据方式还是JDBC。JDBC毕竟不需要额外掌握一种框架,而且基于SQL, 可以很方便的调整优化,完全掌控应用对数据的存取。

Spring JDBC 减少了直接使用JDBC时连接管理和异常处理的代码,并提供了三个JDBC模板类:

  • JdbcTemplate ——Spirng JDBC模板类的基本款,提供基于索引参数的查询

  • NamedParameterJdbcTemplate ——提供可绑定命名参数的查询

  • SimpleJdbcTemplate ——利用Java 5的特性简化JDBC模板的使用,从Spring 3.1开始已经废弃

配置 JdbcTemplate 只要提供一个 DataSource

@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
  return new JdbcTemplate(dataSource);
}

 [TBD]

4.2. 使用ORM框架

4.3. 事务管理

4.4. 对NOSQL的支持

4.5. 缓存数据

4.5.1. 开启缓存

Spring中使用缓存有两种形式:基于注解和基于XML。

如果使用Java配置方式,可以在配置类添加 @EnableCaching 开启缓存:

@Configuration
@EnableCaching                                                       (1)
public class CachingConfig {
  @Bean
  public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager();                          (2)
  }
}
1 开启缓存功能
2 声明缓存管理器

在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: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>

@EnableCaching<cache:annotation-driven /> 在底层是一样的工作机制,它们都是创建一层代理, 根据缓存注解的类型和状态来获取、更新或删除缓存。缓存管理器的作用是用于集成某个特定的缓存实现。

配置缓存管理器

Spring 3.1 自带了5个开箱即用的缓存管理器实现:

  • SimpleCacheManager

  • NoOpCacheManager

  • ConcurrentMapCacheManager

  • CompositeCacheManager

  • EhCacheCacheManager

Spring 3.2 新增一个用于兼容 JCache (JSR-107) 缓存的管理器,除了这些在Spring核心框架中的,Spring Data也提供了两个:

  • RedisCacheManager (Spring Data Redis项目)

  • GemfireCacheManager (Spring Data GemFire项目)

使用哪种缓存管理器取决于想使用的缓存实现,缓存管理器在使用时必须在Spring的上下文中声明为Bean。

下面代码展示如何配置 EhCacheCacheManager

package org.calvin.springfast.cache;

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) {         (1)
    return new EhCacheCacheManager(cm);
  }

  @Bean
  public EhCacheManagerFactoryBean ehcache() {                       (2)
    EhCacheManagerFactoryBean ehCacheFactoryBean = new EhCacheManagerFactoryBean();
    ehCacheFactoryBean.setConfigLocation(
      new ClassPathResource("org/calvin/springfast/cache/ehcache.xml"));
    return ehCacheFactoryBean;
  }
}
1 注意Spring中的 EhCacheCacheManager ,在配置时需要用到Ehcache的 CacheManager
2 EhCacheManagerFactoryBean 是个工厂Bean,用于生成Ehcache的 CacheManager

如果使用Redis,可以用Spring Data Redis项目中的 RedisCacheManager ,不过需要同时配置 RedisTemplateRedisConnectionFactory 的实现类(比如 JedisConnectionFactory ):

package com.myapp;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
@EnableCaching
public class CachingConfig {
  @Bean
  public CacheManager cacheManager(RedisTemplate redisTemplate) {
    return new RedisCacheManager(redisTemplate);
  }
  @Bean
  public JedisConnectionFactory redisConnectionFactory() {
    JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
    jedisConnectionFactory.afterPropertiesSet();
    return jedisConnectionFactory;
  }
  @Bean
  public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisCF) {
    RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>();
    redisTemplate.setConnectionFactory(redisCF);
    redisTemplate.afterPropertiesSet();
    return redisTemplate;
  }
}

另外还可以使用 CompositeCacheManager 组合多个缓存管理器并从其中查询内容。

4.5.2. 为方法添加缓存注解

Spring提供了一些缓存注解:

注解 描述

@Cacheable

告知Spring在调用方法前首先从缓存中查找内容作为该方法的返回值。在缓存里找到就返回, 否则就调用方法并将方法的返回值放入缓存中

@CachePut

指示Spring将方法的返回值放入缓存。不会在方法调用前检查缓存是否存在该值,总是调用方法。

@CacheEvict

指示Spring将某个或某些缓存内容释放

@Caching

一个用于将其他的缓存注解作为一组来使用的注解

可以通过 @Cacheable@CachePut 往缓存里添加值,两者的不同可以从上表中看出来, 但两个注解有些相同的属性:

属性 类型 描述

value

String[]

缓存使用的名字,注意不是key

condition

String

SpEL表达式,如果为 false ,被注解的方法将不启用缓存功能

key

String

SpEL表达式,用于定制key

unless

String

SpEL表达式,如果为 true ,方法的返回值将不被添加到缓存

比如可以这么使用 @Cacheable

@Cacheable("spittleCache")
public Spittle findOne(long id) {                                    (1)
  try {
    return jdbcTemplate.queryForObject(
              SELECT_SPITTLE_BY_ID,
              new SpittleRowMapper(),
              id);
  } catch (EmptyResultDataAccessException e) {
    return null;
  }
}
1 参数 id 的值将作为缓存内容的键值(key)

如果将注解 @Cacheable 添加到接口的方法上,那么该接口的所有实现类都将继承该注解及其规则。

@Cacheable@CachePut 有个 key 属性可通过使用SpEL表达式来自定义缓存key的生成。 Spring提供了许多SpEL的扩展:

表达式 描述

#root.args

可获取到传给被缓存方法的参数,以数组的形式

#root.caches

方法执行的缓存,以数组形式

#root.target

目标对象

#root.targetClass

目标对象的class,是 #root.target.class的简写

#root.method

被缓存的方法

#root.methodName

被缓存的方法的名称,#root.method.name的简写

#result

被缓存的方法返回的值(该属性 @Cacheable 不可用)

#Argument

该方法任何参数的名称或索引

比如可以将 key 定义为返回对象的id属性:

@CachePut(value="spittleCache", key="#result.id")                    (1)
Spittle save(Spittle spittle);
1 spittle对象的id是在保存后由数据库生成的,因此得使用 #result.id 来获取

在方法上添加缓存注解实际上会由Spring对该方法创建一个Around切面,因此可以有条件性地控制缓存的开启, 也就是使用之前图表中的 unlesscondition 两个属性。

如果 unless 的 SpEL 表达式值为 true ,那么被注解方法的返回值将不被放入缓存,类似的, 如果 condition 的 SpEL 表达式值为 false ,那么将不在被注解的方法上启用缓存功能将。

这两个属性表面看来作用一样,但其实 unless 仅是阻止返回值被放入缓存, 但被注解的方法在调用时依然会去查询缓存,并在缓存中找到值后返回。 而 condition 是在 false 时不使用缓存。比如:

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

在返回的Spittle对象中如果其message属性包含 NoCache ,则该对象将不会放入缓存中。

@CacheEvict 用于缓存的移除(比如在数据被删掉时),与 CacheableCachePut 不同的是, CacheEvict 可以用在返回 void 的方法上(毕竟 CacheableCachePut 需要将返回值放入缓存 )。

4.5.3. 在XML中配置

对于无法接触到源码的Bean,添加缓存注解是不可行的,这样就得借助在XML中配置的方式来解决。

在XML中配置需要用到 cache 命名空间,另外因为添加缓存必须使用切面,因此 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:cache="http://www.springframework.org/schema/cache"
    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/cache
    http://www.springframework.org/schema/cache/spring-cache.xsd">

    <!-- 这里放置缓存相关的配置 -->
</beans>

cache 命名空间中提供的一些可能会用到的配置元素如下表:

配置元素 描述

<cache:annotation-driven>

启用注解驱动的缓存配置。等价于Java配置中的 @EnableCaching

<cache:advice>

定义用于配置缓存的通知,配合 <aop:advisor> 使用

<cache:caching>

在缓存通知中定义一些与缓存相关的规则

<cache:cacheable>

指定某个方法可被缓存,等价于 @Cacheable

<cache:cache-put>

不多说了,等价于 @CachePut

<cache:cache-evict>

等价于 @CacheEvict

举个例子示范一下用法:

<?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"
    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/cache
        http://www.springframework.org/schema/cache/spring-cache.xsd">

    <aop:config>
      <aop:advisor advice-ref="cacheAdvice"                                          (1)
        pointcut=
            "execution(* com.springfast.spittr.db.SpittleRepository.*(..))"/>
    </aop:config>

    <cache:advice id="cacheAdvice">
        <cache:caching>
          <cache:cacheable cache="spittleCache" method="findRecent" />               (2)
          <cache:cacheable cache="spittleCache" method="findOne" />
          <cache:cacheable cache="spittleCache" method="findBySpitterId" />

          <cache:cache-put   cache="spittleCache" method="save" key="#result.id" />  (2)
          <cache:cache-evict cache="spittleCache" method="remove" />
        </cache:caching>
    </cache:advice>

    <bean id="cacheManager"
          class="org.springframework.cache.concurrent.ConcurrentMapCacheManager" />
</beans>
1 将缓存的通知绑定到该切面上
2 <cache:cacheable><cache:cache-put> 等的 cache 属性值是重复的, 可以将其放到上层中,如 <cache:caching cache="spittleCache" >…​.</cache:caching>

需要注意的是 <cache:advice> 有个 cache-manager 属性,默认值为 cacheManager , 所以如果像上面代码那样可以不用设置,但如果缓存管理器的ID不等于默认值,则必须设置。

5. Spring Integration

5.1. 远程服务

5.1.1. RMI

5.2. 使用Spring MVC创建REST API

5.2.1. 创建第一个REST服务

使用之前的Spring Web技术就可以用于创建REST API,不过传统的 Spring Web 返回内容为 HTML 格式, 这不能满足大多数使用REST的场景(一般为JSON或XML),对返回内容的展现,Spring 提供了两个选择:

  • Content negotiation 内容协商,根据请求要求的返回内容格式选择视图

  • Message conversion 消息转换,对 Controller 返回的对象进行转换

基于内容的视图展现

Spring 提供的视图解析器 ContentNegotiatingViewResolver 用于根据客户端请求的媒体类型来选择视图。

@Bean
public ViewResolver cnViewResolver() {
    return new ContentNegotiatingViewResolver();
}

内容协商(content-negotiation)的第一步是弄清楚客户端所请求资源的展现格式。 ContentNegotiatingViewResolver 优先考虑 URL 中的文件扩展名,然后才是请求头中的 Accept ,比如 URL 结尾文件扩展名为 .json ,那么就被认为请求的媒体类型为 application/json

如果扩展名和 Accept 都未奏效, ContentNegotiatingViewResolver 将返回默认内容类型(content type) /

决定类型后 ContentNegotiatingViewResolver 将视图的解析交由其他视图解析器来完成,并将这些视图解析器返回的视图放入候选列表中,最后根据内容类型遍历该列表来匹配出可以满足的视图。

另外,媒体类型的选择过程可以通过定制 ContentNegotiationManager 来进行干涉:

  • 设置默认的内容类型

  • 指定通过某个请求参数来决定内容类型

  • 忽略 Accept 请求头

  • 将URL扩展映射为指定的媒体类型

ContentNegotiationManager 可以通过三种方式来配置:

  • 直接声明一个类型为 ContentNegotiationManager 的Bean(不推荐)

  • 间接使用 ContentNegotiationManagerFactoryBean 创建Bean(推荐在XML配置中使用)

  • 重载 WebMvcConfigurerAdapter.configureContentNegotiation() 方法(推荐在Java配置中使用)

XML中如下:

<bean id="contentNegotiationManager"
    class="org.springframework.http.ContentNegotiationManagerFactoryBean"
    p:defaultContentType="application/json">

Java中:

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.defaultContentType(MediaType.APPLICATION_JSON);
}

配置好 ContentNegotiationManager 之后需要将其注入到 ContentNegotiatingViewResolver 的属性中:

@Bean
public ViewResolver cnViewResolver(ContentNegotiationManager cnm) {
    ContentNegotiatingViewResolver cnvr = new ContentNegotiatingViewResolver();
    cnvr.setContentNegotiationManager(cnm);
    return cnvr;
}

// JSON视图
@Bean
public View spittles() {
    return new MappingJackson2JsonView();
}

ContentNegotiatingViewResolver 的好处在于不用修改 Controller 层代码,不足的是,作为视图解析器, 并不能决定 Controller 可以消费的内容类型(比如在客户端发送的数据为 JSON 或 XML 格式时)。

使用 HTTP 信息转换器

信息转换直接将 Controller 返回的数据转换为客户端请求的类型。 Spring 自带了许多信息转换器用于处理 “对象到展现(object-to-representation)” 类的需求。

信息转换器 描述

AtomFeedHttpMessageConverter

ROME 框架中的 Feed 对象转为 Atom feeds(媒体类型为 application/atom+xml )。当在 classpath 中检测到 ROME 库时自动注册。

BufferedImageHttpMessageConverter

将图片的二进制数据转为 BufferedImage

ByteArrayHttpMessageConverter

读或写二进制数组。可读取任意媒体类型( / ),返回 application/ octet-stream

FormHttpMessageConverter

将内容以 application/x-www-form-urlencoded 的媒体类型读取到 MultiValueMap<String,String> 中。 同样也可以将 MultiValueMap<String,String>application/x-www-form-urlencoded 的媒体类型写回,而 MultiValueMap<String, Object> 则返回 multipart/form-data 的媒体类型

Jaxb2RootElementHttpMessageConverter

在xml和JAXB2注解的类之间转换。当在类路径中存在 Jaxb 2.x 时自动注册该转换器

MappingJackson2HttpMessageConverter

在json和类或HashMap之间转换。当在类路径中存在 Jackson 2 JSON库时自动注册

RssChannelHttpMessageConverter

在 RSS 源和 Rome 中的 Channel 对象直接转换。当类路径中存在 Rome 库时自动注册

如果客户端请求中的 Accept 头信息表示可以处理 application/json ,Jackson JSON 库在类路径中, 则 Controller 返回的对象将油 MappingJacksonHttpMessageConverter 转为 JSON 。

有些转换器由Spring自动注册,无需配置,但依然得保证依赖的库在应用的 ClassPath 中可以找到。

正常情况下 Controller 返回的对象会走 Model/View 处理流程,若要跳过转由信息转换器处理, 需要在 Controller 方法上添加 @ResponseBody

@RequestMapping(method=RequestMethod.GET, produces="application/json")     (1)
public @ResponseBody List<Spittle> spittles(                               (2)
    @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
    @RequestParam(value="count", defaultValue="20") int count) {
    return spittleRepository.findSpittles(max, count);
}
1 使用 produces 属性声明该方法将只处理 Acceptapplication/json 的请求
2 使用 @ResponseBody 后返回的 List<Spittle> 将由 MappingJacksonHttpMessageConverterMappingJackson2HttpMessageConverter (会使用哪个取决于类路径下Jackson库的版本)转为 JSON 后返回

Spring 中的信息转换器是可以双向转换的。正如可以用 @ResponseBody 应用转换器返回数据到客户端, 也可以使用 @RequestBody 来让 Spring 选择信息转换器来处理客户端发送来的数据。

@RequestMapping(method=RequestMethod.POST,consumes="application/json")
public @ResponseBody
    Spittle saveSpittle(@RequestBody Spittle spittle) {              (1)
    return spittleRepository.save(spittle);
}
1 添加 @RequestBody 后 Spring 会根据请求中的 Content-Type 头信息来选择合适的转换器, 将请求数据转为 Spittle 对象

在每个方法上添加 @ResponseBody@RequestBody 是件很繁琐的事, Spring 4.0 新增 @RestController 代替 @Controller 用于解决这个问题。添加该注解后 Spring 会在所有的处理方法中使用信息转换器。

5.2.2. 不止是提供资源

一个设计良好的 REST API 可不是只在客户端和服务端直接传输数据,同样也会提供一些与数据相关的元数据信息。

返回错误信息

目前为止,我们的代码在未找到数据的情况下依旧会向客户端返回 200(OK) 的状态码以及一个空的响应体,而显然返回 404(Not Found) 才是合适的。

Spring 提供了一些选择用于这类情况:

  • 使用 @ResponseStatus 指定返回的状态码

  • 控制器方法可以返回 ResponseEntity 以返回更多的元数据信息

  • 使用异常处理器专门处理这些错误的情况,而不用影响原来的控制器方法

除了 @ResponseBody ,控制器方法也可以返回 ResponseEntityResponseEntity 中包含返回的对象数据以及元数据(比如响应头和状态码),比如:

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

ResponseEntity 本身就相当于使用了 @ResponseBody ,所以其携带的数据也会自动被转换。

结合使用异常处理器和 @ResponseStatus 注解修改后如下:

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

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

在 REST 中保存数据成功后应该返回 201 (Created) ,并在响应头的 Loaction 头信息里返回创建资源的地址。

状态码可以使用 @ResponseStatus 设置,而响应头信息则可以通过在 ResponseEntity 中设置 HttpHeaders

@RequestMapping(method=RequestMethod.POST,consumes="application/json")
public ResponseEntity<Spittle> saveSpittle(
    @RequestBody Spittle spittle, UriComponentsBuilder ucb) {               (1)

    Spittle spittle = spittleRepository.save(spittle);
    HttpHeaders headers = new HttpHeaders();
    URI locationUri = ucb.path("/spittles/")
        .path(String.valueOf(spittle.getId()))
        .build()
        .toUri();

    headers.setLocation(locationUri);
    ResponseEntity<Spittle> responseEntity = new ResponseEntity<Spittle>(
        spittle, headers, HttpStatus.CREATED)
    return responseEntity;
}
1 UriComponentsBuilder 由 Spring 提供,用于构建 URL

5.3. Spring中的消息

5.4. 使用Websocket和STOMP发送消息

WebSocket 是一种全双工的通信协议,它可以让服务端和客户端发送异步消息。

Spring 4.0 新增了对WebSocket的支持,包括:

  • 一套底层的API用于发送和接收消息

  • 一套高级的API用于在Spring MVC的控制器(controller)中处理消息

  • 一个用于发送消息的模板

  • 支持SockJS,以应对在不支持websocket的浏览器、服务器、代理中使用的情况

5.4.1. 使用Spirng的底层Websocket API

5.5. 使用Spring发送Email

5.6. 用JMX管理Spring Bean

5.7. 通过Spring Boot简化Spring开发

 [TBD]

]]>
Apache Maven上手指南 2015-04-24T00:00:00+00:00 greycode http://greycode.github.io//2015/04/Introducing_Maven

###什么是Maven Maven 是一个项目管理与构建(Build)工具,不过对于程序员来说我们主要关注它提供的自动构建和依赖jar包的管理功能。

  1. 自动化构建。Maven自定义了一套构建生命周期过程,从清理(clean)、编译(compile)、测试(test)与生成报告,到打包(package)和部署(deploy),只需执行相应命令(如 mvn compile)即可由maven帮我们完成相应的操作。
  2. 第三方依赖(jar包)管理。Maven在构建过程中会自动从配置的maven 库(默认为Maven的中央库)中下载该项目依赖的所有jar包到本地库中,并将其引入项目的编译路径中。

###安装Maven ####命令行 1.确保已经安装过JDK,否则需要先到Oracle官网下载JDK安装。然后到https://maven.apache.org/download.cgi下载apache-maven-x.x.x-bin.zip后解压到安装目录(如D:\Program Files (x86)\apache-maven)。
2.接下来配置环境变量,新增M2_HOME指向之前的解压目录,并在path中增加% %M2_HOME%\bin;,如下:

M2_HOME M2_HOME_PATH

打开命令行,输入mvn -v查看是否安装成功:

C:\Users\Walker>mvn -v
Apache Maven 3.2.5 (12a6b3acb947671f09b81f49094c53f426d8cea1; 2014-12-15T01:29:2
3+08:00)
Maven home: D:\Program Files (x86)\apache-maven
Java version: 1.8.0, vendor: Oracle Corporation
Java home: D:\Java\jdk1.8.0\jre
Default locale: zh_CN, platform encoding: GBK
OS name: "windows 8.1", version: "6.3", arch: "x86", family: "dos"

3.接下来打开maven安装目录下conf\settings.xml文件,查找localRepository,若是没找到或是被注释掉了,则新增一行:

<localRepository>/path/to/local/repo</localRepository>

这个字段主要用来配置本地库的路径,因为以后所有从远程库下载的jar包都会存放在这里,windows下建议改到一个空间较大的盘下(默认路径为${user.home}/.m2/repository),比如将上面的/path/to/local/repo替换为D:/Users/walker/.m2/repository

4.增加Maven远程库镜像。因为默认的远程库可能被墙等原因导致无法访问的,所以需要增加国内的maven库镜像,不过目前国内只有开源中国的源。 同样是修改conf\settings.xml,在<mirrors>..</mirrors>中新增如下:

<mirror>
	<id>nexus-osc</id>
	<mirrorOf>*</mirrorOf>
	<name>Nexus osc</name>
	<url>http://maven.oschina.net/content/groups/public/</url>
</mirror> 

具体的修改可以参考 开源中国 Maven 库使用帮助。 ####IDE插件 如果你使用的是最新版的Eclipse或IntelliJ IDEA,这些工具已经集成maven,否则需要到Eclipse Marketplace或IDEA Plugin Repository下载。
Eclipse需要修改的地方如下:

Eclipse Maven

###使用Maven

通常情况下,我们会从命令行进入pom.xml所在目录,执行命令。
比如运行:

mvn compile

该命令将编译整个工程,生成class文件。 或者运行:

mvn clean intall

将清理整个工程(自动生成的class和其他资源文件都被清理掉)重新构建并安装。
另外Maven 使用惯例优于配置的原则 。它要求项目有如下的结构:

project
 ├─src
 │  ├─main
 │  │  ├─java
 │  │  │  └─org
 │  │  │      └─greycode
 │  │  └─resources
 │  └─test
 │      ├─java
 │      │  └─org
 │      │      └─greycode
 │      └─resources
 └─target
    ├─classes
    │  └─org
    │      └─greycode
    └─...

maintest 分别为项目的源代码和测试代码目录,resources主要是用于存放其他资源文件比如log4j2.xml,spring-root.xml等。 使用Maven编译后的class会放在target/classes目录下。

等等!你可能会想问pom.xml是什么鬼,而mvn compilemvn install有什么区别,这些命令又让maven在后台做了些什么?

嗯……所以有些概念还是得了解一下。 ###一些概念

#####POM(Project Object Model) Maven使用名为pom.xml的文件来描述项目的信息、输入输出目录、依赖的第三方jar包、需要使用的maven插件以及在构建项目过程中会采取的行为(比如如何打包部署等)。 一个典型的pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

	<modelVersion>4.0.0</modelVersion>
	<groupId>org.greycode.demo</groupId>
	<artifactId>maven-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<dependencies>
		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>18.0</version>
		</dependency>
	</dependencies>

	<build>
		<sourceDirectory>src</sourceDirectory>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.1</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

你可能注意到了groupId:artifactId:version这样的组合。
groupId:artifactId:version通常被理解为Maven中的坐标,Maven以此来标记一个唯一的组件,比如org.springframework:spring-context:4.1.6.RELEASE将定位到 spring-context-4.1.6.RELEASE.jar,maven使用坐标主要是为了仓库和依赖管理,因为第三方的jar包实在太多,使用坐标将非常方便地帮助我们找到需要的组件。
其中groupId为某组织机构的项目名称(或者干脆就是项目名),artifactId为项目下模块的名称/标识,version为组件的版本。
我们自己的项目也需要配置groupId:artifactId:version
其他更多pom.xml的选项可以参考https://gist.github.com/greycode/ca9775743f7e4ef827c1 。
#####插件 Maven大多数的功能都是通过插件来完成的,包括我们之前用到的mvn compile命令。 每个插件都包含一些“目标(goal)”,所以使用插件是通常是下面的命令语法:

mvn [插件名]:[目标]

比如几个常见的:

 mvn compiler:compile
 mvn surefire:test
 mvn jar:jar

注意上面的mvn compiler:compile,其实这个才是mvn compile的完全体,我们可以直接使用mvn compile是因为maven内置了一些插件和生命周期的绑定。 插件的配置需要在<plugins>节点下。

#####构建生命周期 Maven内部有三套独立的生命周期,分别为clean、default和site。clean生命周期的主要目的是清理项目,default是构建项目,而site是建立项目站点。
生命周期包含一些阶段(phase),而每个生命周期都是顺序按照这些阶段来执行的。
clean包含了pre-clean、clean、post-clean三个阶段。
site包含了pre-site、site、post-site和site-deploy。
而default生命周期的阶段则定义了构建项目时主要的步骤:

  1. validate
  2. generate-sources
  3. process-sources
  4. generate-resources
  5. process-resources 处理项目主目录的资源文件。主要是对src/main/resources文件夹下的内容进行一些处理并输出到target\classes下。
  6. compile 编译项目主代码。目录为src/main/java,生成class文件到target\classes下。
  7. process-test-sources
  8. process-test-resources 处理项目测试目录的资源文件。主要是对src/test/resources文件夹下的内容进行一些处理并输出到target\test-classes下。
  9. test-compile 编译项目测试代码。目录为src/test/java,生成class文件到target\test-classes
  10. test 使用测试框架测试代码。
  11. package 对编译好的代码进行打包,如jar、、war。
  12. install 将包安装到maven本地库。
  13. deploy 将包安装到maven远程库。

以上并不是完整的default生命周期阶段,详细请参考官方文档。

你会发现在mvn 接上面default生命周期的阶段名都会执行相应的操作,另外执行某个命令比如mvn compile时将先执行compile的前置阶段(上面的 1-5)再执行compile。
因此当运行

mvn clean install

其实是先执行clean生命周期的pre-clean、clean,然后执行上面default生命周期的1-12。

当然平常项目开发中,我们最常用的还是mvn compilemvn test,最多也就执行到package

#####依赖 项目开发会用到很多第三方jar包,如果将所有的jar(编译用的,测试用的,运行时必须用的)全放到lib目录下很不明智,比如至少得区别开测试用的jar包吧,没必要把一个junit-4.12.jar打包放到运行环境中。
而且某个jar包 A.1可能会依赖于其他的第三方jar包同B.1,B.1又依赖于C.1,我们的项目中可能有了C.2。
所以手动新增一个jar时需要同时添加这个jar嵌套依赖的所有jar,同时还要考虑版本冲突的问题以及是否运行时部署的问题,是不是很麻烦?

所以需要Maven这样的工具来帮助我们自动解决依赖的问题。

对于依赖的jar包需要在<dependencies>节点下声明, 如一个项目的pom.xml中:

<dependencies>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.1.6.RELEASE</version>
    </dependency>
    
    <!-- ...其他依赖项...  -->
</dependencies>

当执行mvn compile时,maven 会首先在<localRepository>配置的本地库的org\springframework\spring-context\4.1.6.RELEASE目录中查找该jar,没找到的话再从配置的远程库(默认为Maven官方的中央库)下载到本地库的上面的位置,再将该jar加入到编译路径中完成编译。

上面的命令执行时不只是下载了spring-context-4.1.6.RELEASE.jar一个包,而是所有依赖的以及嵌套依赖的包都会采用同样的方式添加到编译路径参与编译。

###最佳实践 [TBD]

]]>
使用License3j实现简单的License验证 2014-10-26T00:00:00+00:00 greycode http://greycode.github.io//2014/10/use_license3j

在项目中可能会遇到要提供License的情况,虽然商业的解决方案有很多而且足够强大,但有时候我们认为不需要投入太多只是希望借License机制提供基本的限制或提醒功能,使用简单开源的方案就可以。

License3j是一个免费开源可用于商业项目的License库,你可以借助它在项目中实现基本的License验证功能。 我们可能会提供如下的License文件


user=greycode
company=Grey`s Code
email=anywalker@163.com
edition=enterprise
release-version=1.0.0
valid-until=2015.12.31

如果把上面的文件直接在项目中使用Java来读取验证的话很容易被客户篡改,最好是提供如下不可读的方式


-----BEGIN PGP MESSAGE-----
Version: BCPG v1.50

owJ4nJvAy8zAxbgv/llYiaPva8bTB/iSxHMyk1PzilPdMnNS/RJzU3U90/Pyi1JT
QnymNynr6ipApRXSgPK8XMrBpXkK/sklCkZmCoYmViaGVgYmCs7BIQpGBoYmvFxl
iTmZKbqleSWZObZAEVM9QyM9Y0NeruT83ILEvEpb96LUyoRiBef8FKBZpcWpRbbp
QJFkMDc1NxGoC6iqPDEnO7XIwdDMWA+oj5erKDUnNbE4Vbcstag4Mz/P1lDPQM8A
qD4lswTETc0rSS0qKMosBprRyajKwsDIxcDPygRyP4dMXmppcX5aCQMXpwDM12Z/
2P8ZRuxn2CvensAgZF2VrHMwlfmKWdPT8FLlKoPvDtNYCpbomHK7cTmGFJxdLrLW
e+kR7qzDR3bl+UoafWg/Vrxzsvm7l1knL7Nw3Lxq+pHj+iTxgKCMxZlPhU/yykhJ
c1g0Pfnbphv9z2UW93yts739z4JyE/3EK9hT3tTreVvzvV00szh94vPYyJgGtZqS
bWE/zGoOxb55Urtenfkh+56oqo40zn1Xbz16ViHw6xdj94fgTb1fWk0/Fuy5/CBH
KOXMvJ+OWSE823k371v+6VHU5WIht9RlOsphq33us4p6PEvmzb4usJS9Z8uRdTml
8Zcyjp7vTLQQkRVc4G+7/P6rF9d2z9q+8jFb9O4UVgA6F865
=WLRy
-----END PGP MESSAGE-----

上面的文字经过加密后已经不可能被人为任意篡改了,这样在Java程序中使用某个专用于解密的文件解密并读取出相应的配置然后验证该Lisence的有效性即可,而加密解密部分的功能,也正是License3j提供的。

如果你了解非对称加密/解密,可能已经知道这其中的原理。我们使用一对私钥公钥,私钥用于加密License原文,然后我们自己保留私钥,将公钥和加密后的内容发布,而公钥用于解密,加密后的内容只用于读取不可修改,这也算是很简单的一种实现了。


如何使用

License3j使用的是PGP密钥,但本身不提供密钥对生成功能,你可以借助其他工具,如果你安装过Git,那么Git的安装目录下已经有一个可以直接使用的GPG程序,可以执行如下来生成一对密钥:


D:\Program Files (x86)\Git\bin>gpg --gen-key
gpg (GnuPG) 1.4.9; Copyright (C) 2008 Free Software Foundation, Inc.
  This is free software: you are free to change and redistribute it.
  There is NO WARRANTY, to the extent permitted by law.

  Please select what kind of key you want:
     (1) DSA and Elgamal (default)
     (2) DSA (sign only)
     (5) RSA (sign only)
  Your selection? 5

选择加密的算法,选哪个都可以,具体的算法细节区别请自行搜索,需要注意的是RSA算法选项是5 不是 3!!。


  RSA keys may be between 1024 and 4096 bits long.
  What keysize do you want? (2048) 4096

选择key的大小,在1024到4096字节之间,默认2048。


  Requested keysize is 4096 bits
  Please specify how long the key should be valid.
           0 = key does not expire
          = key expires in n days
        w = key expires in n weeks
        m = key expires in n months
        y = key expires in n years
  Key is valid for? (0) 0
  Key does not expire at all
  Is this correct? (y/N) Y
</code></pre>
选择key过期的时间,0就是永不过期,1m就是1个月后密钥过期。

  You need a user ID to identify your key; the software constructs the user ID
  from the Real Name, Comment and Email Address in this form:
      "Heinrich Heine (Der Dichter) <heinrichh@duesseldorf.de>"

  Real name: greycode
  Email address:
  Comment:
  You selected this USER-ID:
      "greycode"

  Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit?
接下来输入用户名等信息,这里除名字外可以跳过,注意记住上面的`USER-ID`,这个之后会用到。

  You need a Passphrase to protect your secret key.
  Enter passphrase:
这里输入密码,然后会提示正在生产密钥,这个时候随便动动鼠标。

We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
.+++++
....+++++
gpg: key C06EB015 marked as ultimately trusted
public and secret key created and signed.

gpg: checking the trustdb
gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model
gpg: depth: 0  valid:   4  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 4u
pub   2048R/C06EB015 2014-10-26
      Key fingerprint = 51AF 8A9B B990 F0EC 7DD7  0DED 1117 E6E2 C06E B01
uid                  greycode

Note that this key cannot be used for encryption.  You may want to use
the command "--edit-key" to generate a subkey for this purpose.

D:\Program Files (x86)\Git\bin>
这样我们的密钥对就生成好了,如果你不知道密钥对文件的位置,可以命令行中输入`gpg -h`:

...
Home: C:/Users/walker/AppData/Roaming/gnupg
Supported algorithms:
Pubkey: RSA, RSA-E, RSA-S, ELG-E, DSA
Cipher: 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH
Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB
...
看到Home那里了吧,在上面的目录中找到`secring.gpg`与`pubring.gpg`文件。 如果觉得上面的gpg命令行太麻烦,可以使用GUI的工具[Gpg4win]( http://www.gpg4win.org/download.html)。 接下来使用`secring.gpg`进行加密。`pom.xml`中加入:

 <dependency>
	 <groupIdd>com.verhas </groupIdd>
	 <artifactIdd>license3j </artifactIdd>
	 <versiond>1.0.5 </versiond>
 </dependencyd>
这里使用1.0.5,可以去Maven Repository中查找最新版本。 Java代码如下:

package org.greycode.demo;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

import com.verhas.licensor.License;

public class LicenseDemo {

	public static void main(String[] args) {
		try {
			File licenseFile = new File("demo.license");
			
			if (!licenseFile.exists()) {
			// license 文件生成
				OutputStream os = new FileOutputStream("demo.license");
				os.write(new License()
				    // license 的原文
				    .setLicense(new File("license-plain.txt"))
				    // 私钥与之前生成密钥时产生的USER-ID
				    .loadKey("secring.gpg","keyUserId")
				    // 生成密钥时输入的密码
				    .encodeLicense("keyPassword").getBytes("utf-8"));
				os.close();
			} else {
			// licence 文件验证
			    License license = new License();

			    if (license
				    .loadKeyRing("pubring.gpg", null)
				    .setLicenseEncodedFromFile("demo.license").isVerified()) {
			    	System.out.println(license.getFeature("edition"));
				System.out.println(license.getFeature("valid-until"));
			    }
			
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}
如上通过在Java代码中获取licence的过期日期、版本号等属性进行验证,或者采取其他手段如自动连接验证服务器、启动定时任务等等。 如果再配合代码混淆将解密部分的代码处理一下,那么license验证这块功能也算是像回事了。不过有点安全常识的都知道,这些手段都是常规的平民级的,没有不可攻破的墙这道理应该都懂。
License3j 的Github地址: https://github.com/verhas/License3j
]]>
Atmosphere Framework 2014-09-24T00:00:00+00:00 greycode http://greycode.github.io//2014/09/Atmosphere_Framework

###What is Atmosphere Atmosphere 是一个可用于开发企业级异步Java程序的框架,对服务器与客户端的双向通讯技术(WebSocket、Server Side Events以及传统的Ajax技术如长轮询(long polling)等)进行了一层封装,从而可以开发各种实时异步的应用,比如仪表板,聊天室,实时报价等,支持主流的服务器与浏览器。 ###Why Atmosphere 对于开发实时或异步应用,现行的解决方案大概有以下几种:
  1.长轮询(Long Polling)
  2.HTTP流(Http Streaming)/Comet
  3.服务器发送事件(Server Sent Events)
  4.JSONP
  5.Websocket
相比较(如Websocket对比SSE、LPComet对比Websocket)而言,Websocket更加标准、性能好,但是出现晚,浏览器和应用服务器(代理服务器)的支持力度不同。其他技术虽有弊端却受更多的支持(微信的网页端貌似用的就是Long Polling)。 Atmosphere所解决的问题,就是在通信的两端提供了一层抽象,隐藏了低端的实现,让我们在开发时更多的专注于业务逻辑而不是浏览器或服务器的通讯技术,若服务器/浏览器支持Websocket协议,则使用Websocket,否则将自动回退(fallback)到其他如Long Polling(怎样使用Atmosphere检测浏览器服务器对上述各种技术是否支持)。
另外该框架(主页):
  ·在Reverb, Wall Street Journal, GameDuell, VMWare, Atlassian等都有使用。
  ·易于扩展(scale)
  ·支持云/集群
Atmosphere可以做到两端(包括Javascript库哦)通吃,其开发的应用完全可以部署到现各种流行的应用服务器如WebLogic, Tomcat, Jetty, GlassFish, Vert.x, Netty Framework 等等。 ###How to Use 最详细的说明请参考框架的WIKISamples

Server


import org.atmosphere.config.service.ManagedService;
import org.atmosphere.cpr.AtmosphereResponse;
import org.atmosphere.handler.OnMessage;

@ManagedService(path = "/echo")
public class Echo {
    @onMessage
    public void onMessage(AtmosphereResponse res, String m) {
        res.write("Echo: " + m);
    }
}

Client


$(function () {
   var request = {
     url: document.location.toString() + 'echo',
     transport : "websocket" ,
     fallbackTransport: 'long-polling'};

   request.onMessage = function (response) {
     console.log(response.responseBody)
   };
   $.atmosphere.subscribe(request).push("Hello");
}

上面是官网给出的样例代码,我的Glist上有个简单的聊天应用,可以参考下,同时建议部署到Tomcat 8上测试。

###More Resource 1.Spring 4 已经支持Websocket。

]]>
Apache Shiro 开发入门 2014-09-17T00:00:00+00:00 greycode http://greycode.github.io//2014/09/introduction_to_shiro 使用Guava编写优雅代码 2014-09-15T00:00:00+00:00 greycode http://greycode.github.io//2014/09/clean_code_with_guava

###What is Guava 这个Guava当然不是指水果,Guava 是来自Google的工具类库集合,包含了collections, caching, primitives support, concurrency libraries, common annotations, string processing, I/O, Math等等。 ###Why Guava Guava有点类似于Apache Commons库,两者之间的区别在Stackoverflow上已经很好的回答了这个问题,总结来说就是Guava相对来说设计更优秀、文档齐全、代码质量高、社区更活跃,Guava更加“Morden”。如果你做Java开发,就该把Guava加入到你的项目中。 Guava vs Apache Commons

###Using Guava Maven

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>18.0</version>
</dependency>

或直接到 Maven中央库下载jar包。
Object common methods


public class Employee implements Comparable {
	private String name;
	private Integer age;
	private Job job;

	// methods ...

	public int compareTo(Employee other) {
		return ComparisonChain.start()
		// 以英文字母(从a到z)的自然顺序,NULL值放在最后
			.compare(this.name, other.name, Ordering.natural().nullsLast())
		// 以数字的反序(从大到小),NULL值处于最后
			.compare(this.age, other.age, Ordering.natural().reverse().nullsLast())
			.compare(this.job, other.job, Ordering.natural().nullsLast())
			.result();
	}

	@Override
	public int hashCode() {
		return Objects.hashCode(name, age, job);
	}

	@Override
	public String toString () {
		return MoreObjects.toStringHelper(this)
			.omitNullValues()
			.add("name", name)
			.add("age", age)
			.add("job", job)
			.toString();
	}
</code></pre>
Lists and MutiMap

	List< Map< String, Object> > maps = Lists.newArrayList();
	List< String > langs = Lists.newArrayList("中文","English","日本語",null);
	String lang = Joiner.on("|").useForNull("Unkown").join(langs);
	// 中文|English|日本語|Unkown
	System.out.println(lang);                                               
	
	// Like Map< Job, Collection< Employee > >
	Multimap< Job, Employee > multimap = ArrayListMultimap.create();
	multimap.put(Job.CEO, new Employee("Tom",45));
	multimap.put(Job.DESIGNER, new Employee("Jack",24));
	multimap.put(Job.DEVELOPER, new Employee("Alice", 31));
	multimap.put(Job.DEVELOPER, new Employee("Jhone", 25));
	multimap.put(Job.DEVELOPER, new Employee("Jim", 27));
更多代码示例可以关注我的[Glist](https://gist.github.com/greycode/2969fe130d345f87a208)。 ###More Resource 1. 首要推荐官方的[WIKI](https://code.google.com/p/guava-libraries/wiki/GuavaExplained?tm=6),这个最新最全,并发编程网上有[中文的翻译](http://ifeve.com/google-guava/),但是中文翻译排版很不好,英文不好又想参考官网文档的可以看看。 2. OSchina上有几篇翻译不错的教程:   [Guava 教程1](http://www.oschina.net/translate/beautiful-code-with-google-collections-guava-and-static-imports-part-1)-使用 Google Collections,Guava,static imports 编写漂亮代码   [Guava 教程2](http://www.oschina.net/translate/diving-into-the-google-guava-library-part-2)-深入探索Google Guava 库   [Guava 教程3](http://www.oschina.net/translate/functional-java-filtering-and-ordering-with-google-collections-part-3)-Java 的函数式编程,通过 Google Collections 过滤和调用   [Guava 教程4](http://www.oschina.net/translate/preconditions-multimaps-and-partitioning-with-google-collections-part-4)-条件,多重映射和分片 3. “使用Google Guava来编写优雅的代码”系列,对Guava的集合做了简单介绍。   [集合1](http://www.letonlife.com/writing-clean-code-with-google-guava-part-4-915)   [集合2](http://www.letonlife.com/writing-clean-code-with-google-guava-part-5-918)   [集合3(Multimap)](http://www.letonlife.com/writing-clean-code-with-google-guava-part-6-multimap-923)   [集合4(BiMap)](http://www.letonlife.com/writing-clean-code-with-google-guava-part-7-bimap-930) 4. 博客园里的某一系列[学习笔记](http://www.cnblogs.com/peida/p/Guava.html)。 5. ITEYE上的一篇[Google Guava 库用法整理](http://macrochen.iteye.com/blog/737058),使用Guava前后的代码对比很直观,没有接触过的童鞋推荐首先看看这篇。 6. Speaker Deck上的这个[Guava By Example](https://speakerdeck.com/eneveu/guava-by-example)也是很赞,同样也是对使用Guava前后的代码做了直观的对比。 7. Oschina上转载的另一篇不错的文章:[使用 Google Guava 美化你的 Java 代码](http://my.oschina.net/leejun2005/blog/172328#OSC_h3_1)。 8. 如果以上都满足不了你的求知欲,可以看看[PACKT]出版的这本[Getting Started with Google Guava](https://www.packtpub.com/application-development/getting-started-google-guava)(电子版网上有没有免费下载什么的我才不知道呢~哼!)。  
]]>
开始 2014-09-14T00:00:00+00:00 greycode http://greycode.github.io//2014/09/Set-up

开始的开始

用了jekyll在Github上搭建个写字的地方,直接用别人做好的主题,效果还不错,作者说明如下:

###终于要开工了,想今后写点东西。 估计会是七拉八扯的,简称七扯。本Blog建在Github上。使用Jekyll进行搭建,Jekyll是一个Ruby写的程序,可以将Markdown写的文章通过模板生成最终的Html静态文件。

博客文章的评论功能使用了Disqus。

如果你直接拷贝或Fork本Git库作为自己的博客,一定不要忘记删除我写的文章以及修改 _includes / comments.md 中的disqus_shortname,以及修改 _layouts / default.html中 google analytics的标识 _gaq.push(['_setAccount', 'UA-12936429-2']);

最后感谢您的配合。

不过改的东西可能比上面说明的还要多一些,中文版的评论改为了多说,所以还需要去多说Duoshuo创建一个站点,相应修改_includes / comments.md 中的short_name为多说注册的帐号;另外得修改_config.yml以及_layouts / post.html中的信息,嘛~,反正弄好后自己看看需要改什么地方就改哪里好了,不过每次都要传到Github上再看样式可能很麻烦,还是推荐搭建一个本地环境,可以参照jekyll 中文文档 ,额,当然可能还需要一个 Ruby的环境,自己搭太麻烦了,最好去Bitmami官网 下载一个 Ruby Stack 的一键安装包(安装的时候除了ruby以外能取消勾选的全取消): bitmabi ruby stack 然后在开始菜单打开一个Use Bitnami Ruby Stack 的命令行程序,执行以下命令:


# 删除自带的源
gem sources --remove https://rubygems.org/
# 改为 taobao 的ruby 源
gem sources -a https://ruby.taobao.org/
# 进入本地库
cd G:\Temp\greycode.github.com
# 安装jekyll
gem install jekyll
# 当前文件夹中的内容将会生成到 ./_site 文件夹中。
jekyll build
# 一个开发服务器将会运行在 http://localhost:4000/
jekyll serve
# 和`jekyll serve`相同,但是会查看变更并且自动再生成。
jekyll serve --watch

原来自带的代码高亮也是各种不如意,还是自己配置下妥当一些(highlight.js),另外需要修改下_layouts/post.html中的图片缩放以及media/css/screen-mobil.css手机端的样式以满足响应式设计的要求。
嗯……大概就折腾这么多,还有的话就是以后要习惯用Markdown 语法写文章了(一点都不习惯啊!!),这里有份Markdown语法的文档(这个跟github最新的说明有些出入,推荐还是看github的说明),还有编辑器
以上。

]]>