精通-Spring-应用开发-全-

精通 Spring 应用开发(全)

原文:zh.annas-archive.org/md5/A95A09924E8304BAE696F70C7C92A54C

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

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 并创建一个数据库:

  1. www.mongodb.org/downloads下载 MongoDB 数据库。

  2. 通过在bin文件夹中执行以下命令来配置数据文件夹:

>mongod.exe -dbpath e:\mongodata\db 

  1. 在另一个命令提示符中启动mongod.exe

  2. 执行以下命令:

>show databaseExecute

>show dbs命令在 MongoDB 中也可以正常工作。

  1. 执行以下命令以创建一个名为eshopdb的新数据库。
>use new-eshopdb

  1. 执行> show dbs仍然会显示eshopdb尚未创建,这是因为它不包含任何集合。一旦添加了集合,我们将在下一步中添加一些集合。

  2. 在命令提示符中执行以下代码片段。以下代码片段将向集合中插入示例文档:

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。这样,我们就不会出现配置错误。这也会节省我们很多时间。

  1. 创建一个mongodbstart.bat文件。

  2. 编辑文件并输入以下命令,然后保存:

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 应用程序项目。

  1. 在 Maven 命令提示符中执行以下命令:
mvn archetype:generate -DgroupId=com.packtpub.spring -DartifactId=spring-mongo -DarchetypeArtifactId=maven-archetype-webapp

  1. 创建一个简单的 Maven 项目,使用 web 应用原型。添加最新的 4.0.2.RELEASE spring 依赖。

  2. 以下是 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 应用程序的步骤:

  1. 创建一个名为 Spring4MongoDB_Chapter1 的基于 web 的 Maven 项目。

  2. 将项目导入 Eclipse 进行实现。我使用的是 Eclipse Juno。

我们需要创建控制器来映射请求。

控制器请求映射到 GETPOST 方法,如下表所示:

请求 请求方法 模型属性
/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 编辑产品信息的输出。

Spring 与 MongoDB 的应用实现

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 实现的 注册产品 和列出产品功能的输出。

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 数据库。

订单管理用例

让我们考虑这一部分的一个复杂场景。在我们考虑的用例中,订单用例在类中具有客户和产品对象。当用户下订单时,用户将选择产品和客户。

我们的目标是直接将customerproduct类存储在 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类中updatesave方法的代码片段。

创建和插入订单

我们看到更新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>

以下是添加订单页面的截图:

JSP files

以下是编辑订单页面的截图:

JSP files

摘要

在本章中,我们学习了如何安装 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 和监听器

众所周知,企业 JavaBeanEJB)提供了一个消息驱动的 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 的步骤如下:

  1. activemq.apache.org/download.html下载最新的Apache ActiveMQ.zip

  2. 将 ZIP 文件解压缩到E:\apachemq\

  3. 在命令提示符中,转到位置E:\apachemq\apache-activemq-5.10-SNAPSHOT\bin\win32,然后单击apachemq.bat启动 Apache ActiveMQ。

  4. Apache ActiveMQ 将在 Jetty 服务器上运行,因此可以通过 URL 访问。

  5. 点击链接http://localhost:8161/admin/index.jsp

  6. 第一次这样做时,会要求您输入凭据;输入admin/admin

  7. 在控制台中,您将看到欢迎部分和代理部分。

  8. 代理部分提供了有关 Apache 消息代理的以下信息:

  • 名称:localhost或服务器的名称

  • 版本 5.10 快照

  • ID:ID:BLRLANJANA-55074-1397199950394-0:1

  • 正常运行时间:1 小时 24 分钟

  • 存储百分比使用:0

  • 内存百分比使用:0

  • 临时百分比使用:0

  1. 单击队列

  2. 队列名称字段中输入orderQueue,然后单击创建

使用 Spring JmsTemplate 的 ApacheMq 用例

在上一章中,我们演示了使用 MongoDB 进行订单管理。假设从一个应用程序下的订单需要被读取到不同的应用程序并存储在不同的数据库中。

订单管理消息代理的设计如下:

使用 Spring JmsTemplate 的 ApacheMq 用例

让我们使用消息代理的相同用例。请求从控制器流出,当用户输入订单详细信息并单击保存时,订单 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 数据库。

使用 SpringJMS 和 ActiveMQ 实现订单管理消息系统

以下表格为我们提供了一个关于在 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);
  }
}

以下是在添加订单案例时的屏幕截图:

配置 dispatcherservlet.xml 以使用 JMS

在 ApacheMQ 中使用多个队列

在前面的部分中,我们演示了使用 Map Message 将消息发送到 Order Queue。现在,我们可以看看如何在 ApacheMQ 中使用多个队列:

  1. 启动 Apache ActiveMQ 服务器,在控制台上点击Queues并创建两个队列。

  2. 让我们创建两个队列,并将队列命名如下:

  • PacktTestQueue1

  • PacktTestQueue2

