Spring 核心任务

spring

在本章中,您将了解与Spring相关的核心任务。Spring框架的核心是Spring控制反转(Io0043)容器。IoC容器用于管理和配置普通的旧Java对象(POJOs)。因为Spring框架的主要诉求之一是使用pojo构建Java应用程序,Spring的许多核心任务都涉及到在IoC容器中管理和配置POJOs。

因此,无论您打算在web应用程序、企业集成或其他类型的项目中使用Spring框架,使用pojo和IoC容器是您需要采取的第一步。本章的大部分食谱涵盖了您将在本书中使用的任务,并在日常基础上开发Spring应用程序。

■注意术语使用POJO bean交替使用实例在春天在这本书和文档。两者都引用从Java类创建的对象实例。此外,在本书和Spring文档中,术语组件可与POJO类互换使用。两者都引用创建对象实例的实际Java类。
■提示下载的源代码组织使用Gradle(通过Gradle包装器)来构建菜谱应用程序。Gradle将负责加载所有必需的Java类和依赖项,并创建一个可执行的JAR文件。第1章描述了如何设置渐变工具。此外,如果一个菜谱演示了不止一种方法,那么源代码将使用罗马字母(例如Recipe_2_1_i、Recipe_2_1_ii、Recipe_2_1_iii等)进行分类。

要构建每个应用程序,请进入Recipe目录(例如Ch2/Recipe_2_1_i/)并执行./gradlew构建命令来编译源代码。编译源代码之后,将使用应用程序可执行文件创建一个build/libs子目录。然后可以从命令行运行应用程序JAR(例如,java -jar Recipe_2_1_i-4.0.0.jar)。

使用Java配置来配置POJOs

问题

您希望使用Spring的IoC容器管理带有注释的POJOs。

解决方案

设计一个POJO类。接下来,使用@Configuration和@Bean注释创建一个Java配置类,以配置POJO实例值,或者使用@Component、@Repository、@Service或@Controller注释设置Java组件,以稍后创建POJO实例值。接下来,实例化Spring IoC容器,以使用注释扫描Java类。然后可以访问POJO实例或bean实例,将它们作为应用程序的一部分放在一起。

它是如何工作的

假设您要开发一个应用程序来生成序列号,并且需要许多系列的序列号来满足不同的用途。每个序列都有自己的前缀、后缀和初始值。因此,您必须为应用程序创建和维护多个生成器实例。创建一个POJO类来使用Java配置创建bean。

按照要求,您创建了一个具有三个属性的SequenceGenerator类:前缀、后缀和initial。您还可以创建一个私有字段计数器来存储每个生成器的数值。每次在生成器实例上调用getSequence()方法时,都会得到最后一个序列号,其中包含前缀和后缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.apress.springrecipes.sequence;

import java.util.concurrent.atomic.AtomicInteger;

public class SequenceGenerator {
private String prefix;
private String suffix;
private int initial;

private final AtomicInteger counter = new AtomicInteger();

public SequenceGenerator() { }

public void setPrefix(String prefix) {
this.prefix = prefix;
}

public void setSuffix(String suffix) {
this.suffix = suffix;
}

public void setInitial(int initial) {
this.initial = initial;
}

public String getSequence() {
StringBuilder builder = new StringBuilder();
builder.append(prefix)
.append(initial)
.append(counter.getAndIncrement())
.append(suffix);
return builder.toString();
}
}

使用@Configuration和@Bean创建一个Java配置来创建POJOs

要在Spring IoC容器中定义POJO类的实例,可以使用实例化值创建一个Java配置类。带有POJO或bean定义的Java配置类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.apress.springrecipes.sequence.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.apress.springrecipes.sequence.SequenceGenerator;

@Configuration
public class SequenceGeneratorConfiguration {
@Bean
public SequenceGenerator sequenceGenerator() {
SequenceGenerator seqgen = new SequenceGenerator();
seqgen.setPrefix("30");
seqgen.setSuffix("A");
seqgen.setInitial("100000");
return seqgen;
}
}

注意,SequenceGeneratorConfiguration类被@Configuration注释修饰;这告诉Spring它是一个配置类。当Spring遇到带有@Configuration注释的类时,它会在类中查找bean实例定义,这是用@Bean注释修饰的Java方法。Java方法创建并返回一个bean实例。

使用@Bean注释修饰的任何方法定义都会基于方法名生成一个bean名。或者,您可以使用name属性显式地在@Bean注释中指定bean名称。例如,@Bean(name=”mys1”)使bean可用作mys1。

■注意如果您显式地指定bean名称,方法名被忽略的bean创建。

实例化Spring IoC容器以扫描注释

您必须实例化Spring IoC容器来扫描包含注释的Java类。这样,Spring将检测@Configuration@Bean注释,以便以后可以从IoC容器本身获得bean实例。

Spring提供了两种类型的IoC容器实现。最基本的一个叫做bean factory。更高级的称为应用程序上下文,它与bean工厂兼容。注意,这两种IoC容器的配置文件是相同的。

应用程序上下文提供比bean工厂更高级的特性,同时保持基本特性的兼容性。因此,我们强烈建议在每个应用程序中使用应用程序上下文,除非应用程序的资源受到限制(例如,当为一个applet运行Spring时,或者移动设备)。bean工厂的接口和应用程序上下文分别是BeanFactory和ApplicationContext。ApplicationContext接口是BeanFactory的一个子接口,用于维护兼容性。

由于ApplicationContext是一个接口,您必须实例化一个实现它。Spring有几个应用程序上下文实现;我们建议您使用AnnotationConfigApplicationContext,这是最新且最灵活的实现。通过这个类,您可以加载Java配置文件。

1
2
ApplicationContext context = new AnnotationConfigApplicationContext
(SequenceGeneratorConfiguration.class);

一旦实例化了应用程序上下文,对象引用(在本例中是上下文)提供了访问POJO实例或bean的入口点。

从IoC容器获取POJO实例或bean

要从bean工厂或应用程序上下文获取已声明的bean,只需调用getBean()方法并传入惟一的bean名称。getBean()方法的返回类型是java.lang.Object,因此必须在使用它之前将它转换为它的实际类型。

1
2
SequenceGenerator generator =
(SequenceGenerator) context.getBean("sequenceGenerator");

getBean()方法还支持另一种变体,您可以在其中提供bean类名,以避免强制转换。

1
SequenceGenerator generator = context.getBean("sequenceGenerator",SequenceGenerator.class);

如果只有一个bean,则可以省略该bean的名称。

1
SequenceGenerator generator = context.getBean(SequenceGenerator.class);

一旦您达到这个步骤,您就可以使用POJO或bean,就像在Spring之外使用构造函数创建的任何对象一样。
运行序列生成器应用程序的主类如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.apress.springrecipes.sequence;

