精通-Spring-应用开发-全-
精通 Spring 应用开发(全)
原文:
zh.annas-archive.org/md5/A95A09924E8304BAE696F70C7C92A54C
译者:飞龙
前言
Spring 是一个开源的 Java 应用程序开发框架,用于构建和部署在 JVM 上运行的系统和应用程序。它通过使用模型-视图-控制器范式和依赖注入,使得构建模块化和可测试的 Web 应用程序变得更加高效。它与许多框架(如 Hibernate、MyBatis、Jersey 等)无缝集成,并在使用标准技术(如 JDBC、JPA 和 JMS)时减少样板代码。
本书的目的是教会中级 Spring 开发人员掌握使用高级概念和额外模块来扩展核心框架,从而进行 Java 应用程序开发。这样可以开发更高级、更强大的集成应用程序。
本书涵盖的内容
第一章,“Spring 与 Mongo 集成”,演示了 Spring MVC 与 MongoDB 的集成,以及安装 MongoDB 来创建数据库和集合。
第二章,“使用 Spring JMS 进行消息传递”,教你安装 Apache ActiveMQ 和不同类型的消息传递。本章还演示了创建多个队列,并使用 Spring 模板与这些队列进行通信,同时提供了屏幕截图的帮助。
第三章,“使用 Spring Mail 进行邮件发送”,创建了一个邮件服务,并使用 Spring API 进行配置,演示了如何使用 MIME 消息发送带附件的邮件。
第四章,“使用 Spring Batch 进行作业”,说明了如何使用 Spring Batch 读取 XML 文件,以及如何创建基于 Spring 的批处理应用程序来读取 CSV 文件。本章还演示了如何使用 Spring Batch 编写简单的测试用例。
第五章,“Spring 与 FTP 集成”,概述了不同类型的适配器,如入站和出站适配器,以及出站网关及其配置。本章还研究了两个重要的类,FTPSessionFactory 和 FTPsSessionFactory,使用 getter 和 setter。
第六章,“Spring 与 HTTP 集成”,介绍了使用多值映射来填充请求并将映射放入 HTTP 标头的用法。此外,它还提供了关于 HTTP 和 Spring 集成支持的信息,可用于访问 HTTP 方法和请求。
第七章,“Spring 与 Hadoop”,展示了 Spring 如何与 Apache Hadoop 集成,并提供 Map 和 Reduce 过程来搜索和计算数据。本章还讨论了在 Unix 机器上安装 Hadoop 实例以及在 Spring 框架中配置 Hadoop 作业。
第八章,“Spring 与 OSGI”,开发了一个简单的 OSGI 应用程序,并演示了 Spring 动态模块如何支持 OSGI 开发,并减少文件的创建,从而使配置变得更加简单。
第九章,“使用 Spring Boot 引导应用程序”,从设置一个简单的 Spring Boot 项目开始,以及使用 Spring Boot 引导应用程序的过程。本章还介绍了 Spring Boot 如何支持云铁路服务器,并帮助在云上部署应用程序。
第十章,“Spring 缓存”,实现了我们自己的缓存算法,并教你制作一个通用算法。本章还讨论了在 Spring 框架中支持缓存机制的类和接口。
第十一章, Spring 与 Thymeleaf 集成,将 Thymeleaf 模板引擎集成到 Spring MVC 应用程序中,并使用 Spring Boot 启动 Spring 与 Thymeleaf 应用程序。
第十二章, Spring 与 Web 服务集成,将 JAX_WS 与 Spring Web 服务集成。它演示了如何创建 Spring Web 服务和端点类,通过访问 WSDL URL 来访问 Web 服务。
你需要什么来阅读这本书
需要一台安装有 Mac OS、Ubuntu 或 Windows 的计算机。为了构建 Spring 应用程序,你至少需要安装 Java 和 Maven 3。
这本书适合谁
如果你是一名有 Spring 应用开发经验的 Java 开发者,那么这本书非常适合你。建议具备良好的 Spring 编程约定和依赖注入的知识,以充分利用本书。
约定
在本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些样式的示例及其含义的解释。
文本中的代码、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名都显示如下:“我们使用@Controller
注解来表示ProductController.java
类是一个控制器类。”
一块代码设置如下:
@Controller
public class ProductController {
@Autowired
private ProductRepository respository;
private List <Product>productList;
public ProductController() {
super();
}
当我们希望引起您对代码块的特定部分的注意时,相关行或项将以粗体显示:
public class MailAdvice {
public void advice (final ProceedingJoinPoint proceedingJoinPoint) {
new Thread(new Runnable() {
public void run() {
任何命令行的输入或输出都是这样写的:
cd E:\MONGODB\mongo\bin
mongod -dbpath e:\mongodata\db
新术语和重要单词以粗体显示。屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:“下一步是创建一个 rest 控制器来发送邮件;为此,请单击提交。”
注意
警告或重要提示会以这样的方式出现在一个框中。
提示
技巧和窍门是这样出现的。
第一章:Spring Mongo 集成
MongoDB 是一种流行的 NoSQL 数据库,也是基于文档的。它是使用流行且强大的 C++语言编写的,这使得它成为一种面向文档的数据库。查询也是基于文档的,它还提供了使用 JSON 样式进行存储和检索数据的索引。MongoDB 基于集合和文档的概念工作。
让我们来看看 MySQL 和 MongoDB 之间的一些术语差异:
MySQL | MongoDB |
---|---|
表 | 集合 |
行 | 文档 |
列 | 字段 |
连接 | 嵌入式文档链接 |
在 MongoDB 中,集合是一组文档。这与 RDBMS 表相同。
在本章中,我们将首先设置 MongoDB NoSQL 数据库,并将集成 Spring 应用程序与 MongoDB 以执行 CRUD 操作。第一个示例演示了更新单个文档值。第二个示例考虑了一个订单用例,其中需要在集合中存储两个文档引用。它演示了使用objectId
引用引用 MongoDB 的不同文档的灵活性。
只有当应用程序具有大量写操作时,我们才需要使用 NoSQL 数据库。MongoDB 也非常适合云环境,我们可以轻松地复制数据库。
在下一节中,我们将看到如何开始使用 MongoDB,从安装开始,使用 Spring 框架,并集成 MongoDB。为了开始,我们将展示各种用例中的基本创建、检索、更新和删除(CRUD)操作。
安装 MongoDB 并创建数据库
在本节中,我们将安装 MongoDB 并创建一个数据库:
-
在
www.mongodb.org/downloads
下载 MongoDB 数据库。 -
通过在
bin
文件夹中执行以下命令来配置数据文件夹:
>mongod.exe -dbpath e:\mongodata\db
-
在另一个命令提示符中启动
mongod.exe
。 -
执行以下命令:
>show databaseExecute
>show dbs
命令在 MongoDB 中也可以正常工作。
- 执行以下命令以创建一个名为
eshopdb
的新数据库。
>use new-eshopdb
-
执行
> show dbs
仍然会显示eshopdb
尚未创建,这是因为它不包含任何集合。一旦添加了集合,我们将在下一步中添加一些集合。 -
在命令提示符中执行以下代码片段。以下代码片段将向集合中插入示例文档:
db.eshopdb.insert({cust_id:1,name:"kishore",address:"jayangar"})
db.eshopdb.insert({cust_id:2,name:"bapi",address:"HAL Layout"})
db.eshopdb.insert({cust_id:3,name:"srini",address:"abbigere street"})
db.eshopdb.insert({cust_id:4,name:"sangamesha",address: "Kattarigupee layout"})
为 MongoDB 设置批处理文件
创建批处理文件来启动 MongoDB 总是很容易,最好创建一个脚本文件来启动 Mongo。这样,我们就不会出现配置错误。这也会节省我们很多时间。
-
创建一个
mongodbstart.bat
文件。 -
编辑文件并输入以下命令,然后保存:
cd E:\MONGODB\mongo\bin
mongod -dbpath e:\mongodata\db
下次要启动 MongoDB 时,只需单击批处理文件。
Spring 和 MongoDB 的订单用例
让我们看一下订单用例,以使用 Spring 和 MongoDB 执行简单的 CRUD 操作。我们正在对产品、客户和订单文档执行 CRUD 操作。情景是这样的:客户选择产品并下订单。
以下是订单用例。操作者是应用程序用户,将有以下选项:
-
对产品文档进行 CRUD 操作
-
对客户文档进行 CRUD 操作
-
通过选择产品和客户对订单执行 CRUD 操作
-
在订单文档中保存产品文档对象 ID 和客户文档对象 ID
将 Mongo 文档映射到 Spring Bean
Spring 提供了一种简单的方法来映射 Mongo 文档。以下表格描述了 Bean 与 MongoDB 集合的映射:
Bean | Mongo 集合 |
---|---|
Customer.java |
db.customer.find() |
Order.java |
db.order.find() |
Product.java |
db.product.find() |
设置 Spring-MongoDB 项目
我们需要使用 Maven 创建一个简单的 Web 应用程序项目。
- 在 Maven 命令提示符中执行以下命令:
mvn archetype:generate -DgroupId=com.packtpub.spring -DartifactId=spring-mongo -DarchetypeArtifactId=maven-archetype-webapp
-
创建一个简单的 Maven 项目,使用 web 应用原型。添加最新的
4.0.2.RELEASE
spring 依赖。 -
以下是
pom.xml
文件的一部分。这些是必须添加到pom.xml
文件中的依赖项。
<!-- Spring dependencies -->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>2.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.2.RELEASE </version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.0.2.RELEASE </version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.0.2.RELEASE </version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.0.2.RELEASE </version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.0.2.RELEASE </version>
<scope>runtime</scope>
</dependency>
应用程序设计
以下表包含用于开发简单 CRUD 应用程序的类。请求从控制器流向模型,然后返回。Repository 类标有 @Repository
注解,并使用 mongoTemplate
类连接到 MongoDB。
控制器 | 模型 | JSP | Bean |
---|---|---|---|
Customer Controller.java |
Customer Repository.java |
customer.jsp``editcutomer.jsp``allcustomers.jsp |
Customer.java |
Order Controller.java |
Order Repository.java |
order.jsp``editorder.jsp``allorders.jsp |
Order.java |
Product Controller.java |
Product Repository.java |
product.jsp``editproduct.jsp``allproducts.jsp |
Product.java |
Spring 与 MongoDB 的应用实现
以下是实现 Spring4MongoDB_Chapter1
应用程序的步骤:
-
创建一个名为
Spring4MongoDB_Chapter1
的基于 web 的 Maven 项目。 -
将项目导入 Eclipse 进行实现。我使用的是 Eclipse Juno。
我们需要创建控制器来映射请求。
控制器请求映射到 GET
和 POST
方法,如下表所示:
请求 | 请求方法 | 模型属性 |
---|---|---|
/product |
GET |
productList |
/product/save |
POST |
productList |
/product/update |
POST |
productList |
/product/geteditproduct |
GET |
productAttribute |
/product/deleteproduct |
GET |
productAttribute |
/product/getallproducts |
GET |
productList |
以下是 ProductController.java
的实现。我们使用 @Controller
注解来指示 ProductController.java
类是一个控制器类。@Autowired
注解将 ProductRepository
类与 ProductController.java
文件绑定。
productList
属性是一个 Product
类型的列表,保存要在屏幕上显示的产品。@PostConstruct
注解将调用由它装饰的方法。一旦类的构造函数被调用并且所有属性被设置,在调用任何业务方法之前,值得注意的是它只被调用一次。
@Controller
public class ProductController {
@Autowired
private ProductRepository respository;
private List <Product>productList;
public ProductController() {
super();
}
@PostConstruct
public void init(){
this.productList=respository.getAllObjects();
}
//to get the list of products
@RequestMapping(value="/product", method = RequestMethod.GET)
public String getaddproduct(Model model) {
model.addAttribute("productList", productList);
model.addAttribute("productAttribute", new Product());
return "product";
}
//to save the product
@RequestMapping(value="/product/save", method = RequestMethod.POST)
public String addproduct(@ModelAttribute Product prod,Model model) {
if(StringUtils.hasText(prod.getProdid())) {
respository.updateObject(prod);
} else {
respository.saveObject(prod);
}
this.productList=respository.getAllObjects();
model.addAttribute("productList", productList);
return "product";
}
//to update the edited product
@RequestMapping(value="/product/update", method = RequestMethod.POST)
public String updatecustomer(@ModelAttribute Product prod,Model model) {
respository.updateObject(prod);
this.productList=respository.getAllObjects();
model.addAttribute("productList", productList);
return "product";
}
//to edit a product based on ID
@RequestMapping(value = "/product/geteditproduct", method = RequestMethod.GET)
public String geteditproduct(
@RequestParam(value = "prodid", required = true) String prodid,
Model model) {
model.addAttribute("productList", productList);
model.addAttribute("productAttribute", respository.getObject(prodid));
return "editproduct";
}
//to delete a product based on ID
@RequestMapping(value="/product/deleteproduct", method = RequestMethod.GET)
public String deleteproduct(
@RequestParam(value = "prodid", required = true) String prodid,Model model) {
respository.deleteObject(prodid);
this.productList=respository.getAllObjects();
model.addAttribute("productList", this.productList);
return "product";
}
//to get all the products
@RequestMapping(value = "/product/getallproducts", method = RequestMethod.GET)
public String getallproducts(Model model) {
this.productList=respository.getAllObjects();
model.addAttribute("productList", this.productList);
return "allproducts";
}
}
Product.java
文件有一个 @Document
注解和一个 @ID
注解,它被识别为 MongoDB 集合,将 Product
实体映射到 MongoDB 中的产品集合。
@Document
public class Product {
/*Bean class product with getter and setters*/
@Id
private String prodid;
private Double price;
private String name;
public Product() {
super();
}
public String getProdid() {
return prodid;
}
public void setProdid(String prod_id) {
this.prodid = prod_id;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
ProducRepository.java
文件有 @Repository
注解。这是持久层,并告诉 Spring 这个类在数据库上执行操作。连接到 Mongo 在 Mongo 模板中设置。
ProductRepository.java
@Repository
public class ProductRepository {
@Autowired
MongoTemplate mongoTemplate;
public void setMongoTemplate(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
public List<Product> getAllObjects() {
return mongoTemplate.findAll(Product.class);
}
/**
* Saves a {@link Product}.
*/
public void saveObject(Product Product) {
Product.setProdid(UUID.randomUUID().toString());
mongoTemplate.insert(Product);
}
/**
* Gets a {@link Product} for a particular id.
*/
public Product getObject(String id) {
return mongoTemplate.findOne(new Query(Criteria.where("_id").is(id)),
Product.class);
}
/**
* Updates a {@link Product} name for a particular id.
*/
public void updateObject(Product object) {
Query query = new Query();
query.addCriteria(Criteria.where("_id").is(object.getProdid()));
Product prod_tempObj = mongoTemplate.findOne(query, Product.class);
System.out.println("cust_tempObj - " + prod_tempObj);
//modify and update with save()
prod_tempObj.setName(object.getName());
prod_tempObj.setPrice(object.getPrice());
mongoTemplate.save(prod_tempObj);
}
/**
* Delete a {@link Product} for a particular id.
*/
public void deleteObject(String id) {
mongoTemplate.remove(new Query(Criteria.where("_id").is(id)),Product.class);
}
/**
* Create a {@link Product} collection if the collection does not already
* exists
*/
public void createCollection() {
if (!mongoTemplate.collectionExists(Product.class)) {
mongoTemplate.createCollection(Product.class);
}
}
/**
* Drops the {@link Product} collection if the collection does already exists
*/
public void dropCollection() {
if (mongoTemplate.collectionExists(Product.class)) {
mongoTemplate.dropCollection(Product.class);
}
}
}
.jsp
文件显示可用的产品,并允许用户对 Product
bean 执行 CRUD 操作。以下截图是使用存储在 MongoDB 中的产品 ObjectId
编辑产品信息的输出。
Product.jsp 文件
这个文件作为用户的视图层。它包含产品创建表单,并包括一个列出 MongoDB 中存储的所有产品的文件。
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Register Product</title>
</head>
<body>
<h1>Register Product</h1>
<ul>
<li><a href="/Spring4MongoDB_Chapter1/customer">Customer</a>
</li>
<li>r<a href="/Spring4MongoDB_Chapter1/order">Product</a>
</li></ul>
<form method="post" action="/Spring4MongoDB_Chapter1/product/save">
<table>
<tr>
<td> Name:</td>
<td><input type=text name="name"/></td>
</tr>
<tr>
<td>Price</td>
<td><input type=text name="price"/></td>
</tr>
</table>
<input type="hidden" name="prod_id" >
<input type="submit" value="Save" />
</form>
<%@ include file="allproducts.jsp" %>
</body>
</html>
如果一切顺利,您应该看到以下屏幕,您可以在其中玩转产品。以下截图是使用 Spring 和 MongoDB 实现的 注册产品 和列出产品功能的输出。
以下的 dispatcher-servlet.xml
文件显示了组件扫描和 MongoDB 模板的配置。它还显示了 MongoDB 数据库名称的配置。
dispatcher-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/data/mongo
http://www.springframework.org/schema/data/mongo/spring-mongo-1.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<context:component-scan base-package="com.packt" />
<!-- Factory bean that creates the Mongo instance -->
<bean id="mongo" class="org.springframework.data.mongodb.core.MongoFactoryBean">
<property name="host" value="localhost" />
</bean>
<mongo:mongo host="127.0.0.1" port="27017" />
<mongo:db-factory dbname="eshopdb" />
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
</bean>
<!-- Use this post processor to translate any MongoExceptions thrown in @Repository annotated classes -->
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/myviews/"
p:suffix=".jsp" />
</beans>
您可以看到mongoDbFactory
bean 已配置 MongoDB 数据库详细信息。您还会注意到mongoTemplate
也已配置。mongoTemplate
bean 的属性是mongoDbFactory
bean,因此在调用模板时连接会建立。
只需在 MongoDB 数据库中运行以下命令以测试订单用例:
-
db.order.find()
-
db.order.remove()
提示
RoboMongo
是一个免费工具,类似于Toad
,用于访问 MongoDB 数据库。
订单管理用例
让我们考虑这一部分的一个复杂场景。在我们考虑的用例中,订单用例在类中具有客户和产品对象。当用户下订单时,用户将选择产品和客户。
我们的目标是直接将customer
和product
类存储在 MongoDB 的Order
集合中。让我们首先实现具有 getter 和 setter 的OrderBean
类。
Order.java
package com.packt.bean;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document
public class Order {
private String order_id;
private Customer customer;
private Product product;
private String date;
private String order_status;
private int quantity;
public Order() {
super();
// TODO Auto-generated constructor stub
}
@Id
public String getOrder_id() {
return order_id;
}
public void setOrder_id(String order_id) {
this.order_id = order_id;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public String getOrder_status() {
return order_status;
}
public void setOrder_status(String order_status) {
this.order_status = order_status;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
}
下一步是在OrderRepository.java
文件中定义方法。
以下是repository
类中update
和save
方法的代码片段。
创建和插入订单
我们看到更新Order
方法接受Order
对象。我们使用addCriteria()
方法根据对象 ID 获取特定订单。检索到的Order
对象存储在temp
对象中。然后根据传递给方法的对象设置值到temp
对象。然后调用mongoTemplate.save(Object)
方法来更新保存的对象。
public void updateObject(Order order) {
Query query = new Query();
query.addCriteria(Criteria.where("_id").is(order.getOrder_id()));
Order order_tempObj = mongoTemplate.findOne(query, Order.class);
order_tempObj.setCustomer(order.getCustomer());
order_tempObj.setProduct(order.getProduct());
order_tempObj.setQuantity(order.getQuantity());
mongoTemplate.save(order_tempObj);
}
saveObject
方法只接受Order
对象并在保存之前将 ID 设置为Order
对象。
我们已经看到如何执行更新和插入。调用以下方法保存订单详情。这表明mongoTemplate
具有insert()
和save()
方法。
public void saveObject(Order Order) {
Order.setOrder_id(UUID.randomUUID().toString());
mongoTemplate.insert(Order);
}
控制器处理请求
controller
类根据用例具有客户存储库和产品存储库的引用。应用程序用户需要选择客户和产品来下订单。
OrderController
的初始 Skelton 如下所示:
@Controller
public class OrderController {
@Autowired
private OrderRepository respository;
@Autowired
private CustomerRepository customerRespository;
@Autowired
private ProductRepository productRespository;
private List<Order> orderList;
private List<Customer> customerList;
private List<Product> productList;
public OrderController() {
super();
}
}
在方法级别添加@Modelattribute
注解
controller
类用于处理Order
请求。在方法中添加了@ModelAttribute
注解。产品列表和客户列表始终作为模型属性可用于控制器。以下是OrderController
类的代码片段:
@ModelAttribute("orderList")
public List<Order> populateOrderList() {
this.orderList = respository.getAllObjects();
return this.orderList;
}
@ModelAttribute("productList")
public List<Product> populateProductList() {
this.productList = productRespository.getAllObjects();
return this.productList;
}
@ModelAttribute("customerList")
public List<Customer> populateCstomerList() {
this.customerList = customerRespository.getAllObjects();
return this.customerList;
}
OrderController 类的 CRUD 操作
这些方法映射到特定请求,@ModelAttribute("Order")
,以便在 JSP 级别轻松访问订单对象。您可以观察到在方法级别使用@ModelAttribute
,这将最小化添加@ModelAttribute
到方法中。
@RequestMapping(value = "/order", method = RequestMethod.GET)
// request show add order page
public String addOrder(@ModelAttribute("Order") Order order,Map<String, Object> model) {
model.put("customerList", customerList);
model.put("productList", productList);
return "order";
}
@RequestMapping(value = "/order/save", method = RequestMethod.POST)
// request to insert the record
public String addorder(@ModelAttribute("Order") Order order,Map<String, Object> model) {
order.setCustomer(customerRespository.getObject(order.getCustomer().getCust_id()));
order.setProduct(product_respository.getObject(order.getProduct().getProdid()));
respository.saveObject(order);
model.put("customerList", customerList);
model.put("productList", productList);
return "order";
}
@RequestMapping(value = "/orde`r/update", method = RequestMethod.POST)
public String updatecustomer(@ModelAttribute("Order") Order order,
Map<String, Object> model) {
order.setCustomer(customerRespository.getObject(order.getCustomer().getCust_id()));
order.setProduct(product_respository.getObject(order.getProduct().getProdid()));
respository.updateObject(order);
model.put("customerList", customerList);
model.put("productList", productList);
return "order";
}
@RequestMapping(value = "/order/geteditorder", method = RequestMethod.GET)
public String editOrder(@RequestParam(value = "order_id", required = true) String order_id, @ModelAttribute("Order") Order order,Map<String, Object> model) {
model.put("customerList", customerList);
model.put("productList", productList);
model.put("Order",respository.getObject(order_id));
return "editorder";
}
@RequestMapping(value = "/order/deleteorder", method = RequestMethod.GET)
public String deleteorder(@RequestParam(value = "order_id", required = true) String order_id, @ModelAttribute("Order") Order order,Map<String, Object> model) {
respository.deleteObject(order_id);
model.put("customerList", customerList);
model.put("productList", productList);
return "order";
}
}
JSP 文件
Order.jsp
文件演示了@ModelAttribute
的用法,它映射到控制器类中定义的模型订单。setter 方法将值设置给对象,从而最小化了编码。这展示了 Spring 中简化编码过程的功能。
Orders.jsp
<h1>Orders </h1>
<ul>
<li><a href="/Spring4MongoDB_Chapter1/customer">Customer</a>
</li>
<li>r<a href="/Spring4MongoDB_Chapter1/product">Product</a>
</li></ul>
<form:form action="/Spring4MongoDB_Chapter1/order/save" modelAttribute="Order">
<table>
<tr>
<td>Add your Order:</td>
<td><form:input path="quantity" size="3"/></td>
</tr>
<tr>
<td>Select Product:</td>
<td>
<form:select path="product.prodid">
<form:option value="" label="--Please Select"/>
<form:options items="${productList}" itemValue="prodid" itemLabel="name"/>
</form:select>
</td>
</tr>
<tr>
<td>Select Customer:</td>
<td>
<form:select path="customer.cust_id">
<form:option value="" label="--Please Select"/>
<form:options items="${customerList}" itemValue="cust_id" itemLabel="name"/>
</form:select>
</td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="Submit" />
</td>
</tr>
</table>
</form:form>
<%@ include file="allorders.jsp" %>
</body>
</html>
allorders.jsp
文件显示订单列表并提供编辑选项。使用 MongoDB 使得显示orderList
更简单。
Allorders.jsp
<h1> E-shop Orders</h1>
<table style="border: 1px solid; width: 500px; text-align:center">
<thead style="background:#fffcc">
<tr>
<th>Order Id</th>
<th>Customer Name</th>
<th>Customer Address</th>
<th>Product Address</th>
<th>Product Price</th>
<th>Product Quantity</th>
<th colspan="2"></th>
</tr>
</thead>
<tbody>
<c:forEach items="${orderList}" var="order">
<c:url var="editUrl" value="/order/geteditorder?order_id=${order.order_id}" />
<c:url var="deleteUrl" value="/order/deleteorder?order_id=${order.order_id}" />
<c:url var="addUrl" value="/order/" />
<tr>
<td><c:out value="${order.order_id}" /></td>
<td><c:out value="${order.customer.name}" /></td>
<td><c:out value="${order.customer.address}" /></td>
<td><c:out value="${order.product.name}" /></td>
<td><c:out value="${order.product.price}" /></td>
<td><c:out value="${order.quantity}" /></td>
<td><a href="${editUrl}">Edit</a></td>
<td><a href="${deleteUrl}">Delete</a></td>
<td><a href="${addUrl}">Add</a></td>
</tr>
</c:forEach>
</tbody>
以下是添加订单页面的截图:
以下是编辑订单页面的截图:
摘要
在本章中,我们学习了如何安装 MongoDB 并创建数据库和集合。在编写本章时,我们使用了最新版本的 Spring。我们还学习了如何将 Spring MVC 与 MongoDB 集成。我们已经构建了 CRUD 操作。我们还看到了诸如@Repository
、@Document
和@Controller
等注解的用法。在下一章中,让我们看看如何使用jms
模板集成 Spring 消息代理。
第二章:使用 Spring JMS 进行消息传递
Java 消息服务(JMS)是用于在应用程序组件之间或应用程序之间进行通信的 API。消息可以在应用程序和组件之间发送和接收。消息代理就像中间人一样创建、接收、读取和发送消息。消息消费者不需要始终可用以接收消息。消息代理存储消息,可以在需要时读取。
架构师会选择 JMS 来实现松耦合的设计。消息是异步的,它们一到达就被传递,不需要为消息发送请求。它还可以防止冗余,并确保特定消息只传递一次。
消息类型
根据需求,有两种选择消息域:
-
点对点消息传递:
-
每条消息只有一个消费者
-
没有时间依赖性
-
发布-订阅消息传递:
-
每条消息有许多消费者
-
消息具有时间依赖性-当应用程序向消息代理发送消息时,消费者需要订阅并保持活动状态以接收消息
消息消费者
这些是 JMS API 提供的消息消费方式:
-
消息监听器
-
它们提供了异步消息模型
-
监听器就像事件观察者/监听器;每当有消息可用时,监听器确保它到达目的地
-
监听器将调用
onMessage()
方法 -
receive()
方法 -
它提供同步消息
model()
-
消息通过显式调用连接工厂的
receive()
方法来消费
消息结构
消息由三部分组成:
-
头部:包含有关目的地和时间戳的信息,并且有
messageID
,由send()
或publish()
方法设置。 -
属性:可以为消息设置一些属性。
-
主体:消息主体可以是以下五种类型之一:
-
TextMessage
:用于发送字符串对象作为消息 -
ObjectMessage
:用于将可序列化对象作为消息发送 -
MapMessage
:用于发送具有键值对的映射 -
BytesMessage
:用于在消息中发送字节 -
StreamMessage
:用于在消息中发送 I/O 流
基于消息的 POJO 和监听器
众所周知,企业 JavaBean(EJB)提供了一个消息驱动的 bean 来与 EJB 容器进行通信。与此类似,Spring 也提供了消息驱动的 Pojo,它使用消息监听器容器与消息中间件进行通信。
消息监听器容器在消息驱动的 Pojo 和消息提供者之间进行通信。它注册消息,并通过获取和释放消息资源来帮助处理事务和异常处理。
以下是 Spring JMS 包提供的消息监听器容器列表:
-
简单消息监听器容器:提供固定数量的 JMS 会话,并且不参与外部管理的事务。
-
默认消息监听器容器:参与外部管理的事务,并提供良好的性能。这个监听器容器被广泛使用。
-
服务器消息监听器容器:提供基于提供程序的运行时调优,并提供消息会话池并参与事务。
开源消息工具
以下是一些可在开源许可下使用的开源消息中间件:
-
Glassfish OpenMQ
-
Apache ActiveMQ
-
JORAM
-
Presumo
Apache ActiveMQ
Apache ActiveMQ 具有许多功能,使其成为消息传递的选择。最新版本是 5.10。使用 ActiveMQ 的优势如下:
-
它支持 REST API
-
它支持 CXF Web 服务
-
它支持 AJAX 实现
-
它完全支持 Spring 框架
-
它可以与所有主要应用服务器一起使用,如 JBoss、Tomcat、Weblogic 和 Glassfish 服务器
设置 ApacheMQ 以进行点对点消息传递
设置 ApacheMQ 的步骤如下:
-
从
activemq.apache.org/download.html
下载最新的Apache ActiveMQ.zip
。 -
将 ZIP 文件解压缩到
E:\apachemq\
。 -
在命令提示符中,转到位置
E:\apachemq\apache-activemq-5.10-SNAPSHOT\bin\win32
,然后单击apachemq.bat
启动 Apache ActiveMQ。 -
Apache ActiveMQ 将在 Jetty 服务器上运行,因此可以通过 URL 访问。
-
点击链接
http://localhost:8161/admin/index.jsp
。 -
第一次这样做时,会要求您输入凭据;输入
admin/admin
。 -
在控制台中,您将看到欢迎部分和代理部分。
-
代理部分提供了有关 Apache 消息代理的以下信息:
-
名称:
localhost
或服务器的名称 -
版本 5.10 快照
-
ID:
ID:BLRLANJANA-55074-1397199950394-0:1
-
正常运行时间:1 小时 24 分钟
-
存储百分比使用:0
-
内存百分比使用:0
-
临时百分比使用:0
-
单击队列。
-
在队列名称字段中输入
orderQueue
,然后单击创建。
使用 Spring JmsTemplate 的 ApacheMq 用例
在上一章中,我们演示了使用 MongoDB 进行订单管理。假设从一个应用程序下的订单需要被读取到不同的应用程序并存储在不同的数据库中。
订单管理消息代理的设计如下:
让我们使用消息代理的相同用例。请求从控制器流出,当用户输入订单详细信息并单击保存时,订单 bean 设置在控制器中,控制器将请求发送到 JMS 发送器,即订单发送器。
订单发送者以 map 的形式将消息发送到队列。接收者读取消息并将消息保存到 MongoDB 数据库中。接收者也可以是不同的应用程序;所有应用程序只需要知道队列名称,以防应用程序中配置了多个队列。
Spring 依赖
使用与第一章相同的源代码,Spring Mongo Integration,以及pom.xml
文件。使用 Spring JMS 依赖项更新pom.xml
文件。对于本章,我们有 Spring 4.0.3 版本可用,这是迄今为止最新的版本。以下是Pom.xml
文件的代码:
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.packt.web</groupId>
<artifactId>Spring4JMS_Chapter2</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>Spring4JMS_Chapter2</name>
<url>http://maven.apache.org</url>
<properties>
<spring.version>4.0.3.RELEASE</spring.version>
</properties>
<dependencies>
<!-- Spring JMS dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>${spring.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-core</artifactId>
<version>5.3.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-jms_1.1_spec</artifactId>
<version>1.1.1</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<finalName>Spring4JMS_Chapter2</finalName>
</build>
</project>
使用 SpringJMS 和 ActiveMQ 实现订单管理消息系统
在前面关于 Apache ActiveMQ 的部分中,我们讨论了创建消息队列所需的步骤,并创建了一个订单队列。现在,让我们从应用程序向队列发送消息。
以下表格描述了集成了 JMS 的应用程序的组件。
请求从 JSP 流向 Spring 控制器,该控制器设置订单 bean 对象并将其发送给orderSender
(这是一个 JMS 消息发送器类)。该类将订单对象放入队列。
JMS 接收器是从队列中读取消息的类。读取的对象被发送到OrderRepository
类,这是一个 Mongo Repository 类,并将消息发布到 MongoDB 数据库。
以下表格为我们提供了一个关于在 Spring MVC 应用程序中使用 JMS 进行通信的类的概述:
JSP | 控制器 | Bean | JMS 发送器 | JMS 接收器 | MongoRepository |
---|---|---|---|---|---|
order.jsp``allorders.jsp |
Order Controller.java |
Order.java |
OrderSender |
OrderReceiver |
OrderRepository |
配置 dispatcherservlet.xml 以使用 JMS
您可以看到我们在 XML 文件中配置了以下内容:
-
connectionFactory
:它创建一个jmsconnection
对象。这个jmsconnection
对象连接到消息导向中间件(MOM),即 Apache ActiveMQ。jmsconnection
对象提供了一个 JMS 会话对象,应用程序使用该对象与 Apache ActiveMQ 交互。代理 URL 提供了有关消息代理接口正在侦听的主机和端口的信息。 -
destination
:这是应用程序需要通信的队列的名称。
<bean id="destination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="orderQueue"/>
</bean>
jmstemplate
:它以目的地和connectionFactory
bean 作为参数。
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<property name="defaultDestination" ref="destination" />
</bean>
orderSender
:这是使用jms
模板向队列发送消息的类。
<bean id="orderSender" class="com.packt.jms.OrderSender" />
orderReceiver
:这个类从队列中读取消息。它有connectionFactory
,以便可以连接到 JMS 提供程序来读取消息。
<bean id="orderReceiver" class="com.packt.jms.OrderReceiver" />
<jms:listener-container connection-factory="connectionFactory">
<jms:listener destination="orderQueue" ref="orderReceiver" method="orderReceived" />
</jms:listener-container>
以下是dispacherservlet.xml
的完整配置。我们将观察到配置文件已更新为activemq
配置。
dispatcherservlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/data/mongo
http://www.springframework.org/schema/data/mongo/spring-mongo-1.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/jms
http://www.springframework.org/schema/jms/spring-jms.xsd
http://activemq.apache.org/schema/core
http://activemq.apache.org/schema/core/activemq-core.xsd">
<context:component-scan base-package="com.packt" />
<!-- JMS Active MQQueue configuration -->
<bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL">
<value>tcp://localhost:61616</value>
</property>
</bean>
<bean id="destination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="orderQueue"/>
</bean>
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<property name="defaultDestination" ref="destination" />
</bean>
<bean id="orderSender" class="com.packt.jms.OrderSender" />
<bean id="orderReceiver" class="com.packt.jms.OrderReceiver" />
<jms:listener-container connection-factory="connectionFactory">
<jms:listener destination="orderQueue" ref="orderReceiver" method="orderReceived" />
</jms:listener-container>
<!-- Factory bean that creates the Mongo instance -->
<bean id="mongo" class="org.springframework.data.mongodb.core.MongoFactoryBean">
<property name="host" value="localhost" />
</bean>
<mongo:mongo host="127.0.0.1" port="27017" />
<mongo:db-factory dbname="eshopdb" />
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
</bean>
<!-- Use this post processor to translate any MongoExceptions thrown in @Repository annotated classes -->
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/myviews/"
p:suffix=".jsp" />
</beans>
Order.java
package com.packt.bean;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document
public class Order {
private String order_id;
private Customer customer;
private Product product;
private String date;
private String order_status;
private int quantity;
public Order() {
super();
// TODO Auto-generated constructor stub
}
@Id
public String getOrder_id() {
return order_id;
}
public void setOrder_id(String order_id) {
this.order_id = order_id;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public String getOrder_status() {
return order_status;
}
public void setOrder_status(String order_status) {
this.order_status = order_status;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
}
OrderController
类调用发送器将订单发送到消息代理队列。控制器使用 MongoDB 执行一些基本的 CRUD 操作。以下代码仅演示了Create
操作。
当调用/order/save
时,控制器将订单对象发送到orderSender
,后者将订单详细信息保存在队列中。
OrderCOntroller.java
Order details is saved with JMS.The Order Object is passed to orderSender, which will store the order details in the queue.
@RequestMapping(value = "/order/save", method = RequestMethod.POST)
// request insert order recordhrecord
public String addorder(@ModelAttribute("Order") Order order,Map<String, Object> model) {
orderSender.sendOrder(order);
model.put("customerList", customerList);
model.put("productList", productList);
return "order";
}
让我们来看看 JMS 发送器和接收器类。这两个类都使用 Spring JMS 模板来接收和发送消息。org.springframework.jms.core.MessageCreator
类创建要放入队列中的消息。
以下是orderSender
的代码,它获取需要传递到队列的对象。JMSTemplate
准备消息格式,以便它可以被队列接受。
OrderSender
package com.packt.jms;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import com.packt.bean.Order;
public class OrderSender {
@Autowired
private JmsTemplate jmsTemplate;
public void sendOrder(final Order order){
jmsTemplate.send(
new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
MapMessage mapMessage = session.createMapMessage();
mapMessage.setInt("quantity", order.getQuantity());
mapMessage.setString("customerId", order.getCustomer().getCust_id());
mapMessage.setString("productId", order.getProduct().getProdid());
return mapMessage;
}
}
);
System.out.println("Order: "+ order);
}
}
以下是在添加订单案例时的屏幕截图:
在 ApacheMQ 中使用多个队列
在前面的部分中,我们演示了使用 Map Message 将消息发送到 Order Queue。现在,我们可以看看如何在 ApacheMQ 中使用多个队列:
-
启动 Apache ActiveMQ 服务器,在控制台上点击Queues并创建两个队列。
-
让我们创建两个队列,并将队列命名如下:
-
PacktTestQueue1
-
PacktTestQueue2
-
使用与本章第一个示例相同的依赖项创建一个新的 Spring 项目。
-
创建一个
PacktMessageListener
类,实现MessageListener
接口。该类覆盖onMessage(Message message)
方法。 -
Spring 的
DefaultMessageListener
从队列中消费消息并调用onMessage(Message message)
方法。
PacktMessageListener:
package com.packt.jms;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
public class PacktMessageListener implements MessageListener{
private PacktMessageSender packtmessagesender;
public void onMessage(Message message){
if (message instanceof TextMessage){
try{
String msgText = ((TextMessage) message).getText();
packtmessagesender.sendMessage(msgText);
}
catch (JMSException jmsexception){
System.out.println(jmsexception.getMessage());
}
}
else{
throw new RuntimeException("exception runtime");
}
}
public void setTestMessageSender(PacktMessageSender packtmessagesender){
this.packtmessagesender = packtmessagesender;
}
}
- 现在让我们来看看消息发送器类,它使用
JmsTemplate
将文本消息发送到队列。
在这里,我们为JmsTemplate
对象和queue
对象提供了 setter,并定义了一个发送消息的方法。该类已在 XML 文件中进行了配置。
PacktMessageSender
package com.packt.jms;
import javax.jms.MessageListener;
import javax.jms.Queue;
import org.springframework.jms.core.JmsTemplate;
public class PacktMessageSender {
private JmsTemplate jmsTemplate;
private Queue queue;
public void setJmsTemplate(JmsTemplate jmsTemplate){
this.jmsTemplate = jmsTemplate;
}
public void setQueue(Queue queue) {
this.queue = queue;
}
public void sendMessage(String msgText) {
jmsTemplate.convertAndSend(queue, msgText);
}
}
- 让我们首先在
meta-inf
文件夹下的context.xml
文件中创建资源引用。这是我们将为 JMS 配置Java 命名和目录接口(JNDI)的地方。
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<!—connection factory details-->
<Resource name="jms/mqConnectionFactory" auth="Container" type="org.apache.activemq.ActiveMQConnectionFactory" description="JMS Connection Factory" factory="org.apache.activemq.jndi.JNDIReferenceFactory" brokerURL="tcp://localhost:61616" />
<!—queue details-->
<Resource name="jms/PacktTestQueue1" auth="Container" type="org.apache.activemq.command.ActiveMQQueue" factory="org.apache.activemq.jndi.JNDIReferenceFactory" physicalName="PacktTestQueue1"/>
<!—queue details-->
<Resource name="jms/PacktTestQueue2" auth="Container" type="org.apache.activemq.command.ActiveMQQueue" factory="org.apache.activemq.jndi.JNDIReferenceFactory" physicalName="PacktTestQueue2"/>
</Context>
- 以下是在
spring-configuration.xml
文件中需要进行的配置更改,以配置多个队列:
-
使用 Spring JNDI 查找
queueNames
和 JMSconnectionFactory
-
将
ConnectionFactory
引用传递给JmsTemplate
-
配置
MessageSender
和MessageListener
类 -
MessageSender
类将具有JmsTemplate
和queue
对象作为属性 -
MessageListener
将具有MessageSender
作为属性 -
配置
DefaultMessageListenerContainer
类,该类从队列中消费消息
- 以下是配置文件的代码:
Spring-configuration.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee-4.0.xsd">
<jee:jndi-lookup id="apachemqConnectionFactory" jndi-name="java:comp/env/jms/mqConnectionFactory" />
<jee:jndi-lookup id="PacktTestQueue1" jndi-name="java:comp/env/jms/PacktTestQueue1" />
<jee:jndi-lookup id="PacktTestQueue2" jndi-name="java:comp/env/jms/PacktTestQueue2" />
<bean id="packtMessageListener" class="com.packt.jms.PacktMessageListener">
<property name="packtMessageSender" ref ="packtMessageSender" />
</bean>
<bean id="defaultMessageListenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref ="apachemqConnectionFactory" />
<property name="destination" ref ="PacktTestQueue1"/>
<property name="messageListener" ref ="packtMessageListener"/>
<property name="concurrentConsumers" value="2" />
</bean>
<bean id="packtMessageSender" class="com.packt.jms.PacktMessageSender">
<property name="jmsTemplate" ref="jmsTemplate"/>
<property name="queue" ref="PacktTestQueue2"/>
</bean>
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="apachemqConnectionFactory" />
</bean>
</beans>
- 以下代码将配置
web.xml
文件。在web.xml
中,我们实际上提供了关于spring-configuration.xml
文件位置的信息,以便 Web 容器可以加载它。
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID"
version="2.5">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/configuration/spring-configuration.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
-
如果您使用 Maven 作为构建工具,请确保编译源代码并在 Tomcat 或其他您选择的服务器上运行应用程序。同时保持 Apache ActiveMQ 服务器控制台处于运行状态。
-
在 ActiveMQ 控制台中,点击队列。
-
点击发送按钮以链接到
PacktTestQueue1
行。 -
输入一些消息文本,然后点击发送按钮。
-
在控制台中,您会看到从队列 1 发送了一条消息到队列 2。我们的应用程序从
PacktTestQueue1
消费消息并将其推送到PacktTestQueue2
。 -
现在,让我们增加要发送的消息数量,看看它的行为。
-
点击PacktTestQueue2,您将看到所有消息都被推送到
PacktTestQueue2
。
配置 JMS 事务
当我们使用事务时,我们可以更好地处理前面的情景。消息将在事务中处理,在监听器中出现异常的情况下,将为完整的源代码回滚。参考repository-Spring4JMS_TransactionChapter2
中的源代码。
包括事务在消息传递中需要以下步骤:
- 将以下属性添加到 ActiveMQ 连接工厂 bean 配置中:
<property name="redeliveryPolicy">
<bean class="org.apache.activemq.RedeliveryPolicy">
<property name="maximumRedeliveries" value="3"/>
</bean>
</property>
- 更新监听器定义如下:
<jms:listener-container connection-factory="connectionFactory" acknowledge="transacted">
<jms:listener destination="orderQueue" ref="orderReceiver" method="orderReceived" />
</jms:listener-container>
让我们重新审视情景,了解在jmsTemplate
中添加事务后发生了什么:
-
场景 1:成功场景
-
场景 2:消息生产者向队列发送信息,消费者读取并将其处理到数据库中;然后出现错误。
添加事务后,代理将三次发送消息。在第四次尝试时,它将发送到新队列,以便消息不会丢失。
- 场景 3:消息生产者向队列发送信息,消费者读取并将其处理到数据库中;然后出现错误。
添加事务后,如果在完成处理之前监听器执行失败,消息代理将重新发送信息。
配置多个 JMS 监听器和适配器
我们可能会遇到需要有更多 JMS 监听器和适配器的情况。当我们需要使用 Spring 模板轻松处理多个队列时,我们可以配置多个监听器。为了处理多个监听器,我们还需要适配器,它将委托给不同的监听器。
<bean id="jmsMessageAdapter" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<property name="delegate" ref="jmsMessageReceiverDelegate" />
<property name="defaultListenerMethod" value="processMessage" />
</bean>
<jms:listener-container container-type="default"
connection-factory="connectionFactory" acknowledge="auto">
<jms:listener destination="queue1"
ref="jmsMessageReceiverDelegate" method="processMessage" />
<jms:listener destination="queue2"
ref="jmsMessageReceiverDelegate" method="processMessage" />
</jms:listener-container>
JMS 事务
在本节中,让我们看看如何在消息传递中包含事务。我们将首先演示不使用事务的消息传递,使用几种情景。我们将首先描述情景并编写一个测试用例。然后,我们将围绕它开发一个应用程序。我们将演示使用convertandsendmessage()
方法发送消息。
- 场景 1:这是一个正面的用例,在之前的部分中我们也看到了。
@Test
public void testCorrectMessage() throws InterruptedException {
Order order = new Order(0, "notification to deliver correctly");
ordersender.convertAndSendMessage(QUEUE_INCOMING, order);
Thread.sleep(6000);
printResults();
assertEquals(1, getSavedOrders());
assertEquals(0, getMessagesInQueue(QUEUE_INCOMING));
assertEquals(0, getMessagesInQueue(QUEUE_DLQ));
}
- 场景 2:在这里,让我们使用一个负面情景。消息生产者向队列发送信息,消费者读取,但在到达数据库之前发生异常。
@Test
public void testFailedAfterReceiveMessage() throws InterruptedException {
Order order = new Order(1, "ordernotification to fail after receiving");
ordersender.convertAndSendMessage(QUEUE_INCOMING, order);
Thread.sleep(6000);
printResults();
assertEquals(0, getSavedOrders());
assertEquals(0, getMessagesInQueue(QUEUE_INCOMING));
assertEquals(1, getMessagesInQueue(QUEUE_DLQ));
//Empty the dead letter queue
jmsTemplate.receive(QUEUE_DLQ);
}
在这种情况下,我们丢失了消息。
- 场景 3:在这里,让我们使用另一个负面情景。消息生产者向队列发送信息,消费者读取并将其处理到数据库中;然后出现错误。
@Test
public void testFailedAfterProcessingMessage() throws InterruptedException {
Order order = new Order(2, "ordernotification to fail after processing");
ordersender.convertAndSendMessage(QUEUE_INCOMING, order);
Thread.sleep(6000);
printResults();
assertEquals(2, getSavedOrders());
assertEquals(0, getMessagesInQueue(QUEUE_INCOMING));
assertEquals(0, getMessagesInQueue(QUEUE_DLQ));
}
消息在失败之前被传递并存储在数据库中。
摘要
在本章中,我们学习了安装 Apache ActiveMQ 和不同类型的消息传递所需的步骤。我们演示了如何将 Spring 的jms
模板与应用程序集成。我们还通过截图演示了如何创建多个队列以及如何使用 Spring 模板与队列进行通信。在下一章中,我们将研究 Spring JAVA 邮件 API。
第三章:使用 Spring 邮件发送邮件
邮件 API 是所有现代 Web 应用的一部分。最终用户更喜欢通过邮件收到有关与应用程序执行的交易的详细信息。
Spring 已经让为任何 Java 应用程序提供邮件功能变得更加容易。在本章中,我们将看到如何使用 Spring 邮件模板向电子邮件接收者发送电子邮件。在上一章中,我们使用消息作为中间件将消息存储在队列中,现在在本章中,我们将演示使用不同场景下的 Spring 邮件模板配置。
Spring 邮件消息处理流程
以下图表描述了 Spring 邮件消息处理的流程。通过这个图表,我们可以清楚地了解使用 Spring 邮件模板发送邮件的过程。
创建并发送消息到与互联网协议交互的传输协议,然后消息被接收者接收。
Spring 邮件框架需要邮件配置或 SMTP 配置作为输入,以及需要发送的消息。邮件 API 与互联网协议交互以发送消息。在下一节中,我们将看一下 Spring 邮件框架中的类和接口。
使用 Spring 发送邮件的接口和类
org.springframework.mail
包用于 Spring 应用程序中的邮件配置。
以下是用于发送邮件的三个主要接口:
-
MailSender
:这个接口用于发送简单的邮件消息。 -
JavaMailSender
:这个接口是MailSender
接口的子接口,支持发送邮件消息。 -
MimeMessagePreparator
:这个接口是一个回调接口,支持JavaMailSender
接口准备邮件消息。
以下类用于使用 Spring 发送邮件:
-
SimpleMailMessage
:这是一个类,具有to
、from
、cc
、bcc
、sentDate
等属性。SimpleMailMessage
接口使用MailSenderImp
类发送邮件。 -
JavaMailSenderImpl
:这个类是JavaMailSender
接口的实现类。 -
MimeMessageHelper
:这个类帮助准备 MIME 消息。
使用@Configuration 注解发送邮件
我们将在这里演示如何使用 Spring 邮件 API 发送邮件。
- 首先,我们在
.properties
文件中提供所有 SMTP 详细信息,并使用@Configuration
注解将其读取到类文件中。类的名称是MailConfiguration
。
mail.properties
文件内容如下:
mail.protocol=smtp
mail.host=localhost
mail.port=25
mail.smtp.auth=false
mail.smtp.starttls.enable=false
mail.from=me@localhost
mail.username=
mail.password=
@Configuration
@PropertySource("classpath:mail.properties")
public class MailConfiguration {
@Value("${mail.protocol}")
private String protocol;
@Value("${mail.host}")
private String host;
@Value("${mail.port}")
private int port;
@Value("${mail.smtp.auth}")
private boolean auth;
@Value("${mail.smtp.starttls.enable}")
private boolean starttls;
@Value("${mail.from}")
private String from;
@Value("${mail.username}")
private String username;
@Value("${mail.password}")
private String password;
@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
Properties mailProperties = new Properties();
mailProperties.put("mail.smtp.auth", auth);
mailProperties.put("mail.smtp.starttls.enable", starttls);
mailSender.setJavaMailProperties(mailProperties);
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setProtocol(protocol);
mailSender.setUsername(username);
mailSender.setPassword(password);
return mailSender;
}
}
- 下一步是创建一个 REST 控制器来发送邮件;为此,请单击提交。我们将使用
SimpleMailMessage
接口,因为我们没有任何附件。
@RestController
class MailSendingController {
private final JavaMailSender javaMailSender;
@Autowired
MailSubmissionController(JavaMailSender javaMailSender) {
this.javaMailSender = javaMailSender;
}
@RequestMapping("/mail")
@ResponseStatus(HttpStatus.CREATED)
SimpleMailMessage send() {
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo("packt@localhost");
mailMessage.setReplyTo("anjana@localhost");
mailMessage.setFrom("Sonali@localhost");
mailMessage.setSubject("Vani veena Pani");
mailMessage.setText("MuthuLakshmi how are you?Call Me Please [...]");
javaMailSender.send(mailMessage);
return mailMessage;
}
}
使用 MailSender 和 SimpleMailMessage 以及 XML 配置发送邮件
“简单邮件消息”意味着发送的电子邮件只是基于文本,没有 HTML 格式,没有图像,也没有附件。在本节中,考虑一个场景,即在用户在应用程序中下订单后,我们会向用户发送欢迎邮件。在这种情况下,邮件将在数据库插入操作成功后发送。
为邮件服务创建一个名为com.packt.mailService
的单独文件夹。以下是使用MailSender
接口和SimpleMailMessage
类发送邮件的步骤。
-
创建一个名为
Spring4MongoDB_MailChapter3
的新 Maven Web 项目。 -
本例中使用了第一章中创建的 MongoDB 数据库,Spring Mongo Integration。我们还在 MongoDB 的 Eshop db 数据库上使用了相同的 CRUD 操作
Customer
、Order
和Product
。我们还使用了相同的mvc
配置和源文件。 -
使用与第二章中使用的相同的依赖项,Spring JMS 消息。
-
我们需要在
pom.xml
文件中添加依赖项:
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mail</artifactId>
<version>3.0.2.RELEASE</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1-rev-1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.3</version>
</dependency>
-
编译 Maven 项目。为邮件服务创建一个名为
com.packt.mailService
的单独文件夹。 -
创建一个名为
MailSenderService
的简单类,并自动装配MailSender
和SimpleMailMessage
类。基本框架如下所示:
public class MailSenderService {
@Autowired
private MailSender mailSender;
@AutoWired
private SimpleMailMessage simplemailmessage;
public void sendmail(String from, String to, String subject, String body){
/*Code */
}
}
- 接下来,创建一个
SimpleMailMessage
对象,并设置邮件属性,如from
、to
和subject
。
public void sendmail(String from, String to, String subject, String body){
SimpleMailMessage message=new SimpleMailMessage();
message.setFrom(from);
message.setSubject(subject);
message.setText(body);
mailSender.send(message);
}
- 我们需要配置 SMTP 详细信息。Spring 邮件支持提供了在 XML 文件中配置 SMTP 详细信息的灵活性。
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="smtp.gmail.com" />
<property name="port" value="587" />
<property name="username" value="username" />
<property name="password" value="password" />
<property name="javaMailProperties">
<props>
<prop key="mail.smtp.auth">true</prop>
<prop key="mail.smtp.starttls.enable">true</prop>
</props>
</property>
</bean>
<bean id="mailSenderService" class=" com.packt.mailserviceMailSenderService ">
<property name="mailSender" ref="mailSender" />
</bean>
</beans>
在订单成功在 MongoDB 数据库中放置后,我们需要向客户发送邮件。更新addorder()
方法如下:
@RequestMapping(value = "/order/save", method = RequestMethod.POST)
// request insert order recordh
public String addorder(@ModelAttribute("Order") Order order,Map<String, Object> model) {
Customer cust=new Customer();
cust=customer_respository.getObject(order.getCustomer().getCust_id());
order.setCustomer(cust);
order.setProduct(product_respository.getObject(order.getProduct().getProdid()));
respository.saveObject(order);
mailSenderService.sendmail("anjana.mprasad@gmail.com",cust.getEmail(),
"Dear"+cust.getName()+"Your order details",order.getProduct().getName()+"-price-"+order.getProduct().getPrice());
model.put("customerList", customerList);
model.put("productList", productList);
return "order";
}
向多个收件人发送邮件
如果您想通知用户应用程序中的最新产品或促销活动,可以创建一个邮件发送组,并使用 Spring 邮件发送支持向多个收件人发送邮件。
我们在同一个类MailSenderService
中创建了一个重载方法,它将接受字符串数组。类中的代码片段将如下所示:
public class MailSenderService {
@Autowired
private MailSender mailSender;
@AutoWired
private SimpleMailMessage simplemailmessage;
public void sendmail(String from, String to, String subject, String body){
/*Code */
}
public void sendmail(String from, String []to, String subject, String body){
/*Code */
}
}
以下是从 MongoDB 中列出已订阅促销邮件的用户集合的代码片段:
public List<Customer> getAllObjectsby_emailsubscription(String status) {
return mongoTemplate.find(query(where("email_subscribe").is("yes")), Customer.class);
}
发送 MIME 消息
多用途互联网邮件扩展(MIME)允许在互联网上发送附件。如果您不发送任何附件,使用 MIME 消息发送器类型类是不可取的。在下一节中,我们将详细了解如何发送带附件的邮件。
使用 MIME 消息准备程序并重写准备method()
以设置邮件属性来更新MailSenderService
类。
public class MailSenderService {
@Autowired
private MailSender mailSender;
@AutoWired
private SimpleMailMessage simplemailmessage;
public void sendmail(String from, String to, String subject, String body){
/*Code */
}
public void sendmail(String from, String []to, String subject, String body){
/*Code */
}
public void sendmime_mail(final String from, final String to, final String subject, final String body) throws MailException{
MimeMessagePreparator message = new MimeMessagePreparator() {
public void prepare(MimeMessage mimeMessage) throws Exception {
mimeMessage.setRecipient(Message.RecipientType.TO,new InternetAddress(to));
mimeMessage.setFrom(new InternetAddress(from));
mimeMessage.setSubject(subject);
mimeMessage.setText(msg);
}
};
mailSender.send(message);
}
发送邮件附件
我们还可以在邮件中附加各种类型的文件。这个功能由MimeMessageHelper
类支持。如果您只想发送一个没有附件的 MIME 消息,可以选择MimeMesagePreparator
。如果要求附件与邮件一起发送,我们可以选择带有文件 API 的MimeMessageHelper
类。
Spring 提供了一个名为org.springframework.core.io.FileSystemResource
的文件类,它具有一个接受文件对象的参数化构造函数。
public class SendMailwithAttachment {
public static void main(String[] args) throws MessagingException {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class);
ctx.refresh();
JavaMailSenderImpl mailSender = ctx.getBean(JavaMailSenderImpl.class);
MimeMessage mimeMessage = mailSender.createMimeMessage();
//Pass true flag for multipart message
MimeMessageHelper mailMsg = new MimeMessageHelper(mimeMessage, true);
mailMsg.setFrom("ANJUANJU02@gmail.com");
mailMsg.setTo("RAGHY03@gmail.com");
mailMsg.setSubject("Test mail with Attachment");
mailMsg.setText("Please find Attachment.");
//FileSystemResource object for Attachment
FileSystemResource file = new FileSystemResource(new File("D:/cp/ GODGOD. jpg"));
mailMsg.addAttachment("GODGOD.jpg", file);
mailSender.send(mimeMessage);
System.out.println("---Done---");
}
}
发送预配置的邮件
在这个例子中,我们将提供一条要发送的邮件,并在 XML 文件中进行配置。有时在 Web 应用程序中,您可能需要在维护时发送消息。想象一下邮件内容发生变化,但发件人和收件人是预先配置的情况。在这种情况下,您可以向MailSender
类添加另一个重载方法。
我们已经固定了邮件的主题,内容可以由用户发送。可以将其视为“一个应用程序,每当构建失败时向用户发送邮件”。
<?xml version="1.0" encoding="UTF-8"?>
<beans xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.packt" />
<!-- SET default mail properties -->
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="smtp.gmail.com"/>
<property name="port" value="25"/>
<property name="username" value="anju@gmail.com"/>
<property name="password" value="password"/>
<property name="javaMailProperties">
<props>
<prop key="mail.transport.protocol">smtp</prop>
<prop key="mail.smtp.auth">true</prop>
<prop key="mail.smtp.starttls.enable">true</prop>
<prop key="mail.debug">true</prop>
</props>
</property>
</bean>
<!-- You can have some pre-configured messagess also which are ready to send -->
<bean id="preConfiguredMessage" class="org.springframework.mail.SimpleMailMessage">
<property name="to" value="packt@gmail.com"></property>
<property name="from" value="anju@gmail.com"></property>
<property name="subject" value="FATAL ERROR- APPLICATION AUTO MAINTENANCE STARTED-BUILD FAILED!!"/>
</bean>
</beans>
现在我们将为主题发送两个不同的正文。
public class MyMailer {
public static void main(String[] args){
try{
//Create the application context
ApplicationContext context = new FileSystemXmlApplicationContext("application-context.xml");
//Get the mailer instance
ApplicationMailer mailer = (ApplicationMailer) context.getBean("mailService");
//Send a composed mail
mailer.sendMail("nikhil@gmail.com", "Test Subject", "Testing body");
}catch(Exception e){
//Send a pre-configured mail
mailer.sendPreConfiguredMail("build failed exception occured check console or logs"+e.getMessage());
}
}
}
使用 Spring 模板和 Velocity 发送 HTML 邮件
Velocity 是 Apache 提供的模板语言。它可以很容易地集成到 Spring 视图层中。本书中使用的最新 Velocity 版本是 1.7。在前一节中,我们演示了如何使用@Bean
和@Configuration
注解来使用 Velocity 发送电子邮件。在本节中,我们将看到如何配置 Velocity 以使用 XML 配置发送邮件。
需要做的就是将以下 bean 定义添加到.xml
文件中。在mvc
的情况下,可以将其添加到dispatcher-servlet.xml
文件中。
<bean id="velocityEngine" class="org.springframework.ui.velocity.VelocityEngineFactoryBean">
<property name="velocityProperties">
<value>
resource.loader=class class.resource.loader.class=org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
</value>
</property>
</bean>
-
创建一个名为
Spring4MongoDB_Mail_VelocityChapter3
的新的 Maven web 项目。 -
创建一个名为
com.packt.velocity.templates
的包。 -
创建一个名为
orderconfirmation.vm
的文件。
<html>
<body>
<h3> Dear Customer,<h3>
<p>${customer.firstName} ${customer.lastName}</p>
<p>We have dispatched your order at address.</p>
${Customer.address}
</body>
</html>
-
使用我们在前几节中添加的所有依赖项。
-
向现有的 Maven 项目中添加此依赖项:
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
- 为了确保 Velocity 在应用程序启动时被加载,我们将创建一个类。让我们把这个类命名为
VelocityConfiguration.java
。我们已经在这个类中使用了注解@Configuration
和@Bean
。
import java.io.IOException;
import java.util.Properties;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.VelocityException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ui.velocity.VelocityEngineFactory;
@Configuration
public class VelocityConfiguration {
@Bean
public VelocityEngine getVelocityEngine()
throws VelocityException, IOException{
VelocityEngineFactory velocityEngineFactory = new VelocityEngineFactory();
Properties props = new Properties();
props.put("resource.loader", "class");
props.put("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader." + "ClasspathResourceLoader");
velocityEngineFactory.setVelocityProperties(props);
return factory.createVelocityEngine();
}
}
- 使用相同的
MailSenderService
类,并在类中添加另一个重载的sendMail()
方法。
public void sendmail(final Customer customer){
MimeMessagePreparator preparator = new MimeMessagePreparator() {
public void prepare(MimeMessage mimeMessage)
throws Exception {
MimeMessageHelper message = new MimeMessageHelper(mimeMessage);
message.setTo(user.getEmailAddress());
message.setFrom("webmaster@packt.com"); // could be parameterized
Map model = new HashMap();
model.put("customer", customer);
String text = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, "com/packt/velocity/templates/orderconfirmation.vm", model);
message.setText(text, true);
}
};
this.mailSender.send(preparator);
}
- 更新控制器类以使用 Velocity 模板发送邮件。
@RequestMapping(value = "/order/save", method = RequestMethod.POST)
// request insert order recordh
public String addorder(@ModelAttribute("Order") Order order,Map<String, Object> model) {
Customer cust=new Customer();
cust=customer_respository.getObject(order.getCustomer().getCust_id());
order.setCustomer(cust);
order.setProduct(product_respository.getObject(order.getProduct().getProdid()));
respository.saveObject(order);
// to send mail using velocity template.
mailSenderService.sendmail(cust);
return "order";
}
通过不同的线程发送 Spring 邮件
还有其他异步发送 Spring 邮件的选项。一种方法是为邮件发送工作创建一个单独的线程。Spring 带有taskExecutor
包,它为我们提供了线程池功能。
-
创建一个名为
MailSenderAsyncService
的类,该类实现MailSender
接口。 -
导入
org.springframework.core.task.TaskExecutor
包。 -
创建一个名为
MailRunnable
的私有类。以下是MailSenderAsyncService
的完整代码:
public class MailSenderAsyncService implements MailSender{
@Resource(name = "mailSender")
private MailSender mailSender;
private TaskExecutor taskExecutor;
@Autowired
public MailSenderAsyncService(TaskExecutor taskExecutor){
this.taskExecutor = taskExecutor;
}
public void send(SimpleMailMessage simpleMessage) throws MailException {
taskExecutor.execute(new MailRunnable(simpleMessage));
}
public void send(SimpleMailMessage[] simpleMessages) throws MailException {
for (SimpleMailMessage message : simpleMessages) {
send(message);
}
}
private class SimpleMailMessageRunnable implements Runnable {
private SimpleMailMessage simpleMailMessage;
private SimpleMailMessageRunnable(SimpleMailMessage simpleMailMessage) {
this.simpleMailMessage = simpleMailMessage;
}
public void run() {
mailSender.send(simpleMailMessage);
}
}
private class SimpleMailMessagesRunnable implements Runnable {
private SimpleMailMessage[] simpleMessages;
private SimpleMailMessagesRunnable(SimpleMailMessage[] simpleMessages) {
this.simpleMessages = simpleMessages;
}
public void run() {
mailSender.send(simpleMessages);
}
}
}
- 在
.xml
文件中配置ThreadPool
执行器。
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor" p:corePoolSize="5"
p:maxPoolSize="10" p:queueCapacity="100" p:waitForTasksToCompleteOnShutdown="true"/>
- 测试源代码。
import javax.annotation.Resource;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration
public class MailSenderAsyncService {
@Resource(name = " mailSender ")
private MailSender mailSender;
public void testSendMails() throws Exception {
SimpleMailMessage[] mailMessages = new SimpleMailMessage[5];
for (int i = 0; i < mailMessages.length; i++) {
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject(String.valueOf(i));
mailMessages[i] = message;
}
mailSender.send(mailMessages);
}
public static void main (String args[]){
MailSenderAsyncService asyncservice=new MailSenderAsyncService();
Asyncservice. testSendMails();
}
}
使用 AOP 发送 Spring 邮件
我们还可以通过将邮件功能与面向切面编程(AOP)集成来发送邮件。这可以用于在用户注册应用程序后发送邮件。想象一下用户在注册后收到激活邮件的情景。这也可以用于发送关于应用程序上下订单的信息。使用以下步骤使用 AOP 创建一个MailAdvice
类:
-
创建一个名为
com.packt.aop
的包。 -
创建一个名为
MailAdvice
的类。
public class MailAdvice {
public void advice (final ProceedingJoinPoint proceedingJoinPoint) {
new Thread(new Runnable() {
public void run() {
System.out.println("proceedingJoinPoint:"+proceedingJoinPoint);
try {
proceedingJoinPoint.proceed();
} catch (Throwable t) {
// All we can do is log the error.
System.out.println(t);
}
}
}).start();
}
}
这个类创建一个新的线程并启动它。在run
方法中,调用了proceedingJoinPoint.proceed()
方法。ProceddingJoinPoint
是AspectJ.jar
中可用的一个类。
- 使用
aop
配置更新dispatcher-servlet.xml
文件。使用以下代码更新xlmns
命名空间:
xmlns:aop=http://www.springframework.org/schema/aop
- 还要更新
xsi:schemalocation
,如下所示:
xsi:schemaLocation="http://www.springframework.org/
schema/aop http://www.springframework.org/
schema/aop/spring-aop-2.5.xsd
- 更新
.xml
文件中的 bean 配置:
<aop:config>
<aop:aspect ref="advice">
<aop:around method="fork" pointcut="execution(* org.springframework.mail.javamail.JavaMailSenderImpl.send(..))"/>
</aop:aspect>
</aop:config>
总结
在本章中,我们演示了如何使用 Spring API 创建邮件服务并进行配置。我们还演示了如何使用 MIME 消息发送带附件的邮件。我们还演示了如何使用ExecutorService
为发送邮件创建一个专用线程。我们看到了一个示例,可以将邮件发送给多个收件人,并看到了使用 Velocity 引擎创建模板并将邮件发送给收件人的实现。在最后一节中,我们演示了 Spring 框架支持如何使用 Spring AOP 和线程发送邮件。
在下一章中,我们将介绍 Spring Batch 框架。
第四章:使用 Spring Batch 的作业
企业应用程序通常需要通过应用复杂的业务规则来处理大量信息。一些应用程序需要自动运行作业并提供大量数据作为进一步处理的输入。这些功能总是基于时间的作业,不需要任何用户干预。批处理广泛应用于银行和保险领域,在那里大量数据在预定时间进行处理。一个作业是一个过程,而批处理作业意味着一组进程,它们在预定时间运行以执行任务。
Spring Batch 简介
Spring Batch 本身是一个用于开发批处理作业的批处理框架。它支持批处理优化和作业分区,并且具有高度可扩展性,这促使我们在批处理应用程序的开发中考虑它。
使用 Spring Batch 的用例
让我们列举一些可以在应用程序中使用 Spring 批处理的用例:
-
在预定时间向用户发送批量邮件
-
从队列中读取消息
-
在给定时间更新交易
-
在给定时间处理用户接收到的所有文件
批处理处理的目标
批处理的主要目标是按顺序完成以下一系列步骤以完成批处理作业:
-
查找作业。
-
识别输入。
-
调度作业。
-
启动作业。
-
处理作业。
-
转到第 2 步(获取新输入)。
批处理作业的架构
让我们描述一下批处理处理器的基本架构;我们还可以看到批处理处理中涉及的组件。从下图中,您可以找出 Spring Batch 的主要组件:
现在让我们逐个查看组件。
-
JobRepository
:这个容器是我们需要注册作业或进程的地方。 -
JobOperator
:这是触发已注册作业的对象。它还提供了访问注册的 API。这是一个接口。 -
Job
:它是jobRepository
中的一个进程或任务。这包括一个以上的步骤。 -
Step
:实际上包含需要执行的逻辑。每个步骤包括一个ItemReader
,ItemProcessor
和ItemWriter
接口。首先,ItemReader
接口一次读取一个步骤的作业并将其传递给ItemProcessor
进行处理。例如,它可能收集一些所需的数据。然后,ItemWriter
接口将数据写入数据库,或者执行事务或记录消息。有两种类型的步骤: -
ChunkStyle
:ChunkStyle
步骤具有一个ItemReader
,一个ItemProcessor
和一个ItemWriter
。 -
BatchLet
:在 Spring 中,BatchLet
被称为TaskLetStep
。BatchLet
是一个自定义步骤,可用于发送批量邮件或短信。
现在我们知道了批处理的基础知识,在下一节中我们将看到如何实现或使用批处理。
使用企业批处理
我们有以下两种实现批处理的选项:
-
使用 JVM 并为每个作业运行启动 JVM
-
在 J2EE 容器中部署批处理作业管理应用程序
JSR-352 是可用于实现批处理的标准规范。Spring 框架在很大程度上支持这个规范。大多数 JEE 容器,如Glassfish,Jboss- JMX和 Web Sphere 都支持 JSR-352 规范。作为开发人员,我们可以选择 Spring 框架并在 J2EE 容器上部署批处理。
您还可以使用 restful API 将数据池化到批处理应用程序中并从中取出。在下一节中,让我们使用 Spring Batch 框架创建一个作业。我们首先来看一下依赖关系。
Spring Batch 的依赖项
要开始使用 Spring Batch,我们需要查看依赖关系。假设用户熟悉 Maven 应用程序,我们可以查看需要添加到pom.xml
文件中以使用 Spring Batch 的以下依赖项:
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-core</artifactId>
<version>3.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
Spring Batch 的关键组件
你可以看到,Spring Batch 的关键组件与 Java 中的批处理的 JSR 规范非常相似。
JobRepository
:这又是一个作业的存储库。但是,在 Spring Batch 框架中,核心 API 有JobRepository
。它为JobLauncher
、JobReader
、ItemProcessor
和ItemWriter
提供create
、update
、read
和delete
方法。在 Spring 框架中负责JobRepository
的类是SimpleJobRepository
。有两种存储作业的方式:一种是在数据库中,另一种是在内存中(这将不得不使用HashMaps
)。
SimpleJobRepositoryConstructor
看起来像这样:
public SimpleJobRepository(JobInstanceDao jobInstanceDao,
JobExecutionDao jobExecutionDao,
StepExecutionDao stepExecutionDao,
ExecutionContextDao ecDao)
JobLauncher
:JobLauncher
只是一个用于启动作业的简单接口。作业在jobRepository
中注册。
public interface JobLauncher {
public JobExecution run(Job job, JobParameters jobParameters)
throws JobExecutionAlreadyRunningException, JobRestartException;
}
SimpleJobLauncher
类实现了JobLauncher
接口。这个类有一个setJobRepository
方法。
public void setJobRepository(JobRepository jobRepository)
ItemReader
:它是org.springframework.batch.item
包中的一个接口。ItemReader 用于提供数据。数据可以来自数据库、XML 或平面文件。
实现类预计是有状态的,并且将在每个批次中被多次调用,每次调用read()
都会返回一个不同的值,最终在所有输入数据耗尽时返回 null。实现类不需要是线程安全的,ItemReader
接口的客户端需要意识到这一点。
public interface ItemReader<T> {
T read() throws Exception, UnexpectedInputException, ParseException;
}
ItemProcessor
:这是一个用于处理数据并进行中间处理的接口。在交给ItemWriter
之前,ItemProcessor
可以用于实现某些业务逻辑。
public interface ItemProcessor<I, O> {
O process(I item) throws Exception;
}
public class ProductBean {}
public class RelatedProductsBean {
public RelatedProductsBean(ProductBean productBean) {}
}
public class ProductBeanProcessor implements ItemProcessor<ProductBean, RelatedProductsBean >{
public RelatedProductsBean process(ProductBean productBean) throws Exception {
//Perform simple transformation, convert a ProductBean to a RelatedProductsBean
return new RelatedProductsBean(productBean);
}
}
public class ProductBeanWriter implements ItemWriter<ProductBean>{
public void write(List<? extends ProductBean> productBeans) throws Exception {
//write productBeans
}
}
假设ItemReader
接口提供了一个类型为ProductBean
的类,这个类需要在写出之前转换为类型RelatedProductsBean
。可以编写一个ItemProcessor
来执行转换。在这个非常简单的例子中,有一个ProductBean
类,一个RelatedProductsBean
类,以及一个符合ItemProcessor
接口的ProductBeanProcessor
类。转换很简单,但任何类型的转换都可以在这里完成。RelatedProductsBean
写入程序将用于写出RelatedProductsBean
对象,如果提供了任何其他类型的对象,则会抛出异常。同样,如果提供的不是ProductBean
,ProductBeanProcessor
也会抛出异常。
ProductBeanProcessor
然后可以被注入到一个步骤中:
<job id="ioSampleJob">
<step name="step1">
<tasklet>
<chunk reader="ProductReader" processor="ProductProcessor" writer="RelatedProductsWriter" commit-interval="2"/>
</tasklet>
</step>
</job>
Item Writer
:这是一个接口,这里是它经常使用的实现类。
write
方法定义了ItemWriter
接口的最基本契约。只要它是打开的,它将尝试写出传入的项目列表。由于预期项目将被批处理到一起形成一个块,然后给出输出,接口接受项目列表而不是单独的项目。一旦项目被写出,可以在从write
方法返回之前执行任何必要的刷新。例如,如果写入到 Hibernate DAO,可以进行多次对write
的调用,每次对应一个项目。
然后写入程序可以在返回之前关闭 hibernate 会话。
这是ItemWriter
的一个经常使用的实现:
FlatFileItemWriter
:这将数据写入文件或流。它使用缓冲写入程序来提高性能。
StaxEventItemWriter: This is an implementation of ItemWriter that uses StAX and Marshaller for serializing objects to XML.
开发一个样本批处理应用
现在我们已经介绍了批处理的基础知识和 Spring Batch 的组件,让我们开发一个简单的例子,在这个例子中,以$$
开头的名称被识别为非素食食品,以##
开头的名称被识别为素食食品。不以这两个字符开头的名称需要被忽略。我们的作业必须生成一个 HTML 字符串,对于非素食食谱使用红色字体颜色,对于素食食谱使用绿色字体颜色。
您需要创建一个名为recipeMarker
的 Maven 项目,并添加先前提到的依赖项。还要添加所有 Spring Framework 核心依赖项。我们将在context.xml
文件上工作。我们需要配置作业存储库和作业启动器。
看看applicationContext.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<bean id="transactionManager" class="org.springframework.batch.support.transaction.ResourcelessTransactionManager"/>
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository"/>
</bean>
<bean id="jobRepository" class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
<property name="transactionManager" ref="transactionManager"/>
</bean>
<bean id="simpleJob" class="org.springframework.batch.core.job.SimpleJob" abstract="true">
<property name="jobRepository" ref="jobRepository" />
</bean>
</beans>
您可以看到我们使用MapJobRepositoryFactoryBean
来创建作业存储库。它是一个FactoryBean
,可以使用非持久性的内存中的数据访问对象(DAO)实现自动创建SimpleJobRepository
。该存储库实际上仅用于测试和快速原型设计。在这种设置中,您可能会发现ResourcelessTransactionManager
很有用(只要您的业务逻辑不使用关系数据库)。它不适用于具有拆分的多线程作业,尽管在多线程步骤中使用应该是安全的。
接下来,我们将使用ItemReader
和ItemWriter
接口创建实现类。
- 以下是
ItemReader
实现类。它在重写的read()
方法中读取数据,该方法返回一个对象。
package com.packt.batchjob;
import java.util.List;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ParseException;
import org.springframework.batch.item.UnexpectedInputException;
public class CustomItemReader implements ItemReader {
private int index = 0;
private List<String> itemList;
public Object read() throws Exception, UnexpectedInputException,
ParseException {
if (index < itemList.size()) {
String str = itemList.get(index++);
System.out.println("Read[ " + index + " ] = " + str);
return str;
} else {return null;}
}
public List<String> getItemList() {
return itemList;
}
public void setItemList(List<String> itemList) {
this.itemList = itemList;}
}
- 在这里我们有
ItemProcessor
。它应用了将食谱列表标记为红色和绿色的逻辑。
package com.packt.batchjob;
import org.springframework.batch.item.ItemProcessor;
public class CustomItemProcessor implements ItemProcessor {
public Object process(Object arg0) throws Exception {
String input = (String) arg0;
if (input.contains("$$")) {
input = input.substring(3, input.length());
input = "<font colour="red">(.Non-Veg)</font> " + input;
} else if (input.contains("##")) {
input = input.substring(3, input.length());
input = "<font colour="green">(.Veg)</font> " + input;
} else
return null;
System.out.println("Process : " + input);
return input;
}
}
- 最后,让我们编写实现类,从
ItemProcessor
中读取修改后的数据并写入。
import java.util.List;
import org.springframework.batch.item.ItemWriter;
public class CustomItemWriter implements ItemWriter {
public void write(List arg0) throws Exception {
System.out.println("Write : " + arg0 + "\n");
}
}
在下一步中,我们将ItemReader
,ItemProcessor
和ItemWriter
组合成一个作业。
让我们创建一个itemreaderprocessorwriter.xml
文件。我们将在 XML 文件中传递食谱列表。我们已经包含了applicationContext.xml
文件。已定义提交间隔,以表示写入两个元素后写入器应该提交。您还可以观察到步骤包括reader
,writer
和jobRepository
。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<import resource="applicationContext.xml"/>
<bean id="customReader" class="com.packt.batchjob.CustomItemReader" >
<property name="itemList" >
<list>
<value>$$Chicken65</value>
<value>$$ChickenTikkaMasala</value>
<value>$$GingerChicken</value>
<value>$$GarlicChicken</value>
<value>##Dal Makani</value>
<value>##Stuffed Capsicum</value>
<value>##Bendi Fry</value>
<value>##Alo Bartha</value>
</list>
</property>
</bean>
<bean id="customProcessor" class="com.packt.batchjob.CustomItemProcessor" />
<bean id="customWriter" class="com.packt.batchjob.CustomItemWriter" />
<bean id="simpleStep" class="org.springframework.batch.core.step.item.SimpleStepFactoryBean">
<property name="transactionManager" ref="transactionManager" />
<property name="jobRepository" ref="jobRepository" />
<property name="itemReader" ref="customReader"/>
<property name="itemProcessor" ref="customProcessor"/>
<property name="itemWriter" ref="customWriter"/>
<property name="commitInterval" value="2" />
</bean>
<bean id="readerWriterJob" parent="simpleJob">
<property name="steps">
<list>
<ref bean="simpleStep"/>
</list>
</property>
</bean>
</beans>
下一步是使用 Spring Batch 框架提供的命令行界面启动作业。
D:\SpringBatch\receipeMarker>java -classpath "lib\*;src"
org.springframework.batch.core.launch.support.CommandLineJobRunner
itemReaderWriter.xml readerWriterJob
让我们创建一个名为itemreaderprocessorwriter.xml
的文件。我们将在 XML 文件中传递食谱列表。我们已经包含了applicationContext.xml
文件。已定义提交间隔,以表示写入两个元素后写入器应该提交。您还可以观察到步骤包括reader
,writer
和jobRepository
。
OUTPUT:
Read[ 1 ] = $$Chicken65
Read[ 2 ] = $$ChickenTikkaMasala
Process : "<font colour="red">(.Non-Veg)</font> $$Chicken65
Process : "<font colour="red">(.Non-Veg)</font>$$ChickenTikkaMasala
Write : [<font colour="red">(.Non-Veg)</font>$$Chicken65 , <font colour="red">(.Non-Veg)</font> $$ChickenTikkaMasala
Read[ 3 ] = $$GingerChicken
Read[ 4 ] = $$GarlicChicken
Process : "<font colour="red">(.Non-Veg)</font> $$GingerChicken
Process : "<font colour="red">(.Non-Veg)</font>$$GarlicChicken
Write : [<font colour="red">(.Non-Veg)</font>$$GingerChicken , <font colour="red">(.Non-Veg)</font> $$GarlicChicken
Read[ 5 ] = ##Dal Makani
Read[ 6 ] = ##Stuffed Capsicum
Process : "<font colour="green">(. Veg)</font> ##Dal Makani
Process : "<font colour=" green ">(.Non-Veg)</font>##Stuffed Capsicum
Write : [<font colour=" green ">(.Veg)</font>##Dal Makani , <font colour=" green ">(. Veg)</font> ##Stuffed Capsicum
Read[ 7 ] = ##Bendi Fry
Read[ 8 ] = ##Alo Bartha
Process : "<font colour=" green ">(. Veg)</font> ##Bendi Fry
Process : "<font colour=" green ">(. Veg)</font>##Alo Bartha
Write : <font colour=" green ">(. Veg)</font>##Bendi Fry , <font colour="red">(.Non-Veg)</font> ##Alo Bartha
使用 Tasklet 接口创建示例批处理应用程序
让我们创建另一个在命令行上运行的批处理应用程序。该批处理应用程序打印消息。我们已经在本章开头讨论了 Tasklet。作业由步骤组成,步骤可以是两种类型之一:块样式步骤和 Tasklet。
在本示例中,我们使用Tasklet
接口。在 Spring Batch 中,Tasklet
是一个接口,用于执行单个任务,例如在步骤执行之前或之后清理或设置资源。该接口带有一个名为executeStatus
的方法,应该由实现它的类重写。
RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext)
throws java.lang.Exception
RepeatStatus: CONTINUABLE and FINISHED
在以下示例中,TaskLetImpl
实现了Tasklet
接口。我们还在配置文件中使用了TaskLetStep
类来配置JobRepository
。公共类TaskletStep
扩展了AbstractStep
。
TaskletStep
是执行步骤的Tasklet
的简单实现,可能是重复的,并且每次调用都被事务包围。因此,结构是一个循环,循环内有事务边界。循环由步骤操作(setStepOperations(RepeatOperations)
)控制。
客户端可以在步骤操作中使用拦截器来拦截或监听步骤范围的迭代,例如在步骤完成时获取回调。那些希望在单个任务级别获得回调的人可以为块操作指定拦截器。
让我们通过以下图表了解流程:
![使用 Tasklet 接口创建示例批处理应用程序
让我们创建一个名为Chapter4-SpringBatchCommandLine
的简单 Java 批处理应用程序项目
- 为
Chapter4-SpringBatchCommandLine
创建一个 Maven 文件夹结构,如下所示:
-
src
/main
/java
-
src
/main
/resources
-
src
/pom.xml
-
创建一个名为
com.packt.example
的包。 -
创建一个名为
TaskletImpl
的类。这个类实现了Tasklet
接口并重写了execute()
方法。
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.ExitStatus;
public class TaskletImpl implements Tasklet{
private String message;
public void setMessage(String message) {
this.message = message;
}
public ExitStatus execute() throws Exception {
System.out.print(message);
return ExitStatus.FINISHED;
}
}
-
配置
simpleJob.xml
文件。 -
将此文件放入
resources
文件夹中。 -
您可以看到我们创建了
TaskletImpl
类的三个实例:object1
,object2
和object3
。 -
在每个实例中,我们设置了消息属性。我们将对象实例传递给
TaskletStep
。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
<import resource="applicationContext.xml"/>
<bean id="object1" class="com.packt.example.TaskletImpl">
<property name="message" value="Dad not well"/>
</bean>
<bean id="object2" class="com.packt.example.TaskletImpl">
<property name="message" value="Call the doctor"/>
</bean>
<bean id="object3" class="com.packt.example.TaskletImpl">
<property name="message" value="He is sweating"/>
</bean>
<bean id="taskletStep" abstract="true" class="org.springframework.batch.core.step.tasklet.TaskletStep">
<property name="jobRepository" ref="jobRepository"/>
</bean>
<bean id="simpleJob" class="org.springframework.batch.core.job.SimpleJob">
<property name="name" value="simpleJob" />
<property name="steps">
<list>
<bean parent="taskletStep">
<property name="tasklet" ref="object1"/>
</bean>
<bean parent="taskletStep">
<property name="tasklet" ref="object2"/>
</bean>
<bean parent="taskletStep">
<property name="tasklet" ref="object3"/>
</bean>
</list>
</property>
<property name="jobRepository" ref="jobRepository"/>
</bean>
</beans>
- 配置
jobLauncher
和JobRepository
。
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository"/>
</bean>
<bean id="jobRepository" class="org.springframework.batch.core.repository.support.SimpleJobRepository">
<constructor-arg>
<bean class="org.springframework.batch.core.repository.dao.MapJobInstanceDao"/>
</constructor-arg>
<constructor-arg>
<bean class="org.springframework.batch.core.repository.dao.MapJobExecutionDao" />
</constructor-arg>
<constructor-arg>
<bean class="org.springframework.batch.core.repository.dao.MapStepExecutionDao"/>
</constructor-arg>
</bean>
- 您可以使用 MVN Compile 运行项目,如下所示:
mvn clean compile exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="simpleJob.xml simpleJob"
OUTPUT:
Dad not well
Call the Doctor
He is sweating
使用 Spring Batch 读取 CSV 文件
让我们创建另一个批处理应用程序,从目录中读取 CSV 文件,并使用commandlinerunner
运行作业。输出再次是一个 CSV 文件,将在output
文件夹中可用。
这个例子是关于展示 Spring Batch 框架中可用的ItemWriter
和ItemReader
实现的各种选项。我们在这里使用了 Spring Framework 中可用的flatFileItemReader
和flatFileItemWriter
实现。
我们将从应用程序开发开始,看看这些ItemReader
实现类是如何使用的。
-
使用 Maven 创建一个名为
SpringBatchCommandLine-Chapter4Example2
的 Spring Java 应用程序。 -
创建一个领域类
Employee
,具有两个实例变量empId
和name
,以及 getter 和 setter:
package com.packt;
public class Employee {
int empId;
String name;
public int getEmpId() {
return empId;
}
public void setEmpId(int empId) {
this.empId = empId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
-
使用
ItemWriter
接口并实现一个CustomeItemWriter
类。这个类重写了ItemWriter
接口中定义的write
方法。 -
您将观察到
write
方法接受List
作为输入。在write
方法中,我们只是解析列表并将列表索引值强制转换为Employee
对象并打印它。
package com.packt;
import java.util.List;
import org.springframework.batch.item.ItemWriter;
public class CustomItemWriter<T> implements ItemWriter<T> {
@Override
public void write(List<? extends T> items) throws Exception {
for (int i = 0; items.size() > i; i++) {
Employee obj = (Employee) items.get(i);
System.out.println(obj.getEmpId() + ":" + obj.getName());
}
}
}
- 创建一个带有
public static void main()
和jobrun()
方法的Main
类:
public class Main {
public static void main(String[] args) {
Main obj = new Main();
obj.run();
}
private void run() {
/*config files are present in the resource folder*/
String[] springConfig = { "spring/batch/config/context.xml", "spring/batch/jobs/job-read-files.xml" };
ApplicationContext context = new ClassPathXmlApplicationContext(springConfig);
JobLauncher jobLauncher = (JobLauncher) context.getBean("jobLauncher");
Job job = (Job) context.getBean("readMultiFileJob");
try {
JobExecution execution = jobLauncher.run(job, new JobParameters());
System.out.println("Exit Status : " + execution.getStatus());
System.out.println("Exit Status : " + execution.getAllFailureExceptions());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("COMPLETED");
}
}
/*config files are present in the resource folder*/
- 让我们在
context.xml
文件中将bean id
设置为JobRepository
:
<bean id="jobRepository" class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
<property name="transactionManager" ref="transactionManager" />
</bean>
<bean id="transactionManager" class="org.springframework.batch.support.transaction.ResourcelessTransactionManager" />
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository" />
</bean>
/*
Job Read files.xml
文件位于资源文件夹*/Job Read files.xml
中。
我们使用了flatfileItemReader
和FlatFileItemWriter
。这些类读取输入并在output
文件夹中重新创建文件。
让我们看一下FlatFileItemReader
的原型,并了解它在应用程序中的作用:
public class FlatFileItemReader<T> extends AbstractItemCountingItemStreamItemReader<T>
implements ResourceAwareItemReaderItemStream<T>, org.springframework.beans.factory.InitializingBean
可重新启动的ItemReader
从输入setResource(Resource)
中读取行。一行由setRecordSeparatorPolicy(RecordSeparatorPolicy)
定义,并使用setLineMapper(LineMapper)
映射到一个项目。
如果在行映射期间抛出异常,则将其作为FlatFileParseException
重新抛出,并添加有关有问题的行及其行号的信息。
public class FlatFileItemWriter<T>
extends AbstractItemStreamItemWriter<T>
implements ResourceAwareItemWriterItemStream<T>, org.springframework.beans.factory.InitializingBean
这个类是一个将数据写入文件或流的项目写入器。写入器还提供了重新启动。输出文件的位置由资源定义,并且必须表示可写文件,并使用缓冲写入器以提高性能。该实现不是线程安全的。
在文件中,我们做了以下事情:
-
我们已经配置了名为
readMultiFileJob
的作业 -
我们必须观察到
tasklet
有一个步骤,该步骤配置了ItemReader
和ItemWriter
类 -
我们再次使用了
tasklet
,但我们使用了步骤作为一个接受MultiResourceReader
的chunk
读取器
为了理解MultiResourceReader
,我们将看一下原型:
public class MultiResourceItemReader<T>extends AbstractItemStreamItemReader<T>
MultiResourceReader
从多个资源中顺序读取项目。资源列表由setResources(Resource[])
给出,实际读取委托给setDelegate(ResourceAwareItemReaderItemStream)
。输入资源使用setComparator(Comparator)
进行排序,以确保在重新启动场景中作业运行之间保留资源排序。
现在,让我们看看chunk
类型的步骤是什么。在一个chunk
中,读取器和写入器是必需的!但是,ItemProcessor
是可选的。
<import resource="../config/context.xml"/>
<bean id="employee" class="com.packt.Employee" />
<job id="readMultiFileJob" >
<step id="step1">
<tasklet>
<chunk reader="multiResourceReader" writer="flatFileItemWriter" commit-interval="1" />
</tasklet>
</step>
</job>
<! --create folder structure in the project root csv/inputsand add the csv files-->
<bean id="multiResourceReader"class=" org.springframework.batch.item.file.MultiResourceItemReader">
<property name="resources" value="file:csv/inputs/employee-*.csv" />
<property name="delegate" ref="flatFileItemReader" />
</bean>
<bean id="flatFileItemReader" class="org.springframework.batch.item.file.FlatFileItemReader">
<property name="lineMapper">
<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<property name="lineTokenizer">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
<property name="names" value="id, name" />
</bean>
</property>
<property name="fieldSetMapper">
<bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
<property name="prototypeBeanName" value="domain" />
</bean>
</property>
</bean>
</property>
</bean>
<bean id="flatFileItemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" >
<!--create folder structure in the project root csv/outputs -->
<property name="resource" value="file:csv/outputs/employee.all.csv" />
<property name="appendAllowed" value="true" />
<property name="lineAggregator">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
<property name="delimiter" value="," />
<property name="fieldExtractor">
<bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
<property name="names" value="id, domain" />
</bean>
</property>
</bean>
</property>
</bean>
创建几个名为employee*.csv
的 CSV 文件,用不同的数字替换*
。每个文件将有两个值:employeeId
和name
。
CSV 文件中的分隔符也可以在 XML 中进行配置,如下所示:
<bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
<property name="delimiter" value="," />
<property name="fieldExtractor">
<bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
<property name="names" value="id, domain" />
</bean>
</property>
这些值将与普通的 Java 对象(Pojo)Employee.java
进行映射,并且输出将被处理。文件位置作为输入传递给MultiResourceItemReader
类。
在下一节中,我们将看到如何在 Spring 中安排批处理作业。
使用 Spring 调度程序的 Spring Batch
在本节中,让我们看看如何在 Spring Batch 框架中安排批处理。我们将看到如何配置调度程序。这是一个示例的jobproduct.xml
文件,需要在类路径中可用。如果您正在使用 Maven 项目,请将其放在资源文件夹中。您需要使用间隔和方法名run()
来注入joblauncher
以在预定时间运行作业。
要使用调度程序,我们需要配置job-product.xml
文件。该文件也用于在下一节中配置外部调度程序的调度程序详细信息。
安排每 600 秒间隔运行任务:
<task:scheduled-tasks>
<task:scheduled ref="MyJobScheduler" method="run" cron="*/600 * * * * *" />
</task:scheduled-tasks>
让我们在MyJobScheduler.class
中使用@Component
和@Autowired
注解。
@Component
public class MyJobScheduler {
@Autowired
private JobLauncher jobLauncher;
@Autowired
private Job job;
public void run() {
try {
String dateParam = new Date().toString();
JobParameters param = new JobParametersBuilder().addString("date", dateParam).toJobParameters();
JobExecution execution = jobLauncher.run(job, param);
System.out.println("Exit Status of the Job: " + execution.getStatus());
} catch (Exception e) {
e.printStackTrace();
}
}
}
使用 Quartz 调度程序配置 Spring Batch
Spring Batch 框架提供了将外部调度程序配置到应用程序中的选项。
让我们将 Quartz 调度程序集成到 Spring Batch 应用程序中。Quartz 是一个开源的基于 Java 的调度程序。我们将使该应用程序读取一个文件,但我们将集成 Quartz 调度程序来进行调度。
-
创建一个名为
SpringBatchQuartzExample
的简单 Maven 应用程序。 -
使用与之前应用程序相同的
pom.xml
文件。 -
在
pom.xml
文件的依赖项中添加 Quartz JAR 文件。 -
添加这些属性:
<quartz.version>1.8.6</quartz.version>
- 然后,添加这些依赖项:
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
</dependency>
让我们创建一个名为quartz-job.xml
的文件。这应该存在于 Maven 项目的资源文件夹中。要配置批处理每分钟运行一次,使用以下代码中的配置:
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="jobDetail" />
<property name="cronExpression" value="*/60 * * * * ?" />
</bean>
</property>
</bean>
要将 Spring Batch 与 Quartz 调度程序集成,使用以下代码:
<bean id="jobDetailBean" class="org.springframework.scheduling.quartz.JobDetailBean">
<property name=" jobQuartzLauncherDetails " value="com.packt.quartz.JobQuartzLauncherDetails" />
<property name="group" value="quartz-batch" />
<property name="jobDataAsMap">
<map>
<entry key="jobName" value="reportJob" />
<entry key="jobLocator" value-ref="jobRegistry" />
<entry key="jobLauncher" value-ref="jobLauncher" />
<entry key="param1" value="anjana" />
<entry key="param2" value="raghu" />
</map>
</property>
</bean>
JobQuartzLauncherDetails
是一个扩展QuartzJobBean
的 bean。
提示
QuartzJobBean
位于org.springframework.scheduling.quartz.QuartzJobBean
包中。
该类具有JobLauncher
和JobLocator
的 setter:
public class JobQuartzLauncherDetails extends QuartzJobBean {
static final String JOB_NAME = "jobName";
private JobLocator jobLocator;
private JobLauncher jobLauncher;
public void setJobLocator(JobLocator jobLocator) {
this.jobLocator = jobLocator;
}
public void setJobLauncher(JobLauncher jobLauncher) {
this.jobLauncher = jobLauncher;
}
为了从配置中读取JobMapDetails
,我们创建了另一个方法,如下所示。我们可以看到,基于从地图中读取的值,这里处理了不同的数据类型,并创建了JobParametersBuilder
。
private JobParameters getJobParametersFromJobMap(Map<String, Object> jobDataMap) {
JobParametersBuilder builder = new JobParametersBuilder();
for (Entry<String, Object> entry : jobDataMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof String && !key.equals(JOB_NAME)) {
builder.addString(key, (String) value);
} else if (value instanceof Float || value instanceof Double){
builder.addDouble(key, ((Number) value).doubleValue());
} else if (value instanceof Integer || value instanceof Long){
builder.addLong(key, ((Number) value).longValue());
} else if (value instanceof Date) {
builder.addDate(key, (Date) value);
} else {
}
}
builder.addDate("run date", new Date());
return builder.toJobParameters();
}
正如我们所知,JobName
和JobParamters
是JobLauncher
运行作业所需的输入。在前面的代码片段中,我们已经得到了JobParameters
。接下来,我们将使用以下代码片段使用JobExecutionContext
获取JobName
:
protected void executeInternal(JobExecutionContext context) {
Map<String, Object> jobDataMap = context.getMergedJobDataMap();
String jobName = (String) jobDataMap.get(JOB_NAME);
JobParameters jobParameters = getJobParametersFromJobMap(jobDataMap);
try {
jobLauncher.run(jobLocator.getJob(jobName), jobParameters);
} catch (JobExecutionException e) {
e.printStackTrace();
}
}
Product.java
是一个领域类,将其映射到.csv
文件中的值。
public class Product {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
name = name;
}
@Override
public String toString() {
return "Product [id=" + id + ", name=" + name + "]";
}
}
CustomeItemWriter
的代码如下,用于写入产品 Pojo 对象的值。
public class CustomItemWriter implements ItemWriter<Product> {
@Override
public void write(List<? extends Product> items) throws Exception {
System.out.println("writer..." + items.size());
for(Product item : items){
System.out.println(item);
}
}
}
接下来,让我们创建Main
类来加载job-quartz.xml
文件,并且每 60 秒运行一次批处理作业,以使用CustomItemWriter
读取 CSV 文件并写入。
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
String springConfig = "spring/batch/jobs/job-quartz.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(springConfig);
}
}
Spring Batch 框架使用 Quartz 调度程序来运行批处理作业,读取文件,将 CSV 值映射到产品 Pojo,并使用CustomeItemWriter
进行写入。
在下一节中,让我们创建一个批处理作业,读取一个文件并更新数据库。
使用 Spring Batch 读取文件并更新 MongoDB 数据库
在这一部分,让我们创建一个批处理作业,读取一个 XML 文件并将其写入 MongoDB 数据库。想象一种情况,我们不断从一个来源获取一个 XML 文件,并且需要将该文件读取并更新到数据库中。
- XML 文件结构如下所示:
<?xml version="1.0" encoding="UTF-8" ?>
<Products>
<product id="1">
<categoryId>3</categoryId>
<brandId>1</brandId>
<name>computer</name>
<price>15000</price>
</product>
<product id="2">
<categoryId>4</categoryId>
<brandId>1</brandId>
<name>mouse</name>
<price>250</price>
</record>
</ product>
< product id="3">
<categoryId>5</categoryId>
<brandId>1</brandId>
<name>mouse</name>
<price>23000</price>
</ product>
</Products>
- 创建一个基于 Maven 的 Java 项目。在
com.packt.model
包中,添加相应的产品 Pojo。
public class Product {
private int id;
private int categoryId;
private int brandId;
private String name;
private int price;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getCategoryId() {
return categoryId;
}
public void setCategoryId(int categoryId) {
this.categoryId = categoryId;
}
public int getBrandId() {
return brandId;
}
public void setBrandId(int brandId) {
this.brandId = brandId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
-
添加与上一节中显示的相同的依赖项。
-
更新
pom.xml
文件。 -
添加 ORM 和 MongoDB 数据库依赖项:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>${mongodb.driver.version}</version>
</dependency>
<!-- Spring data mongodb -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>${spring.data.version}</version>
</dependency>
- 创建一个名为
mongodatabase.xml
的文件,并向其中添加以下配置:
<mongo:mongo host="127.0.0.1" port="27017" />
<mongo:db-factory dbname="eshopdb" />
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
</bean>
- 将以下配置添加到
job-product.xml
文件中。
-
StaxEventItemReader
:这是一个读取products.xml
文件的类。我们需要为这个类提供rootElemenent
名称。 -
fragmentRootElementName
:此属性接受提供的 XML 文件中的根元素的字符串参数。
我们还需要将 XML 文件名作为值提供给资源属性。需要传递的第三个属性是unmarshaller
引用。这个类在 Spring OXM 框架中可用于对 XML 文件进行编组和取消编组。
<bean id="xmlItemReader" class="org.springframework.batch.item.xml.StaxEventItemReader">
<property name="fragmentRootElementName" value="product" />
<property name="resource" value="classpath:xml/product.xml" />
<property name="unmarshaller" ref="productUnmarshaller" />
</bean>
XstreamMarshaller
接受三个属性来执行取消编组过程。它接受一个带有条目键和产品 Pojo 作为值的映射,以便在 XML 中,每个产品记录都被转换为Product
对象并存储在映射中。第二个属性再次是一个创建的 bean,用于将 XML 转换为 POJO。这个名字叫ProductXMLConverter
。
<bean id="productUnmarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
<property name="aliases">
<util:map id="aliases">
<entry key="product" value="com.packt.model.Product" />
</util:map>
</property>
<property name="converters">
<array>
<ref bean="productXMLConverter" />
</array>
</property>
</bean>
<bean id="productXMLConverter" class="com.packt.converter. ProductXMLConverter>
让我们看看ProductXMLConverter
类。这个类实现了converter
接口,该接口位于com.thoughtworks.xstream.converters.converter
包中。该类覆盖了接口中定义的三个方法:
-
public boolean canConvert(Class type)
-
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context)
-
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context)
- 由于我们将在这里执行取消编组,因此我们将清楚地实现
unmarshall
方法。
@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
Product obj = new Product();
obj.setId(Integer.valueOf(reader.getAttribute("id")));
reader.moveDown(); //get id
obj.setCategoryId(Integer.valueOf(reader.getAttribute("categoryId")));
reader.moveDown(); //get categoryId
obj.setBrandId(Integer.valueOf(reader.getAttribute("brandId")));
reader.moveDown(); //get brandId
obj.setName(String.valueOf(reader.getAttribute("name")));
reader.moveDown(); //get name
obj.setPrice(Integer.valueOf(reader.getAttribute("price")));
reader.moveDown(); //get name
return obj;
}
- 在
job-product.xml
中配置MongoDBItemWriter
以将 Pojo 对象写入 MongoDB 数据库:
<bean id="mongodbItemWriter" class="org.springframework.batch.item.data.MongoItemWriter">
<property name="template" ref="mongoTemplate" />
<property name="collection" value="product" />
</bean>
- 在
job-product.xml
文件中配置批处理作业:
<batch:job id="productJob">
<batch:step id="step1">
<batch:tasklet>
<batch:chunk reader="xmlItemReader" writer="mongodbItemWriter" commit-interval="1">
</batch:chunk>
</batch:tasklet>
</batch:step>
</batch:job>
-
编写
Main
类来运行批处理作业。 -
在
Main
类中加载所有配置文件:
public class Main {
public static void main(String[] args) {
String[] springConfig = {"spring/batch/config/mongodatabase.xml", "spring/batch/config/context.xml", "spring/batch/jobs/job-product.xml"
};
ApplicationContext context = new ClassPathXmlApplicationContext(springConfig);
JobLauncher jobLauncher = (JobLauncher) context.getBean("jobLauncher");
Job job = (Job) context.getBean("productJob");
try {
JobExecution execution = jobLauncher.run(job, new JobParameters());
System.out.println("Exit Status of the ProductJOB: " + execution.getStatus());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("YES COMPLETED");
}
}
因此,当我们运行Main
类时,作业将被实例化,并且每 60 秒运行一次。作业将读取 XML 并将其转换为 Pojo product.java
,然后将其插入到 MongoDB 数据库中。配置在 MongoDB 数据库 XML 文件中给出。
在下一节中,我们将看到如何创建一个多线程环境来处理多个作业。
使用 Spring Batch 和线程来分区作业
在 Spring 批处理过程中,一个线程按顺序处理请求。如果我们想要并行执行批处理作业,我们可以选择多线程环境。
想象一种情景,我们正在处理与Employee
Pojo 映射的员工表中的 1000 条记录。我们需要一次读取 1000 条记录并写入 CSV 文件。
作业实际上被分成多个子作业,并且分配了一个单独的线程来处理每个子作业。因此,如果您有 1000 条记录需要读取,使用单个线程会花费更多时间。当我们将 1000 条记录分成 100 个子记录时,我们可以使用同时运行的 10 个不同线程来处理它们。
我们可以通过实现Partitioner
接口来创建一个简单的分区器类。这个分区器将 1000 个作业分成 100 个子作业。您将观察到我们在分区范围中提供了start_range
和end_range
变量。
public class MyJobPartioner implements Partitioner {
@Override
public Map<String, ExecutionContext> partition(int gridSize) {
Map<String, ExecutionContext> result = new HashMap<String, ExecutionContext>();
int range = 100;
int start_range = 1;
int end_range = range;
for (int i = 1; i <= gridSize; i++) {
ExecutionContext execution_context = new ExecutionContext();
System.out.println("\Name: Thread" + i+"start_range : " + start_range+"end_range", end_range);
execution_context.putInt("start_range", start_range);
execution_context.putInt("end_range", end_range);
execution_context.putString("name", "Thread" + i);
result.put("partition" + i, execution_context);
start_range = end_range + 1;
end_range += range;
}
return result;
}
}
在Partitioner
类中使用的ExecutionContext
对象与ItemStream
一起工作,并充当映射的包装器。在 Spring Batch 中,我们可以获得两种类型的执行上下文对象。一个执行对象在作业级别工作,另一个在步骤级别工作。作业级别执行上下文用于在步骤之间共享数据或信息。
让我们实现一个处理分区记录的ItemProcess
类。还要注意,我们在下面的代码中使用了步骤执行上下文。该类覆盖了process
方法。
- 这个类用于分块处理数据。
@Component("itemProcessor")
@Scope(value = "step")
public class EmployeeProcessor implements ItemProcessor<Employee, Employee> {
@Value("#{stepExecutionContext[name]}")
private String threadName;
@Override
public Employee process(Employee emp) throws Exception {
System.out.println(threadName + " processing : " + emp.getId() + " : " + emp.getName());
return emp;
}
public String getThreadName() {
return threadName;
}
public void setThreadName(String threadName) {
this.threadName = threadName;
}
}
- 让我们配置
job-partioner.xml
文件。
<job id="partitionJob" >
<step id="masterStep">
<partition step="slave" partitioner="myJobPartioner">
<handler grid-size="100" task-executor="taskExecutor" />
</partition>
</step>
</job>
<step id="slave" >
<tasklet>
<chunk reader="pagingItemReader" writer="flatFileItemWriter"
processor="itemProcessor" commit-interval="1" />
</tasklet>
</step>
<!—below is the configuration of MyJobPartioner bean-->
<bean id="myJobPartioner" class="com.packt.partition.MyJobPartioner" />
<bean id="taskExecutor" class="org.springframework.core.task.SimpleAsyncTaskExecutor" />
<!—below is the configuration of EmployeeProcesser bean-->
<bean id="itemProcessor" class="com.packt.processor.EmployeeProcessor" scope="step">
<property name="threadName" value="#{stepExecutionContext[name]}" />
</bean>
接下来,让我们配置pagingItemReader
,它的作用与分页相同。它每页获取 100 条记录;它还使用提供的 JDBC 信息连接到数据源,并执行查询以获取指定范围的记录。它还将根据emp_id
列对数据进行排序。
<bean id="pagingItemReader" class="org.springframework.batch.item.database.JdbcPagingItemReader"scope="step">
<property name="dataSource" ref="dataSource" />
<property name="queryProvider">
<bean class="org.springframework.batch.item.database.support.SqlPagingQueryProviderFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="selectClause" value="select emp_id, emp_name, emp_pass, emp_salary" />
<property name="fromClause" value="from users" />
<property name="whereClause" value="where emp_id >= :fromId and id <= :toId" />
<property name="sortKey" value="emp_id" />
</bean>
</property>
<!-- Inject via the ExecutionContext in MyJobPartioner -->
<property name="parameterValues">
<map>
<entry key="fromId" value="#{stepExecutionContext[start_range]}" />
<entry key="toId" value="#{stepExecutionContext[end_range]}" />
</map>
</property>
<property name="pageSize" value="100" />
<property name="rowMapper">
<bean class="com.packt.EmployeeRowMapper" />
</property>
</bean>
<!--After reading it writes to csv file using FlatfileItemwriter class-->
<bean id="flatFileItemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step" >
<property name="resource"
value="file:csv/outputs/employee.processed#{stepExecutionContext[fromId]}-#{stepExecutionContext[toId]}.csv" />
<property name="appendAllowed" value="false" />
<property name="lineAggregator">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
<property name="delimiter" value="," />
<property name="fieldExtractor">
<bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
<property name="names" value="emp_id, emp_name, emp_pass, emp_salary" />
</bean>
</property>
</bean>
</property>
</bean>
<!--Configuring FlatfileItemwriter class- ends-->
- 让我们编写
Main
类,它将加载配置文件,然后运行作业。
public class Main {
public static void main(String[] args) {
Main obj = new Main();
obj.run();
}
private void run() {
String[] springConfig = { "spring/batch/jobs/job-partitioner.xml" };
ApplicationContext context = new ClassPathXmlApplicationContext(springConfig);
JobLauncher jobLauncher = (JobLauncher) context.getBean("jobLauncher");
Job job = (Job) context.getBean("partitionJob");
try {
JobExecution execution = jobLauncher.run(job, new JobParameters());
System.out.println("Exit Status : " + execution.getStatus());
System.out.println("Exit Status : " + execution.getAllFailureExceptions());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("COMPLETED");
}
}
因此,通过前面的配置和类,将创建多个线程来处理每个线程的 100 条记录。记录从数据库中读取并写入 CSV 文件。
在下一节中,我们将使用 Spring Batch 的事件监听器。
使用监听器拦截 Spring Batch 作业
Spring Batch 带有监听器。它们拦截作业执行以执行某些任务。StepListener
是以下提到的监听器的super
类:
-
SkipListener
:SkipListener
最常见的用例之一是记录跳过的项目,以便可以使用另一个批处理过程或甚至人工过程来评估和修复导致跳过的问题。因为有许多情况下原始事务可能被回滚,Spring Batch 提供了两个保证: -
适当的
skip
方法(取决于错误发生的时间)每个项目只会被调用一次。 -
SkipListener
将在事务提交之前始终被调用。这是为了确保监听器调用的任何事务资源不会因ItemWriter
内部的失败而被回滚。 -
ChunkListener
:这些监听器可以配置一个步骤,如果步骤是分块式步骤类型,它将同时具有ItemReader
和ItemWriter
。当ItemReader
完成其读取任务时,监听器将通知ItemWriter
。
public interface ChunkListener extends StepListener {
void beforeChunk();
void afterChunk();
}
<step id="step1">
<tasklet>
<chunk reader="reader" writer="writer" commit-interval="10"/>
<listeners>
<listener ref="chunkListener"/>
</listeners>
</tasklet>
</step>
-
ItemWriterListener
-
ItemReaderListener
-
ItemProcessListener
-
StepExecutionListener
:它代表步骤执行的最通用的监听器。它允许在步骤开始之前和结束之后通知,无论它是正常结束还是失败结束。
您将注意到为ItemReader
、ItemWriter
、ItemProcess
和StepExecution
接口和类配置了监听器。
现在我们可以看看如何在 spring batch.xml
文件中配置监听器。请看:
- 创建实现监听器并覆盖其方法的类。
<bean id="packtStepListener" class="com.packt.listeners.PacktStepListener" />
<bean id="packtItemReaderListener" class="com.packt.listeners.PacktItemReaderListener" />
<bean id="packtItemWriterListener" class="com.packt.listeners.PacktItemWriterListener" />
<job id="readFileJob" >
<step id="step1">
<tasklet>
<chunk reader="multiResourceReader" writer="flatFileItemWriter" commit-interval="1" />
<listeners>
<listener ref="packtStepListener" />
<listener ref="packtItemReaderListener" />
<listener ref="packtItemWriterListener" />
</listeners>
</tasklet>
</step>
</job>
- 让我们看看
PacktItemReaderListener
和PacktItemWriterListner
监听器。IteamReadListener
接口带有三个要实现的方法:
-
beforeRead()
-
afterRead()
-
onReadError()
public class PacktItemReaderListener implements ItemReadListener<Product> {
@Override
public void beforeRead() {
System.out.println("ItemReadListener - beforeRead");
}
@Override
public void afterRead(Product product) {
System.out.println("ItemReadListener - afterRead");
}
@Override
public void onReadError(Exception ex) {
System.out.println("ItemReadListener - onReadError");
}
}
- 接下来让我们看看
PackItemWriterListener
。ItemWriter
接口带有三个abstract
方法:
-
beforeWrite
-
afterWrite
-
onWriteError
public class PacktItemWriterListener implements ItemWriteListener<Product> {
@Override
public void beforeWrite(List<? extends Product> products) {
System.out.println("ItemWriteListener - beforeWrite");
}
@Override
public void afterWrite(List<? extends Product> products) {
System.out.println("ItemWriteListener - afterWrite");
}
@Override
public void onWriteError(Exception exception, List<? extends Product> products) {
System.out.println("ItemWriteListener - onWriteError");
}
}
到目前为止,我们已经看到了如何在spring-job
文件中创建自定义监听器和监听器配置。
现在,让我们尝试将其与读取目录中的多个文件并删除文件的情景集成。
- 我们将再次考虑产品 Pojo,带有
id
和name
作为实例变量,并带有 getter 和 setter。
public class Product {
int id;
String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String Name) {
this.name = name;
}
}
- 我们需要在 XML 中将 Pojo 定义为一个 bean。
<bean id="product" class="com.packt.Product" />
- 接下来是文件删除任务类文件。在读取文件后,需要从目录中删除它们。
<bean id="fileDeletingTasklet" class="com.packt.tasklet.FileDeletingTasklet" >
<property name="directory" value="file:csv/inputs/" />
</bean>
- 让我们来看一下
FileDeletingTasklet
类。这个类实现了TaskLet
接口。这将根据指定的目录删除文件。
public class FileDeletingTasklet implements Tasklet, InitializingBean {
private Resource directory;
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(directory, "directory must be set");
}
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
File dir = directory.getFile();
Assert.state(dir.isDirectory());
File[] files = dir.listFiles();
for (int i = 0; i < files.length; i++) {
boolean deleted = files[i].delete();
if (!deleted) {
throw new UnexpectedJobExecutionException("Could not delete file " + files[i].getPath());
} else {
System.out.println(files[i].getPath() + " is deleted!");
}
}
return RepeatStatus.FINISHED;
}
public Resource getDirectory() {
return directory;
}
public void setDirectory(Resource directory) {
this.directory = directory;
}
}
- 需要在创建的作业配置文件中设置 bean 属性。
<bean id="fileDeletingTasklet" class="com.packt.tasklet.FileDeletingTasklet" >
<property name="directory" value="file:csv/inputs/" />
</bean>
下一个任务将是读取目录中可用的多个文件。由于有多个需要读取的资源,我们将在 bean 中使用MultiResourceReader
配置。
<bean id="multiResourceReader" class=" org.springframework.batch.item.file.MultiResourceItemReader">
<property name="resources" value="file:csv/inputs/product-*.csv" />
<property name="delegate" ref="flatFileItemReader" />
</bean>
flatfileItemReader
将 CSV 值映射到产品 Pojo。因此,请在jobs.xml
文件中提供以下配置:
<bean id="flatFileItemReader" class="org.springframework.batch.item.file.FlatFileItemReader">
<property name="lineMapper">
<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<property name="lineTokenizer">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
<property name="names" value="id, name" />
</bean>
</property>
<property name="fieldSetMapper">
<bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
<property name="prototypeBeanName" value="product" />
</bean>
</property>
</bean>
</property>
</bean>
然后,在读取 CSV 值并将它们从不同的 CSV 文件映射到 Pojo 之后,如果需要合并到单个 CSV 文件,我们可以添加writterListener
。
<bean id="flatFileItemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
<property name="resource" value="file:csv/outputs/product.all.csv" />
<property name="appendAllowed" value="true" />
<property name="lineAggregator">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
<property name="delimiter" value="," />
<property name="fieldExtractor">
<bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
<property name="names" value="id, name" />
</bean>
</property>
</bean>
</property>
</bean>
运行 Main
类时,XML 文件中配置的所有 bean 都会被实例化,以便批处理作业运行。作业在这里的 Main
类的配置中执行了块执行,使用了 ItemReader
和 Writer
。
public class Main {
public static void main(String[] args) {
Main obj = new Main();
obj.run();
}
private void run() {
String[] springConfig = { "spring/batch/jobs/job-read-files.xml" };
ApplicationContext context = new ClassPathXmlApplicationContext(springConfig);
JobLauncher jobLauncher = (JobLauncher) context.getBean("jobLauncher");
Job job = (Job) context.getBean("readMultiFileJob");
try {
JobExecution execution = jobLauncher.run(job, new JobParameters());
System.out.println("Exit Status : " + execution.getStatus());
System.out.println("Exit Status : " + execution.getAllFailureExceptions());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("COMPLTED CHECK THE OUTPUT DIRECTORY");
}
}
在本节中,我们学习了有关监听器的知识,并配置了监听器与作业。
在下一节中,我们将看到如何对 Spring Batch 应用程序进行一些单元测试。
Spring Batch 应用程序的单元测试
让我们演示为 Spring Batch 应用程序编写测试用例:
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<!-- Junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
让我们创建一个名为 Test
类的简单的 Test
类,称为 mport static org.junit.Assert.assertEquals
:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:spring/batch/jobs/job-report.xml",
"classpath:spring/batch/config/context.xml",
"classpath:spring/batch/config/database.xml",
"classpath:spring/batch/config/test-context.xml"})
public class MainTest {
@Autowired
private JobLauncherTestUtils jobLauncherTestUtils;
@Test
public void launchJob() throws Exception {
JobExecution jobExecution = jobLauncherTestUtils.launchStep("step1");
assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
}
}
我们必须创建一个名为 text-context.xml
的文件,以便在批处理中可用,并配置 JobLauncher
以便在 XML 文件和测试包中可用。在 Test
类中,使用 @Test 注释
方法并调用 JobLauncher
执行一个步骤。我们需要使用 assertEquals
来检查批处理作业的状态与 jobExecution
状态是否一致。
总结
在本章中,我们学习了如何创建基于 Spring 的批处理应用程序来读取 CSV 文件。我们还阐明了 Spring Batch 如何用于读取 XML 文件。最高级的主题是将作业分区并将作业运行到单独的线程中。我们还将 Spring Batch 与 Quartz Scheduler 集成。
我们已经演示了使用 Spring Batch 编写简单测试用例。我们还使用监听器拦截了一个定义的作业来执行某些操作,并演示了某些配置。
第五章:Spring 与 FTP 的集成
FTP 涉及使用文件传输协议从一台计算机通过互联网发送文件到另一台计算机。Spring 集成还提供了对文件传输协议的支持。可以通过 FTP 或使用 SFTP(安全 FTP)进行文件传输。
以下是 FTP 场景中使用的一些缩写:
-
FTP:文件传输协议。
-
FTPS:FTP 安全是 FTP 的扩展,它添加了对传输层安全(TLS)和安全套接字层(SSL)加密协议的支持。
-
SFTP:SSH 文件传输协议,即 FTP 通过安全外壳协议。
在实际场景中,文件服务器将具有 FTP 地址、用户名和密码。客户端连接到服务器以传输文件。我们可以使用 FTP 上传文件到远程位置或从远程位置下载文件。
Spring 的集成包支持从 FTP 或 FTPS 服务器发送和接收文件。它提供了一些端点,以下是 Spring 为 FTP/FTPS 提供的端点/适配器:
-
入站通道适配器
-
出站通道适配器
-
出站网关
通道适配器只是消息端点,实际上将消息连接到消息通道。在处理通道适配器时,我们可以明显看到、发送和接收配置和方法。
在本章中,我们将看到 Spring 如何使我们能够使用 FTP,并开发一个演示 Spring 集成能力支持文件传输的示例应用程序。我们还将看到需要编写的配置以及如何使用入站和出站适配器来使用 Spring 集成包在 FTP 上传输文件。
Maven 依赖项
为了使用 Spring 集成框架创建 FTP 应用程序,在 Maven 项目的pom.xml
文件中添加以下依赖项。主要包括 Spring 集成测试和 Spring 集成 FTP。这些库可以从 Maven 仓库下载,也可以添加到项目的pom.xml
文件中。
以下是需要添加到pom.xml
文件中的 Maven 依赖项,以开始使用 Spring Integration FTP 包开发应用程序:
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-ftp</artifactId>
<version>4.0.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-test</artifactId>
<version>4.0.0.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.ftpserver</groupId>
<artifactId>ftpserver-core</artifactId>
<version>1.0.6</version>
<scope>compile</scope>
</dependency>
Spring 的 FTP 的 XSD
让我们看看 Spring 集成包为 FTP 提供的 XSD。这包含了所有模式定义,并提供了 Spring 支持的所有配置可能性,因此配置 XML 文件变得更容易。
XSD(www.springframework.org/schema/integration/ftp/spring-integration-ftp.xsd
)提供了关于 Spring 与 FTP 集成的大量信息。它为我们提供了有关在 XML 配置文件中配置通道适配器的信息。
入站和出站通道适配器是 XSD 中的两个主要元素。以下是从我们刚提到的链接中提取的 XSD 的摘录:
<xsd:element name="outbound-channel-adapter">...</xsd:element>
<xsd:element name="inbound-channel-adapter">...</xsd:element>
<xsd:complexType name="base-ftp-adapter-type">...</xsd:complexType>
</xsd:schema>
在接下来的章节中,我们将看到如何配置入站和出站通道适配器以及 Spring 集成支持的 FTP 的配置选项。
为 FTP 配置出站通道适配器
出站通道适配器配置是针对远程目录的。它旨在执行诸如将文件写入远程服务器(文件上传)、创建新文件或在远程 FTP 服务器上添加后缀等操作。以下是 XSD 中提供的出站通道适配器的一些可用配置:
- 它支持使用正则表达式配置远程目录以写入文件。使用的属性如下:
<xsd:attribute name="remote-directory-expression"type="xsd:string">
- 我们还可以配置自动在远程位置创建目录:
<xsd:attribute name="auto-create-directory" type="xsd:string" default="false">
- 我们还可以配置 Spring 集成框架以与 FTP 一起工作,临时为文件添加后缀:
<xsd:attribute name="temporary-file-suffix" type="xsd:string">
- 另一个重要的配置是在 FTP 服务器的远程位置生成文件名:
<xsd:attribute name="remote-filename-generator" type="xsd:string">
- 前面的功能再次升级以支持正则表达式:
<xsd:attribute name="remote-filename-generator-expression" type="xsd:string">
配置 FTP 的入站通道适配器
入站通道适配器配置是针对本地目录的,即旨在执行从远程服务器写入文件(文件下载)、创建新文件或在本地目录上添加后缀等操作。入站通道适配器确保本地目录与远程 FTP 目录同步。
从 XSD 中可用的入站通道适配器的一些配置如下:
- 它提供了配置选项,以自动创建本地目录(如果不存在):
<xsd:attribute name="auto-create-local-directory" type="xsd:string">
<xsd:annotation>
<xsd:documentation>Tells this adapter if local directory must be auto-created if it doesn't exist. Default is TRUE.</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
- 它提供了配置远程服务器的选项,并在将其复制到本地目录后删除远程源文件:
<xsd:attribute name="delete-remote-files" type="xsd:string">
<xsd:annotation>
<xsd:documentation>Specify whether to delete the remote source file after copying. By default, the remote files will NOT be deleted.</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
- 使用可用的比较器配置对文件进行排序:
<xsd:attribute name="comparator" type="xsd:string">
<xsd:annotation>
指定在排序文件时要使用的比较器。如果没有提供,则顺序将由java.io
文件实现确定:
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
- 使用以下属性配置会话缓存:
<xsd:attribute name="cache-sessions" type="xsd:string" default="true">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
指定会话是否应该被缓存。默认值为true
。
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
- 可以使用 XSD 引用进行的配置如下:
<int-ftp:inbound-channel-adapter id="ftpInbound"
channel="ftpChannel"
session-factory="ftpSessionFactory"
charset="UTF-8"
auto-create-local-directory="true"
delete-remote-files="true"
filename-pattern="*.txt"
remote-directory="some/remote/path"
local-directory=".">
<int:poller fixed-rate="1000"/>
</int-ftp:inbound-channel-adapter>
FTPSessionFactory 和 FTPSSessionFactory
在本节中,让我们看一下使用 Spring 集成的 FTP 的两个核心类FTPSessionFactory
和FTPSSessionFactory
。这些类有很多的 getter、setter 和实例变量,提供有关数据、文件和 FTP 模式的信息。实例变量及其用法如下所述:
类org.springframework.integration.ftp.session.DefaultFtpSessionFactory
用于在应用程序中配置 FTP 详细信息。该类在配置 XML 文件中配置为一个简单的 bean。该类有以下的 getter 和 setter:
-
Session
:这接受会话变量。 -
postProcessClientAfterConnect
:这在执行客户端连接操作后处理额外的初始化。 -
postProcessClientBeforeConnect
:这在执行客户端连接操作之前处理额外的初始化。 -
BufferSize
:这定义了通过 FTP 传输的缓冲数据的大小。 -
ClientMode
:FTP 支持两种模式。它们如下: -
主动 FTP 模式:在 Spring FTP 集成包中指定为
ACTIVE_LOCAL_DATA_CONNECTION_MODE
。在主动 FTP 模式下,服务器必须确保随机端口1023
<通信通道是打开的。在主动 FTP 模式下,客户端从一个随机的非特权端口(N > 1023
)连接到 FTP 服务器的命令端口,端口21
。然后,客户端开始监听端口N + 1
并向 FTP 服务器发送 FTP 命令PORT N + 1
。然后服务器将从其本地数据端口(端口20
)连接回客户端指定的数据端口。 -
被动 FTP 模式:在 Spring FTP 集成包中指定为
PASSIVE_LOCAL_DATA_CONNECTION_MODE
。在被动 FTP 模式下,客户端同时启动到服务器的两个连接,解决了防火墙过滤来自服务器的传入数据端口连接到客户端的问题。在打开 FTP 连接时,客户端在本地打开两个随机的非特权端口(N > 1023
和N + 1
)。第一个端口在端口21
上联系服务器,但是不是然后发出PORT
命令并允许服务器连接回其数据端口,而是客户端将发出PASV
命令。其结果是服务器随后打开一个随机的非特权端口(P > 1023
)并在响应PASV
命令中将P
发送回客户端。然后客户端从端口N + 1
到服务器上的端口P
发起连接以传输数据。包DefaultFTPClientFactory
具有一个设置器方法,其中有一个开关用于设置模式。
**
* Sets the mode of the connection. Only local modes are supported.
*/
private void setClientMode(FTPClient client) {
switch (clientMode ) {
case FTPClient.ACTIVE_LOCAL_DATA_CONNECTION_MODE:
client.enterLocalActiveMode();
break;
case FTPClient.PASSIVE_LOCAL_DATA_CONNECTION_MODE:
client.enterLocalPassiveMode();
break;
default:
break;
}
}
-
Config
:这设置 FTP 配置对象org.apache.commons.net.ftp.FTPClientConfig config
-
ConnectTimeout
:这指定了尝试连接到客户端后的连接超时时间。 -
ControlEncoding
:这设置了编码。 -
Data Timeout
:这设置了文件传输期间的数据超时时间。 -
Default Timeout
:这设置了套接字超时时间。 -
文件类型
:FTP 协议支持多种文件类型。它们列举如下: -
ASCII 文件类型(默认):文本文件以网络虚拟终端(NVT)ASCII 格式通过数据连接传输。这要求发送方将本地文本文件转换为 NVT ASCII,接收方将 NVT ASCII 转换为本地文本文件类型。每行的结尾使用 NVT ASCII 表示回车后跟换行。这意味着接收方必须扫描每个字节,寻找 CR,LF 对。(我们在第 15.2 节中看到了 TFTP 的 ASCII 文件传输中的相同情景。)
-
EBCDIC 文件类型:在两端都是扩展二进制编码十进制交换码(EBCDIC)系统时,传输文本文件的另一种方式。
-
图像文件类型:也称为二进制文件类型。数据以连续的位流发送,通常用于传输二进制文件。
-
本地文件类型:这是在不同字节大小的主机之间传输二进制文件的一种方式。发送方指定每字节的位数。对于使用 8 位的系统,具有 8 字节大小的本地文件类型等同于图像文件类型。我们应该知道 8 位等于 1 字节。
Spring 有抽象类AbstractFtpSessionFactory<T extends org.apache.commons.net.ftp.FTPClient>
,其中定义了以下参数和静态变量,可以在 FTP 的配置中使用:
public static final int ASCII_FILE_TYPE = 0;
public static final int EBCDIC_FILE_TYPE = 1;
public static final int BINARY_FILE_TYPE = 2;
public static final int LOCAL_FILE_TYPE = 3;
-
Host
:指定 FTP 主机。 -
Password
:指定 FTP 密码。 -
Port
:指定 FTP 端口。有两个可用的端口,一个是数据端口,一个是命令端口。数据端口配置为 20,命令端口配置为 21。 -
Username
:指定 FTP 用户名。
以下配置显示了DefaultFtpSessionFactory
类作为一个 bean,其 bean ID 为ftpClientFactory
,并且其属性值根据 FTP 服务器凭据进行设置:
<bean id="ftpClientFactory" class="org.springframework.integration.ftp.session.DefaultFtpSessionFactory">
<property name="host" value="localhost"/>
<property name="port" value="22"/>
<property name="username" value="anjana"/>
<property name="password" value="raghu"/>
<property name="clientMode" value="0"/>
<property name="fileType" value="1"/>
</bean>
org.springframework.integration.ftp.session.DefaultFtpsSessionFactory
类使我们能够使用 FTPS 连接。该类包含以下内容的 getter 和 setter:
-
BufferSize
-
clientMode
-
config
-
ControlEncoding
-
DEFAULT_REMOTE_WORKING_DIRECTORY
-
fileType
-
host
-
password
-
port
-
username
上述字段是从名为AbstarctFtpSessionFactory
的抽象类继承的。
以下是DefaultFtpsClientFactory
的示例 bean 配置及其可以在 XML 文件中配置的属性:
<bean id="ftpClientFactory" class="org.springframework.integration.ftp.client.DefaultFtpsClientFactory">
<property name="host" value="localhost"/>
<property name="port" value="22"/>
<property name="username" value="anju"/>
<property name="password" value="raghu"/>
<property name="clientMode" value="1"/>
<property name="fileType" value="2"/>
<property name="useClientMode" value="true"/>
<property name="cipherSuites" value="a,b.c"/>
<property name="keyManager" ref="keyManager"/>
<property name="protocol" value="SSL"/>
<property name="trustManager" ref="trustManager"/>
<property name="prot" value="P"/>
<property name="needClientAuth" value="true"/>
<property name="authValue" value="anju"/>
<property name="sessionCreation" value="true"/>
<property name="protocols" value="SSL, TLS"/>
<property name="implicit" value="true"/>
</bean>
Spring FTP 使用出站通道示例
在本节中,让我们看一个简单的场景,将文件从 Location1 传输到远程位置 Location2。为了清晰起见,让我们定义它们如下:
-
Location1:
d:\folder1
-
Location2:
d:\folder2
让我们在 Spring 中使用 Spring 集成包创建一个简单的应用程序,以完成从 Location1 到 Location2 的文件传输任务。我们需要两个主要文件来完成这个任务;第一个是配置文件applicationContext.xml
,第二个是一个 Java 类文件,它将通知 Spring 集成框架将文件上传到远程位置。
applicationContext.xml
文件将包含整个必要的 bean 配置,以及使用 Spring 集成包所需的 XMLNS。需要集成的 XMLNS 如下:
xmlns:int-ftp="http://www.springframework.org/schema/integration/ftp"
我们还需要将DefaultFtpSessionFactory
配置为一个 bean,其中包括FtpChannel
和FtpOutBoundAdpater
。DefaultFtpSessionFactory
具有所有 FTP 属性的 setter。FTPOutboundeAdapter
将配置为remoteFTP
位置和outboundchannel
。以下是完整的配置文件:
<beans
xmlns:int-ftp="http://www.springframework.org/schema/integration/ftp"
xsi:schemaLocation="http://www.springframework.org/schema/integration/ftp http://www.springframework.org/schema/integration/ftp/spring-integration-ftp.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="ftpClientFactory" class="org.springframework.integration.ftp.session.DefaultFtpSessionFactory">
<property name="host" value="localhost"/>
<property name="port" value="21"/>
<property name="username" value="myftpusername"/>
<property name="password" value="myftppassword"/>
<property name="clientMode" value="0"/>
<property name="fileType" value="2"/>
<property name="bufferSize" value="100000"/>
</bean>
<int:channel id="ftpChannel" />
<int-ftp:outbound-channel-adapter id="ftpOutbound"
channel="ftpChannel"
remote-directory="D:/folder2"
session-factory="ftpClientFactory"/>
</beans>
现在让我们创建一个简单的 Java 类,通知 Spring 将文件上传到 Location2。这个类将加载applicationContext.xml
文件,并使用在 XML 文件中配置的 bean ID 实例化FTPChannel
。创建一个文件对象,其中包含需要传输到远程位置的文件名。将这个文件对象发送到 Spring 集成消息,然后将消息发送到通道,以便文件被传送到目的地。以下是示例代码:
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.integration.Message;
import org.springframework.integration.MessageChannel;
import org.springframework.integration.support.MessageBuilder;
import java.io.File;
public class SendFileSpringFTP {
public static void main(String[] args) throws InterruptedException {
ConfigurableApplicationContext ctx =
new ClassPathXmlApplicationContext("/applicationContext.xml");
MessageChannel ftpChannel = ctx.getBean("ftpChannel", MessageChannel.class);
File file = new File("D:/folder2/report-Jan.txt");
final Message<File> messageFile = MessageBuilder.withPayload(file).build();
ftpChannel.send(messageFile);
Thread.sleep(2000);
}
}
运行上述类以查看report-Jan.txt
被传输到远程位置。
配置 Spring FTP 以使用网关读取子文件夹中的文件
在这一节中,让我们看看另一个可以用来读取子文件夹报告的配置文件。
我们已经使用了上一节处理 FTP XSD 的表达式属性。我们将进一步看到如何使用表达式属性通知 Spring 集成 FTP 框架触发 FTP 命令。在 FTP 中执行的每个命令都会得到一个回复,通常是三位数,例如:
-
125
:数据连接已打开;传输开始 -
200
:命令 OK -
214
:帮助消息(供人类用户使用) -
331
:用户名正确;需要密码 -
425
:无法打开数据连接 -
452
:写文件错误 -
500
:语法错误(无法识别的命令) -
501
:语法错误(无效参数) -
502
:未实现的模式类型
回复通道由网关创建。在以下代码中,我们为分割器配置了一个回复通道:
<int-ftp:outbound-gateway id="gatewayLS" cache-sessions="false"
session-factory="ftpSessionFactory"
request-channel="inbound"
command="ls"
command-options="-1"
expression="'reports/*/*'"
reply-channel="toSplitter"/>
<int:channel id="toSplitter" />
<int:splitter id="splitter" input-channel="toSplitter" output-channel="toGet"/>
<int-ftp:outbound-gateway id="gatewayGET" cache-sessions="false"
local-directory="localdir"
session-factory="ftpSessionFactory"
request-channel="toGet"
reply-channel="toRemoveChannel"
command="get"
command-options="-P"
expression="payload.filename"/>
使用 Spring 集成支持 FTP,我们还可以将消息分割成多个部分。这是在 XML 文件中使用splitter
属性(AbstractMessageSplitter implements MessageHandler
)进行配置的。
<channel id="inputChannel"/>
<splitter id="splitter"
ref="splitterBean"
method="split"
input-channel="inputChannel"
output-channel="outputChannel" />
<channel id="outputChannel"/>
<beans:bean id="splitterBean" class="sample.PojoSplitter"/>
从逻辑上讲,splitter
类必须分割消息并为每个分割消息附加序列号和大小信息,以便不丢失顺序。可以使用聚合器将断开的消息组合在一起,然后发送到通道。
在 Java 中配置 Spring FTP
在这一节中,让我们看看如何使用注解在 Java 类中配置 FTP 属性,并创建DefaultFTPSession
工厂的实例,并使用实例可用的 setter 方法设置属性。
我们可以使用@Configuration
注解来配置 FTP 属性,如下所示:
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.ftp.session.DefaultFtpSessionFactory;
@Configuration
public class MyApplicationConfiguration {
@Autowired
@Qualifier("myFtpSessionFactory")
private SessionFactory myFtpSessionFactory;
@Bean
public SessionFactory myFtpSessionFactory()
{
DefaultFtpSessionFactory ftpSessionFactory = new DefaultFtpSessionFactory();
ftpSessionFactory.setHost("ftp.abc.org");
ftpSessionFactory.setClientMode(0);
ftpSessionFactory.setFileType(0);
ftpSessionFactory.setPort(21);
ftpSessionFactory.setUsername("anjju");
ftpSessionFactory.setPassword("raghu");
return ftpSessionFactory;
}
}
使用 Spring 集成发送文件到 FTP
想象一种情景,你正在通过 FTP 通道发送文件。假设有两个文件,比如Orders.txt
和vendors.txt
,需要通过 FTP 发送到远程位置。为了实现这一点,我们需要按照以下步骤进行操作:
-
创建
FTPChannel
。 -
使用
baseFolder.mkdirs()
在基本文件夹中创建一个目录。 -
在基本文件夹位置创建两个文件对象。
-
使用
InputStream
为订单和供应商创建两个单独的流。 -
使用 Spring 中可用的文件工具,将输入流复制到它们各自的文件中。
-
使用
MessageBuilder
类,使用withpayload()
方法将文件转换为消息。 -
最后,将消息发送到 FTP 通道并关闭上下文。
让我们写一些示例代码来做到这一点:
public void sendFilesOverFTP() throws Exception{
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("META-INF/spring/integration/FtpOutboundChannelAdapterSample-context.xml");
MessageChannel ftpChannel = ctx.getBean("ftpChannel", MessageChannel.class);
baseFolder.mkdirs();
final File fileToSendOrders = new File(baseFolder, "orders.txt");
final File fileToSendVendors = new File(baseFolder, "vendore.txt");
final InputStream inputStreamOrders = FtpOutboundChannelAdapterSample.class.getResourceAsStream("/test-files/orders.txt");
final InputStream inputStreamVendors = FtpOutboundChannelAdapterSample.class.getResourceAsStream("/test-files/vendors.txt");
FileUtils.copyInputStreamToFile(inputStreamOrders, fileToSendOrders);
FileUtils.copyInputStreamToFile(inputStreamVendors, fileToSendVendors);
assertTrue(fileToSendOrders.exists());
assertTrue(fileToSendVendors.exists());
final Message<File> messageOrders = MessageBuilder.withPayload(fileToSendOrders).build();
final Message<File> messageVendors = MessageBuilder.withPayload(fileToSendVendors).build();
ftpChannel.send(messageOrders);
ftpChannel.send(messageVendors);
Thread.sleep(2000);
assertTrue(new File(TestSuite.FTP_ROOT_DIR + File.separator + "orders.txt").exists());
assertTrue(new File(TestSuite.FTP_ROOT_DIR + File.separator + "vendors.txt").exists());
LOGGER.info("Successfully transfered file 'orders.txt' and 'vendors.txt' to a remote FTP location.");
ctx.close();
}
使用 Spring 集成和 Spring 批处理的 FTP 应用程序
在这一节中,我们将学习如何将 FTP 作为批处理作业。我们将在 Java 中创建一个配置文件,而不是 XML。在这里,我们将使用@Configuration
注解为 Spring 批处理数据库和 tasklet 设置所有属性。然后我们有一个属性文件,它将为ApplicationConfiguration.java
文件中的实例变量设置值。使用 Spring 框架中可用的属性持有者模式加载属性。
- 我们首先要更新配置文件。以下是一个示例配置文件:
@Configuration
public class ApplicationConfiguration {
//Below is the set of instance variables that will be configured.
//configuring the jdbc driver
@Value("${batch.jdbc.driver}")
private String driverClassName;
//configuring the jdbc url
@Value("${batch.jdbc.url}")
private String driverUrl;
//configuring the jdbc username
@Value("${batch.jdbc.user}")
private String driverUsername;
//configuring the jdbc passowrd
@Value("${batch.jdbc.password}")
private String driverPassword;
//configuring the jobrepository autowiring the bean
@Autowired
@Qualifier("jobRepository")
private JobRepository jobRepository;
//configuring the ftpsessionfactory
@Autowired
@Qualifier("myFtpSessionFactory")
private SessionFactory myFtpSessionFactory;
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(driverUrl);
dataSource.setUsername(driverUsername);
dataSource.setPassword(driverPassword);
return dataSource;
}
//setting the ftp as a batch job
@Bean
@Scope(value="step")
public FtpGetRemoteFilesTasklet myFtpGetRemoteFilesTasklet(){
FtpGetRemoteFilesTasklet ftpTasklet = new FtpGetRemoteFilesTasklet();
ftpTasklet.setRetryIfNotFound(true);
ftpTasklet.setDownloadFileAttempts(3);
ftpTasklet.setRetryIntervalMilliseconds(10000);
ftpTasklet.setFileNamePattern("README");
//ftpTasklet.setFileNamePattern("TestFile");
ftpTasklet.setRemoteDirectory("/");
ftpTasklet.setLocalDirectory(new File(System.getProperty("java.io.tmpdir")));
ftpTasklet.setSessionFactory(myFtpSessionFactory);
return ftpTasklet;
}
//setting the ftp sessionfactory
@Bean
public SessionFactory myFtpSessionFactory() {
DefaultFtpSessionFactory ftpSessionFactory = new DefaultFtpSessionFactory();
ftpSessionFactory.setHost("ftp.gnu.org");
ftpSessionFactory.setClientMode(0);
ftpSessionFactory.setFileType(0);
ftpSessionFactory.setPort(21);
ftpSessionFactory.setUsername("anonymous");
ftpSessionFactory.setPassword("anonymous");
return ftpSessionFactory;
}
//Configuring the simple JobLauncher
@Bean
public SimpleJobLauncher jobLauncher() {
SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
jobLauncher.setJobRepository(jobRepository);
return jobLauncher;
}
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}
-
让我们使用
property-placeholder
进一步配置批处理作业。 -
创建一个名为
batch.properties
的文件:
batch.jdbc.driver=org.hsqldb.jdbcDriver
batch.jdbc.url=jdbc:hsqldb:mem:anjudb;sql.enforce_strict_size=true batch.jdbc.url=jdbc:hsqldb:hsql://localhost:9005/anjdb
batch.jdbc.user=anjana
batch.jdbc.password=raghu
- 在
context.xml
文件或一个单独的文件中配置应用程序,以运行 FTP 的 tasklet:
<batch:job id="ftpJob">
<batch:step id="step1" >
<batch:tasklet ref="myApplicationFtpGetRemoteFilesTasklet" />
</batch:step>
</batch:job>
- 这里是
MyApplicationFtpGetRemoteFilesTasklet
:
public class MyApplicationFtpGetRemoteFilesTasklet implements Tasklet, InitializingBean {
private File localDirectory;
private AbstractInboundFileSynchronizer<?> ftpInboundFileSynchronizer;
private SessionFactory sessionFactory;
private boolean autoCreateLocalDirectory = true;
private boolean deleteLocalFiles = true;
private String fileNamePattern;
private String remoteDirectory;
private int downloadFileAttempts = 12;
private long retryIntervalMilliseconds = 300000;
private boolean retryIfNotFound = false;
/**All the above instance variables have setters and getters*/
/*After properties are set it just checks for certain instance variables for null values and calls the setupFileSynchronizer method.
It also checks for local directory if it doesn't exits it auto creates the local directory.
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(sessionFactory, "sessionFactory attribute cannot be null");
Assert.notNull(localDirectory, "localDirectory attribute cannot be null");
Assert.notNull(remoteDirectory, "remoteDirectory attribute cannot be null");
Assert.notNull(fileNamePattern, "fileNamePattern attribute cannot be null");
setupFileSynchronizer();
if (!this.localDirectory.exists()) {
if (this.autoCreateLocalDirectory) {
if (logger.isDebugEnabled()) {
logger.debug("The '" + this.localDirectory + "' directory doesn't exist; Will create.");
}
this.localDirectory.mkdirs();
}
else
{
throw new FileNotFoundException(this.localDirectory.getName());
}
}
}
/*This method is called in afterpropertiesset() method. This method checks if we need to transfer files using FTP or SFTP.
If it is SFTP then it initializes ftpInbounFileSynchronizer using SFTPinbounfFileSynchronizer which has a constructor which takes sessionFactory as the argument and has setter method to set file Filter details with FileNamesPatterns.The method also sets the remoteDirectory location..
*/
private void setupFileSynchronizer() {
if (isSftp()) {
ftpInboundFileSynchronizer = new SftpInboundFileSynchronizer(sessionFactory);
((SftpInboundFileSynchronizer) ftpInboundFileSynchronizer).setFilter(new SftpSimplePatternFileListFilter(fileNamePattern));
}
else
{
ftpInboundFileSynchronizer = new FtpInboundFileSynchronizer(sessionFactory);
((FtpInboundFileSynchronizer) ftpInboundFileSynchronizer).setFilter(new FtpSimplePatternFileListFilter(fileNamePattern));
}
ftpInboundFileSynchronizer.setRemoteDirectory(remoteDirectory);
}
/*This method is called during the file synchronization process this will delete the files in the directory after copying..
*/
private void deleteLocalFiles() {
if (deleteLocalFiles) {
SimplePatternFileListFilter filter = new SimplePatternFileListFilter(fileNamePattern);
List<File> matchingFiles = filter.filterFiles(localDirectory.listFiles());
if (CollectionUtils.isNotEmpty(matchingFiles)) {
for (File file : matchingFiles) {
FileUtils.deleteQuietly(file);
}
}
}
}
/*This is a batch execute method which operates with FTP ,it synchronizes the local directory with the remote directory.
*/
/* (non-Javadoc)
* @see org.springframework.batch.core.step.tasklet.Tasklet#execute(org.springframework.batch.core.StepContribution, org.springframework.batch.core.scope.context.ChunkContext)
*/
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
deleteLocalFiles();
ftpInboundFileSynchronizer.synchronizeToLocalDirectory(localDirectory);
if (retryIfNotFound) {
SimplePatternFileListFilter filter = new SimplePatternFileListFilter(fileNamePattern);
int attemptCount = 1;
while (filter.filterFiles(localDirectory.listFiles()).size() == 0 && attemptCount <= downloadFileAttempts) {
logger.info("File(s) matching " + fileNamePattern + " not found on remote site. Attempt " + attemptCount + " out of " + downloadFileAttempts);
Thread.sleep(retryIntervalMilliseconds);
ftpInboundFileSynchronizer.synchronizeToLocalDirectory(localDirectory);
attemptCount++;
}
if (attemptCount >= downloadFileAttempts && filter.filterFiles(localDirectory.listFiles()).size() == 0) {
throw new FileNotFoundException("Could not find remote file(s) matching " + fileNamePattern + " after " + downloadFileAttempts + " attempts.");
}
}
return null;
}
摘要
在本章中,我们看到了 FTP 及其缩写的概述。我们已经看到了不同类型的适配器,比如入站和出站适配器,以及出站网关及其配置。我们还展示了springs-integration-ftp.xsd
,并引用了每个入站和出站适配器可用的各种选项。我们还展示了使用spring-integration-ftp
包开发 maven 应用程序所需的库。然后我们看了两个重要的类,FTPSessionFactory
和FTPsSessionFactory
,以及它们的 getter 和 setter。我们还演示了使用出站通道的SpringFTP
传输文件的示例。我们还演示了如何使用 Java 通过@Configuration
注解配置 FTP。最后,我们演示了 FTP 作为一个 tasklet。在下一章中,我们将进一步探讨 Spring 与 HTTP 的集成。
第六章:Spring 集成与 HTTP
在本章中,让我们看看 Spring 集成包如何支持 HTTP 协议。我们还将深入了解 HTTP 及其特性,以更好地理解如何使用 Spring 框架执行 HTTP 操作。
HTTP代表超文本传输协议,它又代表安全连接。该协议属于用于数据传输的应用层。它使用传输控制 协议/互联网协议(TCP/IP)通信进行数据传输。HTTP 是一种无连接和无状态的协议,因为服务器和客户端只在请求和响应时相互知晓。只要服务器和客户端能够处理,任何类型的数据都可以通过 HTTP 发送。请求通过 Web URL 发送,即统一资源定位符。URL 包含以下部分:http://www.domainname.com/path/?abc=xyz
-
协议:
http://
或https://
-
主机:
www.domainname.com
-
资源路径:
path
-
查询:
abc=xyz
HTTP 方法和状态代码
让我们来看看 HTTP 方法和状态代码。HTTP 方法是 HTTP 协议上执行操作的通信渠道。
以下是使用的 HTTP 方法:
-
GET
:这获取给定标识符的现有资源。 -
PUT
:这放置一个新资源。 -
POST
:这将更新现有资源。 -
DELETE
:这将删除现有资源。
状态代码是关于 HTTP 消息的人类可读的诊断信息。
以下表格显示了所有可用状态代码及其含义:
状态代码 | 含义 |
---|---|
200 | 请求成功 |
201 | POST 方法成功执行 |
202 | 请求已被接受进行处理 |
203 | 未授权查看信息 |
204 | 服务器没有响应 |
301 | 请求的数据已移至新 URL |
302 | 请求需要进行前向操作才能完全实现 |
303 | 所有 3XX 代码都指向不同的 URL,用于不同的操作,如转发 |
304 | 缓存未正确修改 |
400 | 语法错误 |
401 | 未经授权的请求 |
402 | 头部收费不匹配 |
403 | 禁止请求 |
404 | 根据提供的 URL 未找到资源 |
500 | 服务器发生意外错误 |
501 | 服务器不支持该操作 |
502 | 服务器负载过重 |
503 | 网关超时。服务器正在尝试从其他资源或服务访问数据,但未收到所需的响应。 |
HTTP 标头
这些标头在 HTTP 请求和响应的消息中找到。它们只是由冒号分隔的名称值字符串。内容类型、缓存、响应类型等信息可以直接在标头中给出。标头通常没有任何大小限制,但服务器对标头大小有限制。
HTTP 超时
这是一个 408 状态代码,当服务器尝试访问数据太多次而没有得到任何响应时,它会出现在网页上。即使服务器运行缓慢,也会出现这种错误。
超时可能发生在两种情况下,一种是与 Spring 集成通道交互时,可以是入站通道或出站通道,另一种是与远程位置的 HTTP 服务器交互时。
超时支持是使用 Spring 框架中可用的RestTemplate
类完成的。以下是可用于与 Spring 集成中的 HTTP 网关和出站适配器的示例配置。
<bean id="requestFactory"
class="org.springframework.http.client.SimpleClientHttpRequestFactory">
<property name="connectTimeout" value="5000"/>
<property name="readTimeout" value="5000"/>
</bean>
Java 中的 HTTP 代理设置
代理设置由 Java 系统属性支持。这些属性可以设置为使用具有代理设置的服务器。以下是可以设置的属性:
-
http.proxyHost
:代理服务器的主机名。 -
http.proxyPort
:端口号,默认值为 80。 -
http.nonProxyHosts
:应直接到达的主机列表,绕过代理。这是一个由|
字符分隔的模式列表。这些模式可以以*
字符开始或结束,用作通配符。匹配这些模式之一的任何主机将通过直接连接而不是通过代理到达。
以下是用于安全 HTTP 的代理设置:
-
https.proxyHost
:代理服务器的主机名。 -
https.proxyPort
:端口号,默认值为 80。
Spring 中的代理配置支持
Spring 支持代理配置。我们只需要配置SimpleClientHttpRequestFactory
bean,它具有一个带有java.net.Proxy
bean 的代理属性。以下代码显示了一个示例配置:
<bean id="requestFactory" class="org.springframework.http.client.SimpleClientHttpRequestFactory">
<property name="proxy">
<bean id="proxy" class="java.net.Proxy">
<constructor-arg>
<util:constant static-field="java.net.Proxy.Type.HTTP"/>
</constructor-arg>
<constructor-arg>
<bean class="java.net.InetSocketAddress">
<constructor-arg value="123.0.0.1"/>
<constructor-arg value="8080"/>
</bean>
</constructor-arg>
</bean>
</property>
</bean>
Spring 集成对 HTTP 的支持
Spring 通过适配器扩展了对 HTTP 的支持,就像 FTP 一样,其中包括网关实现。Spring 支持 HTTP 使用以下两种网关实现:
-
HttpInboundEndpoint
:要通过 HTTP 接收消息,我们需要使用适配器或可用的网关。入站适配器称为 HTTP 入站适配器,网关称为 HTTP 入站网关。适配器需要一个 servlet 容器,比如 Tomcat 服务器或 Jetty 服务器。我们需要制作一个带有 servlet 配置的 web 应用程序,并将其部署到 web 服务器上。Spring 本身提供了一个名为的 servlet。 -
HttpRequestHandlerServlet:这个类扩展了普通的
HttpServlet
,并且位于org.springframework.web.context.support.HttpRequestHandlerServlet
包下。由于它扩展了HttpServlet
,它还覆盖了init()
和service()
方法。
以下是web.xml
文件中的 servlet 配置:
<servlet>
<servlet-name>inboundGateway</servlet-name>
<servlet-class>o.s.web.context.support.HttpRequestHandlerServlet</servlet-class>
</servlet>
以下是处理入站 HTTP 请求的网关配置。该网关接受一系列消息转换器,这些转换器将从HttpServletRequest
转换为消息:
<bean id="httpInbound" class="org.springframework.integration.http.inbound.HttpRequestHandlingMessagingGateway">
<property name="requestChannel" ref="httpRequestChannel" />
<property name="replyChannel" ref="httpReplyChannel" />
</bean>
Spring 集成对多部分 HTTP 请求的支持
如果 HTTP 请求被包装,MultipartHttpServletRequest
转换器将把请求转换为消息载荷,这只是一个MultiValueMap
。这个映射将有值,这些值是 Spring 的多部分的实例。值是根据内容类型决定的。值也可以是字节数组或字符串。默认情况下,如果有一个名为MultipartResolver
的 bean,它会被 Spring 的集成框架识别;如果有一个名为multipartResolver
的 bean,它反过来会启用上下文。这将启用入站请求映射。
Spring 集成对 HTTP 响应的支持
对 HTTP 请求的响应通常以 200Ok 状态码发送。要进一步自定义响应,可以使用 Spring MVC 框架。在 Spring MVC 应用程序中,我们有一个选项来自定义响应。我们可以为响应提供一个viewName
,这个viewName
会被 Spring MVC 的ViewResolver
解析。我们可以配置网关以像 Spring 控制器一样运行,它返回一个视图名称作为框架的响应,我们还可以配置 HTTP 方法。
在以下配置中,您可以看到我们使用了一个集成包,并配置了HttpRequestHandlingController
bean 的以下属性:
-
HttpRequestChannel
-
HttpReplyChannel
-
viewName
-
SupportedMedthodNames
-
以下代码片段显示了
HttpInbound
bean 的配置。 -
我们还可以配置支持的 HTTP 方法。
<bean id="httpInbound" class="org.springframework.integration.http.inbound.HttpRequestHandlingController">
<constructor-arg value="true" /> <!-- indicates that a reply is expected -->
<property name="requestChannel" ref="httpRequestChannel" />
<property name="replyChannel" ref="httpReplyChannel" />
<property name="viewName" value="jsonView" />
<property name="supportedMethodNames" >
<list>
<value>GET</value>
<value>DELETE</value>
</list>
</property>
</bean>
配置出站 HTTP 消息
Spring 提供了HttpRequestExecutingMessageHandler
,它以字符串 URL 作为构造函数参数。该类有一个名为ReponseChannel
的属性,也需要进行配置。
该 bean 将通过读取构造函数中配置的 URL 调用RestTemplate
类,RestTemplate
调用HttpMessageConverters
。读取HttpMessageConverters
列表,并生成HttpRequest
主体。
转换器和HttpRequestExecutingMessageHandler
在以下代码中显示:
<bean id="httpOutbound" class="org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler">
<constructor-arg value="http://localhost:8080/myweb" />
<property name="outputChannel" ref="responseChannel" />
</bean>
或者
<bean id="httpOutbound" class="org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler">
<constructor-arg value="http://localhost:8080/myweb" />
<property name="outputChannel" ref="responseChannel" />
<property name="messageConverters" ref="messageConverterList" />
<property name="requestFactory" ref="customRequestFactory" />
</bean>
配置出站网关的 cookies
OutboundGateway
具有传输 cookies 属性,接受 true 或 false 的布尔值。响应中的标头包含一个设置 cookies 参数,如果transfer-cookie
属性设置为True
,则将响应转换为 cookie。
配置既无响应又有响应的入站网关
使用以下代码配置无响应的InboundGateway
请求:
<int-http:inbound-channel-adapter id="httpChannelAdapter" channel="requests"
supported-methods="PUT, DELETE"/>
对于需要响应的请求:
<int-http:inbound-gateway id="inboundGateway"
request-channel="requests"
reply-channel="responses"/>
入站通道适配器或网关的 RequestMapping 支持
requestmapping
配置可以用于入站通道适配器或网关,如下所示:
<inbound-gateway id="inboundController"
request-channel="requests"
reply-channel="responses"
path="/foo/{fooId}"
supported-methods="GET"
view-name="foo"
error-code="oops">
<request-mapping headers="User-Agent"
<!—-headers=""-->
params="myParam=myValue"
consumes="application/json"
produces="!text/plain"/>
</inbound-gateway>
基于此配置,命名空间解析器将创建IntegrationRequestMappingHandlerMapping
的实例(如果尚不存在),HttpRequestHandlingController
bean,并与之关联RequestMapping
的实例,然后将其转换为 Spring MVC 的RequestMappingInfo
。
使用路径和支持的方法,<http:inbound-channel-adapter>
或<http:inbound-gateway>
的属性,<request-mapping>
直接转换为 Spring MVC 中org.springframework.web.bind.annotation.RequestMapping
注解提供的相应选项。
<request-mapping>
子元素允许您配置多个 Spring 集成 HTTP 入站端点到相同的路径(甚至相同的支持方法),并根据传入的 HTTP 请求提供不同的下游消息流。
使用 HTTP 入站端点配置 RequestMapping
我们还可以声明一个 HTTP 入站端点,并在 Spring 集成流程中应用路由和过滤逻辑,以实现相同的结果。这允许您尽早将消息传递到流程中,例如:
<int-http:inbound-gateway request-channel="httpMethodRouter"
supported-methods="GET,DELETE"
path="/process/{entId}"
payload-expression="#pathVariables.entId"/>
<int:router input-channel="httpMe
thodRouter" expression="headers.http_requestMethod">
<int:mapping value="GET" channel="in1"/>
<int:mapping value="DELETE" channel="in2"/>
</int:router>
<int:service-activator input-channel="in1" ref="service" method="getEntity"/>
<int:service-activator input-channel="in2" ref="service" method="delete"/>
配置入站通道适配器以从 URL 读取请求信息
我们还可以配置入站通道适配器以接受使用 URI 的请求。
URI 可以是/param1/{param-value1}/param2/{param-value2}
。 URI 模板变量通过有效负载表达式属性与消息有效负载进行映射。 URI 路径中的某些变量也可以与标头进行映射:
<int-http:inbound-channel-adapter id="inboundAdapterWithExpressions"
path="/var-1/{phone}/var-2/{username}"
channel="requests"
payload-expression="#pathVariables.firstName">
<int-http:header name="var-2" expression="#pathVariables.username"/>
</int-http:inbound-channel-adapter>
以下是可以在配置中使用的有效负载表达式列表:
-
#requestParams
:来自ServletRequest
参数映射的MultiValueMap
。 -
#pathVariables
:URI 模板占位符及其值的映射。 -
#matrixVariables
:MultiValueMap
的映射。 -
#requestAttributes
:与当前请求关联的org.springframework.web.context.request.RequestAttributes
。 -
#requestHeaders
:当前请求的org.springframework.http.HttpHeaders
对象。 -
#cookies
:当前请求的javax.servlet.http.Cookies
的<String,Cookie>
映射。
为 HTTP 响应配置出站网关
出站网关或出站通道适配器配置与 HTTP 响应相关,并提供配置响应的选项。 HTTP 请求的默认响应类型为 null。响应方法通常为 POST。如果响应类型为 null 且 HTTP 状态代码为 null,则回复消息将具有ResponseEntity
对象。在以下示例配置中,我们已配置了预期:
<int-http:outbound-gateway id="example"
request-channel="requests"
URL="http://localhost/test"
http-method="POST"
extract-request-payload="false"
expected-response-type="java.lang.String"
charset="UTF-8"
request-factory="requestFactory"
reply-timeout="1234"
reply-channel="replies"/>
为不同的响应类型配置出站适配器
现在,我们将向您展示两个配置出站适配器的示例,使用不同的响应类型。
在这里,使用预期的响应类型表达式与值有效负载:
<int-http:outbound-gateway id="app1"
request-channel="requests"
URL="http://localhost/myapp"
http-method-expression="headers.httpMethod"
extract-request-payload="false"
expected-response-type-expression="payload"
charset="UTF-8"
request-factory="requestFactory"
reply-timeout="1234"
reply-channel="replies"/>
现在,配置出站通道适配器以提供字符串响应:
<int-http:outbound-channel-adapter id="app1"
url="http://localhost/myapp"
http-method="GET"
channel="requests"
charset="UTF-8"
extract-payload="false"
expected-response-type="java.lang.String"
request-factory="someRequestFactory"
order="3"
auto-startup="false"/>
将 URI 变量映射为 HTTP 出站网关和出站通道适配器的子元素
在本节中,我们将看到 URI 变量和 URI 变量表达式的用法,作为 HTTP 出站网关配置的子元素。
如果您的 URL 包含 URI 变量,可以使用 Uri-variable 子元素进行映射。此子元素适用于 HTTP 出站网关和 HTTP 出站通道适配器:
<int-http:outbound-gateway id="trafficGateway"
url="http://local.yahooapis.com/trafficData?appid=YdnDemo&zip={zipCode}"
request-channel="trafficChannel"
http-method="GET"
expected-response-type="java.lang.String">
<int-http:uri-variable name="zipCode" expression="payload.getZip()"/>
</int-http:outbound-gateway>
Uri-variable
子元素定义了两个属性:name
和expression
。name
属性标识 URI 变量的名称,而expression
属性用于设置实际值。使用expression
属性,您可以利用Spring Expression Language(SpEL)的全部功能,这使您可以完全动态地访问消息负载和消息标头。例如,在上面的配置中,将在消息的负载对象上调用getZip()
方法,并且该方法的结果将用作名为zipCode
的 URI 变量的值。
自 Spring Integration 3.0 以来,HTTP 出站端点支持Uri-variables-expression
属性,用于指定应该评估的Expression
,从而为 URL 模板中的所有 URI 变量占位符生成一个映射。它提供了一种机制,可以根据出站消息使用不同的变量表达式。此属性与<Uri-variable/>
子元素互斥:
<int-http:outbound-gateway
url="http://foo.host/{foo}/bars/{bar}"
request-channel="trafficChannel"
http-method="GET"
Uri-variables-expression="@uriVariablesBean.populate(payload)"
expected-response-type="java.lang.String"/>
使用 HTTP 出站网关和 HTTP 入站网关处理超时
以下表格显示了处理 HTTP 出站和 HTTP 入站网关的差异:
HTTP 出站网关中的超时 | HTTP 入站网关中的超时 |
---|---|
ReplyTimeOut 映射到HttpRequestExecutingMessageHandler 的sendTimeOut 属性。 |
在这里,我们使用RequestTimeOut 属性,它映射到HttpRequestHandlingMessagingGateway 类的requestTimeProperty 。 |
sendTimeOut 的默认值为1 ,发送到MessageChannel 。 |
默认超时属性为 1,000 毫秒。超时属性将用于设置MessagingTemplate 实例中使用的sendTimeOut 参数。 |
Spring 对标头自定义的支持
如果我们需要对标头进行进一步的自定义,则 Spring Integration 包为我们提供了完整的支持。如果在配置中明确指定标头名称,并使用逗号分隔的值,将覆盖默认行为。
以下是进一步标头自定义的配置:
<int-http:outbound-gateway id="httpGateway"
url="http://localhost/app2"
mapped-request-headers="boo, bar"
mapped-response-headers="X-*, HTTP_RESPONSE_HEADERS"
channel="someChannel"/>
<int-http:outbound-channel-adapter id="httpAdapter"
url="http://localhost/app2"
mapped-request-headers="boo, bar, HTTP_REQUEST_HEADERS"
channel="someChannel"/>
另一个选项是使用 header-mapper 属性,该属性采用 DefaultHttpHeaderMapper 类的配置。
该类配备了用于入站和出站适配器的静态工厂方法。
以下是header-mapper
属性的配置:
<bean id="headerMapper" class="o.s.i.http.support.DefaultHttpHeaderMapper">
<property name="inboundHeaderNames" value="foo*, *bar, baz"/>
<property name="outboundHeaderNames" value="a*b, d"/>
</bean>
使用 Spring 的 RestTemplate 发送多部分 HTTP 请求
大多数情况下,我们在应用程序中实现了文件上传功能。文件作为多部分请求通过 HTTP 发送。
在本节中,让我们看看如何使用RestTemplate
配置入站通道适配器以通过 HTTP 请求发送文件。
让我们使用入站通道适配器配置服务器,然后为其编写客户端:
<int-http:inbound-channel-adapter id="httpInboundAdapter"
channel="receiveChannel"
name="/inboundAdapter.htm"
supported-methods="GET, POST"/>
<int:channel id="receiveChannel"/>
<int:service-activator input-channel="receiveChannel">
<bean class="org.springframework.integration.samples.multipart.MultipartReceiver"/>
</int:service-activator>
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>
httpInboundAdapter
将接收请求并将其转换为带有LinkedMultiValueMap
负载的消息。然后,我们将在multipartReceiver
服务激活器中解析它。
public void receive(LinkedMultiValueMap<String, Object> multipartRequest){
System.out.println("### Successfully received multipart request ###");
for (String elementName : multipartRequest.keySet()) {
if (elementName.equals("company")){
System.out.println("\t" + elementName + " - " +((String[]) multipartRequest.getFirst("company"))[0]);
}
else if (elementName.equals("company-logo")){
System.out.println("\t" + elementName + " - as UploadedMultipartFile: " + ((UploadedMultipartFile) multipartRequest.getFirst("company-logo")).getOriginalFilename());
}
}
}
现在,让我们编写一个客户端。通过客户端,我们指的是创建一个地图并向其中添加文件。
- 现在,我们将创建一个
MultiValueMap
:
MultiValueMap map = new LinkedMultiValueMap();
- 地图可以填充值,例如个人的详细信息:
Resource anjanapic = new ClassPathResource("org/abc/samples/multipart/anjana.png");
map.add("username","anjana");
map.add("lastname","mankale");
map.add("city","bangalore");
map.add("country","India");
map.add("photo",anjana.png);
- 此步骤是创建标头并设置内容类型:
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType("multipart", "form-data"));
- 我们需要将
header
和map
作为请求传递给HttpEntity
:
HttpEntity request = new HttpEntity(map, headers);
- 让我们使用
RestTemplate
传递请求:
RestTemplate template = new RestTemplate();
String Uri = "http://localhost:8080/multipart-http/inboundAdapter.htm";
ResponseEntity<?> httpResponse = template.exchange(Uri, HttpMethod.POST, request, null
现在,我们应该得到一个输出,其中照片已上传到服务器。
总结
在本章中,我们已经了解了 HTTP 和 Spring Integration 对访问 HTTP 方法和请求的支持。我们还演示了多部分请求和响应,并展示了如何配置入站和出站 HTTP 网关和适配器。
我们已经学习了通过配置 Spring 的入站和出站网关来发送多部分 HTTP 请求。我们还演示了如何使用多值映射来填充请求并将映射放入 HTTP 头部。最后,我们看到了可用的有效负载表达式列表。
在下一章中,让我们来看看 Spring 对 Hadoop 的支持。
第七章:与 Hadoop 一起使用 Spring
在构建现代 Web 应用程序的架构中,处理大量数据一直是一个主要挑战。 Hadoop 是 Apache 的开源框架,提供了处理和存储大量数据的库。它提供了一种可扩展、成本效益和容错的解决方案,用于存储和处理大量数据。在本章中,让我们演示 Spring 框架如何支持 Hadoop。 Map 和 Reduce、Hive 和 HDFS 是与基于云的技术一起使用的一些 Hadoop 关键术语。除了 Apache Hadoop 之外,Google 还推出了自己的 Map 和 Reduce 以及分布式文件系统框架。
Apache Hadoop 模块
Apache Hadoop 由以下模块组成:
-
Hadoop Common:这是 Hadoop 的其他模块使用的通用模块。它类似于一个实用程序包。
-
Hadoop 分布式文件系统:当我们需要在各种机器或机器集群上存储大量数据时,可以考虑使用 Hadoop 分布式文件系统。
-
Hadoop Yarn:想象一种情景,我们在云上有许多需要在特定时间通过发送电子邮件通知租户重新启动或重启的服务器。 Hadoop Yarn 可用于在计算机或集群之间调度资源。
-
Hadoop Map 和 Reduce:如果我们需要处理大量数据集,可以将其分解为小集群并将它们作为单元进行处理,然后稍后合并它们。这可以通过 Apache map 和 reduce 提供的库来实现。
Hadoop 的 Spring 命名空间
以下是需要用来将 Hadoop 框架与 Spring 集成的命名空间。www.springframework.org/schema/hadoop/spring-hadoop.xsd
定义了 Spring-Hadoop 的 XSD,通常在application-context.xml
文件中使用。 XSD 详细说明了如何使用 Spring 框架配置 Hadoop 作业。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/hadoop http://www.springframework.org/schema/hadoop/spring-hadoop.xsd">
<bean id ... >
4<hdp:configuration ...>
</beans>
Hadoop 分布式文件系统
Hadoop 分布式文件系统(HDFS)用于在分布式文件系统上存储大量数据。 HDFS 将元数据和应用程序数据分别存储在不同的服务器上。用于存储元数据的服务器称为NameNode
服务器。用于存储应用程序数据的服务器称为DataNode
服务器。NameNode
和DataNodes
以主从架构运行。通常,一个NameNode
会有许多DataNodes
。NameNodes
存储文件的命名空间,并将文件分割成许多小块存储在DataNodes
上。DataNodes
通常根据NameNode
的指令执行功能,如块创建、复制和删除。因此,与 Hadoop 的主要任务将涉及与文件系统的交互。这可能包括创建文件、解析文件进行处理或删除文件。
可以通过多种方式访问 Hadoop 文件系统。我们在这里列出了一些:
-
hdfs
:它使用 RPC 进行通信,使用的协议是hdfs://
。客户端、服务器和集群需要具有相同的版本,否则将发生序列化错误。 -
hftp
和hsftp
:这些是基于 HTTP 的、与版本无关的协议,前缀为hftp://
。 -
webhdfs
:这是基于 REST API 的 HTTP,并且也是版本无关的。
抽象类org.apache.hadoop.fs.FileSystem
的行为类似于 Hadoop 文件系统实现的入口点。Spring 框架通过子类SimplerFileSystem
扩展了这个类。这个子类包含了所有的文件操作方法,比如从一个位置复制到另一个位置。
Spring 框架提供了一个处理 Hadoop 分布式文件系统的包。包org.springframework.data.hadoop.fs
中有处理文件资源的类。
HdfsResourceLoader
是 Spring 的 Hadoop 文件系统包中的一个类,用于加载 Hadoop 文件系统中的资源。它有以配置对象作为输入的构造函数。HdfsResourceLoader
的构造函数如下所示。它还有从指定路径获取资源和在使用后关闭文件流的方法。
HdfsResourceLoader(Configuration config)
HdfsResourceLoader(Configuration config)
HdfsResourceLoader(Configuration config, URI uri)
HdfsResourceLoader(Configuration config, URI uri, String user) HdfsResourceLoader(FileSystem fs)
使用以下命令配置 Spring 使用webhdfs
:
<hdp:configuration>
fs.default.name=webhdfs://localhost
...
</hdp:configuration>
要手动配置 URI 和文件系统 ID,可以给出以下配置:
<!-- manually creates the default SHDP file-system named 'hadoopFs' -->
<hdp:file-system uri="webhdfs://localhost"/>
<!-- creates a different FileSystem instance -->
<hdp:file-system id="old-cluster" uri="hftp://old-cluster/"/>
诸如Rhino和Groovy之类的语言提供了 Java 脚本或使用 Python 来进行 HDFS 配置。以下是一个示例。可以配置脚本在启动时或有条件的启动时运行。可以用于此配置的两个脚本变量是run-at-start-up
和evaluate
。脚本也可以配置为作为任务启动(这意味着作为批处理作业启动)。
<beans ...>
<hdp:configuration .../>
<hdp:script id="inlined-js" language="javascript" run-at-startup="true">
importPackage(java.util);
name = UUID.randomUUID().toString()
scriptName = "src/test/resources/test.properties"
// fs - FileSystem instance based on 'hadoopConfiguration' bean
// call FileSystem#copyFromLocal(Path, Path)
fs.copyFromLocalFile(scriptName, name)
// return the file length
fs.getLength(name)
</hdp:script>
</beans>
这里显示了一些与隐式变量和与隐式变量相关的类:
-
hdfsRL-org.springframework.data.hadoop.io.HdfsResourceLoader
:一个 HDFS 资源加载器(依赖于hadoop-resource-loader
或单例类型匹配,根据'cfg
'自动创建)。 -
distcp-org.springframework.data.hadoop.fs.DistributedCopyUtil
:对DistCp
进行编程访问。 -
fs-org.apache.hadoop.fs.FileSystem
:一个 Hadoop 文件系统(依赖于'hadoop-fs
' bean 或单例类型匹配,根据'cfg'创建)。 -
fsh-org.springframework.data.hadoop.fs.FsShell
:一个文件系统 shell,将 hadoopfs
命令作为 API 暴露出来。
HBase
Apache HBase 主要是 Hadoop 的键值存储。它实际上是一个易于扩展的数据库,可以容纳数百万行和列。它可以跨硬件进行扩展,类似于 NoSQL 数据库。它与 Map 和 Reduce 集成,并且最适合使用 RESTFUL API。HBase 源自 Google 的 bigdata。它已经被 Netflix、Yahoo 和 Facebook 使用。它也是内存密集型的,因为它旨在处理大量数据并且必须针对硬件进行扩展。
让我们使用 Eclipse 和 Hadoop HBase 创建一个简单的员工表。在 Eclipse 中,只需添加以下 JAR 文件,或者如果您使用 Maven,请确保在 Maven 的pom.xml
文件中更新以下 JAR 文件:
-
hbase-0.94.8.jar
-
commons-logging-1.1.1.jar
-
log4j-1.2.16.jar
-
zookeeper-3.4.5.jar
-
hadoop-core-1.1.2.jar
-
commons-configuration-1.6.jar
-
common-lang-2.5.jar
-
protobuf-java-2.4.0a.jar
-
slf4j-api-1.4.3.jar
-
slf4j-log4j12-1.4.3.jar
创建一个Main
类,并使用以下代码。这个类将使用HbaseAdmin
类创建一个包含 ID 和 Name 两列的员工表。这个类有用于在 Hadoop 中创建、修改和删除表的方法。
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.client.HBaseAdmin;
public class HbaseTableCreation
{
public static void main(String[] args) throws IOException {
HBaseConfiguration hc = new HBaseConfiguration(new Configuration());
HTableDescriptor ht = new HTableDescriptor("EmployeeTable");
ht.addFamily( new HColumnDescriptor("Id"));
ht.addFamily( new HColumnDescriptor("Name"));
System.out.println( "connecting" );
HBaseAdmin hba = new HBaseAdmin( hc );
System.out.println( "Creating Table EmployeeTable" );
hba.createTable( ht );
System.out.println("Done....EmployeeTable..");
}
}
HBase 得到了 Spring Framework 的支持,并且 Spring Hadoop 包中还创建了一个factoryBean
来支持它。HbaseConfigurationFactoryBean
bean 位于org.springframework.data.hadoop.hbase
包中。HBaseAccessor
类是一个抽象类,并且已经被两个子类HbaseTemplate
和HbaseInterceptors
扩展。
Spring 提供了一个名为HBaseTemplate
的核心类。当 HBase 被实现时,这个类是应用程序的第一个接触点。这个类有访问表的所有方法,比如execute
、find
、find all
等等。
这个类有以下构造函数:
HbaseTemplate()
HbaseTemplate(Configuration configuration)
这是可以在应用程序的context.xml
或Hbasecontext.xml
文件中使用的 HBase 模板配置:
// default HBase configuration
<hdp:hbase-configuration/>
// wire hbase configuration (using default name 'hbaseConfiguration') into the template
<bean id="htemplate" class="org.springframework.data.hadoop.hbase.HbaseTemplate" p:configuration-ref="hbaseConfiguration"/>
让我们也看看如何使用HBaseTemplate
来检索表信息,以下是一个示例代码片段:
// writing to 'EmployeeTable'
template.execute("EmployeeTable", new TableCallback<Object>() {
@Override
public Object doInTable(HTable table) throws Throwable {
Put p = new Put(Bytes.toBytes("Name"));
p.add(Bytes.toBytes("Name"), Bytes.toBytes("SomeQualifier"), Bytes.toBytes("Anjana"));
table.put(p);
return null;
}
});
// read each row from 'EmployeeTable'
List<String> rows = template.find("EmployeeTable", "Name", new RowMapper<String>() {
@Override
public String mapRow(Result result, int rowNum) throws Exception {
return result.toString();
}
}));
Spring 还支持 AOP 与 Hadoop HBase 的集成,并有一个包来处理所有 AOP 事件,使用HBaseInterceptors
。这个类实现了以下接口:
-
org.aopalliance.aop.Advice
-
org.aopalliance.intercept.Interceptor
-
org.aopalliance.intercept.MethodInterceptor
-
InitializingBean
HBaseInterceptors
与HBaseSynchronizationManager
可用于在方法调用之前将 HBase 表绑定到线程,或在方法调用之后将其分离。
- 这是 Spring 的 Hadoop HBase 配置,用于创建一个 HBase 配置对象来管理 HBase 配置连接:
<!-- default bean id is 'hbaseConfiguration' that uses the existing 'hadoopCconfiguration' object ->
<hdp:hbase-configuration configuration-ref="hadoopCconfiguration" />
- 这是 Spring 的 Hadoop HBase 配置,用于在应用程序上下文为空或由于某种原因不可用时管理代理和连接:
<!-- delete associated connections but do not stop the proxies -->
<hdp:hbase-configuration stop-proxy="false" delete-connection="true">
toooo=baaaa
property=value
</hdp:hbase-configuration>
- 这是一个名为
ZooKeeper
的高性能协调服务器的配置,它用于 Hadoop 分布式系统:
<!-- specify ZooKeeper host/port -->
<hdp:hbase-configuration zk-quorum="${hbase.host}" zk-port="${hbase.port}">
我们还可以从文件中加载属性,如下所示:
<hdp:hbase-configuration properties-ref="some-props-bean" properties-location="classpath:/conf/testing/hbase.properties"/>
Map 和 Reduce
Map 和 Reduce是一种允许大规模扩展的编程方法。术语“Map 和 Reduce”意味着我们将使用映射来处理数据。我们可以看到这里有两个步骤。第一个是创建映射(创建具有键值对的映射),第二个是减少,它读取第一步创建的映射,并将其分解成许多较小的映射。
让我们想象一个与 Map 和 Reduce 相关的场景——假设我们需要获取印度老虎的数量,并做一些工作来改善它们的生存条件,以免它们灭绝。我们可能有老虎数量的平均数字。假设我们派遣人员到不同的邦,他们收集到的信息如下:卡纳塔克邦(100),泰米尔纳德邦(150),等等。然后我们将这些数字合并成一个数字,以得到老虎的总数量。人口的映射可以被看作是一个并行过程(映射作业),而合并结果可以被看作是一个减少作业。
为 Map 和 Reduce 在 Spring 中创建一个配置对象
配置对象保存有关 Map 和 Reduce 作业的信息。配置对象本身是一个映射到类ConfigurationFactoryBean
的 bean 定义,具有默认名称hadoopConfiguration
。
配置对象可以简单地配置如下:
<hdp:configuration />
这是配置对象的另一种变化:
<hdp:configuration resources="classpath:/custom-site.xml, classpath:/hq-site.xml">
另一种变化是直接在configuration
标记中使用java.properties
直接配置 Hadoop 资源,如下所示:
<hdp:configuration>
fs.default.name=hdfs://localhost:9000
hadoop.tmp.dir=/tmp/hadoop
electric=sea
</hdp:configuration>
您还可以使用 Spring 的属性占位符来外部化属性,如下所示:
<hdp:configuration>
fs.default.name=${hd.fs}
hadoop.tmp.dir=file://${java.io.tmpdir}
hangar=${number:18}
</hdp:configuration>
<context:property-placeholder location="classpath:hadoop.properties" />
使用 Spring 创建 Map 和 Reduce 作业
可以使用 Spring Framework 将 Map 和 Reduce 安排为作业。Spring Framework 带有spring-data-hadoop
包,支持 Map 和 Reduce。为此,我们需要确保我们有 Apache Hadoop 核心包。
让我们实现一个简单的场景,统计输入文件中每个单词的出现次数。创建一个简单的 Maven Java 项目,具有以下所述的依赖关系。
Maven 项目的依赖关系
我们需要在pom.xml
文件中添加这些依赖项:
< !-- Spring Data Apache Hadoop -- >
< dependency >
< groupId > org.springframework.data </ groupId >
< artifactId > spring-data-hadoop </ artifactId >
< version > 1.0.0.RELEASE </ version >
< /dependency >
< !-- Apache Hadoop Core –- >
< dependency >
< groupId > org.apache.hadoop </ groupId >
< artifactId > hadoop-core </ artifactId >
< version > 1.0.3 </version >
</dependency>
Apache Hadoop Map 和 Reduce 带有一个映射器类,可用于创建映射,以解决读取内容并存储单词出现次数的问题,使用键值对。文件中的每一行将被分解为要存储在映射中的单词。
我们可以通过扩展ApacheMapper
类并覆盖 map 方法来创建自定义映射器,如下所示:
public class CustomWordMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private Text myword = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
StringTokenizer lineTokenz = new StringTokenizer(line);
while (lineTokenz.hasMoreTokens()) {
String cleaned_data = removeNonLettersNonNumbers(lineTokenz.nextToken());
myword.set(cleaned_data);
context.write(myword, new IntWritable(1));
}
}
/**
* Replace all Unicode characters that are neither numbers nor letters with an empty string.
* @param original, It is the original string
* @return a string object that contains only letters and numbers
*/
private String removeNonLettersNonNumbers (String original) {
return original.replaceAll("[^\\p{L}\\p{N}]", "");
}
}
CustomWordMapper
类执行以下操作:
-
创建
Text()
类的myword
实例。 -
覆盖超类
Mapper
的map
方法,并实现以下步骤: -
文本对象转换为字符串,并赋值给字符串
line
。 -
Line 是一个传递给字符串标记器的字符串对象。
-
使用
while
循环遍历字符串标记器,并调用removeNonLettersNonNumbers
方法。返回的字符串赋值给myword
文本实例。 -
调用
context.write(myword,newIntwritable(1))
方法。 -
有一个方法可以删除非字母和非数字,使用
string.replaceAll()
方法。最后返回一个只包含数字和字母的字符串对象。
接下来我们将创建一个 reducer 组件。reducer 组件将执行以下任务:
-
扩展
reducer
类。 -
为 reducer 类创建一个字符串属性,该属性接受需要搜索的字符串及其需要找到的出现次数。
-
覆盖
reduce
方法。 -
删除不需要的键值对。
-
保留所需的键值对。
-
检查输入键是否已经存在。如果存在,它将获取出现次数,并将最新值存储。
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class CustomWordReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
protected static final String MY_TARGET_TEXT = "SPRING";
@Override
protected void reduce(Text keyTxt, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
if (containsTargetWord(keyTxt)) {
int wCount = 0;
for (IntWritable value: values) {
wCount += value.get();
}
context.write(key, new IntWritable(wCount));
}
}
private boolean containsTargetWord(Text keyTxt) {
return keyTxt.toString().equals(MY_TARGET_TEXT);
}
}
-
使用 HDFS 端口和输入输出文件路径配置
application.properties
文件。 -
这是示例
application.properties
文件:
fs.default.name=hdfs://localhost:9000
mapred.job.tracker=localhost:9001
input.path=/path/to/input/file/
output.path=/path/to/output/file
一旦属性被配置,它应该在 Spring 上下文中可用。因此,在 Spring 的application-context.xml
文件中使用property-placeholder
配置属性文件。这是需要在application-conext.xml
文件中添加的配置片段。
<context:property-placeholder location="classpath:application.properties" />
您可以直接在application-context.xml
文件中配置 Apache Hadoop,也可以使用属性文件并从属性文件中读取键值对。由于我们使用了属性文件,我们将从属性文件中读取值。以下代码片段显示${mapred.job.tracker}
是属性文件中的一个键。您可以看到默认名称也是使用键${fs.default.name}
从属性文件中配置的。在application-context.xml
文件中配置 Apache Hadoop 如下:
<hdp:configuration>
fs.default.name=${fs.default.name}
mapred.job.tracker=${mapred.job.tracker}
</hdp:configuration>
-
接下来,我们需要在 Spring 中配置 Hadoop 作业:
-
提供作业 ID。
-
指定输入路径;它将从属性文件中读取。
-
指定输出路径;它将从属性文件中读取。
-
按类进行 Jar。
-
Mapper 类引用自定义 mapper 类。
-
Reducer 类引用自定义 reducer 类。
-
这是需要在
application-xccontext.xml
文件中可用的配置片段。在application-context.xml
文件中配置 Hadoop 作业如下:
<hdp:job id="wordCountJobId"
input-path="${input.path}"
output-path="${output.path}"
jar-by-class="net.qs.spring.data.apachehadoop.Main"
mapper="com.packt.spring.data.apachehadoop.CustomWordMapper"
reducer="com.packt.spring.data.apachehadoop.CustomWordReducer"/>
-
最后,我们需要在
application-context.xml
文件中配置作业运行器。作业运行器配置告诉 Spring 框架何时启动作业。在这里,我们已经配置了作业运行器在启动时启动wordcountjob
。 -
这是作业运行器的配置片段。配置
application-context.xml
文件以运行 Hadoop 作业。
<hdp:job-runner id="wordCountJobRunner" job-ref="wordCountJobId" run-at-startup="true"/>
由于这是一个独立的 Spring 应用程序,我们没有一个将调用应用程序上下文的 web 模块。上下文需要在一个类文件中加载。因此,让我们创建一个带有static
方法的Main
类来加载application-context.xml
文件。
我们可以创建一个在启动时加载application-context.xml
文件的类,如下所示:
import org.springframework.context.ApplicationContext;
importorg.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] arguments) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("application-context.xml");
}
}
让我们创建一个名为myinput.txt
的文件,内容如下:
SPRING IS A SEASON. SPRING IS A FRAMEWORK IN JAVA. ITS SPRING IN INDIA. SPRING IS GREEEN. SPRING SPRING EVERY WHERE
接下来,我们需要通过执行此命令向 HDFS 提供输入文件:
hadoop dfs -put myinput.txt /input/myinput.txt
hadoop dfs -ls /input
运行Main
类以查看输出。
使用 Hadoop 流和 Spring DataApache Hadoop 进行 Map 和 Reduce 作业
在本节中,我们将演示使用 Unix shell 命令进行 Map 和 Reduce 数据流。由于这与 Hadoop 流相关,我们将在 Unix 系统上设置一个 Hadoop 实例。Hadoop 实例始终在 Unix 机器上以生产模式运行,而在开发中,将使用 Windows Hadoop 实例。
- 这些是设置要求的要求:
-
JAVA 1.7.x
-
必须安装 SSH
-
下载最新的 Apache Hadoop 分发二进制包。
-
解压并将包提取到一个文件夹中。
-
设置以下环境变量:
-
JAVA_HOME
-
HADOOP_HOME
-
HADOOP_LOG_DIR
-
PATH
我们还需要配置 Hadoop 安装目录的conf
文件夹中存在的文件:
-
Core-site.xml
-
Hdfs-site.xml
-
Mapred-site.xml
我们需要设置一个默认的 Hadoop 文件系统。
- 要配置默认的 Hadoop 文件系统,请在
core-site.xml
文件中提供设置信息。
<configuration>
<property>
<name>fs.default.name</name>
<value>hdfs://localhost:9000</value>
</property>
</configuration>
- 还要配置复制因子。复制因子配置确保文件的副本存储在 Hadoop 文件系统中。在
hdfs-site.xml
文件中设置属性dfs.replication
及其值。
<configuration>
<property>
<name>dfs.replication</name>
<value>1</value>
</property>
</configuration>
- 最后,配置作业跟踪器;此配置在
mapred-site.xml
文件中完成。
<configuration>
<property>
<name>mapred.job.tracker</name>
<value>localhost:9001</value>
</property>
</configuration>
- 要在伪分布式模式下运行 Hadoop,我们只需要格式;在
bin
文件夹中,有start
和stop
Hadoop 实例命令。
接下来,我们将演示如何将 Python 与 Apache Hadoop 数据集成。
我们将使用 Maven 创建一个简单的项目。这些是依赖关系:
<!-- Spring Data Apache Hadoop -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-hadoop</artifactId>
<version>1.0.0.RC2</version>
</dependency>
<!-- Apache Hadoop Core -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-core</artifactId>
<version>1.0.3</version>
</dependency>
<!-- Apache Hadoop Streaming -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-streaming</artifactId>
<version>1.0.3</version>
</dependency>
我们需要一个 mapper 和 reducer Python 脚本。Python 中的 mapper 脚本应该实现以下功能:
-
脚本应该从标准输入流中读取,一次读取一行输入,并将其转换为 UTF-8
-
行中的单词必须分割成单词
-
行中的特殊字符需要替换为空字符,然后得到一个键值对作为制表符;它们被限定到标准输出
这是 Python 中的 mapper 脚本:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
import unicodedata
# Removes punctuation characters from the string
def strip_punctuation(word):
return ''.join(x for x in word if unicodedata.category(x) != 'Po')
#Process input one line at the time
for line in sys.stdin:
#Converts the line to Unicode
line = unicode(line, "utf-8")
#Splits the line to individual words
words = line.split()
#Processes each word one by one
for word in words:
#Removes punctuation characters
word = strip_punctuation(word)
#Prints the output
print ("%s\t%s" % (word, 1)).encode("utf-8")
Python 中的 Reducer 脚本应该实现以下功能:
- 脚本应该读取从
mapper
类生成的键值对输出。然后,计算关键字的出现次数。
#!/usr/bin/python
# -*- coding: utf-8 -*-s
import sys
wordCount = 0
#Process input one line at the time
for line in sys.stdin:
#Converts the line to Unicode
line = unicode(line, "utf-8")
#Gets key and value from the current line
(key, value) = line.split("\t")
if key == "Amily":
#Increase word count by one
wordCount = int(wordCount + 1);
#Prints the output
print ("Watson\t%s" % wordCount).encode("utf-8")
- 一旦 Python 脚本准备就绪,我们需要在属性文件中提供 mapper 和 reducer 类名和配置。这是
.properties
文件:
#Configures the default file system of Apache Hadoop
fs.default.name=hdfs://localhost:9000
#The path to the directory that contains our input files
input.path=/input/
#The path to the directory in which the output is written
output.path=/output/
#Configure the path of the mapper script
mapper.script.path=pythonmapper.py
#Configure the path of the reducer script
reducer.script.path=pythonreducer.py
- 我们还需要在
context.xml
文件中配置property-placeholder
和 Apache Hadoop。这是配置:
<context:property-placeholder location="classpath:application.properties" />
<hdp:configuration>
fs.default.name=${fs.default.name}
</hdp:configuration>
- 最后,我们需要配置 Hadoop 作业并将作业分配给作业运行器,该运行器将初始化作业。
<hdp:configuration>
fs.default.name=${fs.default.name}
</hdp:configuration>
<hdp:streaming id="streamingJob"
input-path="${input.path}"
output-path="${output.path}"
mapper="${mapper.script.path}"
reducer="${reducer.script.path}"/>
<hdp:job-runner id="streamingJobRunner" job-ref="streamingJob" run-at-startup="true"/>
- 现在,我们需要使用应用程序上下文来调用配置,以便应用程序上下文加载 Spring 框架中的所有配置。
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] arguments) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
}
}
- 在命令提示符中运行以下命令以提供输入文件。让文件放在名为
input
的文件夹中:
hadoop dfs -put MILLSANDBOON.txt /input/ MILLSANDBOON.txt
- 输出可以在输出目录中使用以下命令读取。
hadoop dfs -rmr /output
hadoop dfs -cat /output/part-00000
您应该看到一个输出,显示提供的文本中单词“Amily”的出现次数。
摘要
到目前为止,我们已经看到了 Spring 如何与 Apache Hadoop 集成,并提供了搜索和计数数据的 Map 和 Reduce 过程。我们还讨论了 Python 与 Apache Hadoop 的集成。我们已经演示了如何在 Spring 框架中配置 Hadoop 作业,并且还看到了 HDFS 配置。
Hadoop 是一个庞大的概念。有关更多信息,请参阅docs.spring.io/spring-hadoop/docs/current/reference/html/
和github.com/spring-projects/spring-hadoop-samples
。
我们已经演示了如何在 Unix 机器上安装 Hadoop 实例。在下一章中,我们将看到如何在 OSGI 中使用 Spring 动态模块。
第八章:Spring 与 OSGI
OSGI是Open Service Gateway Intiative的缩写。这是一个规范,包括用于动态部署模块的模块化系统和服务平台。根据规范,应用程序可以分解为模块并独立部署。当我们考虑开发 OSGI 应用程序时,这意味着我们需要使用可用的 OSGI API 来开发应用程序。第二步是将其部署到 OSGI 容器中。因此,在 OSGI 中开发应用程序时,我们可以将应用程序分解为模块并独立部署它们,然后卸载;我们还可以并行运行应用程序的各个版本。在本章中,我们将看到 Spring 如何支持 OSGI 捆绑开发及其应用程序的部署。我们将首先从 OSGI 开始,然后逐渐转向 Spring 的支持。
OSGI 容器
OSGI 容器必须实现一组服务,并且 OSGI 容器与应用程序之间建立了一项合同。以下提到的所有 OSGI 容器都是开源的:
-
KnoplerFish:Knopler 框架可以很容易地安装,并且更容易地将模块捆绑和部署到容器中。捆绑应用程序需要一个
.manifest
文件和构建.xml
文件。必须拥有该框架。 JAR 文件应该在 Java 构建路径中可用。需要在 KnoplerFish 容器中部署的捆绑包将具有一个实现BundleActivator
接口的类。该接口带有需要实现的start()
和stop()
方法。通常还会创建一个线程类,并且在BundleActivator
接口实现类的 start 方法中启动该线程,并在 stop 方法中停止。您还可以通过创建一个接口和实现类来创建一个 OSGI 服务。该服务可以在BundleActivator
类的start()
方法中注册。这是实现BundleActivator
接口的类。有ServiceListeners
和ServiceTrackers
来监视容器中的 OSGI 服务。 -
Equinox:这是核心 OSGI 框架的实现。它提供各种可选的 OSGI 服务。Eclipse 提供了一个 OSGI 插件来开发 OSGI 捆绑应用程序。Eclipse 提供了一个 JAR 文件,可以使用 Eclipse 的安装启动、停止命令轻松安装。
-
Apache Felix:Apache Felix 是 Apache 项目的另一个 OSGI 容器。Felix 有各种子项目可以插入。它还支持与 Knoplerfish 下的应用程序开发类似的方式。它还有一个 Maven 捆绑插件。
OSGI 使用
让我们列出 OSGI 框架的关键用途:
-
该框架提供了应用程序的模块化
-
该框架实现了基于捆绑包的架构
-
可以并行运行同一项目的多个版本
-
我们还可以将 OSGI 应用程序和 OSGI 捆绑包集成到 Web 容器中
-
使其与 Web 应用程序的前端配合工作也存在一些挑战
-
有很多框架,至少有四个框架,可用于在 OSGI 规范之上开发 POJO 应用程序
-
OSGI 捆绑包的大小相对较小
Spring 与 OSGI 的集成
Spring 为 OSGI 开发提供了完整的支持。OSGI 模块支持被称为 Spring OSGI,目前已更新为一组新的库和版本,称为 Spring Dynamic Modules。Spring 动态模块允许您在 OSGI 框架之上编写 Spring 应用程序。其挑战之一是使简单的 POJO 能够与 OSGI 框架无缝配合,并将 Spring Beans 集成为 OSGI 服务。Spring Beans 可以导出为 OSGI 服务
<bean name="authorService"
class="com.packt.osgi.authorservice.impl.AuthorServiceImpl"/>
<osgi:service id="auhtorServiceOsgi"
ref="authorService"
interface="com.packt.osgi.authorservice.AuthorService"/>
Spring 动态编程模型提供了 API 编程,Spring Beans 在捆绑中可见。Spring 动态模型为我们提供了跨捆绑的依赖注入,并且通过 Spring 动态服务提供了对 OSGI 的所有支持,处理变得更加容易。
每个捆绑理想上都应该有一个单独的应用上下文。应用上下文随着捆绑的启动和停止而创建和销毁。这些上下文文件位于 META-INF 下。
典型的捆绑结构如下图所示:
下图展示了 OSGI 模块如何成为 Web 应用程序的一部分,以及每个捆绑如何与 OSGI 框架交互。您还可以看到 Web 容器上有许多 Web 应用程序,它们使用 OSGI 框架作为服务访问应用程序捆绑。
Spring 动态模块和 OSGI
让我们看看 Spring 动态模块是如何工作的。Spring 带有其 OSGI 集成框架,其中有一个名为extender
的类。这个类检查所有现有的捆绑,并标记由 Spring 提供支持的捆绑。只有具有 Spring 上下文清单标头或META-INF
/spring
文件夹中的 XML 文件的捆绑才会被标记为 Spring 兼容。所有前面的步骤都是在启动时触发的,extender
包在org.springframeork.osgi.bundle.extender
中可用。现在,我们必须知道为什么 Spring 动态模块会标记 Spring 支持的捆绑。具有 Spring 配置文件的捆绑会进一步转换为应用上下文对象。extender
不仅标记 Spring 支持的捆绑以创建应用上下文对象,还会检查捆绑是否导入任何 OSGI 服务。如果发现任何导出外部服务的捆绑,则这些 bean 将移动到 OSGI 共享服务注册表中。extender
使用监听器和事件注册导出 OSGI 服务的捆绑。OSGI 还与 Equinox、Felix 和 KnoplerFish 进行了测试。
在 Eclipse IDE 中设置 Spring DM 时,需要遵循以下步骤:
-
下载 Spring DM;寻找最新的 Spring OSGI DM。
-
将 ZIP 文件提取到本地目录中;将其命名为
c:\OSGI-SPRING
。 -
在启动 Eclipse 时创建一个新的工作空间。
-
通过选择插件开发选项或 Java 中的安装插件选项导入所有必要的 JAR 文件和所有 Spring DM JAR 文件。确保在 Eclipse 环境中拥有以下提到的所有插件。
-
org.springframeork.osgi.bundle.core
-
org.springframeork.osgi.bundle.extender
-
org.springframeork.osgi.bundle.io
-
org.springframeork.bundle.spring.aop
-
org.springframeork.bundle.spring.beans
-
org.springframeork.bundle.spring.context
-
org.springframeork.bundle.spring.core
-
org.springframeork.bundle.spring.jdbc
-
org.springframeork.bundle.spring.tx
-
org.springframeork.osgi.aopalliance.osgi
简单的 OSGI 应用程序
在本节中,让我们首先开发一个简单的 OSGI 应用程序。我们将创建两个捆绑——一个提供打印字符串的服务,另一个捆绑会以相等的时间间隔消费该服务。
- 以下是第一个捆绑:
package com.packt.osgi.provider.able;
public interface MySimpleOSGIService {
void mysimplemethod();
}
package com.packt.osgi.provider.impl;
import com.bw.osgi.provider.able.MySimpleOSGIService;
public class MySimpleOSGIServiceImpl implements MySimpleOSGIService {
@Override
void mysimplemethod(){
System.out.println("this my simple method which is the implementation class");
}
}
- 使用激活器导出服务:
package com.packt.osgi.provider;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import com.bw.osgi.provider.able.MySimpleOSGIService;
import com.bw.osgi.provider.impl.MySimpleOSGIServiceImpl;
public class MyProviderActivator implements BundleActivator {
private ServiceRegistration registration;
@Override
public void start(BundleContext bundleContext) throws Exception {
registration = bundleContext.registerService(
MySimpleOSGIService.class.getName(),
new MySimpleOSGIServiceImpl(),
null);
}
@Override
public void stop(BundleContext bundleContext) throws Exception {
registration.unregister();
}
}
- 现在,我们已经准备好第一个捆绑,我们将使用 Maven 来构建它。我们还需要 Maven 捆绑插件来构建 XML 文件。
?xml version="1.0" encoding="UTF-8"?>
<project 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>OSGiDmMySimpleProvider</groupId>
<artifactId>OSGiDmMySimpleProvider</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.osgi.core</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<Bundle-SymbolicName>OSGiDmMySimpleProvider</Bundle-SymbolicName>
<Export-Package>com.packt.osgi.provider.able</Export-Package>
<Bundle-Activator>com.packt.osgi.provider.MyProviderActivator</Bundle-Activator>
<Bundle-Vendor>PACKT</Bundle-Vendor>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
</project>
-
要构建它,只需简单的
mvn install
命令即可。 -
接下来,让我们尝试消费服务:
package com.packt.osgi.consumer;
import javax.swing.Timer;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import com.packt.osgi.provider.able.MySimpleOSGIService;
public class MySimpleOSGIConsumer implements ActionListener {
private final MySimpleOSGIService service;
private final Timer timer;
public MySimpleOSGIConsumer(MySimpleOSGIService service) {
super();
this.service = service;
timer = new Timer(1000, this);
}
public void startTimer(){
timer.start();
}
public void stopTimer() {
timer.stop();
}
@Override
public void actionPerformed(ActionEvent e) {
service.mysimplemethod();
}
}
- 现在,我们必须再次为消费者创建一个激活器:
package com.packt.osgi.consumer;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import com.packt.osgi.provider.able.MySimpleOSGIService;
public class MySimpleOSGIActivator implements BundleActivator {
private MySimpleOSGIConsumer consumer;
@Override
public void start(BundleContext bundleContext) throws Exception {
ServiceReference reference = bundleContext.getServiceReference(MySimpleOSGIService.class.getName());
consumer = new MySimpleOSGIConsumer((MySimpleOSGIService) bundleContext.getService(reference));
consumer.startTimer();
}
@Override
public void stop(BundleContext bundleContext) throws Exception {
consumer.stopTimer();
}
}
将 Spring 动态模块与 OSGI 集成
在本节中,让我们演示如何将 Spring 动态模块集成到 OSGI 应用程序中。Spring 动态模块(Spring DM)使得基于 OSGI 的应用程序的开发变得更加容易。我们可以像任何其他 Spring bean 一样轻松地注入服务。
我们将看一下集成 Spring 动态模块所需的以下依赖项:
-
OSGI 服务
-
BundleActivator
类 -
Context.xml
文件配置以注入服务
以下是需要在应用程序类路径中提供的依赖项列表:
-
com.springsource.net.sf.cglib-2.1.3.jar
-
com.springsource.org.aopalliance-1.0.0.jar
-
log4j.osgi-1.2.15-SNAPSHOT.jar
-
com.springsource.slf4j.api-1.5.0.jar
-
com.springsource.slf4j.log4j-1.5.0.jar
-
com.springsource.slf4j.org.apache.commons.logging-1.5.0.jar
-
org.springframework.aop-3.x.jar
-
org.springframework.beans-3.x.jar
-
org.springframework.context-3.x.jar
-
org.springframework.core-3.x.jar
-
spring-osgi-core-1.2.1.jar
-
spring-osgi-extender-1.2.1.jar
-
spring-osgi-io-1.2.1.jar
所以,让我们创建一个简单的HelloWorldService
接口类:
package com.packt.osgi.provider.able;
public interface HelloWorldService {
void hello();
}
接下来,我们将实现service
类。这是一个简单的类
package com.packt.osgi.provider.impl;
import com.packt.osgi.provider.able.HelloWorldService;
public class HelloWorldServiceImpl implements HelloWorldService {
@Override
public void hello(){
System.out.println("Hello World !");
}
}
我们将编写一个激活器类,需要激活服务BundleActivator
。我们需要调用的ProviderActivator
类是HelloWorldService
。实际上,我们正在注册服务。但是,使用 Spring DM 集成使我们的配置变得简单。我们不需要这个集成类。
package com.packt.osgi.provider;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import com. packt.osgi.provider.able.HelloWorldService;
import com. packt.osgi.provider.impl.HelloWorldServiceImpl;
public class ProviderActivator implements BundleActivator {
private ServiceRegistration registration;
@Override
public void start(BundleContext bundleContext) throws Exception {
registration = bundleContext.registerService(
HelloWorldService.class.getName(),
new HelloWorldServiceImpl(),null);
}
@Override
public void stop(BundleContext bundleContext) throws Exception {
registration.unregister();
}
}
我们只需要在META-INF
/spring
文件夹中创建一个provider-context.xml
文件。这是一个简单的 XML 文件上下文,但我们使用一个新的命名空间来注册服务 - www.springframework.org/schema/osgi
。所以,让我们开始:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi.xsd">
<bean id="helloWorldService" class="com.packt.osgi.provider.impl.HelloWorldServiceImpl"/>
<osgi:service ref="helloWorldService" interface="com.packt.osgi.provider.able.HelloWorldService"/>
</beans>
唯一与 OSGI 相关的是osgi:service
声明。这一行表示我们需要将HelloWorldService
注册为 OSGI 服务,使用HelloWorldService
接口作为服务的名称。
如果您将上下文文件放在META-INF
/spring
文件夹中,它将被 Spring Extender 自动检测,并创建一个应用程序上下文。
- 现在我们可以转到消费者 bundle。在第一阶段,我们创建了那个消费者:
package com.packt.osgi.consumer;
import javax.swing.Timer;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import com.bw.osgi.provider.able.HelloWorldService;
public class HelloWorldConsumer implements ActionListener {
private final HelloWorldService service;
private final Timer timer;
public HelloWorldConsumer(HelloWorldService service) {
super();
this.service = service;
timer = new Timer(1000, this);
}
public void startTimer(){
timer.start();
}
public void stopTimer() {
timer.stop();
}
@Override
public void actionPerformed(ActionEvent e) {
service.hello();
}
}
- 接下来,让我们编写
BundleActivator
类:
package com.packt.osgi.consumer;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import com. packt.osgi.provider.able.HelloWorldService;
public class HelloWorldActivator implements BundleActivator {
private HelloWorldConsumer consumer;
@Override
public void start(BundleContext bundleContext) throws Exception {
ServiceReference reference = bundleContext.getServiceReference(HelloWorldService.class.getName());
consumer = new HelloWorldConsumer((HelloWorldService) bundleContext.getService(reference));
consumer.startTimer();
}
@Override
public void stop(BundleContext bundleContext) throws Exception {
consumer.stopTimer();
}
}
注入不再是必要的。我们可以在这里保留计时器的启动,但是,再次,我们可以使用框架的功能来启动和停止计时器。
- 所以,让我们删除激活器并创建一个应用程序上下文来创建消费者并自动启动它,并将其放在
META-INF
/spring
文件夹中:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/osgi
http://www.springframework.org/schema/osgi/spring-osgi.xsd">
<bean id="consumer" class="com.packt.osgi.consumer.HelloWorldConsumer" init-method="startTimer" destroy-method="stopTimer" lazy-init="false" >
<constructor-arg ref="eventService"/>
</bean>
<osgi:reference id="eventService" interface="com.packt.osgi.provider.able.HelloWorldService"/>
</beans>
我们使用init
方法和destroy
方法属性来启动和停止与框架的时间,并使用constructor-arg
来将引用注入到服务中。使用osgi:reference
字段和使用接口作为服务的键来获取对服务的引用。
这就是我们需要做的所有事情。比第一个版本简单多了,不是吗?除了简化之外,您还可以看到源代码既不依赖于 OSGI 也不依赖于 Spring Framework;这就是纯 Java,这是一个很大的优势。
Maven POM 文件与第一阶段相同,只是我们可以削减对 OSGI 的依赖。
提供者:
<?xml version="1.0" encoding="UTF-8"?>
<project
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>OSGiDmHelloWorldProvider</groupId>
<artifactId>OSGiDmHelloWorldProvider</artifactId>
<version>1.0</version>
<packaging>bundle</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<Bundle-SymbolicName>OSGiDmHelloWorldProvider</Bundle-SymbolicName>
<Export-Package>com.bw.osgi.provider.able</Export-Package>
<Bundle-Vendor>Baptiste Wicht</Bundle-Vendor>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
</project>
消费者:
<?xml version="1.0" encoding="UTF-8"?>
<project
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>OSGiDmHelloWorldConsumer</groupId>
<artifactId>OSGiDmHelloWorldConsumer</artifactId>
<version>1.0</version>
<packaging>bundle</packaging>
<dependencies>
<dependency>
<groupId>OSGiDmHelloWorldProvider</groupId>
<artifactId>OSGiDmHelloWorldProvider</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<Bundle-SymbolicName>OSGiDmHelloWorldConsumer</Bundle-SymbolicName>
<Bundle-Vendor>Baptiste Wicht</Bundle-Vendor>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
</project>
我们可以使用 Maven install 构建这两个 bundle。所以,让我们在 Felix 中测试一下我们的东西:
Welcome to Apache Felix Gogo
g! install file:../com.springsource.slf4j.org.apache.commons.logging-1.5.0.jar
Bundle ID: 5
g! install file:../com.springsource.slf4j.log4j-1.5.0.jar
Bundle ID: 6
g! install file:../com.springsource.slf4j.api-1.5.0.jar
Bundle ID: 7
g! install file:../log4j.osgi-1.2.15-SNAPSHOT.jar
Bundle ID: 8
g! install file:../com.springsource.net.sf.cglib-2.1.3.jar
Bundle ID: 9
g! install file:../com.springsource.org.aopalliance-1.0.0.jar
Bundle ID: 10
g! install file:../org.springframework.core-2.5.6.SEC01.jar
Bundle ID: 11
g! install file:../org.springframework.context-2.5.6.SEC01.jar
Bundle ID: 12
g! install file:../org.springframework.beans-2.5.6.SEC01.jar
Bundle ID: 13
g! install file:../org.springframework.aop-2.5.6.SEC01.jar
Bundle ID: 14
g! install file:../spring-osgi-extender-1.2.1.jar
Bundle ID: 15
g! install file:../spring-osgi-core-1.2.1.jar
Bundle ID: 16
g! install file:../spring-osgi-io-1.2.1.jar
Bundle ID: 17
g! start 5 7 8 9 10 11 12 13 14 15 16 17
log4j:WARN No appenders could be found for logger (org.springframework.osgi.extender.internal.activator.ContextLoaderListener).
log4j:WARN Please initialize the log4j system properly.
g! install file:../OSGiDmHelloWorldProvider-1.0.jar
Bundle ID: 18
g! install file:../OSGiDmHelloWorldConsumer-1.0.jar
Bundle ID: 19
g! start 18
g! start 19
g! Hello World !
Hello World !
Hello World !
Hello World !
Hello World !
Hello World !
Hello World !
Hello World !
stop 19
g!
总之,Spring DM 确实使与 OSGI 的开发变得更加容易。使用 Spring DM,您还可以启动 bundle。它还允许您创建 Web bundle 并轻松使用 OSGI compendium 的服务。
摘要
在本章中,我们开发了一个简单的 OSGI 应用程序。我们还演示了 Spring DM 如何支持 OSGI 开发,减少文件的创建,并通过配置使事情变得更容易。
第九章:使用 Spring Boot 引导应用程序
在本章中,我们将看到另一个 Spring 包——Spring Boot,它允许用户快速开始使用 Spring 框架。使用Spring Boot 抽象层的应用程序称为Spring Boot 应用程序。Spring 推出了一个 Spring 初始化器 Web 应用程序,其中有一个 Web 界面,我们可以在其中选择需要启动的应用程序类型。
如果您曾经在不同的应用服务器上运行过,新开发人员通常必须配置许多设置才能启动和运行。Spring Boot 方法允许开发人员立即启动和运行,而无需配置应用服务器,从而可以专注于开发代码。
Spring 还推出了一个命令行界面,帮助我们快速开始 Spring 开发。在本章中,让我们深入了解 Spring Boot 并看看它提供了什么。
设置 Spring Boot
Spring Boot 应用程序可以通过以下方式设置:
-
使用 Maven 从存储库下载依赖项
-
使用 Gradle
-
从 Spring 指南存储库下载源代码
-
下载 Spring STS 并使用启动器项目
Spring Gradle MVC 应用程序
Gradle类似于 Maven;它有助于构建应用程序。我们需要在build.gradle
文件中提供所有依赖信息。Spring Boot 还有一个 Gradle 插件。Gradle 插件有助于将所有依赖的 JAR 文件放置在类路径上,并最终构建成一个可运行的单个 JAR 文件。可运行的 JAR 文件将具有一个application.java
文件;这个类将有一个public static void main()
方法。这个类将被标记为可运行的类。
这里显示了一个示例 Gradle 文件:
buildscript {
repositories {
maven { url "http://repo.spring.io/libs-milestone" }
mavenLocal()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.3.RELEASE")
}
}
apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'spring-boot'
jar {
baseName = PacktSpringBootMVCDemo '
version = '1.0'
}
repositories {
mavenCentral()
maven { url "http://repo.spring.io/libs-milestone" }
}
configurations {
providedRuntime
}
dependencies {
compile ("org.springframework.boot:spring-boot-starter-web")
providedRuntime("org.apache.tomcat.embed:tomcat-embed-jasper")
}
task wrapper(type: Wrapper) {
gradleVersion = '2.0'
}
如果您正在使用 Eclipse 作为 IDE,STS 已经推出了适用于 Eclipse 的 Gradle 插件(gradle.org/docs/current/userguide/eclipse_plugin.html
),可以从www.gradle.org/tooling
下载并安装。Gradle 也提供了类似的设置来清理和构建应用程序。
下一步是在属性文件中定义应用程序上下文根。Gradle 项目结构类似于 Maven 项目结构。将application.properties
文件放在resources
文件夹中。我们需要提供服务器上下文路径和服务器上下文端口。以下是示例属性文件:
server.contextPath=/PacktSpringBootMVCDemo
server.port=8080
-
让我们创建一个简单的包:
com.packt.controller
-
在包中创建一个简单的 Spring 控制器类,并使用@ Controller 注解。
-
让我们创建一个带有
@Request
映射注解的方法。@RequestMapping
注解将请求映射到 JSP 页面。在这个方法中,我们将请求映射到方法。这些方法返回一个字符串变量或模型视图对象。
package com.packt.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class PacktController{
@RequestMapping(value = "/saygoodmorning method = RequestMethod.GET)
public ModelAndView getGoodmorning() {
return new ModelAndView("greet").addObject("greet", "goodmorning");
}
@RequestMapping(value = "/saygoodafternoon method = RequestMethod.GET)
public ModelAndView getGoodmorning() {
return new ModelAndView("greet").addObject("greet ", "goodafternoon");
}
@RequestMapping(value = "/saygoodnight method = RequestMethod.GET)
public ModelAndView getGoodmorning() {
return new ModelAndView("greet").addObject("greet ", "goodnight");
}
}
- 创建一个 Spring MVC 配置文件,使用
@Configuration
和@WebMVC
annotation
如下。我们还为应用程序文件配置了内部视图解析器。
package com.packt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@EnableWebMvc
public class ApplicationConfigurerAdapter extends WebMvcConfigurerAdapter{
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("WEB-INF/jsp/");
resolver.setSuffix(".jsp");
return resolver;
}
}
让我们创建一个名为greet.jsp
的简单 JSP 页面:
<html>
<head><title>Hello world Example</title></head>
<body>
<h1>Hello ${name}, How are you?</h1>
</body>
</html>
接下来创建一个简单的应用程序类,使用@EnableAutoConfiguration
和@ComponentScan
注解。@ComponenetScan
注解表示 Spring 框架核心应搜索包下的所有类。@EnableAutoConfiguration
注解用于代替在web.xml
文件中配置 dispatcher servlet。
以下是示例文件:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
访问以下 URL:
-
http://localhost:8080/PacktSpringBootMVCDemo/saygoodmorning
-
http://localhost:8080/PacktSpringBootMVCDemo/saygoodafternoon
-
http://localhost:8080/PacktSpringBootMVCDemo/saygoodnight
使用 Spring Boot 进行热交换
热交换或热部署意味着您可以对类文件或应用程序中的任何文件进行更改,并立即在运行中的应用程序中看到更改。我们可能需要重新加载 Web 浏览器上的应用程序,或者只需刷新页面。Spring Loaded 是一个支持热部署的依赖 JAR 文件。让我们看看 Spring Boot 应用程序中的热交换。
让我们使用 Thymeleaf 模板引擎创建一个简单的 Spring MVC 应用程序:
- 首先,我们需要从 GitHub 存储库下载 Spring Loaded JAR。检查以下 URL 以获取最新版本:
github.com/spring-projects/spring-loaded
。
- 确保您在
pom.xml
文件中具有所有提到的依赖项,或者将它们明确添加到您的项目中:
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
-
下一步是将下载的 Spring loaded JAR 添加到 Eclipse 或 Eclipse STS 环境中。按照给定的步骤将 Spring loaded JAR 添加为运行时配置:
-
在 Eclipse 中创建一个
PacktSpringBootThymeLeafExample
项目。 -
右键单击您的项目。
-
搜索Run As。
-
点击Run Configuration。
-
点击 Java 应用程序。
-
点击项目名称。
-
在VM Argument部分中选择Arguments;添加以下命令:
- javaagent:/<provide the path to the jar>/springloaded-1.2.0.RELEASE.jar -noverify
- 点击Apply和Run。
我们还需要配置application.properties
文件,以便对Thymeleaf页面进行任何修改时不需要重新启动服务器:
spring.thymeleaf.cache: false.
我们可以使用 Spring STS starter 项目并创建一个 Spring Boot 类。Spring Eclipse STS 将为我们提供以下两个类:
-
Application.java
-
ApplicationTest.java
Application.java
是 Spring Boot 的主类,因为它在其中有 public static void main 方法。在这个方法中,使用SpringApplication
类对ApplicationContext
进行初始化。SpringApplication
类具有以下一些注解:
-
@ConditionalOnClass
-
@ConditionalOnMissingBean
这些用于检查类路径上可用的 bean 列表。如果您想要查看框架放置在类路径下的 bean,可以对生成的Application.java
文件进行轻微修改,如下所示:
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
System.out.println("---------------------------LIST BEANS PROVIDED BY SPRING BOOT_---------------------");
String[] beanNames = ctx.getBeanDefinitionNames();
Arrays.sort(beanNames);
for (String beanName : beanNames) {
System.out.println(beanName);
}
}
}
输出:
---------------------------LIST BEANS PROVIDED BY SPRING BOOT_---------------------
JSPController
application
applicationContextIdFilter
auditEventRepository
auditListener
autoConfigurationAuditEndpoint
basicErrorController
beanNameHandlerMapping
beanNameViewResolver
....
mappingJackson2HttpMessageConverter
messageConverters
messageSource
tomcatEmbeddedServletContainerFactory
traceEndpoint
traceRepository
viewControllerHandlerMapping
viewResolver
webRequestLoggingFilter
SpringApplication
类位于org.springframework.boot.SpringApplication
包中。
这里显示了SpringApplication
类的一个简单示例,其中显示了SpringApplication
类的静态运行方法:
@Configuration
@EnableAutoConfiguration
public class MyPacktApplication {
// ... Bean definitions
public static void main(String[] args) throws Exception {
SpringApplication.run(MyPacktApplication.class, args);
}
在这里看另一个例子,首先初始化一个SpringApplication
类,然后调用.run
方法:
@Configuration
@EnableAutoConfiguration
public class MyPacktApplication {
// ... Bean definitions
public static void main(String[] args) throws Exception {
SpringApplication app = new SpringApplication(MyPacktApplication.class);
// ... customize app settings here
app.run(args)
}
}
以下是SpringApplication
类可用的构造函数:
-
SpringApplication(Object... sources)
-
SpringApplication(ResourceLoader resourceLoader, Object... sources)
- 让我们创建一个带有 Spring 最新版本 4.x 中可用的
@RestController
注解的简单 Controller 类。
@RestController
public class MyPacktController {
@RequestMapping("/")
public String index() {
return "Greetings ";
}
@RequestMapping("/greetjsontest")
public @ResponseBody Map<String, String> callSomething () {
Map<String, String> map = new HashMap<String, String>();
map.put("afternoon", " Good afternoon");
map.put("morning", " Good Morning");
map.put("night", " Good Night");
return map;
}
}
- 接下来,我们将配置 Spring Boot 来处理 JSP 页面;默认情况下,Spring Boot 不配置 JSP,因此我们将创建一个 JSP 控制器,如下面的代码片段所示:
@Controller
public class SpringBootJSPController {
@RequestMapping("/calljsp")
public String test(ModelAndView modelAndView) {
return "myjsp";
}
}
- 按照以下方式配置属性文件:
spring.view.prefix: /WEB-INF/jsp/
spring.view.suffix: .jsp
- 让我们创建一个 JSP 文件
myjsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Hello world</h1>
</body>
</html>
以下是EmbededServletContainerCustomizer
的实现类,它实际上将 Web 服务器容器嵌入应用程序中。它调用服务器并将应用程序部署到其中。
@ComponentScan
@EnableAutoConfiguration
public class Application implements EmbeddedServletContainerCustomizer {
@Value("${someproperty:webapp/whereever }")
private String documentRoot;
@Override
public void customize(ConfigurableEmbeddedServletContainerFactory factory) {
factory.setDocumentRoot(new File(documentRoot));
}
}
将 Spring Boot 与 Spring 安全集成
在本节中,我们将看到如何使用注解将 Spring Boot 与 Spring 安全集成。我们可以很容易地将 Spring 安全与 Spring Boot 集成。
- 让我们首先在 Spring boot 中嵌入一个 tomcat 服务器来接受请求;以下是我们需要创建一个密钥库文件使其更安全的代码:
@Bean
EmbeddedServletContainerCustomizer containerCustomizer (
@Value("${https.port}") final int port,
@Value("${keystore.file}") Resource keystoreFile,
@Value("${keystore.alias}") final String alias,
@Value("${keystore.password}") final String keystorePass,
@Value("${keystore.type}") final String keystoreType) throws Exception {
final String absoluteKeystoreFile = keystoreFile.getFile().getAbsolutePath();
return new EmbeddedServletContainerCustomizer() {
public void customize(ConfigurableEmbeddedServletContainer container) {
TomcatEmbeddedServletContainerFactory tomcat = (TomcatEmbeddedServletContainerFactory) container;
tomcat.addConnectorCustomizers(new TomcatConnectorCustomizer() {
public void customize(Connector connector) {
connector.setPort(port);
connector.setSecure(true);
connector.setScheme("https");
Http11NioProtocol proto = (Http11NioProtocol) connector.getProtocolHandler();
proto.setSSLEnabled(true);
proto.setKeystoreFile(absoluteKeystoreFile);
proto.setKeyAlias(alias);
proto.setKeystorePass(keystorePass);
proto.setKeystoreType(keystoreType);
}
});
}
};
}
- 让我们还使用
@Configuration
和@EnableWebMVCSecurity
注解在 java 中创建一个简单的安全配置文件。安全配置文件扩展了WebSecurityConfigurerAdapter
。
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${ldap.domain}")
private String DOMAIN;
@Value("${ldap.url}")
private String URL;
@Value("${http.port}")
private int httpPort;
@Value("${https.port}")
private int httpsPort;
@Override
protected void configure(HttpSecurity http) throws Exception {
/*
* Set up your spring security config here. For example...
*/
http.authorizeRequests().anyRequest().authenticated().and().formLogin().loginUrl("/login").permitAll();
/*
* Use HTTPs for ALL requests
*/
http.requiresChannel().anyRequest().requiresSecure();
http.portMapper().http(httpPort).mapsTo(httpsPort);
}
@Override
protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {
authManagerBuilder.authenticationProvider(activeDirectoryLdapAuthenticationProvider()).userDetailsService(userDetailsService());
}
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(activeDirectoryLdapAuthenticationProvider()));
}
@Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(DOMAIN, URL);
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
return provider;
}
}
Eclipse Spring Boot 的 Cloud Foundry 支持
在本节中,让我们看看如何使用 Spring boot 在 Cloud Foundry 上开发应用程序。Cloud Foundry是一个用作服务云应用程序的平台。它是一个开放的 PaaS。PaaS 使得在云上运行、部署和运行应用程序成为可能。
参考以下链接,其中提供了有关作为服务可用的 Spring 平台的完整信息以及如何配置 Spring 以与 Cloud Foundry 一起工作。您将看到它提供了从 MongoDB 到 RabbitMQ 消息服务器的平台作为服务。
docs.cloudfoundry.org/buildpacks/java/spring-service-bindings.html
Eclipse 还推出了一个针对云平台的插件,可以从以下给定位置下载和安装。该插件支持 Spring boot 和 grails 应用程序。您还可以创建一个服务器实例到使用自签名证书的私有云。
github.com/cloudfoundry/eclipse-integration-cloudfoundry
我们所需要做的就是开发一个简单的启动应用程序,并将其拖放到 Cloud Foundry 服务器中,然后重新启动服务器。
使用 Spring Boot 开发 RestfulWebService
在本节中,让我们开发一个简单的 restful 服务,并使用SpringBoot
引导应用程序。我们还将创建一个简单的 restful 服务,将产品信息存储到数据库中。
产品创建场景应满足以下提到的用例:
-
假设不存在具有相同
Product_id
的产品,它应该将新产品存储在数据库中并立即返回存储的对象。 -
假设存在一个具有相同
Product_id
的产品,它不应该存储,而是返回一个带有相关消息的错误状态。 -
假设以前存储的产品,它应该能够检索它们的列表。
以下是pom.xml
文件,用于应用程序中使用的依赖项引用。您可以看到我们在这里使用了父 Spring boot 引用,以便我们可以解析所有依赖项引用。我们还在pom.xml
文件中设置了 Java 版本为 1.7。
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.packt.restfulApp</groupId>
<artifactId>restfulSpringBootApp</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.1.RELEASE</version>
</parent>
<name>Example Spring Boot REST Service</name>
<properties>
<java.version>1.7</java.version>
<guava.version>16.0.1</guava.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
</project>
让我们看看pom.xml
文件中使用的依赖项。以下是使用的 Spring boot 依赖项。还要注意,版本信息没有指定,因为它由前面提到的spring-boot-starter-parent
管理。
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Hibernate validator -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<!-- HSQLDB -->
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Java EE -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
</dependencies>
我们还将看到为什么这些依赖项被用于 Spring boot。当涉及到 Spring boot 时,它的功能分布在 starter 模块之间:
-
spring-boot-starter
:这是 Spring boot 的主要核心模块 -
spring-boot-starter-test
:这里有一些用于单元测试的工具,包括 JUnit4 和 Mockito -
spring-boot-starter-web
:这会拉取 Spring MVC 依赖项,还有将用于 JSON 的 Jackson,最重要的是 Tomcat,它充当嵌入式 Servlet 容器 -
spring-boot-starter-data-jpa
:用于设置 Spring Data JPA,并与 Hibernate 捆绑在一起 -
Guava
:它使用@Inject
注释而不是@Autowired
最后,添加一个 Spring boot Maven 插件如下。spring-boot-maven
插件的功能如下:
-
它为 Maven 提供了一个
spring-boot:run
目标,因此应用程序可以在不打包的情况下轻松运行。 -
它钩入一个打包目标,以生成一个包含所有依赖项的可执行 JAR 文件,类似于
maven-shade
插件,但方式不那么混乱。
<build>
<plugins>
<!-- Spring Boot Maven -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
到目前为止,我们已经看了依赖项及其功能,现在让我们开始构建应用程序。
Bean 类或实体类:
- 让我们创建一个简单的
Product.java
文件如下:
@Entity
public class Product {
@Id
@Column(name = "id", nullable = false, updatable = false)
@NotNull
private Long product_id;
@Column(name = "password", nullable = false)
@NotNull
@Size(max = 64)
private String product_name;
public Action(Long product_id, String product_name) {
this. produc_id = product_id;
this. produc_name = produc_name;
}
- 接下来创建一个
Jparepository
子接口;我们不需要为此提供任何实现,因为它由 Spring JPA 数据处理:
public interface ProductRepository extends JpaRepository<Product, String>{
}
服务类:
- 让我们创建一个处理保存的服务接口。
public interface ProductService {
Product save(Product product);
}
- 我们还应该为服务接口创建一个实现类:
@Service
public class ProductServiceImpl implements ProductService {
private final ProductRepository repository;
@Inject
public ProductServiceImpl(final ProductRepository repository) {
this.repository = repository;
}
@Override
@Transactional
public Product save(final Product product) {
Product existing = repository.findOne(Product.getId());
if (existing != null) {
throw new ProductAlreadyExistsException(
String.format("There already exists a Product with id=%s", product.getId()));
}
return repository.save(product);
}
- 在下一步中,我们还将创建一个用于服务
Impl
的测试类,如下所示:
@RunWith(MockitoJUnitRunner.class)
public class ProductControllerTest {
@Mock
private ProductService ProductService;
private ProductController ProductController;
@Before
public void setUp() {
ProductController = new ProductController(ProductService);
}
@Test
public void shouldCreateProduct() throws Exception {
final Product savedProduct = stubServiceToReturnStoredProduct();
final Product Product = new Product();
Product returnedProduct = ProductController.createProduct(Product);
// verify Product was passed to ProductService
verify(ProductService, times(1)).save(Product);
assertEquals("Returned Product should come from the service", savedProduct, returnedProduct);
}
private Product stubServiceToReturnStoredProduct() {
final Product Product = new Product();
when(ProductService.save(any(Product.class))).thenReturn(Product);
return Product;
}
- 让我们使用
@RestController
注解创建一个控制器;还要注意我们使用了@Inject
注解。
-
@RestController
:这与@Controller
注解的区别在于前者在每个方法上也意味着@ResponseBody
,这意味着写的内容更少,因为从 restful web 服务中我们无论如何都会返回 JSON 对象。 -
@RequestMapping
:这将createProduct()
映射到/Product
URL 上的POST
请求。该方法将产品对象作为参数。它是从请求的主体中创建的,这要归功于@RequestBody
注解。然后进行验证,这是由@Valid
强制执行的。 -
@Inject
:ProductService
将被注入到构造函数中,并且产品对象将被传递给其save()
方法进行存储。存储后,存储的产品对象将被自动转换回 JSON,即使没有@ResponseBody
注解,这是@RestController
的默认值。
@RestsController
public class ProductController {
private final ProductService ProductService;
@Inject
public ProductController(final ProductService ProductService) {
this.ProductService = ProductService;
}
@RequestMapping(value = "/Product", method = RequestMethod.POST)
public Product createProduct(@RequestBody @Valid final Product Product) {
return ProductService.save(Product);
}
}
- 让我们创建一个带有
public static void main()
的Main
类。我们还可以使用这些注解:
-
@Configuration
- 这告诉 Spring 框架这是一个配置类 -
@ComponentScan
- 这使得可以扫描包和子包以寻找 Spring 组件 -
@EnableAutoConfiguration
该类进一步扩展了SpringBootServletInitializer
,它将为我们配置调度程序 servlet 并覆盖configure
方法。
以下是Main
类:
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application extends SpringBootServletInitializer {
public static void main(final String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
protected final SpringApplicationBuilder configure(final SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
- 现在,让我们使用 Maven 和 Bootstrap 运行应用程序:
mvn package
java -jar target/restfulSpringBootApp.jar
现在,您可以做到这一点:
curl -X POST -d '{ "id": "45", "password": "samsung" }' http://localhost:8080/Product
并查看 http://localhost:8080/的响应是否如下:
{ "id": "45", "password": "samsung" }
总结
在本章中,我们演示了使用 Spring Boot 启动应用程序的过程。我们从设置一个简单的 Spring Boot 项目开始。我们还创建了一个带有 Gradle 支持的简单 MVC 应用程序。接下来,我们讨论了使用 Spring Boot 进行热交换 Java 文件的方法。
我们还提供了关于 Spring Boot 如何支持云平台服务器并帮助在云上部署应用程序的信息。最后,我们演示了一个使用 Spring Boot 的 restful 应用程序。
在下一章中,我们将讨论 Spring 缓存。
第十章:Spring 缓存
自 Spring 3.1 版本以来,Spring 缓存已经开始起作用。Spring 还添加了注释来支持缓存机制。缓存抽象层提供了很多支持来使用不同的缓存解决方案。在本章中,我们将探讨 Spring 缓存。我们将看到如何设置 Spring 缓存。您可以理想地将您的缓存代码与业务逻辑绑定在一起。
缓存避免重新计算。理想情况下,您不必再次重复相同的过程来获取相同的值。缓存将值存储在内存中。您可以随时选择您想要缓存和不想要缓存的内容。这是架构设计的一部分。一旦数据被缓存,它将从缓存的内存中检索,从而节省计算时间。
用于缓存的 Spring 注释
Spring 提出了两个主要的用于缓存的注释;我们将在整个章节中使用这些。以下是这两个注释:
-
@Cacheable
:这可以用于标记将存储在缓存中的方法和返回值。这可以应用于方法或类型级别。 -
当应用于方法级别时,被注释方法的返回值将被缓存
-
当应用于类型级别时,每个方法的返回值都被缓存
-
@CacheEvict
:用于释放缓存内存中的对象。
@Cacheable 用法
让我们看一下在类型级别应用@Cacheable
注解的小实现。我们考虑一个简单的 DAO 类,有两个不同名称的方法。我们使用了@Cacheable
注解,它接受三个参数:
-
值
-
键
-
条件
现在我们可以实现它:
@Cacheable(value = "product")
public class ProductDAO {
public Product findProduct(String Name, int price) {
return new Product(Name,price);
}
public Product findAnotherProduct(String Name, int price) {
return new Product(Name,price);
}
}
在上述代码中,Spring 缓存默认会分配一个缓存键,带有注释的签名。
我们还可以提供自定义键。使用 SpEL 表达式,以下是提供自定义缓存键的演示:
public class ProductDAO {
public Product findProduct(String productName, int price) {
return new Product(productName,price);
}
@Cacheable(value = "product" ,key="#productName")
public Product findAnotherProduct(String productName, int price) {
return new Product(productName,price);
}
}
我们也可以执行条件缓存。让我们对价格大于 1000 的产品进行条件缓存:
@Cacheable(value = "product", condition = "#price>1000")
public Product findProductByPrice(String productName, int price) {
return new Product(String productName, int price);
}
@CacheEvict 用法
让我们看一下如何使用@CacheEvict
来刷新缓存中的单个对象和多个对象。每次用户添加评分时,productId
都将有新的缓存值。以前的评分将被清除:
@Transactional
@CacheEvict(value="products", key="#rating.producttId")
public ItemRatingResponse addRatingForproduct(Rating rating, Integer currentNumberOfRatings, Float currentRating) {
return addRatingForItem(rating, currentNumberOfRatings, currentRating);
}
以下是用于刷新所有缓存对象的@CacheEvict
用法。您可以看到一次刷新多个对象。
@Caching(evict = {
@CacheEvict(value="referenceData", allEntries=true),
@CacheEvict(value="product", allEntries=true),
@CacheEvict(value="searchResults", allEntries=true),
@CacheEvict(value="newestAndRecommendedproducts", allEntries=true),
@CacheEvict(value="randomAndTopRatedproducts", allEntries=true)
})
public void flushAllCaches() {
LOG.warn("All caches have been completely flushed");
}
Spring 缓存存储库
缓存存储库是实际对象保存的地方。Spring 支持两种类型的存储库:
使用ConcurrentMap
也是在应用程序中实现缓存的选项。存储库对代码几乎没有(如果有的话)影响,并且在不同存储库之间切换应该非常容易。我们的对象将被缓存在 ConcurrentMap 中。
我们可以根据以下代码配置 ConcurrentMap:
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="task" />
</set>
</property>
</bean>
Ehcache 流行的库
这个缓存被许多流行的框架用来处理应用程序中的缓存。ehcache 被 hibernate 框架用来处理应用程序的 DAO(数据访问)层中的缓存。
我们可以有多个存储库。请注意,此存储库的名称必须与注释中使用的名称相同。
Spring CacheManager
让我们看一下在 Spring 缓存框架中用于配置缓存的核心接口和实现类。Spring CacheManager 实际上是 Spring 缓存框架中的一个接口。以下是实现 CacheManager 接口的类的列表:
-
AbstractCacheManager
:这个抽象类实现了CacheManager
接口。它对于静态环境很有用,其中后备缓存不会改变。 -
CompositeCacheManager
:这是复合CacheManager
实现,它遍历给定的CacheManager
实例集合。它允许自动将NoOpCacheManager
添加到列表中,以处理没有后备存储的缓存声明。 -
ConcurrentMapCacheManager
:这是CacheManager
的实现,它会为每个getCache(java.lang.String)
请求懒惰地构建ConcurrentMapCache
实例。它还支持一个静态模式,其中缓存名称集合是通过setCacheNames(java.util.Collection)
预定义的,不会在运行时动态创建更多的缓存区域。 -
ehCacheCacheManager
:由 EhCacheCacheManager
支持的CacheManager
。 -
NoOpCacheManager
:适用于禁用缓存的基本的无操作 CacheManager 实现,通常用于支持缓存声明而没有实际的后备存储。它将简单地接受任何项目到缓存中,而不实际存储它们。 -
SimpleCacheManager
:Simple CacheManager 针对给定的缓存集合工作。这对于测试或简单的缓存声明很有用。
Spring 的 Maven 依赖与缓存
如果您正在使用 Maven 作为构建工具,请确保在pom.xml
文件中添加 ehcache 依赖项。以下是在 Spring 的缓存框架中使用缓存的 Maven 依赖项:
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.7.4</version>
</dependency>
ehcache 的声明式配置
在下一节中,我们可以看到如何以声明方式配置缓存存储。ecache.xml
文件如下:
<ehcache
xsi:noNamespaceSchemaLocation="ehcache.xsd"
updateCheck="true"
monitoring="autodetect"
dynamicConfig="true"
maxBytesLocalHeap="150M"
>
<diskStore path="java.io.tmpdir"/>
<cache name="searchResults"
maxBytesLocalHeap="100M"
eternal="false"
timeToIdleSeconds="300"
overflowToDisk="true"
maxElementsOnDisk="1000"
memoryStoreEvictionPolicy="LRU"/>
<cache name="Products"
maxBytesLocalHeap="40M"
eternal="false"
timeToIdleSeconds="300"
overflowToDisk="true"
maxEntriesLocalDisk="1000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<cache name="referenceData"
maxBytesLocalHeap="5M"
eternal="true"
memoryStoreEvictionPolicy="LRU">
<pinning store="localMemory"/>
</cache>
<cache name="newestAndRecommendedProducts"
maxBytesLocalHeap="3M"
eternal="true"
memoryStoreEvictionPolicy="LRU">
<pinning store="localMemory"/>
</cache>
<cache name="randomAndTopRatedProducts"
maxBytesLocalHeap="1M"
timeToLiveSeconds="300"
memoryStoreEvictionPolicy="LRU">
</cache>
</ehcache>
让我们也看看echace.xml
中使用的以下属性的含义,以便在正确使用它们时有所帮助:
maxBytesLocalHeap
:这定义了缓存可以从 VM 堆中使用多少字节。如果已定义了 CacheManagermaxBytesLocalHeap
,则此缓存的指定数量将从 CacheManager 中减去。其他缓存将共享余额。此属性的值以<number>k|K|m|M|g|G
表示,用于千字节(k|K)、兆字节(m|M)和千兆字节(g|G)。例如,maxBytesLocalHeap="2g"
分配了 2 千兆字节的堆内存。如果指定了maxBytesLocalHeap
,则不能使用maxEntriesLocalHeap
属性。如果设置了 CacheManagermaxBytesLocalHeap
,则不能使用maxEntriesLocalHeap
。
注意
在最高级别设置,此属性定义了为所有定义的缓存分配的内存。之后您必须将其与各个缓存分开。
-
eternal
:这设置了元素是否是永恒的。如果是永恒的,超时将被忽略,元素永远不会过期。 -
timeToIdleSeconds
:这设置了元素在过期之前的空闲时间。也就是说,元素在过期之前的访问之间的最长时间。只有在元素不是永久的情况下才会使用。可选属性。值为0
表示元素可以无限期地空闲。默认值为0
。 -
timeToLiveSeconds
:这设置了元素在过期之前的生存时间,即创建时间和元素过期时间之间的最长时间。只有在元素不是永久的情况下才会使用。可选属性。值为0
表示元素可以永久存活。默认值为 0。 -
memoryStoreEvictionPolicy
:在达到maxEntriesLocalHeap
限制时将执行该策略。默认策略为最近最少使用(LRU)。
注意
如果您想从数据库中卸载一些负载,还可以使用localTempSwap
持久性策略,在这种情况下,您可以在缓存或 CacheManager 级别使用maxEntriesLocalDisk
或maxBytesLocalDisk
来控制磁盘层的大小。
已配置的两个缓存,参考数据和newestAndRecommendedPodcasts
都固定在本地内存中(<pinning store="localMemory"/>
),这意味着数据将始终保留在缓存中。要从缓存中取消固定数据,您必须清除缓存。
带缓存的 Spring MVC
在本节中,让我们开发一个简单的 MVC 应用程序来演示简单的 Spring 缓存。让我们从配置开始。
要启用缓存,我们需要将以下配置添加到应用程序context.xml
文件中:
<beans
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 />
//your beans
</beans>
<cache:annotation-driven />
将识别 spring 缓存注释@Cacheable
和@CacheEvict
。
让我们演示一个带有简单缓存配置的应用程序context.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans
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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Scans within the base package of the application for @Components to configure as beans -->
<context:component-scan base-package="com" />
<!-- Process cache annotations -->
<cache:annotation-driven />
<!-- Configuration for using Ehcache as the cache manager-->
<bean id="cacheManager" p:cache-manager-ref="ehcache"/>
<bean id="ehcache" p:config-location="classpath:ehcache.xml"/>
<bean id="author" class="com.packt.model.Author"/>
</beans>
接下来让我们演示ehchace.xml
文件:
<ehcache>
<diskStore path="java.io.tmpdir"/>
<cache name="authorCache"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxElementsOnDisk="10000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
接下来,我们将看到一个简单的 POJO 类Author.java
:
package com.packt.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
public class Author {
Logger logger = LoggerFactory.getLogger(getClass());
@Cacheable(value="authorCache", key = "#id")
public String getAuthor(Integer id){
logger.info("get author called");
return "author"+id;
}
}
接下来,我们将编写一个带有注入的 Author pojo 的简单控制器:
package com.packt.web;
import java.util.HashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.packt.model.Author;
@Controller
public class WebController {
@Autowired
Author author;
@RequestMapping("/index.htm")
public String authorPage(@RequestParam(required= false) Integer id, HashMap<String, String> map){
map.put("message", author.getAuthor(id));
return "index";
}
}
最后,我们将编写一个.jsp
文件:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Cache Example</title>
</head>
<body>
<h1>This is ${message }</h1>
</body>
</html>
当我们使用http://localhost:8080/springcachedemo/index.htm?id=1
运行应用程序时,数据被缓存,第二次访问 URL 时,您将能够观察到该值是从缓存中检索出来的。
现在在 URL 中更新 ID id=2.访问 http://localhost:8080/springcachedemo/index.htm?id=2
,数据不是从缓存中检索出来的,但它被缓存了。
实现自己的缓存算法
在这一部分,让我们首先实现一个简单的缓存算法,看看它的缺点,然后展示 spring 缓存如何解决这些问题。
让我们绘制一个简单的流程图来看看缓存场景:
让我们看看如何以简单的方式实现缓存。想象一下生成一个斐波那契数。斐波那契数是通过将其前两个斐波那契数相加而生成的。因此,我们可以在 java 中计算一个简单的类,并看看我们如何在这里使用缓存。
让我们创建一个用于缓存对象的映射:
import java.util.HashMap;
import java.util.Map;
public class FibonacciCache {
private Map<Long, Long> cachemap = new HashMap<>();
public FibonacciCache() {
// The base case for the Fibonacci Sequence
cachemap.put(0L, 1L);
cachemap.put(1L, 1L);
}
public Long getNumber(long index) {
// Check if value is in cache
if (cachemap.containsKey(index)) {
return cachemap.get(index);
}
// Compute value and save it in cache
long value = getNumber(index - 1) + getNumber(index - 2);
cachemap.put(index, value);
return value;
}
}
这种方法不是线程安全的,同样的值会被计算多次。当两个线程运行在这个类上时,它们最终会缓存相同的值。
我们可以通过实现并发哈希映射来克服这一问题。上述代码可以重写如下:
import java.util.HashMap;
import java.util.Map;
public class FibonacciConncurentCache {
private Map<Long, Long> concurrent_cachemap = new ConcurrentHashMap<>();
public FibonacciCache() {
// The base case for the Fibonacci Sequence
concurrent_cachemap.put(0L, 1L);
concurrent_cachemap.put(1L, 1L);
}
public Long getNumber(long index) {
// Check if value is in cache
if (concurrent_cachemap.containsKey(index)) {
return concurrent_cachemap.get(index);
}
// Compute value and save it in concurrent_cachemap
long value = getNumber(index - 1) + getNumber(index - 2);
concurrent_cachemap.put(index, value);
return value; }}
上述代码将使算法线程安全,防止相同值的重新计算。但这种设计不能用于其他算法。如果我们要找出下一个斐波那契数是奇数还是质数,这是不支持的。
让我们使用 Future、Callable ExecutorService 和 Concurrent HashMap 来解决这个问题。我们还将看到 Future callable 和 executor Service 的含义。
ExecutorService提供了创建线程池的选项。ExecutorService 是并发包中的一个接口。ThreadPoolExecutor
和ScheduledThreadPoolExecutor
是实现ExecutorService
的两个类。
有几种不同的方法可以将任务委托给ExecutorService
进行执行:
-
execute (Runnable)
-
submit (Runnable)
-
submit (Callable)
-
invokeAny (...)
-
invokeAll (...)
Callable是类似于 Runnable 的接口。它是一个返回结果并可能抛出异常的任务。实现者定义了一个没有参数的方法叫做call
。
Callable 接口类似于 Runnable,两者都设计用于其实例可能由另一个线程执行的类。然而,Runnable 不返回结果,也不能抛出已检查的异常。
Executors 类包含了将其他常见形式转换为 Callable 类的实用方法。
让我们创建一个通用类;MyCache
,这个类实例接受键和值对。它使用并发HashMap
。
- 让我们在条件上调用
getter
和setter
方法;如果值已经在缓存中,那么只需获取该值,并且只有在不存在时才设置它。
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class MyCache<K, V> {
private final ConcurrentMap<K, Future<V>> cache = new ConcurrentHashMap<>();
private Future<V> createFutureIfAbsent(final K key, final Callable<V> callable) {
Future<V> future = cache.get(key);
if (future == null) {
final FutureTask<V> futureTask = new FutureTask<V>(callable);
future = cache.putIfAbsent(key, futureTask);
if (future == null) {
future = futureTask;
futureTask.run();
}
}
return future;
}
public V getValue(final K key, final Callable<V> callable) throws InterruptedException, ExecutionException {
try {
final Future<V> future = createFutureIfAbsent(key, callable);
return future.get();
} catch (final InterruptedException e) {
cache.remove(key);
throw e;
} catch (final ExecutionException e) {
cache.remove(key);
throw e;
} catch (final RuntimeException e) {
cache.remove(key);
throw e;
}
}
public void setValueIfAbsent(final K key, final V value) {
createFutureIfAbsent(key, new Callable<V>() {
@Override
public V call() throws Exception {
return value;
} });
}}
- 接下来的步骤是在我们的斐波那契数列代码中使用缓存算法:
import java.util.concurrent.Callable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyFibonacci {
private static final Logger LOGGER = LoggerFactory.getLogger(MyFibonacci.class);
public static void main(final String[] args) throws Exception {
final long index = 12;
final MyFibonacci myfibi = new MyFibonacci();
final long fn = myfibi.getNumber(index);
MyFibonacci.LOGGER.debug("The {}th Fibonacci number is: {}", index, fn);
}
private final MyCache<Long, Long> cache = new MyCache<>();
public MyFibonacci() {
cache.setValueIfAbsent(0L, 1L);
cache.setValueIfAbsent(1L, 1L);
}
public long getNumber(final long index) throws Exception {
return cache.getValue(index, new Callable<Long>() {
@Override
public Long call() throws Exception {
MyFibonacci.LOGGER.debug("Computing the {} MyFibonacci number", index);
return getNumber(index - 1) + getNumber(index - 2);
}
});
}
}
正如您在前面的示例中所看到的,所需的修改非常少。所有缓存代码都封装在缓存算法中,我们的代码只是与之交互。缓存算法是线程安全的,由于所有状态都由缓存算法保存,我们的类本质上是线程安全的。使用这种新方法,我们可以让这个类(MyFibonacci
)专注于其业务逻辑,即计算斐波那契数列。每个斐波那契数只计算一次。所有其他时间,这些都是从缓存中检索的。在下面的示例中,我们将看到如何在另一个上下文中使用相同的缓存算法。想象一个需要使用缓存的长时间学习任务。我们将使用org.spring.framework.util.StopWatch
包中的 Spring Stop Watch 类。该类有两个构造函数:
-
StopWatch()
: 这构造一个新的秒表 -
StopWatch(String id)
: 这构造一个带有给定 ID 的新秒表
简单的秒表允许计时多个任务,公开总运行时间,并为每个命名任务提供运行时间。它隐藏了System.currentTimeMillis()
的使用,提高了应用程序代码的可读性,并减少了计算错误的可能性。
注意
请注意,这个对象不是设计为线程安全的,并且不使用同步或线程。因此,可以从 EJB 中调用它是安全的。
这个类通常用于验证概念证明和开发中的性能,而不是作为生产应用程序的一部分。
让我们看看代码:
import java.util.concurrent.Callable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StopWatch;
public class LongRunningTaskExample {
private static final Logger LOGGER =
public static void main(final String[] args) throws Exception {
final LongRunningTaskExample task = new LongRunningTaskExample();
final StopWatch stopWatch = new StopWatch(" Long Running Task");
stopWatch.start("First Run");
task.computeLongTask("a");
stopWatch.stop();
stopWatch.start("Other Runs");
for (int i = 0; i < 100; i++) {
task.computeLongTask("a");
}
stopWatch.stop();
LongRunningTaskExample.LOGGER.debug("{}", stopWatch);
}
private final MyCache<String, Long> cache = new MyCache<>();
public long computeLongTask(final String key) throws Exception {
return cache.getValue(key, new Callable<Long>() {
@Override
public Long call() throws Exception {
FictitiousLongRunningTask.LOGGER.debug("Computing Long Running Task: {}", key);
Thread.sleep(10000); // 10 seconds
return System.currentTimeMillis();
}
});
}
}
前面代码的输出:
[main] DEBUG LongRunningTask.java:36 - Computing Long Running Task: a
[main] DEBUG LongRunningTask.java:27 - StopWatch ' Long Running Task': running time (millis) = 10006; [First Run] took 10005 = 100%; [Other Runs] took 1 = 0%
对缓存算法没有进行任何更改,并且实现起来非常容易。前面的代码将产生类似于以下代码的结果。如前面的输出所示,一旦第一个值被计算并保存在缓存中,所有其他检索都会立即发生,而不会引入任何明显的延迟。
让我们进一步实现前面的长时间运行任务,并使用 spring 缓存缓存计算值。
我们将创建两个简单的类:Worker
和Main
。Worker
类有两个方法,这些方法从main
类中调用:
Import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(final String[] args) {
final String xmlFile = "META-INF/spring/app-context.xml";
try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(xmlFile)) {
final Worker worker = context.getBean(Worker.class);
worker.longTask(1);
worker.longTask(1);
worker.longTask(1);
worker.longTask(2);
worker.longTask(2);
}
}
import org.springframework.stereotype.Component;
@Component
public class Worker {
public String longTask(final long id) {
System.out.printf("Running long task for id: %d...%n", id);
return "Long task for id " + id + " is done";
}
public String shortTask(final long id) {
System.out.printf("Running short task for id: %d...%n", id);
return "Short task for id " + id + " is done";
}
}
您可以观察到 Longtask 已经传递了相同的值进行重新计算。我们可以使用@Cacheable
注解来解决这个问题。前面的代码可以重写如下。这将防止对相同值的 Longtask 进行重新编译。
import org.springframework.stereotype.Component;
@Component
public class Worker {
@Cacheable("task")
public String longTask(final long id) {
System.out.printf("Running long task for id: %d...%n", id);
return "Long task for id " + id + " is done";
}
public String shortTask(final long id) {
System.out.printf("Running short task for id: %d...%n", id);
return "Short task for id " + id + " is done";
}
}
总结
在本章中,我们看到了如何实现自己的缓存算法以及如何制作一个通用算法。我们研究了 Spring 对缓存的支持,以及 Spring 缓存框架中不同类型的缓存存储库。我们演示了如何在 Spring MVC 应用程序中使用注解来使用缓存。我们还讨论了移除缓存的场景以及何时最适合选择缓存。最后,我们还讨论了在 Spring 框架中支持缓存机制的类和接口。
在接下来的章节中,我们将研究 Spring 与 thymeleaf 框架集成和 Spring Webservices。
第十一章:Spring 与 Thymeleaf 集成
Thymeleaf 是一个完全用 Java 编写的模板引擎。它支持 XML/XHTML/HTML5,这意味着我们可以使用 Thymeleaf 模板引擎库使用 XML 或 XHTML 或 HTML5 开发模板。它提供了一个可选的模块,用于 Spring MVC 和 Spring Web Flow 集成。模板引擎帮助我们在 UI 中创建可重用的组件。模板通常按照约定包括标题、菜单、消息、正文、内容和页脚组件。内容部分动态加载消息。我们可以使用模板创建不同的布局。
Thymeleaf 可以用来代替 JSP。到目前为止,我们已经使用了 JSP 和自定义标签制作模板。Thymeleaf 模板是 XHTML、XML、HTML5 模板引擎。甚至网页设计师也可以很容易地与之交互。所使用的表达语言与 JSP 表达语言相比非常先进。
在本章中,我们将演示如何将 Spring MVC 与 Thymeleaf 模板集成。我们将看到如何使用可用的依赖项开始使用 Spring Thymeleaf。
Thymeleaf 属性
让我们看一些 Thymeleaf 提供的用于设计页面的基本属性。我们还将看一下它如何与 Java 对象和循环交互。Thymeleaf 使用了许多属性。
- 显示消息:
<p th:text="#{msg.greet}">Helloo Good Morning!</p>
- 要显示循环,我们有
th:each
:
<li th:each="product : ${products}" th:text="${product.title}">XYZLLDD</li>
- 现在,让我们看一个表单提交操作:
<form th:action="@{/buyBook}">
- 如果我们必须提交按钮,那么添加:
<input type="button" th:value="#{form.submit}" />
Spring Thymeleaf 依赖
要开始使用 Thymeleaf 模板引擎,我们需要在pom.xml
文件中添加以下依赖项:
-
Thyemleaf 库:
-
groupId
:org.thymeleaf
-
artifactId
:thymeleaf
-
version
: 2.1.4 Release -
Spring-Thymeleaf 插件库:
-
groupId
:org.thymeleaf
-
artifactId
:thymeleaf-spring4
-
version
: 2.1.4. Release
为了测试框架(注意版本不一定与核心版本匹配),Thymeleaf 需要 Java SE 5.0 或更新版本。此外,它依赖于以下库:
-
unbescape 1.1.0 或更高版本
-
ONGL 3.0.8 或更高版本
-
Javassist 3.16.1-GA 或更高版本
-
slf4j 1.6.6 或更高版本
-
此外,如果您使用 LEGACYHTML5 模板模式,则需要 NekoHTML 1.9.21 或更高版本
Spring MVC 和 Thymeleaf
在本节中,让我们看一下如何在 Spring MVC 框架中配置 Thymeleaf。我们也可以使用SpringContext.xml
文件进行 Thymeleaf 配置,但由于我们已经看到了许多这样的例子,其中我们在 XML 文件中执行了配置,我们将看一下如何在 Java 文件中使用 Spring 注解添加配置。让我们创建一个简单的类CustomPacktConfiguration
,并为该类使用@Configuration
注解,告诉框架这个类有配置。
在配置类中,将模板模式设置为应用程序中使用的格式,即 XHTML 或 XML 模板。然后我们需要将模板配置设置为thymeleafviewResolver
对象,还需要实际传递templateResolver
类。
@Configuration
@ComponentScan(basePackageClasses = PacktController.class)
public class CutomPacktConfiguration {
@Bean public ViewResolver viewResolver() {
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setTemplateMode("XHTML");
templateResolver.setPrefix("views/");
templateResolver.setSuffix(".html");
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(templateResolver);
ThymeleafViewResolver thymeleafviewResolver = new ThymeleafViewResolver();
thymeleafviewResolver.setTemplateEngine(engine);
return thymeleafviewResolver;
}
}
@Controller
public class MyPacktControllerController {
@Autowired private PacktService packtService;
@RequestMapping("/authors")
public String authors(Model model) {
model.addAttribute("authors",packtService.getAuthors));
return "authors";
}
}
使用 Spring Thymeleaf 的 MVC
在本节中,我们将深入探讨 Thymeleaf 在 Spring 应用程序中的集成,并开发一个简单的 MVC 应用程序,列出作者并允许用户添加、编辑和删除作者。在 Java 文件中进行配置而不是在 XML 文件中进行配置的优势是代码安全性。您的 XML 可以很容易被更改,但在 Java 文件中进行配置的情况下,我们可能需要将类文件部署到服务器上以查看更改。在本例中,让我们使用JavaConfig
方法来配置 bean。我们可以省略 XML 配置文件。
- 让我们首先从控制器开始,它有方法来插入和列出数据库中可用的作者。
package demo.packt.thymeleaf.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import demo.packt.thymeleaf.exception.AuthorFoundException;
import demo.packt.thymeleaf.model.Author;
import demo.packt.thymeleaf.model.AuthorData;
import demo.packt.thymeleaf.service.AuthorService;
@Controller
public class AuthorController {
private static final String HOME_VIEW = "home";
private static final String RESULTS_FRAGMENT = "results :: resultsList";
@Autowired
private AuthorService authorService;
@ModelAttribute("author")
public Author prepareAuthorModel() {
return new Author();
}
@ModelAttribute("authorData")
public AuthorData prepareAuthorDataModel() {
return authorService.getAuthorData();
}
@RequestMapping(value = "/home", method = RequestMethod.GET)
public String showHome(Model model) {
prepareAuthorDataModel();
prepareAuthorModel();
return HOME_VIEW;
}
@RequestMapping(value = "/authors/{surname}", method = RequestMethod.GET)
public String showAuthorListwithSurname(Model model, @PathVariable("surname") String surname) {
model.addAttribute("authors", authorService.getAuthorsList(surname));
return RESULTS_FRAGMENT;
}
@RequestMapping(value = "/authors", method = RequestMethod.GET)
public String showAuthorList(Model model) {
model.addAttribute("authors", authorService.getAuthorsList());
return RESULTS_FRAGMENT;
}
@RequestMapping(value = "/authors/insert", method = RequestMethod.POST)
public String insertAuthor(Author newAuthor, Model model) {
authorService.insertNewAuthor(newAuthor);
return showHome(model);
}
@ExceptionHandler({AuthorFoundException.class})
public ModelAndView handleDatabaseError(AuthorFoundException e) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("home");
modelAndView.addObject("errorMessage", "error.user.exist");
modelAndView.addObject("Author", prepareAuthorModel());
modelAndView.addObject("authorData", prepareAuthorDataModel());
return modelAndView;
}
}
- 接下来通过扩展
RuntimeException
类定义自定义RuntimeException
:
package demo.packt.thymeleaf.exception;
public class AuthorFoundException extends RuntimeException {
private static final long serialVersionUID = -3845574518872003019L;
public AuthorFoundException() {
super();
}
public AuthorFoundException(String message) {
super(message);
}
}
- 在这一步中,我们将从 Thymeleaf 服务开始,编写一个接口和实现类。
- 接口描述了接口中使用的方法:
package demo.packt.thymeleaf.service;
import java.util.List;
import demo.packt.thymeleaf.model.Author;
import demo.packt.thymeleaf.model.AuthorData;
public interface AuthorService {
HotelData getAuthorData();
List<Author> getAuthorsList();
List<Author> getAuthorList(String surname);
void insertNewAuthor(Author newAuthor);
}
- 接下来我们将实现接口:
package demo.packt.thymeleaf.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import demo.packt.thymeleaf.exception.AuthorFoundException;
import demo.packt.thymeleaf.model.Author;
import demo.packt.thymeleaf.model.AuthorData;
import demo.packt.thymeleaf.repository.AuthorRepository;
@Service("authorServiceImpl")
public class AuthorServiceImpl implements AuthorService {
@Autowired
AuthorRepository authorRepository;
@Override
public AuthorData getAuthorData() {
AuthorData data = new AuthorData();
data.setAddress("RRNAGAR, 225");
data.setName("NANDA");
return data;
}
@Override
public List<Author> getAuthorsList() {
return authorRepository.findAll();
}
@Override
public List<Author> getAuthorsList(String surname) {
return authorRepository.findAuthorsBySurname(surname);
}
@Override
public void insertNewGuest(Author newAuthor) {
if (authorRepository.exists(newAuthor.getId())) {
throw new AuthorFoundException();
}
authorRepository.save(newAuthor);
}
}
- 让我们实现应用程序服务实现类中使用的存储库类:
package demo.packt.thymeleaf.repository;
import java.util.List;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import demo.packt.thymeleaf.model.Guest;
public interface AuthorRepository extends MongoRepository<Author, Long> {
@Query("{ 'surname' : ?0 }")
List<Author> findAuthorsBySurname(String surname);
}
- 接下来在应用程序中实现 Model 类(
Author
和AuthorData
)。
- 首先让我们实现
Author
类:
package demo.packt.thymeleaf.model;
import java.io.Serializable;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "authors")
public class Author implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Long id;
private String name;
private String surname;
private String country;
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the surname
*/
public String getSurname() {
return surname;
}
/**
* @param surname the surname to set
*/
public void setSurname(String surname) {
this.surname = surname;
}
/**
* @return the id
*/
public Long getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(Long id) {
this.id = id;
}
/**
* @return the country
*/
public String getCountry() {
return country;
}
/**
* @param country the country to set
*/
public void setCountry(String country) {
this.country = country;
}
}
- 接下来,让我们实现
AuthorData
类:
package demo.packt.thymeleaf.model;
import java.io.Serializable;
public class AuthorData implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
- 在这一步中,我们将创建配置类;如前所述,我们不使用 XML 进行配置。我们有两个配置文件——一个用于数据库配置的 MongoDB,另一个是组件扫描配置文件:
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import com.mongodb.Mongo;
@Configuration
@EnableMongoRepositories(«demo.packt.thymeleaf.repository»)
public class MongoDBConfiguration extends AbstractMongoConfiguration {
@Override
protected String getDatabaseName() {
return "author-db";
}
@Override
public Mongo mongo() throws Exception {
return new Mongo();
}
}
这个类是一个重要的类,标志着应用程序实例化的开始。在这里,我们还配置了 Thymeleaf 模板视图解析器并提供了组件扫描信息。模板和视图解析器也在这个类中进行了配置:
package demo.packt.thymeleaf.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring3.SpringTemplateEngine;
import org.thymeleaf.spring3.view.ThymeleafViewResolver;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
@EnableWebMvc
@Configuration
@ComponentScan("demo.packt.thymeleaf")
@Import(MongoDBConfiguration.class)
public class WebAppConfiguration extends WebMvcConfigurerAdapter {
@Bean
@Description("Thymeleaf template resolver serving HTML 5")
public ServletContextTemplateResolver templateResolver() {
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver();
templateResolver.setPrefix("/WEB-INF/html/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("HTML5");
return templateResolver;
}
@Bean
@Description("Thymeleaf template engine with Spring integration")
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
return templateEngine;
}
@Bean
@Description("Thymeleaf view resolver")
public ThymeleafViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
return viewResolver;
}
@Bean
@Description("Spring message resolver")
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
return messageSource;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/WEB-INF/resources/");
}
}
- 下一步是在
WEB-INF
文件夹下创建 HTML 文件,创建一个home.html
文件如下:
<!DOCTYPE html>
<html
lang="en">
<head>
<meta charset="UTF-8"/>
<title>Thymeleaf example</title>
<link rel="stylesheet" th:href="@{/spring/resources/css/styles.css}" type="text/css" media="screen"/>
<script th:src="img/functions.js}" type="text/javascript"></script>
<script th:src="img/jquery-min-1.9.1.js}" type="text/javascript"></script>
</head>
<body>
<div style="width:800px; margin:0 auto;">
<h1 th:text="#{home.title}">Thymeleaf example</h1>
<div class="generic-info">
<h3 th:text="#{author.information}">Author Information</h3>
<span th:text="${authorData.name}">Author name</span><br />
<span th:text="${authorData.address}">Author address</span><br />
</div>
<div class="main-block">
<!-- Insert new Author -->
<span class="subtitle">Add Author form</span>
<form id="guestForm" th:action="@{/spring/authors/insert}" th:object="${Author}" method="post">
<div class="insertBlock">
<span class="formSpan">
<input id="authorId" type="text" th:field="*{id}" required="required"/>
<br />
<label for="authorId" th:text="#{insert.id}">id:</label>
</span>
<span class="formSpan" style="margin-bottom:20px">
<input id="authorName" type="text" th:field="*{name}" required="required"/>
<br />
<label for="authorName" th:text="#{insert.name}">name:</label>
</span>
<span class="formSpan">
<input id="authorSurname" type="text" th:field="*{surname}" required="required"/>
<br />
<label for="authorSurname" th:text="#{insert.surname}">surname:</label>
</span>
<span class="formSpan" style="margin-bottom:20px">
<input id="authorCountry" type="text" th:field="*{country}" required="required"/>
<br />
<label for="authorCountry" th:text="#{insert.country}">country:</label>
</span>
<input type="submit" value="add" th:value="#{insert.submit}"/>
<span class="messageContainer" th:unless="${#strings.isEmpty(errorMessage)}" th:text="#{${errorMessage}}"></span>
</div>
</form>
<!-- Guests list -->
<form>
<span class="subtitle">Author list form</span>
<div class="listBlock">
<div class="search-block">
<input type="text" id="searchSurname" name="searchSurname"/>
<br />
<label for="searchSurname" th:text="#{search.label}">Search label:</label>
<button id="searchButton" name="searchButton" onclick="retrieveAuthors()" type="button" th:text="#{search.button}">Search button</button>
</div>
<!-- Results block -->
<div id="resultsBlock">
</div>
</div>
</form>
</div>
</div>
</body>
</html>
- 最后,创建一个简单的
results.html
文件:
<!DOCTYPE html>
<html
lang="en">
<head>
</head>
<body>
<div th:fragment="resultsList" th:unless="${#lists.isEmpty(authors)}" class="results-block">
<table>
<thead>
<tr>
<th th:text="#{results.author.id}">Id</th>
<th th:text="#{results.author.surname}">Surname</th>
<th th:text="#{results.author.name}">Name</th>
<th th:text="#{results.author.country}">Country</th>
</tr>
</thead>
<tbody>
<tr th:each="author : ${authors}">
<td th:text="${author.id}">id</td>
<td th:text="${author.surname}">surname</td>
<td th:text="${author.name}">name</td>
<td th:text="${author.country}">country</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
这将为用户提供一个作者列表和一个用于将作者信息插入 MongoDB 数据库的表单,使用 Thymeleaf 模板。
Spring Boot 与 Thymeleaf 和 Maven
在本节中,我们将看到如何使用 Spring boot 创建一个带有 Thymeleaf 应用程序的 Spring。
这个操作的前提是 Maven 必须安装。要检查 Maven 是否已安装,请在命令提示符中键入以下命令:
mvn –version
- 使用原型来生成一个带有
thymeleaf
项目的 Spring boot:
mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart -DgroupId=com.packt.demo -DartifactId=spring-boot-thymeleaf -interactiveMode=false
上述命令将创建一个spring-boot-thymeleaf
目录。这可以导入到 Eclipse IDE 中。
- 您将打开
pom.xml
文件并添加一个parent
项目:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.8.RELEASE</version>
</parent>
- 开始向
pom.xml
文件添加一个依赖项:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
- 最后添加 Spring boot 插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
让我们开始修改 web。但等一下,这不是 web 应用程序!
- 因此,让我们修改
App
类,使其成为 Spring Boot 应用程序的入口点:
package com.packt.demo
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class);
}
}
- 接下来,让我们配置 Thymeleaf 模板。为了配置它,我们需要在
src/main/resources/templates
目录下添加模板:
<!DOCTYPE html>
<html>
<head>
<title>Hello Spring Boot!</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<p>Hello Spring Boot!</p>
</body>
<html>
- 您可以通过添加 CSS 和 JavaScript 引用来升级 Thymeleaf 模板,如下所示:
<!DOCTYPE html>
<html>
<head>
<title>Hello Spring Boot!</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link href="../static/css/core.css"
th:href="@{/css/core.css}"
rel="stylesheet" media="screen" />
</head>
<body>
<p>Hello Spring Boot!</p>
</body>
</html>
- Spring boot 支持开箱即用的 WebJars。将以下依赖项添加到
pom.xml
文件中。
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>2.1.1</version>
</dependency>
并在模板中引用库,如下所示:
<link href="http://cdn.jsdelivr.net/webjars/bootstrap/3.2.0/css/bootstrap.min.css"
th:href="@{/webjars/bootstrap/3.2.0/css/bootstrap.min.css}"
rel="stylesheet" media="screen" />
<script src="img/jquery.min.js"
th:src="img/jquery.min.js}"></script>
如您所见,对于静态原型设计,库是从 CDN 下载的,将打包从 JAR 转换为 WAR
使用 Spring boot 作为普通 web 应用程序运行这个项目非常容易。首先,我们需要将pom.xml
中的打包类型从 JAR 改为 WAR(打包元素)。其次,确保 Tomcat 是一个提供的依赖项:
<packaging>war</packaging>
我们还需要创建一个控制器来处理应用程序请求:
package com.packt.demo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
class HomeController {
@RequestMapping("/")
String index() {
return "index";
}
}
最后一步是引导一个 servlet 配置。创建一个Init
类并继承自SpringBootServletInitializer
:
package packt.demo;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(App.class);
}
}
我们可以使用mvn clean package
命令检查配置是否与 Maven 一起工作。WAR 文件将被创建:
Building war: C:\Projects\demos\spring-boot-thymeleaf\target\spring-boot-thymeleaf-1.0-SNAPSHOT.war
使用 Maven 直接从 WAR 文件启动应用程序,使用以下命令:
java-jar target\spring-boot-thymeleaf-1.0-SNAPSHOT.war
创建 WAR 项目后,我们将在 Eclipse 中运行应用程序。在我们改变了打包方式后,Eclipse 将检测项目中的更改并向其添加 web facet。下一步是配置 Tomcat 服务器并运行它。导航到Edit Configurations,并添加带有解压的 WAR 构件的 Tomcat 服务器。现在你可以像运行其他 web 应用程序一样运行应用程序。
重新加载 Thymeleaf 模板
由于应用程序在 Eclipse 中运行在本地 Tomcat 服务器上,我们将重新加载静态资源(例如 CSS 文件)而无需重新启动服务器。但是,默认情况下,Thymeleaf 会缓存模板,因此为了更新 Thymeleaf 模板,我们需要改变这种行为。
-
将
application.properties
添加到src/main/resources
目录中,其中包含spring.thymeleaf.cache=false
属性 -
重新启动服务器,从现在开始您可以重新加载 Thymeleaf 模板而无需重新启动服务器
-
更改其他配置默认值
缓存配置并不是我们可以调整的唯一可用配置。请查看ThymeleafAutoConfiguration
类,了解您可以更改的其他内容。举几个例子:spring.thymeleaf.mode
,spring.thymeleaf.encoding
。
使用 Thymeleaf 的 Spring 安全
由于我们使用了 Spring 安全,我们将在我们的 Spring 应用程序中使用 JSP 中的自定义登录表单。在本节中,让我们看看如何引入 Thymeleaf 模板来保护基于 Spring 的应用程序。
您可以像这样使用 Spring 安全方言来显示已登录用户的信息。属性sec:authorize
在属性表达式评估为True
时呈现其内容。您可以在成功认证后显示的基本文件中使用此代码:
?
<div sec:authorize="hasRole('ROLE_ADMIN')">
This content is only shown to administrators.
</div>
<div sec:authorize="hasRole('ROLE_USER')">
This content is only shown to users.
</div>
The attribute sec:authentication is used to print logged user name and roles:
?
Logged user: <span sec:authentication="name">Bob</span>
Roles: <span sec:authentication="principal.authorities">[ROLE_USER, ROLE_ADMIN]</span>
正如我们所知,以下是我们在 Spring 应用程序中添加 Spring 安全所执行的一些必要步骤。但是,您会注意到我们已经配置了一个 Thymeleaf 文件的 HTML 文件。
-
配置 Spring 安全过滤器。
-
将
applicationContext-springsecurity.xml
文件配置为上下文参数。 -
在
applicationContext-springsecurity.xml
中配置需要保护的 URL。 -
示例配置如下:
<?
<http auto-config="true">
<form-login login-page="/login.html" authentication-failure-url="/login-error.html" />
<logout />
...
</http>
- 配置 Spring 控制器:
@Controller
public class MySpringController {
...
// Login form
@RequestMapping("/login.html")
public String login() {
return "login.html";
}
// Login form with error
@RequestMapping("/login-error.html")
public String loginError(Model model) {
model.addAttribute("loginError", true);
return "login.html";
}
}
- 让我们看一下
Login.html
文件,这是 Thymeleaf 文件。这可以通过文件开头给出的 XMLNS 来识别。还要注意,我们正在处理 JSP 文件中的错误;当登录失败时,它会显示错误消息。我们还将创建一个error.html
文件来处理错误:
<!DOCTYPE html>
<html >
<head>
<title>Login page</title>
</head>
<body>
<h1>Login page</h1>
<p th:if="${loginError}">Wrong user or password</p>
<form th:action="@{/j_spring_security_check}" method="post">
<label for="j_username">Username</label>:
<input type="text" id="j_username" name="j_username" /> <br />
<label for="j_password">Password</label>:
<input type="password" id="j_password" name="j_password" /> <br />
<input type="submit" value="Log in" />
</form>
</body>
</html>
/*Error.html file*/
?
<!DOCTYPE html>
<html >
<head>
<title>Error page</title>
</head>
<body>
<h1 th:text="${errorCode}">500</h1>
<p th:text="${errorMessage}">java.lang.NullPointerException</p>
</body>
</html>
这一步是关于配置错误页面。错误页面可以在web.xml
文件中配置。首先,我们需要向web.xml
文件添加<error-page>
标签。一旦配置了错误页面,我们需要通知控制器类有关错误页面的信息:
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/error.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error.html</location>
</error-page>
- 在控制器中为
error
页面添加请求映射:
@RequestMapping("/error.html")
public String error(HttpServletRequest request, Model model) {
model.addAttribute("errorCode", request.getAttribute("javax.servlet.error.status_code"));
Throwable throwable = (Throwable) request.getAttribute("javax.servlet.error.exception");
String errorMessage = null;
if (throwable != null) {
errorMessage = throwable.getMessage();
}
model.addAttribute("errorMessage", errorMessage);
return "error.html";
}
}
访问www.thymeleaf.org/doc/tutorials/2.1/usingthymeleaf.html
了解更多详情。
摘要
在本章中,我们已经看到了如何将 Thymeleaf 模板引擎集成到 Spring MVC 应用程序中,以及如何使用 Spring boot 启动 Spring 与 Thymeleaf 应用程序。我们还演示了如何使用 Spring Thymeleaf 模板为 Spring 安全创建自定义表单。
在下一章中,我们将看到 Spring 与 Web 服务集成,并了解它为开发 SOAP 和 REST Web 服务提供了什么。
第十二章:Spring 与 Web 服务集成
在本章中,我们将看到 Spring 如何支持JAX_WS
网络服务,以及如何在Spring Web Service (Spring-WS)框架中创建网络服务。我们还将看到 Spring Web Service 如何被消费,演示一个客户端应用程序,以及 Spring 支持的 Web 服务的注解。
Spring 与 JAX-WS
在本节中,让我们创建一个简单的 JAX-WS 网络服务。我们还将看到如何将 JAX-WS 网络服务与 Spring 集成。JAX-WS 是 JAX-RPC 的最新版本,它使用远程方法调用协议来访问 Web 服务。
我们在这里需要做的就是将 Spring 的服务层公开为JAX_WS
服务提供程序层。这可以使用@webservice
注解来完成,只需要几个步骤。让我们记下其中涉及的步骤。
-
在 Eclipse 中创建一个
PACKTJAXWS-Spring
简单的 Maven web 项目或动态 web 项目。 -
现在,我们需要在
web.xml
文件中配置 JAX-WS servlet:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<display-name>JAXWS-Spring</display-name>
<servlet>
<servlet-name>jaxws-servlet</servlet-name>
<servlet-class>
com.sun.xml.ws.transport.http.servlet.WSSpringServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>jaxws-servlet</servlet-name>
<url-pattern>/jaxws-spring</url-pattern>
</servlet-mapping>
<!-- Register Spring Listener -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
- 创建一个
Context.xml
应用文件,并在其中添加网络服务信息。我们将在这里提供网络服务名称和服务提供者类信息。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://jax-ws.dev.java.net/spring/core
http://jax-ws.java.net/spring/core.xsd
http://jax-ws.dev.java.net/spring/servlet
http://jax-ws.java.net/spring/servlet.xsd">
<wss:binding url="/jaxws-spring">
<wss:service>
<ws:service bean="#packWs"/>
</wss:service>
</wss:binding>
<!-- Web service bean -->
<bean id="packtWs" class="com.packt.webservicedemo.ws.PacktWebService">
<property name="myPACKTBObject" ref="MyPACKTBObject" />
</bean>
<bean id="MyPACKTBObject" class="com.packt.webservicedemo.bo.impl.MyPACKTBObjectImpl" />
</beans>
- 接下来,我们需要使所有的 jar 文件在类路径中可用。由于这是一个 Maven 项目,我们只需要更新
pom.xml
文件。
<project 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>com.javacodegeeks.enterprise.ws</groupId>
<artifactId>PACKTJAXWS-Spring</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.jvnet.jax-ws-commons.spring</groupId>
<artifactId>jaxws-spring</artifactId>
<version>1.9</version>
</dependency>
</dependencies>
<properties>
<spring.version>3.2.3.RELEASE</spring.version>
</properties>
</project>
- 我们现在将创建一个带有
@WebService
注解的网络服务类。我们还定义了可能需要的绑定类型,比如SOAPBinding
和Style
。@Webmethod
注解指定了提供服务的方法。
package com.packt.webservicedemo.ws;
import javax.jws.WebMethod;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;
import javax.jws.soap.SOAPBinding.Style;
import javax.jws.soap.SOAPBinding.Use;
import com.packt.webservicedemo.bo.*;
@WebService(serviceName="PacktWebService")
@SOAPBinding(style = Style.RPC, use = Use.LITERAL)
public class PacktWebService{
//Dependency Injection (DI) via Spring
MyPACKTBObject myPACKTBObject;
@WebMethod(exclude=true)
public void setMyPACKTBObject(MyPACKTBObject myPACKTBObject) {
this.myPACKTBObject = myPACKTBObject;
}
@WebMethod(operationName="printMessage")
public String printMessage() {
return myPACKTBObject.printMessage();
}
}
package com.packt.webservicedemo.bo;
public interface MyPACKTBObject {
String printMessage();
}
public class MyPACKTBObjectImpl implements MyPACKTBObject {
@Override
public String printMessage() {
return "PACKT SPRING WEBSERVICE JAX_WS";
}
}
-
我们应该将 Maven JAR 文件添加到 Eclipse 项目的构建路径中。
-
运行应用程序:
http://localhost:8080/PACKTJAXWS-Spring/jaxws-spring
。
您应该能够看到 WSDL URL,并在单击链接时,WSDL 文件应该打开。
使用 JAXB 编组的 Spring Web 服务请求
在本节中,让我们看看如何使用 Spring Web Service 框架开发一个简单的网络服务。我们需要 JAXB 来对 XML 请求进行编组和解组。Spring Web Service 支持契约优先的网络服务。我们需要首先设计 XSD/WSDL,然后启动网络服务。
我们正在创建一个作者网络服务,它将给我们提供作者列表。
- 配置 web.xml 文件:让我们首先在
web.xml
文件中进行网络服务配置。我们需要配置 Spring Web Service servlet。需要定义消息分发 servlet 和它将处理的 URL 模式。指定contextConfigLocation
而不是允许默认值(/WEB-INF/spring-ws-servlet.xml
),因为这个位置使得配置更容易与单元测试共享。
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<servlet>
<servlet-name>spring-ws</servlet-name>
<servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/spring-ws-context.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>spring-ws</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
- 配置 Spring 上下文文件(
/src/main/resources/spring-ws-context.xml
):EndPoint
类需要在spring-ws-context.xml
中进行配置。该类带有@EndPointAnnotation
注解。AuthorEndpoint
被定义为一个 bean,并且将自动注册到 Spring Web Services 中,因为该类被@Endpoint
注解标识为端点。此配置使用了author.xsd
,这是一个用于生成 JAXB bean 以生成 WSDL 的 xml 模式描述符文件。位置 URI 与web.xml
中指定的 URL 模式匹配。
使用 Spring OXM 配置 JAXB 编组器/解组器,并设置在MarshallingMethodEndpointAdapter
bean 上。
<?xml version="1.0" encoding="UTF-8"?>
<beans
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. packtws.ws.service" />
<bean id="person" class="org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition"
p:portTypeName="Author"
p:locationUri="/authorService/"
p:requestSuffix="-request"
p:responseSuffix="-response">
<property name="schema">
<bean class="org.springframework.xml.xsd.SimpleXsdSchema"
p:xsd="classpath:/author.xsd" />
</bean>
</property>
</bean>
<bean class="org.springframework.ws.server.endpoint.mapping.PayloadRootAnnotationMethodEndpointMapping">
<description>An endpoint mapping strategy that looks for @Endpoint and @PayloadRoot annotations.</description>
</bean>
<bean class="org.springframework.ws.server.endpoint.adapter.MarshallingMethodEndpointAdapter">
<description>Enables the MessageDispatchServlet to invoke methods requiring OXM marshalling.</description>
<constructor-arg ref="marshaller"/>
</bean>
<bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"
p:contextPath="org.packtws.author.schema.beans" />
</beans>
- 定义 XSD Author.xsd:一个非常简单的 XSD 定义了一个元素,用于指示获取所有作者的传入请求(name 元素未使用),以及包含作者元素列表的作者响应元素。
author.xsd
<xsd:schema
targetNamespace=" http://www.packtws.org/author/schema/beans "
>
<xsd:element name="get-authors-request">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="name" type="xsd:string" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="author-response">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="author" type="author"
minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="author">
<xsd:sequence>
<xsd:element name="id" type="xsd:int" />
<xsd:element name="first-name" type="xsd:string" />
<xsd:element name="last-name" type="xsd:string" />
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
- 编组 AuthorService:让我们创建一个接口
MarshallingAuthorService
,用于使用以下 JAXB 生成的 bean 获取作者:
-
对于
get-authors-request
元素:GetAuthorsRequst
-
对于
author-response
元素:AuthorResponse
它还具有与命名空间(与 XSD 匹配)和请求常量相匹配的常量:
public interface MarshallingAuthorService {
public final static String NAMESPACE = " http://www.packtws.org/author/schema/beans ";
public final static String GET_Authors_REQUEST = "get-authors-request";
public AuthorResponse getAuthors(GetAuthorsRequest request);
}
- 创建端点类:让我们创建一个标有
@Endpoint
注解的端点类。这个类将实现MarshallingAuthorService
的方法。getAuthors
方法被指示处理特定的命名空间和传入的请求元素。端点只是准备一个静态响应,但这很容易可以注入一个 DAO,并从数据库中检索信息,然后映射到 JAXB beans 中。AuthorResponse 是使用 JAXB Fluent API 创建的,比标准的 JAXB API 更简洁。
@Endpoint
public class AuthorEndpoint implements MarshallingAuthorService {
/**
* Gets Author list.
*/
@PayloadRoot(localPart=GET_AuthorS_REQUEST, namespace=NAMESPACE)
public AuthorResponse getAuthors(GetPersonsRequest request) {
return new AuthorResponse().withAuthor(
new Author().withId(1).withFirstName("Anjana").withLastName("Raghavendra"),
new Author().withId(2).withFirstName("Amrutha").withLastName("Prasad"));
}
}
- 添加依赖信息:还要确保在 maven 的
pom.xml
文件中添加以下依赖项:
<dependency>
<groupId>org.springframework.ws</groupId>
<artifactId>org.springframework.ws</artifactId>
<version>${spring.ws.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ws</groupId>
<artifactId>org.springframework.ws.java5</artifactId>
<version>${spring.ws.version}</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>com.springsource.javax.xml.bind</artifactId>
<version>2.1.7</version>
</dependency>
<dependency>
<groupId>com.sun.xml</groupId>
<artifactId>com.springsource.com.sun.xml.bind.jaxb1</artifactId>
<version>2.1.7</version>
</dependency>
<dependency>
<groupId>javax.wsdl</groupId>
<artifactId>com.springsource.javax.wsdl</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>javax.xml.soap</groupId>
<artifactId>com.springsource.javax.xml.soap</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml</groupId>
<artifactId>com.springsource.com.sun.xml.messaging.saaj</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>com.springsource.javax.activation</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>javax.xml.stream</groupId>
<artifactId>com.springsource.javax.xml.stream</artifactId>
<version>1.0.1</version>
</dependency>
- 构建和部署应用程序:我们需要在 tomcat 上进行这个操作以查看 WSDL URL。因此,我们已经完成了提供 web 服务的所有步骤。
使用 JAXB unmarshalling 为 Spring Web Services 编写客户端应用程序
让我们为作者服务编写一个简单的客户端应用程序。org.springbyexample.ws.service
包被扫描以查找AuthorServiceClient
,并将 web 服务模板注入其中。JAXB marshaller/umarshaller 被定义并设置在这个模板上。
jetty-context.xml
的导入对于创建客户端并不重要,但它创建了一个嵌入式的 Jetty 实例,加载了spring-ws-context.xml
和它的服务。单元测试中的客户端能够独立运行。
AuthorServiceClientTest.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans
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">
<import resource="jetty-context.xml"/>
<context:component-scan base-package="org.springbyexample.ws.client" />
<context:property-placeholder location="org/springbyexample/ws/client/ws.properties"/>
<bean id="authorWsTemplate" class="org.springframework.ws.client.core.WebServiceTemplate"
p:defaultUri="http://${ws.host}:${ws.port}/${ws.context.path}/authorService/"
p:marshaller-ref="marshaller"
p:unmarshaller-ref="marshaller" />
<bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"
p:contextPath="org.springbyexample.author.schema.beans" />
</beans>
AuthorServiceClient:
在这一点上,Spring Web Services 几乎可以处理所有事情。只需要调用模板,它将从服务端点返回AuthorResponse
。客户端可以像这样使用:AuthorResponse response = client.getAuthors(new GetAuthorsRequest());
public class AuthorServiceClient implements MarshallingAuthorService {
@Autowired
private WebServiceTemplate wsTemplate;
/**
* Gets author list.
*/
public AuthorResponse getAuthors(GetAuthorsRequest request) {
PersonResponse response = (PersonResponse) wsTemplate.marshalSendAndReceive(request);
return response;
}
}
总结
在本章中,我们看到了如何将JAX_WS
与 Spring Web Service 集成。我们还演示了如何创建 Spring Web Services 和端点类,以及如何通过访问 WSDL URL 来访问 web 服务。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)