在 ApacheMQ 中使用多个队列

  1. 使用与本章第一个示例相同的依赖项创建一个新的 Spring 项目。

  2. 创建一个PacktMessageListener类,实现MessageListener接口。该类覆盖onMessage(Message message)方法。

  3. 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;
  }
}
  1. 现在让我们来看看消息发送器类,它使用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);
  }
}
  1. 让我们首先在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>
  1. 以下是在spring-configuration.xml文件中需要进行的配置更改,以配置多个队列:
  • 使用 Spring JNDI 查找queueNames和 JMSconnectionFactory

  • ConnectionFactory引用传递给JmsTemplate

  • 配置MessageSenderMessageListener

  • MessageSender类将具有JmsTemplatequeue对象作为属性

  • MessageListener将具有MessageSender作为属性

  • 配置DefaultMessageListenerContainer类,该类从队列中消费消息

  1. 以下是配置文件的代码:

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>
  1. 以下代码将配置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>
  1. 如果您使用 Maven 作为构建工具,请确保编译源代码并在 Tomcat 或其他您选择的服务器上运行应用程序。同时保持 Apache ActiveMQ 服务器控制台处于运行状态。

  2. 在 ActiveMQ 控制台中,点击队列

  3. 点击发送按钮以链接到PacktTestQueue1行。在 ApacheMQ 中使用多个队列

  4. 输入一些消息文本,然后点击发送按钮。

  5. 在控制台中,您会看到从队列 1 发送了一条消息到队列 2。我们的应用程序从PacktTestQueue1消费消息并将其推送到PacktTestQueue2在 ApacheMQ 中使用多个队列

  6. 现在,让我们增加要发送的消息数量,看看它的行为。在 ApacheMQ 中使用多个队列

  7. 点击PacktTestQueue2,您将看到所有消息都被推送到PacktTestQueue2在 ApacheMQ 中使用多个队列

配置 JMS 事务

当我们使用事务时,我们可以更好地处理前面的情景。消息将在事务中处理,在监听器中出现异常的情况下,将为完整的源代码回滚。参考repository-Spring4JMS_TransactionChapter2中的源代码。

包括事务在消息传递中需要以下步骤:

  1. 将以下属性添加到 ActiveMQ 连接工厂 bean 配置中:
<property name="redeliveryPolicy">
  <bean class="org.apache.activemq.RedeliveryPolicy">
<property name="maximumRedeliveries" value="3"/>
  </bean>
</property>
  1. 更新监听器定义如下:
<jms:listener-container connection-factory="connectionFactory" acknowledge="transacted">
  <jms:listener destination="orderQueue" ref="orderReceiver" method="orderReceived" />
</jms:listener-container>

让我们重新审视情景,了解在jmsTemplate中添加事务后发生了什么:

  • 场景 1:成功场景

  • 场景 2:消息生产者向队列发送信息,消费者读取并将其处理到数据库中;然后出现错误。配置 JMS 事务

添加事务后,代理将三次发送消息。在第四次尝试时,它将发送到新队列,以便消息不会丢失。

  • 场景 3:消息生产者向队列发送信息,消费者读取并将其处理到数据库中;然后出现错误。配置 JMS 事务

添加事务后,如果在完成处理之前监听器执行失败,消息代理将重新发送信息。

配置多个 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:这是一个正面的用例,在之前的部分中我们也看到了。JMS 事务
@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:在这里,让我们使用一个负面情景。消息生产者向队列发送信息,消费者读取,但在到达数据库之前发生异常。JMS 事务
@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:在这里,让我们使用另一个负面情景。消息生产者向队列发送信息,消费者读取并将其处理到数据库中;然后出现错误。JMS 事务
@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 邮件消息处理流程

Spring 邮件框架需要邮件配置或 SMTP 配置作为输入,以及需要发送的消息。邮件 API 与互联网协议交互以发送消息。在下一节中,我们将看一下 Spring 邮件框架中的类和接口。

使用 Spring 发送邮件的接口和类

org.springframework.mail包用于 Spring 应用程序中的邮件配置。

使用 Spring 发送邮件的接口和类

以下是用于发送邮件的三个主要接口:

  • MailSender:这个接口用于发送简单的邮件消息。

  • JavaMailSender:这个接口是MailSender接口的子接口,支持发送邮件消息。

  • MimeMessagePreparator:这个接口是一个回调接口,支持JavaMailSender接口准备邮件消息。

以下类用于使用 Spring 发送邮件:

  • SimpleMailMessage:这是一个类,具有tofromccbccsentDate等属性。SimpleMailMessage接口使用MailSenderImp类发送邮件。使用 Spring 发送邮件的接口和类

  • JavaMailSenderImpl:这个类是JavaMailSender接口的实现类。使用 Spring 发送邮件的接口和类

  • MimeMessageHelper:这个类帮助准备 MIME 消息。

使用@Configuration 注解发送邮件

我们将在这里演示如何使用 Spring 邮件 API 发送邮件。

  1. 首先,我们在.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;
  }
}
  1. 下一步是创建一个 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类发送邮件的步骤。

  1. 创建一个名为Spring4MongoDB_MailChapter3的新 Maven Web 项目。

  2. 本例中使用了第一章中创建的 MongoDB 数据库,Spring Mongo Integration。我们还在 MongoDB 的 Eshop db 数据库上使用了相同的 CRUD 操作CustomerOrderProduct。我们还使用了相同的mvc配置和源文件。

  3. 使用与第二章中使用的相同的依赖项,Spring JMS 消息

  4. 我们需要在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>

  1. 编译 Maven 项目。为邮件服务创建一个名为com.packt.mailService的单独文件夹。

  2. 创建一个名为MailSenderService的简单类,并自动装配MailSenderSimpleMailMessage类。基本框架如下所示:

public class MailSenderService {
  @Autowired
  private MailSender mailSender;
  @AutoWired 
  private SimpleMailMessage simplemailmessage;
  public void sendmail(String from, String to, String subject, String body){
    /*Code */
  }

}
  1. 接下来,创建一个SimpleMailMessage对象,并设置邮件属性,如fromtosubject
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);
}
  1. 我们需要配置 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>
  1. 创建一个名为Spring4MongoDB_Mail_VelocityChapter3的新的 Maven web 项目。

  2. 创建一个名为com.packt.velocity.templates的包。

  3. 创建一个名为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>
  1. 使用我们在前几节中添加的所有依赖项。

  2. 向现有的 Maven 项目中添加此依赖项:

<dependency>
  <groupId>org.apache.velocity</groupId>
  <artifactId>velocity</artifactId>
  <version>1.7</version>
</dependency>
  1. 为了确保 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();
  }
}
  1. 使用相同的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);
}
  1. 更新控制器类以使用 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包,它为我们提供了线程池功能。

  1. 创建一个名为MailSenderAsyncService的类,该类实现MailSender接口。

  2. 导入org.springframework.core.task.TaskExecutor包。

  3. 创建一个名为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);
    }
  }
}
  1. .xml文件中配置ThreadPool执行器。
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor" p:corePoolSize="5"
  p:maxPoolSize="10" p:queueCapacity="100" p:waitForTasksToCompleteOnShutdown="true"/>
  1. 测试源代码。
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类:

  1. 创建一个名为com.packt.aop的包。

  2. 创建一个名为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()方法。ProceddingJoinPointAspectJ.jar中可用的一个类。

  1. 使用aop配置更新dispatcher-servlet.xml文件。使用以下代码更新xlmns命名空间:
xmlns:aop=http://www.springframework.org/schema/aop
  1. 还要更新xsi:schemalocation,如下所示:
xsi:schemaLocation="http://www.springframework.org/
  schema/aop http://www.springframework.org/
  schema/aop/spring-aop-2.5.xsd

  1. 更新.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 批处理的用例:

  • 在预定时间向用户发送批量邮件

  • 从队列中读取消息

  • 在给定时间更新交易

  • 在给定时间处理用户接收到的所有文件

批处理处理的目标

批处理的主要目标是按顺序完成以下一系列步骤以完成批处理作业:

  1. 查找作业。

  2. 识别输入。

  3. 调度作业。

  4. 启动作业。

  5. 处理作业。

  6. 转到第 2 步(获取新输入)。

批处理作业的架构

让我们描述一下批处理处理器的基本架构;我们还可以看到批处理处理中涉及的组件。从下图中,您可以找出 Spring Batch 的主要组件:

批处理作业的架构

现在让我们逐个查看组件。

  • JobRepository:这个容器是我们需要注册作业或进程的地方。

  • JobOperator:这是触发已注册作业的对象。它还提供了访问注册的 API。这是一个接口。

  • Job:它是jobRepository中的一个进程或任务。这包括一个以上的步骤。

  • Step:实际上包含需要执行的逻辑。每个步骤包括一个ItemReaderItemProcessorItemWriter接口。首先,ItemReader接口一次读取一个步骤的作业并将其传递给ItemProcessor进行处理。例如,它可能收集一些所需的数据。然后,ItemWriter接口将数据写入数据库,或者执行事务或记录消息。有两种类型的步骤:

  • ChunkStyleChunkStyle步骤具有一个ItemReader,一个ItemProcessor和一个ItemWriter

  • BatchLet:在 Spring 中,BatchLet被称为TaskLetStepBatchLet是一个自定义步骤,可用于发送批量邮件或短信。

现在我们知道了批处理的基础知识,在下一节中我们将看到如何实现或使用批处理。

使用企业批处理

我们有以下两种实现批处理的选项:

  • 使用 JVM 并为每个作业运行启动 JVM

  • 在 J2EE 容器中部署批处理作业管理应用程序

JSR-352 是可用于实现批处理的标准规范。Spring 框架在很大程度上支持这个规范。大多数 JEE 容器,如GlassfishJboss- 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。它为JobLauncherJobReaderItemProcessorItemWriter提供createupdatereaddelete方法。在 Spring 框架中负责JobRepository的类是SimpleJobRepository。有两种存储作业的方式:一种是在数据库中,另一种是在内存中(这将不得不使用HashMaps)。

SimpleJobRepositoryConstructor看起来像这样:

public SimpleJobRepository(JobInstanceDao jobInstanceDao,
  JobExecutionDao jobExecutionDao,
  StepExecutionDao stepExecutionDao,
  ExecutionContextDao ecDao) 
  • JobLauncherJobLauncher只是一个用于启动作业的简单接口。作业在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对象,如果提供了任何其他类型的对象,则会抛出异常。同样,如果提供的不是ProductBeanProductBeanProcessor也会抛出异常。

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很有用(只要您的业务逻辑不使用关系数据库)。它不适用于具有拆分的多线程作业,尽管在多线程步骤中使用应该是安全的。