import com.apress.springrecipes.sequence.config.SequenceGeneratorConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(SequenceGeneratorConfiguration.class);
SequenceGenerator generator = context.getBean(SequenceGenerator.class);
System.out.println(generator.getSequence());
System.out.println(generator.getSequence());
}
}

如果Java类路径中所有内容都可用(SequenceGenerator POJO类和Spring JAR)
您应该看到以下输出,以及一些日志消息:

1
2
30100000A
30100001A

使用@Component注释创建POJO类,以使用DAO创建Beans

到目前为止,Spring bean实例化是通过硬编码Java配置类中的值来完成的。这是简化Spring示例的首选方法。

然而,大多数应用程序的POJO实例化过程都是通过数据库或用户输入完成的。所以,现在是时候向前迈进,使用更真实的场景了。对于本节,我们将使用域类和数据访问对象(DAO)类来创建pojo。您仍然不需要设置数据库—实际上,您将在DAO类中硬编码值—但是熟悉这种类型的应用程序结构非常重要,因为它是大多数真实应用程序和未来菜谱的基础。

假设您被要求开发一个序列生成器应用程序,就像您在前一节中所做的那样。您需要稍微修改类结构以适应域类和DAO模式。首先,创建一个名为Sequence的域类,其中包含id、前缀和后缀属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.apress.springrecipes.sequence;

public class Sequence {
private final String id;
private final String prefix;
private final String suffix;

public Sequence(String id, String prefix, String suffix) {
this.id = id;
this.prefix = prefix;
this.suffix = suffix;
}

public String getId() {
return id;
}
public String getPrefix() {
return prefix;
}
public String getSuffix() {
return suffix;
}
}

然后,为DAO创建一个接口,这个接口负责从数据库访问数据。getSequence()方法通过ID从数据库表中加载POJO或Sequence对象,而getNextValue()方法检索特定数据库序列的下一个值。

1
2
3
4
5
6
package com.apress.springrecipes.sequence;

public interface SequenceDao {
public Sequence getSequence(String sequenceId);
public int getNextValue(String sequenceId);
}

在生产应用程序中,您将实现这个DAO接口来使用数据访问技术。但是为了简化这个示例,您将在映射中实现一个带有硬编码值的DAO来存储序列实例和值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.apress.springrecipes.sequence;

import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

@Component("sequenceDao")
public class SequenceDaoImpl implements SequenceDao {
private final Map<String, Sequence> sequences = new HashMap<>();
private final Map<String, AtomicInteger> values = new HashMap<>();

public SequenceDaoImpl() {
sequences.put("IT", new Sequence("IT", "30", "A"));
values.put("IT", new AtomicInteger(10000));
}

public Sequence getSequence(String sequenceId) {
return sequences.get(sequenceId);
}

public int getNextValue(String sequenceId) {
AtomicInteger value = values.get(sequenceId);
return value.getAndIncrement();
}
}

观察SequenceDaoImpl类是如何用@Component(“sequenceDao”)注释来修饰的。这将标记类,以便Spring可以从中创建POJOs。@Component注释内的值定义了bean实例ID,在本例中为sequenceDao。如果@Component注释中没有提供bean值名,那么默认情况下,bean名被指定为大写的非限定类名。例如,对于SequenceDaoImpl类,默认的bean名称是SequenceDaoImpl。
对getSequence方法的调用返回给定sequenceID的值。对getNextValue方法的调用基于给定的sequenceID的值创建一个新值,并返回新值。

POJOs在应用程序层中被分类。在Spring中,有三个层:persistence, service, 和 presentation。@Component是一个通用的注释,用于装饰用于Spring检测的POJOs,而@Repository、@Service和@Controller是@Component的专门化,用于处理与持久性、服务和表示层相关的pojo的更具体的情况。

如果您不确定POJO的用途,可以使用@Component注释来修饰它。但是,最好尽可能地使用专门化注释,因为这些注释提供了基于POJO目的的额外功能(例如,@Repository将异常包装为DataAccessExceptions,这使调试更容易)。

用过滤器实例化Spring IoC容器,以扫描注释

在默认情况下,Spring检测所有使用@Configuration、@Bean、@Component、@Repository、@Service和@Controller注释的类,以及其他。您可以自定义扫描过程以包含一个或多个包含/排除过滤器。当Java包有几十或几百个类时,这是很有用的。对于某些Spring应用程序上下文,可能需要排除或包含带有特定注释的pojo。

■警告扫描每个包都可以降低不必要的启动过程。

Spring支持四种类型的过滤器表达式。注释和可分配的类型是指定一个注释类型和一个用于过滤的类/接口。regex和aspectj类型允许您指定正则表达式和aspectj切入点表达式来匹配类。您还可以使用use-default-filters属性禁用默认过滤器。

例如,下面的组件扫描包括com.apress.springrecipe中的所有类。序列,其名称包含单词Dao或服务,并排除带有@Controller注释的类:

1
2
3
4
5
6
7
8
9
10
11
12
@ComponentScan(
includeFilters = {
@ComponentScan.Filter(
type = FilterType.REGEX,
pattern = {"com.apress.springrecipes.sequence.*Dao",
"com.apress.springrecipes.sequence.*Service"})
},
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = {org.springframework.stereotype.Controller.class}) }
)

当应用包含过滤器时,可以检测所有名称包含“Dao”或“服务”的类。
没有注释的类被自动检测。

从IoC容器获取POJO实例或bean

然后,您可以使用以下主要类来测试前面的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.apress.springrecipes.sequence;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext("com.apress.springrecipes.sequence");
SequenceDao sequenceDao = context.getBean(SequenceDao.class);
System.out.println(sequenceDao.getNextValue("IT"));
System.out.println(sequenceDao.getNextValue("IT"));
}
}

通过调用构造函数创建POJOs

问题

您希望通过调用其构造函数在Spring IoC容器中创建POJO实例或bean,这是创建bean的最常见和最直接的方式。这相当于使用新的操作符在Java中创建对象。

解决方案

使用构造函数或构造函数定义POJO类。接下来,创建一个Java配置类,用构造函数为Spring IoC容器配置POJO实例值。接下来,实例化Spring IoC容器,以使用注释扫描Java类。POJO实例或bean实例可以作为应用程序的一部分放在一起。

它是如何工作的

假设您要开发一个商店应用程序来在线销售产品。首先,创建Product POJO类,它有几个属性,比如产品名称和价格。由于您的商店中有许多类型的产品,所以您将产品类抽象为不同的产品子类扩展它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.apress.springrecipes.shop;

public abstract class Product {
private String name;
private double price;

public Product() {}

public Product(String name, double price) {
this.name = name;
this.price = price;
}
// Getters and Setters
...
public String toString() {
return name + " " + price;
}
}

使用构造函数创建POJO类

然后创建两个产品子类:电池和磁盘。它们每个都有自己的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.apress.springrecipes.shop;

public class Battery extends Product {
private boolean rechargeable;
public Battery() { super();
}
public Battery(String name, double price) {
super(name, price);
}
// Getters and Setters
...
}

package com.apress.springrecipes.shop;

public class Disc extends Product {
private int capacity;
public Disc() { super();
}
public Disc(String name, double price) {
super(name, price);
}
// Getters and Setters
...
}

为POJO创建一个Java配置

要在Spring IoC容器中定义POJO类的实例,必须创建具有实例化值的Java配置类。
通过调用构造函数生成具有POJO或bean定义的Java配置类应该如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.apress.springrecipes.shop.config;

import com.apress.springrecipes.shop.Battery;
import com.apress.springrecipes.shop.Disc;
import com.apress.springrecipes.shop.Product;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShopConfiguration {
@Bean
public Product aaa() {
Battery p1 = new Battery("AAA", 2.5);
p1.setRechargeable(true);
return p1;
}
@Bean
public Product cdrw() {
Disc p2 = new Disc("CD-RW", 1.5);
p2.setCapacity(700);
return p2;
}
}

接下来,通过从Spring IoC容器中检索产品,您可以编写以下主要类来测试产品:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.apress.springrecipes.shop;
import com.apress.springrecipes.shop.config.ShopConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
public static void main(String[] args) throws Exception {
ApplicationContext context =
new AnnotationConfigApplicationContext(ShopConfiguration.class);
Product aaa = context.getBean("aaa", Product.class);
Product cdrw = context.getBean("cdrw", Product.class);
System.out.println(aaa);
System.out.println(cdrw);
}
}

使用POJO引用和自动连接与其他POJO交互

问题

组成应用程序的POJO实例或bean通常需要相互协作才能完成应用程序的功能。您希望使用注释来使用POJO引用和自动连接。

解决方案

对于Java配置类中定义的POJO实例,可以使用标准Java代码在bean之间创建引用。要自动连接POJO引用,可以使用@Autowired注释标记字段、setter方法、构造函数,甚至任意方法。

它是如何工作的

首先,我们将介绍使用构造函数、字段和属性的不同的自动配线方法。最后,您将了解如何解决自动装配中的问题。

在Java配置类中引用POJOs

当POJO实例在Java配置类中定义时(如2-1和2- POJO引用中所示),使用起来很简单,因为一切都是Java代码。在下面的示例中,bean属性引用另一个bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.apress.springrecipes.sequence.config;

import com.apress.springrecipes.sequence.DatePrefixGenerator;
import com.apress.springrecipes.sequence.SequenceGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SequenceConfiguration {
@Bean
public DatePrefixGenerator datePrefixGenerator() {
DatePrefixGenerator dpg = new DatePrefixGenerator();
dpg.setPattern("yyyyMMdd");
return dpg;
}
@Bean
public SequenceGenerator sequenceGenerator() {
SequenceGenerator sequence = new SequenceGenerator();
sequence.setInitial(100000);
sequence.setSuffix("A");
sequence.setPrefixGenerator(datePrefixGenerator());
return sequence;
}
}

SequenceGenerator类的前缀生成器属性是DatePrefixGenerator bean的实例。
第一个bean声明创建一个DatePrefixGenerator POJO。
按照约定,bean可以通过bean名称datePrefixGenerator(例如。方法名)。
但是由于bean实例化逻辑也是一种标准的Java方法,因此通过进行标准Java调用也可以访问bean。
当prefixGenerator属性被放置到第二个bean中时,通过settera对datePrefixGenerator()方法进行标准的Java调用,以引用bean。

使用@Autowired注释自动连接POJO字段

接下来,让我们在2-1的第二部分介绍的DAO SequenceDaoImpl类的SequenceDao字段上使用自动布线。您将向应用程序添加一个服务类,以说明与DAO类的自动连接。
一个服务类生成服务对象是另一个真实世界的应用程序的最佳实践,充当faç正面直接访问DAOs-instead访问DAOs。在内部,服务对象与DAO交互以处理序列生成请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.apress.springrecipes.sequence;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class SequenceService {
@Autowired
private SequenceDao sequenceDao;

public void setSequenceDao(SequenceDao sequenceDao) {
this.sequenceDao = sequenceDao;
}

public String generate(String sequenceId) {
Sequence sequence = sequenceDao.getSequence(sequenceId);
int value = sequenceDao.getNextValue(sequenceId);
return sequence.getPrefix() + value + sequence.getSuffix();
}
}

SequenceService类使用@Component注释进行修饰。这允许Spring检测POJO。因为@Component注释没有名称,所以默认的bean名称是sequenceService,它基于类名。

SequenceService类的sequenceDao属性使用@Autowired注释进行修饰。这允许Spring使用sequenceDao bean(即SequenceDaoImpl类)。

@Autowired注解还可以应用于数组类型的属性,使Spring自动连接所有匹配的bean。例如,可以使用@Autowired对前缀生成器[]属性进行注释。然后,Spring将一次自动连接所有类型与前缀生成器兼容的bean。

1
2
3
4
5
6
7
8
9
package com.apress.springrecipes.sequence;

import org.springframework.beans.factory.annotation.Autowired;

public class SequenceGenerator {
@Autowired
private PrefixGenerator[] prefixGenerators;
...
}

如果有多个bean的类型与IoC容器中定义的前缀生成器兼容,它们将自动添加到前缀生成器数组中。
以类似的方式,您可以将@Autowired注释应用于类型安全的集合。Spring可以读取此集合的类型信息,并自动连接类型兼容的所有bean。

1
2
3
4
5
6
7
8
9
package com.apress.springrecipes.sequence;

import org.springframework.beans.factory.annotation.Autowired;

public class SequenceGenerator {
@Autowired
private List<PrefixGenerator> prefixGenerators;
...
}

如果Spring注意到@Autowired注释被应用到类型安全的 java.util.Map,字符串为键,它将把兼容类型的所有bean(以bean名称作为键)添加到这个map中。

1
2
3
4
5
6
7
8
package com.apress.springrecipes.sequence;

import org.springframework.beans.factory.annotation.Autowired;
public class SequenceGenerator {
@Autowired
private Map<String, PrefixGenerator> prefixGenerators;
...
}

使用@Autowired注解的Autowire POJO方法和构造函数,并可选择自动配线

@Autowired注解也可以直接应用于POJO的setter方法。例如,可以使用@Autowired对prefixGenerator属性的setter方法进行注释。然后,Spring尝试连接类型与前缀生成器兼容的bean。

1
2
3
4
5
6
7
8
9
10
11
12
package com.apress.springrecipes.sequence;

import org.springframework.beans.factory.annotation.Autowired;

public class SequenceGenerator {
...

@Autowired
public void setPrefixGenerator(PrefixGenerator prefixGenerator) {
this.prefixGenerator = prefixGenerator;
}
}