接下来,我们将使用ItemReaderItemWriter接口创建实现类。

  1. 以下是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;}
}
  1. 在这里我们有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;
  }
}
  1. 最后,让我们编写实现类,从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");
  }
}

在下一步中,我们将ItemReaderItemProcessorItemWriter组合成一个作业。

让我们创建一个itemreaderprocessorwriter.xml文件。我们将在 XML 文件中传递食谱列表。我们已经包含了applicationContext.xml文件。已定义提交间隔,以表示写入两个元素后写入器应该提交。您还可以观察到步骤包括readerwriterjobRepository

<?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文件。已定义提交间隔,以表示写入两个元素后写入器应该提交。您还可以观察到步骤包括readerwriterjobRepository

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 批处理应用程序项目

  1. Chapter4-SpringBatchCommandLine创建一个 Maven 文件夹结构,如下所示:
  • src/main/java

  • src/main/resources

  • src/pom.xml

  1. 创建一个名为com.packt.example的包。

  2. 创建一个名为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;
  }
}
  1. 配置simpleJob.xml文件。

  2. 将此文件放入resources文件夹中。

  3. 您可以看到我们创建了TaskletImpl类的三个实例:object1object2object3

  4. 在每个实例中,我们设置了消息属性。我们将对象实例传递给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>
  1. 配置jobLauncherJobRepository
  <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>
  1. 您可以使用 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 框架中可用的ItemWriterItemReader实现的各种选项。我们在这里使用了 Spring Framework 中可用的flatFileItemReaderflatFileItemWriter实现。

我们将从应用程序开发开始,看看这些ItemReader实现类是如何使用的。

  1. 使用 Maven 创建一个名为SpringBatchCommandLine-Chapter4Example2的 Spring Java 应用程序。

  2. 创建一个领域类Employee,具有两个实例变量empIdname,以及 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;
  }
}
  1. 使用ItemWriter接口并实现一个CustomeItemWriter类。这个类重写了ItemWriter接口中定义的write方法。

  2. 您将观察到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());
    }

  }

}
  1. 创建一个带有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*/
  1. 让我们在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中。

我们使用了flatfileItemReaderFlatFileItemWriter。这些类读取输入并在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有一个步骤,该步骤配置了ItemReaderItemWriter

  • 我们再次使用了tasklet,但我们使用了步骤作为一个接受MultiResourceReaderchunk读取器

为了理解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 文件,用不同的数字替换*。每个文件将有两个值:employeeIdname

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 对象PojoEmployee.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 调度程序来进行调度。

  1. 创建一个名为SpringBatchQuartzExample的简单 Maven 应用程序。

  2. 使用与之前应用程序相同的pom.xml文件。

  3. pom.xml文件的依赖项中添加 Quartz JAR 文件。

  4. 添加这些属性:

<quartz.version>1.8.6</quartz.version>
  1. 然后,添加这些依赖项:
<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包中。

该类具有JobLauncherJobLocator的 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();
}

正如我们所知,JobNameJobParamtersJobLauncher运行作业所需的输入。在前面的代码片段中,我们已经得到了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 文件,并且需要将该文件读取并更新到数据库中。

  1. 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>
  1. 创建一个基于 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;
  }

}
  1. 添加与上一节中显示的相同的依赖项。

  2. 更新pom.xml文件。

  3. 添加 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>
  1. 创建一个名为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>
  1. 将以下配置添加到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)

  1. 由于我们将在这里执行取消编组,因此我们将清楚地实现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;
}
  1. 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>
  1. 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>
  1. 编写Main类来运行批处理作业。

  2. 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_rangeend_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方法。

  1. 这个类用于分块处理数据。
@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;
  }

}
  1. 让我们配置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 &gt;= :fromId and id &lt;= :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-->
  1. 让我们编写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类:

  • SkipListenerSkipListener最常见的用例之一是记录跳过的项目,以便可以使用另一个批处理过程或甚至人工过程来评估和修复导致跳过的问题。因为有许多情况下原始事务可能被回滚,Spring Batch 提供了两个保证:

  • 适当的skip方法(取决于错误发生的时间)每个项目只会被调用一次。

  • SkipListener将在事务提交之前始终被调用。这是为了确保监听器调用的任何事务资源不会因ItemWriter内部的失败而被回滚。

  • ChunkListener:这些监听器可以配置一个步骤,如果步骤是分块式步骤类型,它将同时具有ItemReaderItemWriter。当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:它代表步骤执行的最通用的监听器。它允许在步骤开始之前和结束之后通知,无论它是正常结束还是失败结束。

您将注意到为ItemReaderItemWriterItemProcessStepExecution接口和类配置了监听器。

现在我们可以看看如何在 spring batch.xml文件中配置监听器。请看:

  1. 创建实现监听器并覆盖其方法的类。