默认情况下,所有带有@Autowired的属性都是必需的。当Spring无法找到匹配的bean时,它将抛出异常。如果您希望某个属性是可选的,那么将@Autowired的required属性设置为false。然后,当Spring无法找到匹配的bean时,它将保留此属性未设置。

1
2
3
4
5
6
7
8
9
10
11
package com.apress.springrecipes.sequence;

import org.springframework.beans.factory.annotation.Autowired;

public class SequenceGenerator {
...
@Autowired(required=false)
public void setPrefixGenerator(PrefixGenerator prefixGenerator) {
this.prefixGenerator = prefixGenerator;
}
}

您还可以将@Autowired注释应用于具有任意名称和任意数量参数的方法;在这种情况下,Spring尝试用兼容的类型为每一个方法参数连接一个bean。

1
2
3
4
5
6
7
8
9
10
package com.apress.springrecipes.sequence;
import org.springframework.beans.factory.annotation.Autowired;

public class SequenceGenerator {
...
@Autowired
public void myOwnCustomInjectionName(PrefixGenerator prefixGenerator) {
this.prefixGenerator = prefixGenerator;
}
}

最后,还可以将@Autowired注释应用到希望用于自动连接的构造函数。构造函数可以有任意数量的参数,Spring将尝试为每个构造函数参数连接具有兼容类型的bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class SequenceService {
private final SequenceDao sequenceDao;

@Autowired
public SequenceService(SequenceDao sequenceDao) {
this.sequenceDao=sequenceDao;
}

public String generate(String sequenceId) {
Sequence sequence = sequenceDao.getSequence(sequenceId);
int value = sequenceDao.getNextValue(sequenceId);
return sequence.getPrefix() + value + sequence.getSuffix();
}
}

用注释解决自动连接的模糊性

默认情况下,当IoC容器中有多个具有兼容类型的bean,并且属性不是组类型(如数组、列表、映射)时,按类型自动连接将不起作用。但是,如果有多个相同类型的bean: @Primary注释和@Qualifier注释,则有两个方法可以根据类型自动连接。

用@Primary注释解决自动连接的歧义

Spring允许您通过使用@Primary注释装饰候选bean,按类型指定候选bean。@Primary注释在多个候选对象符合自动连接单值依赖项的条件时,为bean提供优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.apress.springrecipes.sequence;

...
import org.springframework.stereotype.Component;
import org.springframework.context.annotation.Primary;

@Component
@Primary
public class DatePrefixGenerator implements PrefixGenerator {
public String getPrefix() {
DateFormat formatter = new SimpleDateFormat("yyyyMMdd");
return formatter.format(new Date());
}
}

注意,前面的POJO实现了PrefixGenerator接口,并使用@Primary注释进行修饰。如果您尝试使用PrefixGenerator类型自动连接一个bean,即使Spring有多个具有相同PrefixGenerator类型的bean实例,Spring也会自动连接DatePrefixGenerator,因为它使用@Primary注释进行标记。

用@Qualifier注解解决自动连接的歧义

Spring还允许您通过在@Qualifier注释中提供其名称来按类型指定候选bean。

1
2
3
4
5
6
7
8
9
10
11
package com.apress.springrecipes.sequence;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

public class SequenceGenerator {

@Autowired @Qualifier("datePrefixGenerator")
private PrefixGenerator prefixGenerator;
...
}

完成此操作之后,Spring尝试在IoC容器中查找具有该名称的bean,并将其连接到属性中。
@Qualifier注释也可以应用于自动连接的方法参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.apress.springrecipes.sequence;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

public class SequenceGenerator {
...
@Autowired
public void myOwnCustomInjectionName(
@Qualifier("datePrefixGenerator") PrefixGenerator prefixGenerator) {
this.prefixGenerator = prefixGenerator;
}
}

如果希望按名称自动连接bean属性,可以使用下一个菜谱中描述的JSR-250 @Resource注释一个setter方法、构造函数或字段。

解析来自多个位置的POJO引用

随着应用程序的增长,在单个Java配置类中管理每个POJO会变得很困难。

一个常见的实践是,根据pojo的功能将它们分成多个Java配置类。当您创建多个Java配置类时,获取在不同类中定义的引用和自动连接pojo并不像在单个Java配置类中一样简单。

一种方法是使用每个Java配置类的位置初始化应用程序上下文。通过这种方式,每个Java配置类的pojo都被加载到上下文和引用中,pojo之间的自动连接是可能的。

1
2
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext
(PrefixConfiguration.class, SequenceGeneratorConfiguration.class);

另一种选择是使用@Import注释,因此Spring使来自一个配置文件的pojo在另一个配置文件中可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.apress.springrecipes.sequence.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import com.apress.springrecipes.sequence.SequenceGenerator;
import com.apress.springrecipes.sequence.PrefixGenerator;

@Configuration @Import(PrefixConfiguration.class)
public class SequenceConfiguration {
@Value("#{datePrefixGenerator}")
private PrefixGenerator prefixGenerator;

@Bean
public SequenceGenerator sequenceGenerator() {
SequenceGenerator sequence= new SequenceGenerator();
sequence.setInitial(100000); sequence.setSuffix("A");
sequence.setPrefixGenerator(prefixGenerator);
return sequence;
}
}

sequenceGenerator bean要求您设置一个prefixGenerator bean。但是请注意,Java配置类中没有定义前缀生成器bean。prefixGenerator bean在一个名为PrefixConfiguration的单独Java配置类中定义。使用@Import(PrefixConfiguration.class)注释,Spring将Java配置类中的所有pojo都带到当前配置类的范围中。在作用域中使用来自PrefixConfiguration的pojo,您可以使用@Value注释和SpEL将名为datePrefixGenerator的bean注入到prefixGenerator字段中。注入bean之后,可以使用它为sequenceGenerator bean设置一个prefixGenerator bean。

使用@Resource和@Inject注解来连接POJOs

问题

您希望使用Java标准@Resource和@Inject注解来通过自动连接引用pojo,而不是使用特定于spring的@Autowired注解。

解决方案

JSR-250,或Java平台的通用注释,定义了@Resource注释,以名称自动连接POJO引用。JSR-330,或用于注入的标准注解,定义了@Inject注解,按类型来连接POJO引用。

它是如何工作的

前面的菜谱中描述的@Autowired注释属于Spring框架,具体到org.springframework.beans.factory。注释包。这意味着它只能在Spring框架的上下文中使用。
在Spring添加了对@Autowired注释的支持之后,Java语言标准化了各种注释,以实现@Autowired注释的相同目的。这些注释是@Resource,属于javax。注释包和@Inject,属于javax.inject package。

使用@Resource注释自动连接POJOs

默认情况下,@Resource注释的工作方式类似于Spring的@Autowired注释,并尝试通过类型来自动连接。例如,下面的POJO属性被修饰为@Resource注释,因此Spring尝试定位匹配PrefixGenerator类型的POJO。