<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>
  1. 让我们看看PacktItemReaderListenerPacktItemWriterListner监听器。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");
  }

}
  1. 接下来让我们看看PackItemWriterListenerItemWriter接口带有三个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文件中创建自定义监听器和监听器配置。

现在,让我们尝试将其与读取目录中的多个文件并删除文件的情景集成。

  1. 我们将再次考虑产品 Pojo,带有idname作为实例变量,并带有 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;
  }
}
  1. 我们需要在 XML 中将 Pojo 定义为一个 bean。
  <bean id="product" class="com.packt.Product" />
  1. 接下来是文件删除任务类文件。在读取文件后,需要从目录中删除它们。
<bean id="fileDeletingTasklet" class="com.packt.tasklet.FileDeletingTasklet" >
  <property name="directory" value="file:csv/inputs/" />
</bean>
  1. 让我们来看一下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;
  }
}
  1. 需要在创建的作业配置文件中设置 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 类的配置中执行了块执行,使用了 ItemReaderWriter

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文件传输协议

  • FTPSFTP 安全是 FTP 的扩展,它添加了对传输层安全TLS)和安全套接字层SSL)加密协议的支持。

  • SFTPSSH 文件传输协议,即 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 的两个核心类FTPSessionFactoryFTPSSessionFactory。这些类有很多的 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 > 1023N + 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,其中包括FtpChannelFtpOutBoundAdpaterDefaultFtpSessionFactory具有所有 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.txtvendors.txt,需要通过 FTP 发送到远程位置。为了实现这一点,我们需要按照以下步骤进行操作:

  1. 创建FTPChannel

  2. 使用baseFolder.mkdirs()在基本文件夹中创建一个目录。

  3. 在基本文件夹位置创建两个文件对象。

  4. 使用InputStream为订单和供应商创建两个单独的流。

  5. 使用 Spring 中可用的文件工具,将输入流复制到它们各自的文件中。

  6. 使用MessageBuilder类,使用withpayload()方法将文件转换为消息。

  7. 最后,将消息发送到 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 框架中可用的属性持有者模式加载属性。

  1. 我们首先要更新配置文件。以下是一个示例配置文件:
@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());
  }

}
  1. 让我们使用property-placeholder进一步配置批处理作业。

  2. 创建一个名为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
  1. context.xml文件或一个单独的文件中配置应用程序,以运行 FTP 的 tasklet:
<batch:job id="ftpJob">
  <batch:step id="step1"  >
  <batch:tasklet ref="myApplicationFtpGetRemoteFilesTasklet" />
  </batch:step>
</batch:job>
  1. 这里是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 应用程序所需的库。然后我们看了两个重要的类,FTPSessionFactoryFTPsSessionFactory,以及它们的 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 模板占位符及其值的映射。

  • #matrixVariablesMultiValueMap的映射。

  • #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&amp;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子元素定义了两个属性:nameexpressionname属性标识 URI 变量的名称,而expression属性用于设置实际值。使用expression属性,您可以利用Spring Expression LanguageSpEL)的全部功能,这使您可以完全动态地访问消息负载和消息标头。例如,在上面的配置中,将在消息的负载对象上调用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映射到HttpRequestExecutingMessageHandlersendTimeOut属性。 在这里,我们使用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());
    }
  }
}

现在,让我们编写一个客户端。通过客户端,我们指的是创建一个地图并向其中添加文件。

  1. 现在,我们将创建一个MultiValueMap
MultiValueMap map = new LinkedMultiValueMap();
  1. 地图可以填充值,例如个人的详细信息:
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);
  1. 此步骤是创建标头并设置内容类型:
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType("multipart", "form-data"));
  1. 我们需要将headermap作为请求传递给HttpEntity
HttpEntity request = new HttpEntity(map, headers);
  1. 让我们使用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服务器。NameNodeDataNodes以主从架构运行。通常,一个NameNode会有许多DataNodesNameNodes存储文件的命名空间,并将文件分割成许多小块存储在DataNodes上。DataNodes通常根据NameNode的指令执行功能,如块创建、复制和删除。因此,与 Hadoop 的主要任务将涉及与文件系统的交互。这可能包括创建文件、解析文件进行处理或删除文件。

可以通过多种方式访问 Hadoop 文件系统。我们在这里列出了一些:

  • hdfs:它使用 RPC 进行通信,使用的协议是hdfs://。客户端、服务器和集群需要具有相同的版本,否则将发生序列化错误。

  • hftphsftp:这些是基于 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/"/>

诸如RhinoGroovy之类的语言提供了 Java 脚本或使用 Python 来进行 HDFS 配置。以下是一个示例。可以配置脚本在启动时或有条件的启动时运行。可以用于此配置的两个脚本变量是run-at-start-upevaluate。脚本也可以配置为作为任务启动(这意味着作为批处理作业启动)。

<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,将 hadoop fs命令作为 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类是一个抽象类,并且已经被两个子类HbaseTemplateHbaseInterceptors扩展。

HBase

Spring 提供了一个名为HBaseTemplate的核心类。当 HBase 被实现时,这个类是应用程序的第一个接触点。这个类有访问表的所有方法,比如executefindfind all等等。

这个类有以下构造函数:

HbaseTemplate() 
HbaseTemplate(Configuration configuration)

这是可以在应用程序的context.xmlHbasecontext.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

HBaseInterceptorsHBaseSynchronizationManager可用于在方法调用之前将 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类执行以下操作:

  1. 创建Text()类的myword实例。

  2. 覆盖超类Mappermap方法,并实现以下步骤:

  3. 文本对象转换为字符串,并赋值给字符串line

  4. Line 是一个传递给字符串标记器的字符串对象。

  5. 使用while循环遍历字符串标记器,并调用removeNonLettersNonNumbers方法。返回的字符串赋值给myword文本实例。

  6. 调用context.write(myword,newIntwritable(1))方法。

  7. 有一个方法可以删除非字母和非数字,使用string.replaceAll()方法。最后返回一个只包含数字和字母的字符串对象。

接下来我们将创建一个 reducer 组件。reducer 组件将执行以下任务:

  1. 扩展reducer类。

  2. 为 reducer 类创建一个字符串属性,该属性接受需要搜索的字符串及其需要找到的出现次数。

  3. 覆盖reduce方法。

  4. 删除不需要的键值对。

  5. 保留所需的键值对。

  6. 检查输入键是否已经存在。如果存在,它将获取出现次数,并将最新值存储。

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);
    }
}
  1. 使用 HDFS 端口和输入输出文件路径配置application.properties文件。

  2. 这是示例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>
  1. 接下来,我们需要在 Spring 中配置 Hadoop 作业:

  2. 提供作业 ID。

  3. 指定输入路径;它将从属性文件中读取。

  4. 指定输出路径;它将从属性文件中读取。

  5. 按类进行 Jar。

  6. Mapper 类引用自定义 mapper 类。

  7. Reducer 类引用自定义 reducer 类。

  8. 这是需要在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"/>
  1. 最后,我们需要在application-context.xml文件中配置作业运行器。作业运行器配置告诉 Spring 框架何时启动作业。在这里,我们已经配置了作业运行器在启动时启动wordcountjob

  2. 这是作业运行器的配置片段。配置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 实例。

  1. 这些是设置要求的要求:
  • JAVA 1.7.x

  • 必须安装 SSH

  1. 下载最新的 Apache Hadoop 分发二进制包。

  2. 解压并将包提取到一个文件夹中。

  3. 设置以下环境变量:

  • JAVA_HOME

  • HADOOP_HOME

  • HADOOP_LOG_DIR

  • PATH

我们还需要配置 Hadoop 安装目录的conf文件夹中存在的文件:

  • Core-site.xml

  • Hdfs-site.xml

  • Mapred-site.xml

我们需要设置一个默认的 Hadoop 文件系统。

  1. 要配置默认的 Hadoop 文件系统,请在core-site.xml文件中提供设置信息。
<configuration>
  <property>
  <name>fs.default.name</name>
  <value>hdfs://localhost:9000</value>
  </property>
</configuration>
  1. 还要配置复制因子。复制因子配置确保文件的副本存储在 Hadoop 文件系统中。在hdfs-site.xml文件中设置属性dfs.replication及其值。
<configuration>
  <property>
    <name>dfs.replication</name>
    <value>1</value>
  </property>
</configuration>
  1. 最后,配置作业跟踪器;此配置在mapred-site.xml文件中完成。
<configuration>
  <property>
    <name>mapred.job.tracker</name>
    <value>localhost:9001</value>
  </property>
</configuration>
  1. 要在伪分布式模式下运行 Hadoop,我们只需要格式;在bin文件夹中,有startstop 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 脚本应该实现以下功能:

  1. 脚本应该读取从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")

  1. 一旦 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

  1. 我们还需要在context.xml文件中配置property-placeholder和 Apache Hadoop。这是配置:
<context:property-placeholder location="classpath:application.properties" />
<hdp:configuration>
  fs.default.name=${fs.default.name}
</hdp:configuration>
  1. 最后,我们需要配置 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"/>
  1. 现在,我们需要使用应用程序上下文来调用配置,以便应用程序上下文加载 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");
  }
}
  1. 在命令提示符中运行以下命令以提供输入文件。让文件放在名为input的文件夹中:
hadoop dfs -put MILLSANDBOON.txt /input/ MILLSANDBOON.txt

  1. 输出可以在输出目录中使用以下命令读取。
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

OSGIOpen 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接口的类。有ServiceListenersServiceTrackers来监视容器中的 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 下。

典型的捆绑结构如下图所示:

Spring integration with OSGI

下图展示了 OSGI 模块如何成为 Web 应用程序的一部分,以及每个捆绑如何与 OSGI 框架交互。您还可以看到 Web 容器上有许多 Web 应用程序,它们使用 OSGI 框架作为服务访问应用程序捆绑。

Spring integration with 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 时,需要遵循以下步骤:

  1. 下载 Spring DM;寻找最新的 Spring OSGI DM。

  2. 将 ZIP 文件提取到本地目录中;将其命名为c:\OSGI-SPRING

  3. 在启动 Eclipse 时创建一个新的工作空间。

  4. 通过选择插件开发选项或 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 应用程序。我们将创建两个捆绑——一个提供打印字符串的服务,另一个捆绑会以相等的时间间隔消费该服务。

  1. 以下是第一个捆绑:
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");
  }
}
  1. 使用激活器导出服务:
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();
  }
}
  1. 现在,我们已经准备好第一个捆绑,我们将使用 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>
  1. 要构建它,只需简单的mvn install命令即可。

  2. 接下来,让我们尝试消费服务:

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();
  }
}
  1. 现在,我们必须再次为消费者创建一个激活器:
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 自动检测,并创建一个应用程序上下文。

  1. 现在我们可以转到消费者 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();
  }
}
  1. 接下来,让我们编写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();
  }
}

注入不再是必要的。我们可以在这里保留计时器的启动,但是,再次,我们可以使用框架的功能来启动和停止计时器。

  1. 所以,让我们删除激活器并创建一个应用程序上下文来创建消费者并自动启动它,并将其放在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 应用程序可以通过以下方式设置:

  • 使用start.spring.io/

  • 使用 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
  1. 让我们创建一个简单的包:com.packt.controller

  2. 在包中创建一个简单的 Spring 控制器类,并使用@ Controller 注解。

  3. 让我们创建一个带有@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");
  }
}
  1. 创建一个 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 应用程序:

  1. 首先,我们需要从 GitHub 存储库下载 Spring Loaded JAR。检查以下 URL 以获取最新版本:

github.com/spring-projects/spring-loaded

  1. 确保您在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>
  1. 下一步是将下载的 Spring loaded JAR 添加到 Eclipse 或 Eclipse STS 环境中。按照给定的步骤将 Spring loaded JAR 添加为运行时配置:

  2. 在 Eclipse 中创建一个PacktSpringBootThymeLeafExample项目。

  3. 右键单击您的项目。

  4. 搜索Run As

  5. 点击Run Configuration

  6. 点击 Java 应用程序。

  7. 点击项目名称。

  8. VM Argument部分中选择Arguments;添加以下命令:

- javaagent:/<provide the path to the jar>/springloaded-1.2.0.RELEASE.jar -noverify
  1. 点击ApplyRun

我们还需要配置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)

  1. 让我们创建一个带有 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;
  }
}
  1. 接下来,我们将配置 Spring Boot 来处理 JSP 页面;默认情况下,Spring Boot 不配置 JSP,因此我们将创建一个 JSP 控制器,如下面的代码片段所示:
@Controller
public class SpringBootJSPController {
  @RequestMapping("/calljsp")
  public String test(ModelAndView modelAndView) {

    return "myjsp";
  }
}
  1. 按照以下方式配置属性文件:
spring.view.prefix: /WEB-INF/jsp/
spring.view.suffix: .jsp
  1. 让我们创建一个 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 集成。

  1. 让我们首先在 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);
          }
        });
      }
    };
  }
  1. 让我们还使用@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 类或实体类

  1. 让我们创建一个简单的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;
  }
  1. 接下来创建一个Jparepository子接口;我们不需要为此提供任何实现,因为它由 Spring JPA 数据处理:
public interface ProductRepository extends JpaRepository<Product, String>{

}

服务类

  1. 让我们创建一个处理保存的服务接口。
public interface ProductService {

  Product save(Product product);

}
  1. 我们还应该为服务接口创建一个实现类:
@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);
  }
  1. 在下一步中,我们还将创建一个用于服务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;
  }
  1. 让我们使用@RestController注解创建一个控制器;还要注意我们使用了@Inject注解。
  • @RestController:这与@Controller注解的区别在于前者在每个方法上也意味着@ResponseBody,这意味着写的内容更少,因为从 restful web 服务中我们无论如何都会返回 JSON 对象。

  • @RequestMapping:这将createProduct()映射到/Product URL 上的POST请求。该方法将产品对象作为参数。它是从请求的主体中创建的,这要归功于@RequestBody注解。然后进行验证,这是由@Valid强制执行的。

  • @InjectProductService将被注入到构造函数中,并且产品对象将被传递给其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);
  }
}
  1. 让我们创建一个带有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);
  }
}
  1. 现在,让我们使用 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:由 EhCache CacheManager支持的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 堆中使用多少字节。如果已定义了 CacheManager maxBytesLocalHeap,则此缓存的指定数量将从 CacheManager 中减去。其他缓存将共享余额。此属性的值以<number>k|K|m|M|g|G表示,用于千字节(k|K)、兆字节(m|M)和千兆字节(g|G)。例如,maxBytesLocalHeap="2g"分配了 2 千兆字节的堆内存。如果指定了maxBytesLocalHeap,则不能使用maxEntriesLocalHeap属性。如果设置了 CacheManager maxBytesLocalHeap,则不能使用maxEntriesLocalHeap

注意