1
2
3
4
5
6
7
8
9
package com.apress.springrecipes.sequence;

import javax.annotation.Resource;

public class SequenceGenerator {
@Resource
private PrefixGenerator prefixGenerator;
...
}

但是,与@Autowired注释不同,它需要@Qualifier注释来按名称自动连接POJO,如果存在多个相同类型的POJO类型,则可以消除@Resource歧义。本质上,@Resource注释提供了与将@Autowired注释和@Qualifier注释放在一起相同的功能。

使用@Inject注释的Autowire POJOs

另外,@Inject注释尝试按类型自动连接,比如@Resource和@Autowired注释。例如,下面的POJO属性被修饰为@Inject注释,因此Spring尝试定位与PrefixGenerator类型匹配的POJO:

1
2
3
4
5
6
7
8
9
package com.apress.springrecipes.sequence;

import javax.inject.Inject;

public class SequenceGenerator {
@Inject
private PrefixGenerator prefixGenerator;
...
}

但是,就像@Resource和@Autowired注解一样,如果存在多个相同类型的POJO类型,则必须使用不同的方法来匹配POJO的名称或避免歧义。使用@Inject注释进行名称自动连接的第一步是创建自定义注释,以标识POJO注入类和POJO注入点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.apress.springrecipes.sequence;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.inject.Qualifier;

@Qualifier
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface DatePrefixAnnotation {
}

注意,自定义注释使用了@Qualifier注释。这个注释与Spring的@Qualifier注释使用的注释不同,因为最后一个类与@Inject注释属于同一个Java包。(javax.inject)。

一旦完成了自定义注释,就需要装饰生成bean实例的POJO注入类,在本例中是DatePrefixGenerator类。

1
2
3
4
5
6
package com.apress.springrecipes.sequence;
...
@DatePrefixAnnotation
public class DatePrefixGenerator implements PrefixGenerator {
...
}

最后,POJO属性或注入点使用相同的自定义注释来限定POJO并消除任何歧义。

1
2
3
4
5
6
7
8
9
package com.apress.springrecipes.sequence;

import javax.inject.Inject;

public class SequenceGenerator {
@Inject @DataPrefixAnnotation
private PrefixGenerator prefixGenerator;
...
}

正如您在菜谱2-3和2-4中看到的,@Autowired、@Resource和@Inject这三个注解可以实现相同的结果。@Autowired注释是基于spring的解决方案,而@Resource和@Inject注释是Java标准(即:JSR)解决方案。如果要进行基于名称的自动连接,@Resource注释提供了最简单的语法。对于按类类型自动连接,这三个注释都很容易使用,因为这三个注释都需要一个注释。

使用@Scope注释设置POJO的范围

问题

当您使用像@Component这样的注释声明POJO实例时,您实际上是在为bean创建定义一个模板,而不是一个实际的bean实例。当getBean()方法请求bean或从其他bean引用时,Spring根据bean作用域决定应该返回哪个bean实例。有时,您必须为bean设置适当的范围,而不是默认的范围。

解决方案

使用@Scope注释设置bean的范围。默认情况下,Spring为IoC容器中声明的每个bean创建一个实例,这个实例在整个IoC容器的范围内共享。对于所有后续的getBean()调用和bean引用,将返回这个惟一的bean实例。这个范围称为singleton,它是所有bean的默认范围。表2-1列出了Spring中所有有效的bean作用域。

Scope 描述
singleton 每个Spring IoC容器创建一个bean实例
prototype 每次请求时创建一个新的bean实例
request 每个HTTP请求创建一个bean实例;仅在web应用程序的上下文中有效
session 每个HTTP会话创建一个单独的bean实例;仅在web应用程序的上下文中有效
globalSession 每个全局HTTP会话创建一个bean实例;仅在门户应用程序的上下文中有效

它是如何工作的

为了演示bean作用域的概念,让我们考虑购物应用程序中的购物车示例。
首先,创建ShoppingCart类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.apress.springrecipes.shop;
...

@Component
public class ShoppingCart {
private List<Product> items = new ArrayList<>();
public void addItem(Product item) {
items.add(item);
}
public List<Product> getItems() {
return items;
}
}

然后,在Java配置文件中声明一些产品bean,以便稍后将它们添加到购物车中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.apress.springrecipes.shop.config;

import com.apress.springrecipes.shop.Battery;
import com.apress.springrecipes.shop.Disc;
import com.apress.springrecipes.shop.Product;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.apress.springrecipes.shop")
public class ShopConfiguration {
@Bean
public Product aaa() {
Battery p1 = new Battery();
p1.setName("AAA");
p1.setPrice(2.5);
p1.setRechargeable(true);
return p1;
}
@Bean
public Product cdrw() {
Disc p2 = new Disc("CD-RW", 1.5);
p2.setCapacity(700);
return p2;
}
@Bean
public Product dvdrw() {
Disc p2 = new Disc("DVD-RW", 3.0);
p2.setCapacity(700);
return p2;
}
}

一旦您这样做了,您就可以通过添加一些产品来定义一个主类来测试购物车。假设有两个客户同时在您的商店中导航。第一个通过getBean()方法获得购物车并向其中添加两个产品。然后,第二个客户还通过getBean()方法获得一个购物车,并向其中添加另一个产品。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.apress.springrecipes.shop;

import com.apress.springrecipes.shop.config.ShopConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
public static void main(String[] args) throws Exception {
ApplicationContext context =
new AnnotationConfigApplicationContext(ShopConfiguration.class);
Product aaa = context.getBean("aaa", Product.class);
Product cdrw = context.getBean("cdrw", Product.class);
Product dvdrw = context.getBean("dvdrw", Product.class);
ShoppingCart cart1 = context.getBean("shoppingCart", ShoppingCart.class);
cart1.addItem(aaa);
cart1.addItem(cdrw);
System.out.println("Shopping cart 1 contains " + cart1.getItems());
ShoppingCart cart2 = context.getBean("shoppingCart", ShoppingCart.class);
cart2.addItem(dvdrw);
System.out.println("Shopping cart 2 contains " + cart2.getItems());
}
}

由于前面的bean声明,您可以看到两个客户获得了相同的购物车实例。

1
2
Shopping cart 1 contains [AAA 2.5, CD-RW 1.5]
Shopping cart 2 contains [AAA 2.5, CD-RW 1.5, DVD-RW 3.0]

这是因为Spring的默认bean作用域是单例的,这意味着Spring每个IoC容器只创建一个购物车实例。
在您的shop应用程序中,当调用getBean()方法时,您希望每个客户获得不同的购物车实例。为了确保这种行为,需要将shoppingCart bean的范围设置为prototype。然后Spring为每个getBean()方法调用创建一个新的bean实例。