在最高级别设置,此属性定义了为所有定义的缓存分配的内存。之后您必须将其与各个缓存分开。

  • eternal:这设置了元素是否是永恒的。如果是永恒的,超时将被忽略,元素永远不会过期。

  • timeToIdleSeconds:这设置了元素在过期之前的空闲时间。也就是说,元素在过期之前的访问之间的最长时间。只有在元素不是永久的情况下才会使用。可选属性。值为0表示元素可以无限期地空闲。默认值为0

  • timeToLiveSeconds:这设置了元素在过期之前的生存时间,即创建时间和元素过期时间之间的最长时间。只有在元素不是永久的情况下才会使用。可选属性。值为0表示元素可以永久存活。默认值为 0。

  • memoryStoreEvictionPolicy:在达到maxEntriesLocalHeap限制时将执行该策略。默认策略为最近最少使用LRU)。

注意

如果您想从数据库中卸载一些负载,还可以使用localTempSwap持久性策略,在这种情况下,您可以在缓存或 CacheManager 级别使用maxEntriesLocalDiskmaxBytesLocalDisk来控制磁盘层的大小。

已配置的两个缓存,参考数据和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 是并发包中的一个接口。ThreadPoolExecutorScheduledThreadPoolExecutor是实现ExecutorService的两个类。

有几种不同的方法可以将任务委托给ExecutorService进行执行:

  • execute (Runnable)

  • submit (Runnable)

  • submit (Callable)

  • invokeAny (...)

  • invokeAll (...)

Callable是类似于 Runnable 的接口。它是一个返回结果并可能抛出异常的任务。实现者定义了一个没有参数的方法叫做call

Callable 接口类似于 Runnable,两者都设计用于其实例可能由另一个线程执行的类。然而,Runnable 不返回结果,也不能抛出已检查的异常。

Executors 类包含了将其他常见形式转换为 Callable 类的实用方法。

让我们创建一个通用类;MyCache,这个类实例接受键和值对。它使用并发HashMap

  1. 让我们在条件上调用gettersetter方法;如果值已经在缓存中,那么只需获取该值,并且只有在不存在时才设置它。
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;
      } }); 
}}
  1. 接下来的步骤是在我们的斐波那契数列代码中使用缓存算法:
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 缓存缓存计算值。

我们将创建两个简单的类:WorkerMainWorker类有两个方法,这些方法从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 配置文件。

  1. 让我们首先从控制器开始,它有方法来插入和列出数据库中可用的作者。
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;
  }
}
  1. 接下来通过扩展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);
  }
}
  1. 在这一步中,我们将从 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);
  }
}
  1. 让我们实现应用程序服务实现类中使用的存储库类:
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);
}
  1. 接下来在应用程序中实现 Model 类(AuthorAuthorData)。
  • 首先让我们实现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;
  }
}
  1. 在这一步中,我们将创建配置类;如前所述,我们不使用 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/");
  }
}
  1. 下一步是在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>
  1. 最后,创建一个简单的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

  1. 使用原型来生成一个带有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 中。

  1. 您将打开pom.xml文件并添加一个parent项目:
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>1.1.8.RELEASE</version>
</parent>
  1. 开始向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>
  1. 最后添加 Spring boot 插件:
<build>
  <plugins>
  <plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  </plugin>
  </plugins>
</build>

让我们开始修改 web。但等一下,这不是 web 应用程序!

  1. 因此,让我们修改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);
  }
}
  1. 接下来,让我们配置 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>
  1. 您可以通过添加 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>
  1. 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.modespring.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 文件。

  1. 配置 Spring 安全过滤器。

  2. applicationContext-springsecurity.xml文件配置为上下文参数。

  3. applicationContext-springsecurity.xml中配置需要保护的 URL。

  4. 示例配置如下:

<?
<http auto-config="true">
  <form-login login-page="/login.html" authentication-failure-url="/login-error.html" />
  <logout />
  ...
</http>
  1. 配置 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";
  }
}
  1. 让我们看一下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>
  1. 在控制器中为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注解来完成,只需要几个步骤。让我们记下其中涉及的步骤。

  1. 在 Eclipse 中创建一个PACKTJAXWS-Spring简单的 Maven web 项目或动态 web 项目。

  2. 现在,我们需要在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>
  1. 创建一个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>
  1. 接下来,我们需要使所有的 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>
  1. 我们现在将创建一个带有@WebService注解的网络服务类。我们还定义了可能需要的绑定类型,比如SOAPBindingStyle@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";
  }
}
  1. 我们应该将 Maven JAR 文件添加到 Eclipse 项目的构建路径中。

  2. 运行应用程序:http://localhost:8080/PACKTJAXWS-Spring/jaxws-spring

您应该能够看到 WSDL URL,并在单击链接时,WSDL 文件应该打开。

使用 JAXB 编组的 Spring Web 服务请求

在本节中,让我们看看如何使用 Spring Web Service 框架开发一个简单的网络服务。我们需要 JAXB 来对 XML 请求进行编组和解组。Spring Web Service 支持契约优先的网络服务。我们需要首先设计 XSD/WSDL,然后启动网络服务。

我们正在创建一个作者网络服务,它将给我们提供作者列表。

  1. 配置 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>
  1. 配置 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>
  1. 定义 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>
  1. 编组 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);
}
  1. 创建端点类:让我们创建一个标有@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"));
  }

}
  1. 添加依赖信息:还要确保在 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>
  1. 构建和部署应用程序:我们需要在 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 服务。

posted @   绝不原创的飞龙  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
点击右上角即可分享
微信分享提示