1
2
3
4
5
6
7
package com.apress.springrecipes.shop;
...
import org.springframework.stereotype.Component;
import org.springframework.context.annotation.Scope;
@Component
@Scope("prototype")
public class ShoppingCart { ... }

现在,如果您再次运行Main类,您可以看到两个客户获得不同的购物车实例。

1
2
Shopping cart 1 contains [AAA 2.5, CD-RW 1.5]
Shopping cart 2 contains [DVD-RW 3.0]

使用来自外部资源的数据(文本文件、XML文件、属性文件或图像文件)

问题

有时应用程序需要从不同的位置(例如文件系统、类路径或URL)读取外部资源(例如,文本文件、XML文件、属性文件或图像文件)。通常,您必须处理从不同位置加载资源的不同api。

解决方案

Spring提供了@PropertySource注释作为加载.properties文件内容的工具(例如,键-值对)来设置bean属性。
Spring还有一个资源加载器机制,该机制提供一个统一的资源接口,用于通过资源路径检索任何类型的外部资源。可以为该路径指定不同的前缀,以便使用@Value注释从不同位置加载资源。要从文件系统加载资源,可以使用文件前缀。要从类路径中加载资源,可以使用类路径前缀。还可以在资源路径中指定URL。

它是如何工作的

读取属性文件的内容(例如要设置bean属性,可以使用带有PropertySourcesPlaceholderConfigurer的Spring的@PropertySource注释。如果希望读取任何文件的内容,可以使用用@Value注释修饰的Spring的资源机制。

使用属性文件数据设置POJO实例化值

让我们假设您希望在属性文件中访问一系列值来设置bean属性。通常,这可以是数据库的配置属性或由键值组成的其他应用程序值。例如,取存储在名为discounts.properties文件中的下列关键值:

1
2
3
specialcustomer.discount=0.1
summer.discount=0.15
endofyear.discount=0.2

要阅读国际化(i18n)目的的属性文件,请参阅下一个菜谱。
提供折扣的内容。属性文件可用于设置其他bean,您可以使用@PropertySource注释将键值转换为Java配置类中的bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.apress.springrecipes.shop.config;

import com.apress.springrecipes.shop.Battery;
import com.apress.springrecipes.shop.Disc;
import com.apress.springrecipes.shop.Product;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;

@Configuration @PropertySource("classpath:discounts.properties")
@ComponentScan("com.apress.springrecipes.shop")
public class ShopConfiguration {
@Value("${endofyear.discount:0}")
private double specialEndofyearDiscountField;

@Bean
public static PropertySourcesPlaceholderConfigurer
propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public Product dvdrw() {
Disc p2 = new Disc("DVD-RW", 3.0, specialEndofyearDiscountField);
p2.setCapacity(700);
return p2;
}
}

您可以定义一个带有类路径值:折扣的@PropertySource注释。属性来修饰Java配置类。类路径:前缀告诉Spring查找折扣。Java类路径中的属性文件。
定义了@PropertySource注释以加载属性文件之后,还需要使用@Bean注释定义PropertySourcePlaceholderConfigurer bean。Spring会自动连接@PropertySource折扣。属性文件,使其属性可作为bean属性访问。
接下来,您需要定义Java变量,以便从折扣折扣中获取值。属性文件。要使用这些值定义Java变量值,可以使用带有占位符表达式的@Value注释。
语法是@Value("${key:default_value}")。搜索所有加载的应用程序属性中的键值。如果在属性文件中找到了匹配的键=值,则将相应的值分配给bean属性。如果加载的应用程序属性中没有找到匹配的key=value,那么default_value(即,在${key:)之后分配给bean属性。
在用折扣值设置Java变量之后,您可以使用它为bean的折扣属性设置bean实例。
如果您希望将属性文件数据用于与设置bean属性不同的目的,那么您应该使用Spring的资源机制,后面将对此进行描述。

在POJO中使用来自任何外部资源文件的数据

假设您想在应用程序启动时显示一个横幅。banner由以下字符组成,存储在一个名为banner.txt的文本文件中。这个文件可以放在应用程序的类路径中。

1
2
3
*************************
* Welcome to My Shop! *
*************************

接下来,让我们编写一个BannerLoader POJO类来加载横幅并将其输出到控制台。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.apress.springrecipes.shop;

import org.springframework.core.io.Resource;
...
import javax.annotation.PostConstruct;

public class BannerLoader {
private Resource banner;
public void setBanner(Resource banner) {
this.banner = banner;
}

@PostConstruct
public void showBanner() throws IOException {
InputStream in = banner.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while (true) {
String line = reader.readLine();
if (line == null) break;
System.out.println(line);
}
reader.close();
}
}

注意,POJO banner字段是一个Spring资源类型。当创建bean实例时,将通过setter注入填充字段值——稍后进行解释。showBanner()方法调用getInputStream()方法来从资源字段检索输入流。一旦有了InputStream,就可以使用标准的Java文件操作类。在本例中,文件内容是逐行读取的,带有BufferedReader和输出到控制台。

还请注意showBanner()方法使用@PostConstruct注释进行装饰。因为您希望在启动时显示banner,所以使用这个注释告诉Spring在创建后自动调用方法。这保证了showBanner()方法是应用程序运行的第一个方法之一,因此确保了横幅出现在开始。
接下来,需要将POJO BannerLoader初始化为实例。此外,还需要注入BannerLoader的banner字段。因此,让我们为这些任务创建一个Java配置类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration @PropertySource("classpath:discounts.properties")
@ComponentScan("com.apress.springrecipes.shop")
public class ShopConfiguration {
@Value("classpath:banner.txt")
private Resource banner;

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

@Bean
public BannerLoader bannerLoader() {
BannerLoader bl = new BannerLoader();
bl.setBanner(banner);
return bl;
}
}

查看如何使用@Value(“classpath:banner.txt”)注释修饰banner属性。这告诉春天去搜索横幅。在类路径中插入txt文件。Spring使用预先注册的属性编辑器ResourceEditor将文件定义转换为资源对象,然后将其注入到bean中。
注入banner属性后,通过setter注入将其分配给BannerLoader bean实例。
由于banner文件位于Java类路径中,资源路径从classpath: prefix开始。前面的资源路径指定文件系统的相对路径中的资源。也可以指定绝对路径。

file:c:/shop/banner.txt

当资源位于Java的类路径中时,必须使用类路径前缀。如果没有显示路径信息,它将从类路径的根加载。

classpath:banner.txt

如果资源位于特定的包中,您可以指定来自类路径根的绝对路径。

classpath:com/apress/springrecipes/shop/banner.txt

除了支持从文件系统路径或类路径加载外,还可以通过指定URL来加载资源。

http://springrecipes.apress.com/shop/banner.txt

由于bean类在showBanner()方法上使用@PostConstruct注释,所以banner在IoC容器建立时被发送到输出。正因为如此,没有必要去修补应用程序的上下文或显式调用bean来输出横幅。然而,有时需要访问外部资源来与应用程序的上下文进行交互。现在,假设您希望在应用程序的末尾显示一个图例。图例包含了折扣中先前描述的折扣。属性文件。要访问属性文件的内容,还可以利用Spring的资源机制。
接下来,让我们使用Spring的资源机制,但这一次是直接在应用程序的主类中,在应用程序完成时输出一个图例。

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
...
...
public class Main {
public static void main(String[] args) throws Exception {
...
Resource resource = new ClassPathResource("discounts.properties");
Properties props = PropertiesLoaderUtils.loadProperties(resource);
System.out.println("And don't forget our discounts!");
System.out.println(props);
}
}

Spring的ClassPathResource类用于访问discounts.properties文件,该文件将文件的内容转换为资源对象。接下来,资源对象被处理成具有Spring的PropertiesLoaderUtils类的属性对象。最后,属性对象的内容作为应用程序的最终输出发送到控制台。
因为图例文件(即在Java类路径中,资源通过Spring的ClassPathResource类来访问。如果外部资源位于文件系统路径中,则资源将被加载到Spring的文件系统资源中。

1
Resource resource = new FileSystemResource("c:/shop/banner.txt")

如果外部资源位于URL中,则资源将被加载到Spring的UrlResource中。

1
Resource resource = new UrlResource("http://www.apress.com/")

解析属性文件中不同地区的I18N文本消息

问题

您希望应用程序通过注释支持国际化。

解决方案

MessageSource是一个接口,定义了用于解析资源包中的消息的几种方法。ResourceBundleMessageSource是最常见的MessageSource实现,它从不同的地区解决来自资源包的消息。在实现ResourceBundleMessageSource POJO之后,您可以在Java配置文件中使用@Bean注释,以使应用程序中的i18n数据可用。

它是如何工作的

例如,在美国为英语创建一个名为messages_en_US.properties的资源包。资源包是从类路径的根加载的,所以要确保它在Java类路径中可用。在文件中放置以下键值:

1
2
alert.checkout=A shopping cart has been checked out.
alert.inventory.checkout=A shopping cart with {0} has been checked out at {1}.

为了解析资源包中的消息,让我们使用ReloadableResourceBundleMessageSource bean的实例创建一个Java配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.apress.springrecipes.shop.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;

@Configuration
public class ShopConfiguration {
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:messages");
messageSource.setCacheSeconds(1);
return messageSource;
}
}

bean实例必须有应用程序上下文的名称messageSource来检测它。
在bean定义中,您通过setBasenames方法声明一个字符串列表,以便为ResourceBundleMessageSource定位bundle。在这种情况下,您只需指定默认约定来查找位于以消息开头的Java类路径中的文件。此外,setCacheSeconds方法将缓存设置为1秒,以避免读取过时的消息。注意,刷新尝试在重新加载属性文件之前首先检查属性文件的最后修改时间戳,因此如果文件没有更改,则可以将setCacheSeconds间隔设置得相当低,因为刷新尝试实际上并没有重新加载。
对于这个MessageSource定义,如果您查找美国语言环境的文本消息(其首选语言是英语),则查找messages_en_US资源包。属性被认为是第一个。如果没有这样的资源束,或者无法找到消息,那么messages_en。考虑与语言匹配的属性文件。如果仍然找不到资源包,则默认消息。属性
选择所有地区。有关资源包加载的更多信息,可以参考java.util.ResourceBundle类的Javadoc。
接下来,您可以配置应用程序上下文来使用getMessage()方法解析消息。第一个参数是消息对应的键,第三个参数是目标语言环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.apress.springrecipes.shop;

import com.apress.springrecipes.shop.config.ShopConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Date;
import java.util.Locale;

public class Main {
public static void main(String[] args) throws Exception {
ApplicationContext context =
new AnnotationConfigApplicationContext(ShopConfiguration.class);
String alert = context.getMessage("alert.checkout", null, Locale.US);
String alert_inventory = context.getMessage("alert.inventory.checkout", new Object[] {"[DVD-RW 3.0]", new Date()}, Locale.US);
System.out.println("The I18N message for alert.checkout is: " + alert);
System.out.println("The I18N message for alert.inventory.checkout is: " + alert_inventory);
}
}

getMessage()方法的第二个参数是消息参数数组。在第一个字符串语句中,值为null,在第二个字符串语句中,使用一个对象数组来填充消息参数。
在Main类中,可以解析文本消息,因为可以直接访问应用程序上下文。但是要让bean解析文本消息,您必须将MessageSource实现注入到需要解析文本消息的bean中。让我们为购物应用程序实现一个出纳员类,它演示了如何解析消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.apress.springrecipes.shop;
...

@Component
public class Cashier {
@Autowired
private MessageSource messageSource;

public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}

public void checkout(ShoppingCart cart) throws IOException {
String alert = messageSource.getMessage("alert.inventory.checkout",
new Object[] { cart.getItems(), new Date() }, Locale.US);
System.out.println(alert);
}
}

注意,POJO messageSource字段是Spring messageSource类型。字段值用@Autowired注释修饰,因此在创建bean实例时通过注入填充。然后,checkout方法可以访问messageSource字段,该字段允许bean访问getMessage方法,以便基于i18n标准访问文本消息。

使用注释定制POJO初始化和销毁

问题

有些pojo在使用前必须执行某些类型的初始化任务。这些任务可以包括打开文件、打开网络/数据库连接、分配内存等。此外,这些pojo必须在生命周期结束时执行相应的销毁任务。因此,有时需要在Spring IoC容器中定制bean的初始化和销毁。

解决方案

Spring可以通过在Java配置类中设置@Bean定义的initMethod和驱逐舰方法属性来识别初始化和销毁回调方法。或者,如果POJO方法分别用@PostConstruct和@PreDestroy注解修饰,Spring也可以识别初始化和销毁回调方法。Spring还可以使用@Lazy注释将bean的创建延迟到需要的时候(一个称为惰性初始化的过程)。Spring还可以使用@DependsOn注释确保某些bean的初始化。

它是如何工作的

定义在使用@Bean初始化和销毁POJO之前要运行的方法。让我们以购物应用程序为例,考虑一个涉及checkout函数的示例。让我们修改出纳员类,将购物车的产品和结帐时间记录到文本文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.apress.springrecipes.shop;

import java.io.*;
import java.util.Date;

public class Cashier {

private String fileName;
private String path;
private BufferedWriter writer;

public void setFileName(String fileName) {
this.fileName = fileName;
}

public void setPath(String path) {
this.path = path;
}

public void openFile() throws IOException {
File targetDir = new File(path);
if (!targetDir.exists()) {
targetDir.mkdir();
}
File checkoutFile = new File(path, fileName + ".txt");
if (!checkoutFile.exists()) {
checkoutFile.createNewFile();
}
writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(checkoutFile, true)));
}

public void checkout(ShoppingCart cart) throws IOException {
writer.write(new Date() + "\t" + cart.getItems() + "\r\n"); writer.flush();
}

public void closeFile() throws IOException {
writer.close();
}
}

在出纳员类中,openFile()方法首先验证要写入数据的目标目录和文件是否存在。然后在指定的系统路径中打开文本文件并将其分配给writer字段。然后每次调用checkout()方法时,将日期和购物车项目附加到文本文件中。最后,closeFile()方法关闭文件,释放其系统资源。
接下来,让我们研究如何在Java config类中设置这个bean定义,以便在创建bean之前执行openFile()方法,在销毁bean之前执行closeFile()方法。

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class ShopConfiguration {
@Bean(initMethod = "openFile", destroyMethod = "closeFile")
public Cashier cashier() {
String path = System.getProperty("java.io.tmpdir") + "/cashier";
Cashier c1 = new Cashier();
c1.setFileName("checkout");
c1.setPath(path);
return c1;
}
}

注意,POJO的初始化和销毁任务是使用@Bean注释的initMethod和驱逐舰方法属性定义的。通过在bean声明中设置这两个属性,当创建出纳类时,它首先触发openFile()方法,验证目标目录和文件是否存在,以及打开文件以添加记录。当bean被销毁时,它触发closeFile()方法,确保关闭文件引用以释放系统资源。

定义使用@PostConstruct和@PreDestroy在POJO初始化和销毁之前运行的方法

如果在Java配置类之外定义POJO实例(例如,使用@Component注释),另一种替代方法是直接在POJO类中使用@PostConstruct和@PreDestroy注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Component
public class Cashier {

@Value("checkout")
private String fileName;
@Value("c:/Windows/Temp/cashier")
private String path;
private BufferedWriter writer;

public void setFileName(String fileName) {
this.fileName = fileName;
}

public void setPath(String path) {
this.path = path;
}

@PostConstruct
public void openFile() throws IOException {
File targetDir = new File(path);
if (!targetDir.exists()) {
targetDir.mkdir();
}
File checkoutFile = new File(path, fileName + ".txt");
if(!checkoutFile.exists()) {
checkoutFile.createNewFile();
}
writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(checkoutFile, true)));
}

public void checkout(ShoppingCart cart) throws IOException {
writer.write(new Date() + "\t" +cart.getItems() + "\r\n"); writer.flush();
}

@PreDestroy
public void closeFile() throws IOException {
writer.close();
}
}

@Component注释告诉Spring管理POJO,就像在以前的菜谱中使用的那样。POJO字段的两个值用@Value注释设置,在之前的菜谱中也讨论了这个概念。openFile()方法由@PostConstruct注释修饰,该注释告诉Spring在构造bean之后立即执行该方法。closeFile()方法被修饰为@PreDestroy注释,它告诉Spring在销毁bean之前执行这个方法。

使用@Lazy为pojo定义延迟初始化

默认情况下,Spring对所有pojo执行热切初始化。这意味着pojo在启动时初始化。但是,在某些情况下,可以方便地将POJO初始化过程延迟到需要bean时。延迟初始化称为延迟初始化。
延迟初始化有助于限制启动时的资源消耗峰值,并保存整个系统资源。延迟初始化对于执行重量级操作(例如,网络连接、文件操作)的pojo尤其相关。要使用惰性初始化标记bean,您可以使用@Lazy注释来修饰bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.apress.springrecipes.shop;

...
import org.springframework.stereotype.Component;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.Lazy;

@Component @Scope("prototype")
@Lazy
public class ShoppingCart {
private List<Product> items = new ArrayList<>();
public void addItem(Product item) {
items.add(item);
}
public List<Product> getItems() {
return items;
}
}

在前面的声明中,由于POJO被@Lazy注释修饰,如果应用程序从不需要POJO或被另一个POJO引用,它就不会实例化。

在使用@DependsOn的其他pojo之前定义pojo的初始化

作为应用程序的POJO增长,POJO初始化的数量也是如此。如果pojo相互引用,并且分布在不同的Java配置类中,则可以创建竞争条件。如果bean C需要bean B和bean F的逻辑,会发生什么呢?如果首先检测到bean C,而Spring没有初始化bean B和bean F,您将会得到一个很难检测到的错误。为了确保在其他pojo之前对某些pojo进行初始化,并在初始化过程失败时获得更具描述性的错误,Spring提供了@DependsOn注释。@DependsOn注释确保给定的bean在另一个bean之前被初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.apress.springrecipes.sequence.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Configuration;
import com.apress.springrecipes.sequence.DatePrefixGenerator;
import com.apress.springrecipes.sequence.NumberPrefixGenerator;
import com.apress.springrecipes.sequence.SequenceGenerator;

@Configuration
public class SequenceConfiguration {
@Bean
@DependsOn("datePrefixGenerator")
public SequenceGenerator sequenceGenerator() {
SequenceGenerator sequence= new SequenceGenerator();
sequence.setInitial(100000);
sequence.setSuffix("A");
return sequence;
}
}

创建Post-Processors以验证和修改POJOs

问题

您希望在构建期间将任务应用到所有bean实例或特定类型的实例,以根据特定的标准验证或修改bean属性。

解决方案

bean后处理器允许在初始化回调方法之前和之后进行bean处理
(i.e. 分配给@Bean注释的initMethod属性或用@PostConstruct注释修饰的方法的属性)。bean后处理器的主要特性是它处理IoC容器中的所有bean实例,而不只是单个bean实例。通常,bean后处理器用于检查bean属性的有效性,根据特定的标准修改bean属性,或者将某些任务应用到所有bean实例。
Spring还支持@Required注释,它由内置的Spring post-processor RequiredAnnotationBeanPostProcessor支持。RequiredAnnotationBeanPostProcessor bean事后处理器检查是否设置了所有带有@Required注释的bean属性。

它是如何工作的

假设您希望审计每个bean的创建。您可能希望这样做来调试应用程序、验证每个bean的属性,或者在其他场景中。bean后处理器是实现这个特性的理想选择,因为您不必修改任何现有的POJO代码。

创建POJO来处理每个Bean实例

使用工厂(静态方法、实例方法、Spring的FactoryBean)创建POJOs

使用Spring环境和Profiles加载不同的POJOs集

让POJOs了解Spring的IoC容器资源

使用面向切面的编程和注释

访问连接点信息

用@Order注释指定切面的优先级

重用方面切入点定义

写AspectJ切入点表达式

将AOP用于POJOs的介绍

用AOP向POJOs引入状态

在Spring使用Load-Time编织AspectJ切面

在Spring中配置AspectJ切面

用AOP将POJOs注入域对象

使用Spring和TaskExecutors应用并发

在POJOs之间通信应用程序事件

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2021 朝着牛逼的道路一路狂奔 All Rights Reserved.

访客数 : | 访问量 :