Spring5-高级教程-七-

113 阅读28分钟

Spring5 高级教程(七)

原文:Pro Spring 5

协议:CC BY-NC-SA 4.0

十一、任务调度

任务调度是企业应用中的一个常见功能。任务调度主要由三部分组成:任务(需要在特定时间或定期运行的业务逻辑)、触发器(指定任务执行的条件)和调度器(根据触发器的信息执行任务)。具体来说,本章涵盖以下主题:

  • Spring 中的任务调度:我们讨论 Spring 如何支持任务调度,重点是 Spring 3 中引入的TaskScheduler抽象。我们还涵盖了调度场景,比如固定间隔调度和cron表达式。
  • 异步任务执行:我们展示了如何在 Spring 中使用@Async注释来异步执行任务。
  • Spring 中的任务执行:我们简单讨论一下 Spring 的TaskExecutor接口以及任务是如何执行的。

任务计划示例的相关性

您可以在下面的 Gradle 配置片段中看到本章所需的依赖项:

//pro-spring-15/build.gradle
ext {
   springDataVersion = '2.0.0.M3'

   //logging libs
   slf4jVersion = '1.7.25'
   logbackVersion = '1.2.3'

   guavaVersion = '21.0'
   jodaVersion = '2.9.9'
   utVersion = '6.0.1.GA'

   junitVersion = '4.12'

   spring = [
         data          :
             "org.springframework.data:spring-data-jpa:$springDataVersion",
         ...
   ]

   testing = [
         junit: "junit:junit:$junitVersion"
   ]

   misc = [
         slf4jJcl     : "org.slf4j:jcl-over-slf4j:$slf4jVersion",
         logback      : "ch.qos.logback:logback-classic:$logbackVersion",
         guava        : "com.google.guava:guava:$guavaVersion",
         joda         : "joda-time:joda-time:$jodaVersion",
         usertype     : "org.jadira.usertype:usertype.core:$utVersion",
         ...
   ]
   ...
}

...
//chapter11/build.gradle
dependencies {

      compile (spring.contextSupport) {
         exclude module: 'spring-context'
         exclude module: 'spring-beans'
         exclude module: 'spring-core'
      }
      compile misc.slf4jJcl, misc.logback, misc.lang3, spring.data,
         misc.guava, misc.joda, misc.usertype, db.h2

      testCompile testing.junit
}

Spring 任务调度

企业应用经常需要调度任务。在许多应用中,各种任务(例如向客户发送电子邮件通知、运行日终作业、进行数据整理和批量更新数据)需要定期运行,或者以固定的时间间隔(例如每小时)运行,或者按照特定的时间表运行(例如从周一到周五每天晚上 8 点运行)。如前所述,任务调度由三部分组成:调度定义(触发器)、任务执行(调度器)和任务本身。

在 Spring 应用中,有许多方法可以触发任务的执行。一种方法是从应用部署环境中已经存在的调度系统外部触发作业。例如,许多企业使用商业系统,如 Control-M 或 CA AutoSys 来调度任务。如果应用运行在 Linux/Unix 平台上,可以使用crontab调度程序。可以通过向 Spring 应用发送 RESTful-WS 请求并让 Spring 的 MVC 控制器触发任务来完成任务触发。

另一种方法是使用 Spring 中的任务调度支持。Spring 在任务调度方面提供了三个选项。

  • 支持 JDK 定时器:Spring 支持 JDK 的Timer对象进行任务调度。
  • 与 Quartz 集成:Quartz 调度器 1 是一个流行的开源调度库。
  • Spring 自己的 Spring TaskScheduler 抽象:Spring 3 引入了TaskScheduler抽象,它提供了一种简单的任务调度方式,并支持大多数典型的需求。

本节重点介绍如何使用 Spring 的TaskScheduler抽象来进行任务调度。

介绍 Spring TaskScheduler 抽象

Spring 的TaskScheduler抽象主要有三个参与者。

  • 触发接口:org.springframework.scheduling.Trigger接口支持定义触发机制。Spring 提供了两个Trigger实现。CronTrigger类支持基于cron表达式的触发,而PeriodicTrigger类支持基于初始延迟和固定间隔的触发。
  • 任务:任务是需要调度的业务逻辑的一部分。在 Spring 中,任务可以被指定为任何 Spring bean 中的一个方法。
  • TaskScheduler 接口:org.springframework.scheduling.TaskScheduler接口提供了对任务调度的支持。Spring 提供了三个TaskScheduler接口的实现类。TimerManagerTaskScheduler类(在包org.springframework. scheduling.commonj中)包装了 CommonJ 的commonj.timers.TimerManager接口,该接口通常用于商业 JEE 应用服务器,如 WebSphere 和 WebLogic。ConcurrentTaskSchedulerThreadPoolTaskScheduler类(都在包org.springframework.scheduling.concurrent下)包装java.util.concurrent.ScheduledThreadPoolExecutor类。这两个类都支持从共享线程池执行任务。

图 11-1 显示了Trigger接口、TaskScheduler接口以及实现java.lang.Runnable接口的任务实现之间的关系。要使用 Spring 的TaskScheduler抽象来调度任务,您有两种选择。一种是在 Spring 的 XML 配置中使用task-namespace,另一种是使用注释。让我们逐一查看。

A315511_5_En_11_Fig1_HTML.jpg

图 11-1。

Relationship between trigger, task, and scheduler

探索示例任务

为了演示 Spring 中的任务调度,让我们首先实现一个简单的作业,即维护汽车信息数据库的应用。下面的代码片段显示了作为 JPA 实体类实现的Car类:

package com.apress.prospring5.ch11;

import static javax.persistence.GenerationType.IDENTITY;

import javax.persistence.Column;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;

import org.hibernate.annotations.Type;
import org.joda.time.DateTime;

@Entity
@Table(name="car")
public class Car {
    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "ID")
    private Long id;

    @Column(name="LICENSE_PLATE")
    private String licensePlate;

    @Column(name="MANUFACTURER")
    private String manufacturer;

    @Column(name="MANUFACTURE_DATE")
    @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
    private DateTime manufactureDate;
    @Column(name="AGE")
    private int age;

    @Version
    private int version;

    //getters and setters
    ...

     @Override
    public String toString() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        return String.format("{License: %s, Manufacturer: %s,
           Manufacture Date: %s, Age: %d}",
           licensePlate, manufacturer, sdf.format(manufactureDate.toDate()), age);
    }
}

这个实体类用作 Hibernate 生成的CAR表的模型。数据访问和服务层的配置,在本章中全部合并为一个,由下面代码片段中描述的DataServiceConfig类提供:

package com.apress.prospring5.ch11.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;

@Configuration
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch11.repos"})
@ComponentScan(basePackages = {"com.apress.prospring5.ch11"} )
public class DataServiceConfig {

    private static Logger logger =
        LoggerFactory.getLogger(DataServiceConfig.class);

    @Bean
    public DataSource dataSource() {
        try {
            EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
            return dbBuilder.setType(EmbeddedDatabaseType.H2).build();
        } catch (Exception e) {
            logger.error("Embedded DataSource bean cannot be created!", e);
            return null;
        }
    }

    @Bean
    public Properties hibernateProperties() {
        Properties hibernateProp = new Properties();
        hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
        //hibernateProp.put("hibernate.format_sql", true);
        hibernateProp.put("hibernate.show_sql", true);
        hibernateProp.put("hibernate.max_fetch_depth", 3);
        hibernateProp.put("hibernate.jdbc.batch_size", 10);
        hibernateProp.put("hibernate.jdbc.fetch_size", 50);
        return hibernateProp;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new JpaTransactionManager(entityManagerFactory());
    }

    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Bean
    public EntityManagerFactory entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean factoryBean =
           new LocalContainerEntityManagerFactoryBean();
        factoryBean.setPackagesToScan("com.apress.prospring5.ch11.entities");
        factoryBean.setDataSource(dataSource());
        factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        factoryBean.setJpaProperties(hibernateProperties());
        factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
        factoryBean.afterPropertiesSet();
        return factoryBean.getNativeEntityManagerFactory();
    }
}

名为DBInitializer的类负责填充CAR表。

package com.apress.prospring5.ch11.config;

import com.apress.prospring5.ch11.entities.Car;
import com.apress.prospring5.ch11.repos.CarRepository;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;

@Service
public class DBInitializer {

    private Logger logger = LoggerFactory.getLogger(DBInitializer.class);
    @Autowired CarRepository carRepository;

    @PostConstruct
    public void initDB() {
        logger.info("Starting database initialization...");
        DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd");

        Car car = new Car();
        car.setLicensePlate("GRAVITY-0405");
        car.setManufacturer("Ford");
        car.setManufactureDate(DateTime.parse("2006-09-12", formatter));
        carRepository.save(car);

        car = new Car();
        car.setLicensePlate("CLARITY-0432");
        car.setManufacturer("Toyota");
        car.setManufactureDate(DateTime.parse("2003-09-09", formatter));
        carRepository.save(car);

        car = new Car();
        car.setLicensePlate("ROSIE-0402");
        car.setManufacturer("Toyota");
        car.setManufactureDate(DateTime.parse("2017-04-16", formatter));
        carRepository.save(car);

        logger.info("Database initialization finished.");
    }
}

让我们为Car实体定义一个 DAO 层。我们将使用 Spring Data 的 JPA 及其存储库抽象支持。这里你可以看到CarRepository接口,它是CrusRepository的简单扩展,因为我们对任何特殊的 DAO 操作都不感兴趣。

package com.apress.prospring5.ch11.repos;

import com.apress.prospring5.ch11.entities.Car;
import org.springframework.data.repository.CrudRepository;

public interface CarRepository extends CrudRepository<Car, Long> {
}

服务层由CarService接口及其实现CarServiceImpl表示。

package com.apress.prospring5.ch11.services;
//CarService.jar
import com.apress.prospring5.ch11.entities.Car;

import java.util.List;

public interface CarService {
    List<Car> findAll();
    Car save(Car car);
    void updateCarAgeJob();
    boolean isDone();
}

//CarServiceImpl.jar
...
@Service("carService")
@Repository
@Transactional
public class CarServiceImpl implements CarService {
    public boolean done;

    final Logger logger = LoggerFactory.getLogger(CarServiceImpl.class);

    @Autowired
    CarRepository carRepository;

    @Override
    @Transactional(readOnly=true)
    public List<Car> findAll() {
        return Lists.newArrayList(carRepository.findAll());
    }

    @Override
    public Car save(Car car) {
        return carRepository.save(car);
    }

    @Override
    public void updateCarAgeJob() {
        List<Car> cars = findAll();

        DateTime currentDate = DateTime.now();
        logger.info("Car age update job started");

       cars.forEach(car -> {
            int age = Years.yearsBetween(car.getManufactureDate(),
               currentDate).getYears();

            car.setAge(age);
            save(car);
            logger.info("Car age update --> " + car);
       });

       logger.info("Car age update job completed successfully");
       done = true;
    }

    @Override
    public boolean isDone() {
        return done;
    }
}

提供了四种方法,如下所示:

  • 检索所有汽车的信息:List<Car> findAll()
  • 一个持久化更新的Car对象:Car save(Car car)
  • 第三种方法,void updateCarAgeJob(),是需要定期运行的作业,根据汽车的制造日期和当前日期来更新车龄。
  • 第四个方法是boolean isDone(),这是一个实用方法,用来知道作业何时结束,这样应用就可以正常关闭。

像 Spring 中对其他名称空间的支持一样,task-namespace通过使用 Spring 的TaskScheduler抽象为调度任务提供了一个简化的配置。下面的 XML 配置片段显示了task-namespace-app-context.xml文件的内容,并显示了包含预定任务的 Spring 应用的配置。使用task-namespace进行任务调度非常简单。

<?xml version="1.0" encoding="UTF-8"?>
<beans 
       xmlns:task="http://www.springframework.org/schema/task"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/task

         http://www.springframework.org/schema/task/spring-task.xsd">

       <task:scheduler id="carScheduler" pool-size="10"/>

       <task:scheduled-tasks scheduler="carScheduler">
           <task:scheduled ref="carService"
              method="updateCarAgeJob" fixed-delay="10000"/>
       </task:scheduled-tasks>
</beans>

当遇到<task:scheduler>标签时,Spring 实例化一个ThreadPoolTaskScheduler类的实例,而属性pool-size指定调度程序可以使用的线程池的大小。在<task:scheduled-tasks>标记中,可以调度一个或多个任务。在<task:scheduled>标签中,一个任务可以引用一个 Spring bean(在本例中是carService bean)和 bean 中的一个特定方法(在本例中是updateCarAgeJob()方法)。属性fixed-delay指示 Spring 将PeriodicTrigger实例化为TaskSchedulerTrigger实现。

通过声明一个新的配置类并使用@Import导入两个配置(对于配置类使用@Import,对于 XML 配置使用@ImportResource),任务调度配置与数据访问配置相结合。

package com.apress.prospring5.ch11.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;

@Configuration
@Import({ DataServiceConfig.class })
@ImportResource("classpath:spring/task-namespace-app-context.xml")
public class AppConfig {}

这个配置类AppConfig用于创建一个 Spring ApplicationContext来测试 Spring 调度功能:

package com.apress.prospring5.ch11;

import com.apress.prospring5.ch11.config.AppConfig;
import com.apress.prospring5.ch11.services.CarService;
import com.apress.prospring5.ch11.services.CarServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

public class ScheduleTaskDemo {

    final static Logger logger = LoggerFactory.getLogger(CarServiceImpl.class);

    public static void main(String... args) throws Exception{
        GenericApplicationContext ctx =
           new AnnotationConfigApplicationContext(AppConfig.class);
        CarService carService = ctx.getBean("carService", CarService.class);

        while (!carService.isDone()) {
            logger.info("Waiting for scheduled job to end ...");
            Thread.sleep(250);
        }
        ctx.close();
    }
}

运行该程序会产生以下批处理作业输出:

[main] INFO c.a.p.c.s.CarServiceImpl - Waiting for scheduled job to end ...
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update job started
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
   GRAVITY-0405, Manufacturer: Ford, Manufacture Date: 2006-09-12, Age: 10}
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
   CLARITY-0432, Manufacturer: Toyota, Manufacture Date: 2003-09-09, Age: 13}
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
   ROSIE-0402, Manufacturer: Toyota, Manufacture Date: 2017-04-16, Age: 0}
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl -
   Car age update job completed successfully

在前面的示例中,应用仅在计划任务运行一次后停止。正如我们已经声明的,我们希望任务每 10 秒运行一次,通过设置fixed-delay="10000"属性;我们应该通过让应用运行直到用户按下一个键来允许任务的重复运行。修改ScheduleTaskDemo如下:

package com.apress.prospring5.ch11;

import com.apress.prospring5.ch11.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

public class ScheduleTaskDemo {

    public static void main(String... args) throws Exception {
        GenericApplicationContext ctx =
           new AnnotationConfigApplicationContext(AppConfig.class);

        System.in.read();
        ctx.close();
    }
}

从输出中,您可以看到汽车的age属性得到了更新。除了固定的时间间隔,更灵活的调度机制是使用一个cron表达式。在 XML 配置文件中,更改以下代码行:

<task:scheduled ref="carService" method="updateCarAgeJob" fixed-delay="10000"/>

致以下内容:

<task:scheduled ref="carService" method="updateCarAgeJob" cron="0 * * * * *"/>

更改后,再次运行ScheduleTaskDemo类,让应用运行一分多钟。您将看到该作业每分钟都会运行。

使用注释进行任务调度

使用 Spring 的TaskScheduler抽象来调度任务的另一个选择是使用注释。Spring 为此提供了@Scheduled注释。为了启用任务调度的注释支持,我们需要在 Spring 的 XML 配置中提供<task:annotation-driven>标签。或者,如果使用了配置类,必须用@EnableScheduling进行注释。让我们采用这种方法,完全去掉 XML。

package com.apress.prospring5.ch11.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@Import({DataServiceConfig.class})

@EnableScheduling

public class AppConfig {
}

是的,这就是所需要的。您甚至不再需要自己声明调度程序,因为 Spring 会处理它。在@Configuration类上使用的@EnableScheduling注释支持检测容器中任何 Spring 管理的 bean 或它们的方法上的@Scheduled注释。有趣的是,用@Scheduled注释的方法甚至可以直接在@Configuration类中声明。这个注释告诉 Spring 寻找一个相关的调度器定义:或者是上下文中唯一的TaskScheduler bean,或者是名为taskSchedulerTaskScheduler bean,或者是ScheduledExecutorService bean。如果没有找到,将在注册器中创建并使用一个本地单线程默认调度程序。

要在 Spring bean 中调度一个特定的方法,该方法必须用@Scheduled进行注释,并传递调度要求。在下面的代码片段中,CarServiceImpl类被扩展并用于声明一个带有预定方法的新 bean,该方法覆盖了父类中的updateCarAgeJob()方法以利用@Scheduled注释:

package com.apress.prospring5.ch11.services;

import com.apress.prospring5.ch11.entities.Car;
import org.joda.time.DateTime;
import org.joda.time.Years;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service("scheduledCarService")
@Repository
@Transactional

public class ScheduledCarServiceImpl extends CarServiceImpl{

    @Override
    @Scheduled(fixedDelay=10000)
    public void updateCarAgeJob() {
        List<Car> cars = findAll();

        DateTime currentDate = DateTime.now();
        logger.info("Car age update job started");

        cars.forEach(car -> {
            int age = Years.yearsBetween(
               car.getManufactureDate(), currentDate).getYears();

            car.setAge(age);
            save(car);
            logger.info("Car age update --> " + car);
        });

        logger.info("Car age update job completed successfully");
    }
}

测试程序如下所示:

package com.apress.prospring5.ch11;

import com.apress.prospring5.ch11.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

public class ScheduleTaskAnnotationDemo {

    public static void main(String... args) throws Exception {
        GenericApplicationContext ctx =
           new AnnotationConfigApplicationContext(AppConfig.class);

        System.in.read();

        ctx.close();
    }
}

运行该程序产生的输出与使用task-namespace几乎相同。您可以通过改变@Scheduled注释中的属性来尝试不同的触发机制(即fixedDelayfixedRatecron)。你可以自己测试。

[main] DEBUG o.s.s.a.ScheduledAnnotationBeanPostProcessor - Could not find default
   TaskScheduler bean
org.springframework.beans.factory.NoSuchBeanDefinitionException:
   No qualifying bean of type 'org.springframework.scheduling.TaskScheduler' available
... // more stacktrace here
[main] DEBUG o.s.s.a.ScheduledAnnotationBeanPostProcessor - Could not find default
   ScheduledExecutorService bean
org.springframework.beans.factory.NoSuchBeanDefinitionException:
   No qualifying bean of type 'java.util.concurrent.ScheduledExecutorService' available
... // more stacktrace here
[pool-1-thread-1] INFO c.a.p.c.s.CarServiceImpl - Car age update job started
[pool-1-thread-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
   GRAVITY-0405, Manufacturer: Ford, Manufacture Date: 2006-09-12, Age: 10}
[pool-1-thread-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
    CLARITY-0432, Manufacturer: Toyota, Manufacture Date: 2003-09-09, Age: 13}
[pool-1-thread-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
   ROSIE-0402, Manufacturer: Toyota, Manufacture Date: 2017-04-16, Age: 0}
[pool-1-thread-1] INFO c.a.p.c.s.CarServiceImpl - Car age update job
    completed successfully

此外,如果愿意,您可以定义自己的TaskScheduler bean。下面的例子声明了一个ThreadPoolTaskScheduler bean,它等同于上一节 XML 配置中声明的 bean:

package com.apress.prospring5.ch11.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Configuration
@Import({DataServiceConfig.class})
@EnableScheduling
public class AppConfig {

    @Bean TaskScheduler carScheduler() {
        ThreadPoolTaskScheduler carScheduler =
           new ThreadPoolTaskScheduler();
        carScheduler.setPoolSize(10);
        return carScheduler;
    }
}

如果您现在运行测试示例,您将会看到日志中不再打印异常,并且执行该方法的调度程序的名称已经更改,因为TaskScheduler bean 被命名为carScheduler

[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update job started
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
   GRAVITY-0405, Manufacturer: Ford, Manufacture Date: 2006-09-12, Age: 10}
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
    CLARITY-0432, Manufacturer: Toyota, Manufacture Date: 2003-09-09, Age: 13}
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
   ROSIE-0402, Manufacturer: Toyota, Manufacture Date: 2017-04-16, Age: 0}
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update job
    completed successfully

Spring 中的异步任务执行

从 3.0 版本开始,Spring 也支持使用注释来异步执行任务。要做到这一点,你只需要用@Async对方法进行注释。

package com.apress.prospring5.ch11;

import java.util.concurrent.Future;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

@Service("asyncService")
public class AsyncServiceImpl implements AsyncService {
    final Logger logger = LoggerFactory.getLogger(AsyncServiceImpl.class);

    @Async
    @Override
    public void asyncTask() {
        logger.info("Start execution of async. task");

        try {
            Thread.sleep(10000);
        } catch (Exception ex) {
            logger.error("Task Interruption", ex);
        }

        logger.info("Complete execution of async. task");
    }
    @Async
    @Override
    public Future<String> asyncWithReturn(String name) {
        logger.info("Start execution of async. task with return for "+ name);

        try {
            Thread.sleep(5000);
        } catch (Exception ex) {
            logger.error("Task Interruption", ex);
        }

        logger.info("Complete execution of async. task with return for " + name);

        return new AsyncResult<>("Hello: " + name);
    }
}

AsyncService定义了两种方法。asyncTask()方法是一个简单的任务,它将信息记录到记录器中。方法asyncWithReturn()接受一个String参数并返回一个java.util.concurrent.Future<V>接口的实例。在完成asyncWithReturn()之后,结果被存储在org.springframework.scheduling.annotation.AsyncResult<V>类的一个实例中,该类实现了Future<V>接口,调用者可以使用它在以后检索执行的结果。通过启用 Spring 的异步方法执行功能来获得@Async注释,这是通过用@EnableAsync注释 Java 配置类来完成的。 2

package com.apress.prospring5.ch11.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableAsync
@ComponentScan(basePackages = {"com.apress.prospring5.ch11"} )
public class AppConfig {
}

测试程序如下所示:

package com.apress.prospring5.ch11;

import java.util.concurrent.Future;

import com.apress.prospring5.ch11.config.AppConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

public class AsyncTaskDemo {
    private static Logger logger =
        LoggerFactory.getLogger(AsyncTaskDemo.class);

    public static void main(String... args) throws Exception{
        GenericApplicationContext ctx =
             new AnnotationConfigApplicationContext(AppConfig.class);

        AsyncService asyncService = ctx.getBean("asyncService",
           AsyncService.class);

        for (int i = 0; i < 5; i++) {
            asyncService.asyncTask();
        }

        Future<String> result1 = asyncService.asyncWithReturn("John Mayer");
        Future<String> result2 = asyncService.asyncWithReturn("Eric Clapton");
        Future<String> result3 = asyncService.asyncWithReturn("BB King");
        Thread.sleep(6000);

        logger.info("Result1: " + result1.get());
        logger.info("Result2: " + result2.get());
        logger.info("Result3: " + result3.get());

        System.in.read();
        ctx.close();
    }
}

我们用不同的参数调用asyncTask()方法五次,然后调用asyncWithReturn()三次,然后在休眠六秒后检索结果。运行该程序会产生以下输出:

...
17:55:31.851 [SimpleAsyncTaskExecutor-1] INFO  c.a.p.c.AsyncServiceImpl -
  Start execution of async. task
17:55:31.851 [SimpleAsyncTaskExecutor-2] INFO  c.a.p.c.AsyncServiceImpl -
  Start execution of async. task
17:55:31.851 [SimpleAsyncTaskExecutor-3] INFO  c.a.p.c.AsyncServiceImpl -
  Start execution of async. task
17:55:31.851 [SimpleAsyncTaskExecutor-4] INFO  c.a.p.c.AsyncServiceImpl -
  Start execution of async. task
17:55:31.852 [SimpleAsyncTaskExecutor-5] INFO  c.a.p.c.AsyncServiceImpl -
  Start execution of async. task
17:55:31.852 [SimpleAsyncTaskExecutor-6] INFO  c.a.p.c.AsyncServiceImpl -
  Start execution of async. task with return for John Mayer
17:55:31.852 [SimpleAsyncTaskExecutor-7] INFO  c.a.p.c.AsyncServiceImpl -
  Start execution of async. task with return for Eric Clapton
17:55:31.852 [SimpleAsyncTaskExecutor-8] INFO  c.a.p.c.AsyncServiceImpl -
  Start execution of async. task with return for BB King
17:55:36.856 [SimpleAsyncTaskExecutor-8] INFO  c.a.p.c.AsyncServiceImpl -
  Complete execution of async. task with return for BB King
17:55:36.856 [SimpleAsyncTaskExecutor-6] INFO  c.a.p.c.AsyncServiceImpl -
  Complete execution of async. task with return for John Mayer
17:55:36.856 [SimpleAsyncTaskExecutor-7] INFO  c.a.p.c.AsyncServiceImpl -
  Complete execution of async. task with return for Eric Clapton
17:55:37.852 [main] INFO  c.a.p.c.AsyncTaskDemo - Result1: Hello: John Mayer
17:55:37.853 [main] INFO  c.a.p.c.AsyncTaskDemo - Result2: Hello: Eric Clapton
17:55:37.853 [main] INFO  c.a.p.c.AsyncTaskDemo - Result3: Hello: BB King
17:55:41.852 [SimpleAsyncTaskExecutor-1] INFO  c.a.p.c.AsyncServiceImpl -

  Complete execution of async. task
17:55:41.852 [SimpleAsyncTaskExecutor-4] INFO  c.a.p.c.AsyncServiceImpl -
  Complete execution of async. task
17:55:41.852 [SimpleAsyncTaskExecutor-3] INFO  c.a.p.c.AsyncServiceImpl -
  Complete execution of async. task
17:55:41.852 [SimpleAsyncTaskExecutor-5] INFO  c.a.p.c.AsyncServiceImpl -
  Complete execution of async. task
17:55:41.852 [SimpleAsyncTaskExecutor-2] INFO  c.a.p.c.AsyncServiceImpl -
  Complete execution of async. task

从输出中,您可以看到所有的调用都是同时开始的。这三个带有返回值的调用首先完成,并显示在控制台输出中。最后,调用的五个asyncTask()方法也完成了。

Spring 任务执行

从 Spring 2.0 开始,框架通过TaskExecutor接口提供了执行任务的抽象。一个TaskExecutor做的和它听起来一样:它执行一个由 Java Runnable实现代表的任务。开箱即用,Spring 提供了许多适合不同需求的TaskExecutor实现。您可以在 http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/task/TaskExecutor.html 找到 TaskExecutor 实现的完整列表。

下面列出了一些常用的TaskExecutor实现:

  • SimpleAsyncTaskExecutor:每次调用时创建新线程;不重用现有线程
  • SyncTaskExecutor:不异步执行;调用发生在调用线程中
  • SimpleThreadPoolTaskExecutor:石英SimpleThreadPool的子类;当需要石英和非石英组件共享一个线程池时使用
  • ThreadPoolTaskExecutor : TaskExecutor实现提供了通过 bean 属性配置ThreadPoolExecutor并将其公开为 Spring TaskExecutor的能力

每个TaskExecutor实现都有自己的用途,调用约定也是一样的。唯一的变化是在配置中,当定义您想要使用哪个TaskExecutor实现及其属性时,如果有的话。让我们看一个简单的例子,它打印出许多消息。我们将使用的TaskExecutor实现是SimpleAsyncTaskExecutor。首先,让我们创建一个包含任务执行逻辑的 bean 类,如下所示:

package com.apress.prospring5.ch11;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Component;

@Component
public class TaskToExecute {
    private final Logger logger =
       LoggerFactory.getLogger(TaskToExecute.class);

    @Autowired
    private TaskExecutor taskExecutor;

    public void executeTask() {
        for(int i=0; i < 10; ++ i) {
            taskExecutor.execute(() ->
                logger.info("Hello from thread: " +
                Thread.currentThread().getName()));
        }
    }
}

这个类只是一个普通的 bean,需要将TaskExecutor作为依赖项注入,并定义一个方法executeTask()executeTask()方法通过创建一个新的Runnable实例来调用所提供的TaskExecutorexecute方法,该实例包含我们想要为该任务执行的逻辑。这在这里可能不明显,因为 lambda 表达式用于创建Runnable实例。配置相当简单;它类似于上一节中描述的配置。这里我们唯一要考虑的是我们需要为一个TaxExecutor bean 提供一个声明,它需要被注入到TaskToExecute bean 中。

package com.apress.prospring5.ch11.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Configuration
@EnableAsync
@ComponentScan(basePackages = {"com.apress.prospring5.ch11"} )
public class AppConfig {

    @Bean TaskExecutor taskExecutor() {
        return new SimpleAsyncTaskExecutor();
    }
}

在前面的配置中声明了一个简单的名为taskExecutor的类型为SimpleAsyncTaskExecutor的 bean。Spring IoC 容器将这个 bean 注入到TaskToExecute bean 中。要测试执行情况,您可以使用以下程序:

package com.apress.prospring5.ch11;

import com.apress.prospring5.ch11.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

public class TaskExecutorDemo {

    public static void main(String... args) throws Exception {
        GenericApplicationContext ctx =
            new AnnotationConfigApplicationContext(AppConfig.class);

        TaskToExecute taskToExecute = ctx.getBean(TaskToExecute.class);
        taskToExecute.executeTask();

        System.in.read();
        ctx.close();
    }
}

当该示例运行时,它应该打印类似于以下内容的输出:

Hello from thread: SimpleAsyncTaskExecutor-1
Hello from thread: SimpleAsyncTaskExecutor-5
Hello from thread: SimpleAsyncTaskExecutor-3
Hello from thread: SimpleAsyncTaskExecutor-10
Hello from thread: SimpleAsyncTaskExecutor-8
Hello from thread: SimpleAsyncTaskExecutor-6
Hello from thread: SimpleAsyncTaskExecutor-2
Hello from thread: SimpleAsyncTaskExecutor-4
Hello from thread: SimpleAsyncTaskExecutor-9
Hello from thread: SimpleAsyncTaskExecutor-7

从输出中可以看到,每个任务(我们正在打印的消息)在执行时都会显示出来。我们打印出消息加上线程名(默认情况下是类名SimpleAsyncTaskExecutor)和线程号。

摘要

在这一章中,我们介绍了 Spring 对任务调度的支持。我们重点介绍了 Spring 内置的TaskScheduler抽象,并通过一个示例批处理数据更新作业演示了如何使用它来满足任务调度需求。我们还介绍了 Spring 如何支持异步执行任务的注释。此外,我们简要介绍了 Spring 的TaskExecutor和常见实现。

不需要 Spring Boot 部分,因为任务注释的调度和异步执行是spring-context库的一部分,它们也必须与 Spring Boot 配置一起使用。另外,配置调度和异步任务已经和使用 Spring 一样简单了;在这个问题上,Spring Boot 能做的改进不多。3

Footnotes 1

你可以在 www.quartz-scheduler.org 找到官方页面。

  2

在 XML 中,只需声明<task:annotation-driven />就可以了。

  3

但如果你是古玩,想把提供的项目转换成 Spring Boot,可以在这里找到一个小教程: https://spring.io/guides/gs/scheduling-tasks/

十二、使用 Spring 远程处理

企业应用通常需要与其他应用通信。举个例子,一个卖产品的公司;当客户下订单时,订单处理系统处理该订单并生成交易。在订单处理过程中,会对库存系统进行查询,以检查产品是否有货。订单确认后,通知会发送到履行系统,以便将产品交付给客户。最后,信息被发送到会计系统,生成发票,并处理付款。

大多数情况下,这个业务流程不是由单个应用完成的,而是由许多应用协同工作完成的。一些应用可能是内部开发的,其他的可能是从外部供应商那里购买的。此外,应用可以在不同位置的不同机器上运行,并且用不同的技术和编程语言(例如,Java。NET 或 C++)。在设计和实现应用时,执行应用之间的握手以构建高效的业务流程始终是一项关键任务。因此,应用要很好地参与到企业环境中,就需要通过各种协议和技术提供远程支持。

在 Java 世界中,自从 Java 第一次被创建以来,远程支持就已经存在了。在早期(Java 1.x ),大多数远程需求是通过使用传统的 TCP 套接字或 Java 远程方法调用(RMI)来实现的。在 J2EE 出现之后,EJB 和 JMS 成为应用间服务器通信的常见选择。XML 和互联网的快速发展带来了通过 HTTP 使用 XML 的远程支持,包括基于 XML 的 RPC 的 Java API(JAX RPC)、XML Web 服务的 Java API(JAX WS)和基于 HTTP 的技术(例如,Hessian 和 Burlap)。Spring 还提供了自己的基于 HTTP 的远程支持,称为 Spring HTTP invoker。近年来,为了应对互联网的爆炸式增长和更具响应性的 web 应用需求(例如,通过 Ajax),对应用更轻量级和高效的远程支持已经成为企业成功的关键。因此,用于 RESTful Web 服务的 Java API(JAX-RS)应运而生并迅速流行起来。其他协议,如 Comet 和 HTML5 WebSocket,也吸引了很多开发者。不用说,远程技术一直在快速发展。

就远程处理而言,如前所述,Spring 提供了自己的支持(通过 Spring HTTP invoker),以及支持前面提到的许多技术(例如,RMI、EJB、JMS、Hessian、Burlap、JAX-RPC、JAX- WS 和 JAX-RS)。这一章不可能涵盖所有的内容。因此,这里我们重点关注那些最常用的。具体来说,本章涵盖以下主题:

  • Spring HTTP invoker:如果需要通信的两个应用都是基于 Spring 的,那么 Spring HTTP invoker 提供了一种简单有效的方法来调用其他应用公开的服务。我们将向您展示如何使用 Spring HTTP invoker 在服务层中公开服务,以及调用远程应用提供的服务。
  • 在 Spring 中使用 JMS:Java 消息服务(JMS)提供了另一种在应用之间交换消息的异步和松耦合方式。我们将向您展示 Spring 如何使用 JMS 简化应用开发。
  • 在 Spring 中使用 RESTful web 服务:RESTful web 服务专门围绕 HTTP 设计,是为应用提供远程支持以及使用 Ajax 支持高度交互式 web 应用前端的最常用技术。我们展示了 Spring MVC 如何使用 JAX-RS 为公开服务提供全面的支持,以及如何使用RestTemplate类调用服务。我们还讨论了如何保护服务免受未经授权的访问。
  • 在 Spring 中使用 AMQP:Spring Advanced Message Queuing Protocol(AMQP)的姐妹项目围绕 AMQP 提供了一个典型的类似 Spring 的抽象以及一个 RabbitMQ 实现。这个项目提供了丰富的功能,但是在这一章中,我们通过 RPC 支持的项目来关注它的远程功能。

对样本使用数据模型

在本章的示例中,我们将使用一个简单的数据模型,它只包含用于存储信息的SINGER表。该表由 Hibernate 基于下面显示的Singer类生成。该类及其属性是用标准 JPA 注释修饰的。

package com.apress.prospring5.ch12.entities;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

import static javax.persistence.GenerationType.IDENTITY;

@Entity
@Table(name = "singer")
public class Singer implements Serializable {
    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "ID")
    private Long id;

    @Version
    @Column(name = "VERSION")
    private int version;

    @Column(name = "FIRST_NAME")
    private String firstName;

    @Column(name = "LAST_NAME")
    private String lastName;

    @Temporal(TemporalType.DATE)
    @Column(name = "BIRTH_DATE")
    private Date birthDate;

    //setters and getters
    ...
}

要填充这个表,您需要使用一个初始化器 bean。该类如下所示:

package com.apress.prospring5.ch12.services;

import com.apress.prospring5.ch12.entities.Singer;
import com.apress.prospring5.ch12.repos.SingerRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.GregorianCalendar;

@Service
public class DBInitializer {

    private Logger logger = LoggerFactory.getLogger(DBInitializer.class);
    @Autowired
    SingerRepository singerRepository;

    @PostConstruct
    public void initDB() {
        logger.info("Starting database initialization...");
        Singer singer = new Singer();
        singer.setFirstName("John");
        singer.setLastName("Mayer");
        singer.setBirthDate(new Date(
                (new GregorianCalendar(1977, 9, 16)).getTime().getTime()));
        singerRepository.save(singer);

        singer = new Singer();
        singer.setFirstName("Eric");
        singer.setLastName("Clapton");
        singer.setBirthDate(new Date(
                (new GregorianCalendar(1945, 2, 30)).getTime().getTime()));
        singerRepository.save(singer);

        singer = new Singer();
        singer.setFirstName("John");
        singer.setLastName("Butler");
        singer.setBirthDate(new Date(
                (new GregorianCalendar(1975, 3, 1)).getTime().getTime()));

        singerRepository.save(singer);
        logger.info("Database initialization finished.");
    }
}

为 JPA 后端添加所需的依赖项

我们需要将所需的依赖项添加到项目中。下面的配置片段显示了使用 JPA 2 和 Hibernate 作为持久性提供者来实现服务层所需的依赖关系。此外,将使用 Spring Data JPA。

//pro-spring-15/build.gradle
ext {
    //spring libs
    springVersion = '5.0.0.RC1'
    springDataVersion = '2.0.0.M3'

    //logging libs
    slf4jVersion = '1.7.25'
    logbackVersion = '1.2.3'

    junitVersion = '4.12'

    //database library
    h2Version = '1.4.194'

    //persistency libraries
    hibernateVersion = '5.2.10.Final'
    hibernateJpaVersion = '1.0.0.Final'
    atomikosVersion = '4.0.0M4'

    spring = [
        context       : "org.springframework:spring-context:$springVersion",
        aop           : "org.springframework:spring-aop:$springVersion",
        aspects       : "org.springframework:spring-aspects:$springVersion",
        tx            : "org.springframework:spring-tx:$springVersion",
        jdbc          : "org.springframework:spring-jdbc:$springVersion",
        contextSupport: "org.springframework:spring-context-support:$springVersion",
        orm           : "org.springframework:spring-orm:$springVersion",
        data          : "org.springframework.data:spring-data-jpa:$springDataVersion",
        test          : "org.springframework:spring-test:$springVersion"
    ]

    hibernate = [
        ...
        em        : "org.hibernate:hibernate-entitymanager:$hibernateVersion",
        tx        : "com.atomikos:transactions-hibernate4:$atomikosVersion"
    ]

    testing = [
        junit: "junit:junit:$junitVersion"
    ]

    misc = [
        ...
        slf4jJcl     : "org.slf4j:jcl-over-slf4j:$slf4jVersion",
        logback      : "ch.qos.logback:logback-classic:$logbackVersion",
        lang3        : "org.apache.commons:commons-lang3:3.5",
        guava        : "com.google.guava:guava:$guavaVersion"
    ]

    db = [
        ...
        h2   : "com.h2database:h2:$h2Version"
    ]
}

//chapter12/spring-invoker/build.gradle
dependencies {
    //we specify these dependencies for all submodules, except
    // the boot module, that defines its own
    if (!project.name.contains("boot")) {
        //we exclude transitive dependencies, because spring-data
        //will take care of these
           compile (spring.contextSupport) {
           exclude  module: 'spring-context'
           exclude  module: 'spring-beans'
           exclude  module: 'spring-core'
        }
        //we exclude the 'hibernate' transitive dependency
        //to have control over the version used
        compile (hibernate.tx) {
       exclude group: 'org.hibernate', module: 'hibernate'
    }
        compile misc.slf4jJcl, misc.logback, misc.lang3,
           hibernate.em, misc.guava
     }
     testCompile testing.junit
}

实现和配置 SingerService

概述完依赖关系后,我们开始展示如何为本章中的示例实现和配置服务层。在接下来的小节中,我们将讨论使用 JPA 2、Spring Data JPA 和 Hibernate 作为持久性服务提供者来实现SingerService。然后,我们将介绍如何在 Spring 项目中配置服务层。

实现单一服务

在示例中,我们展示了如何向远程客户端公开针对歌手信息的各种操作的服务;这里显示的是SingerService界面:

package com.apress.prospring5.ch12.services;

import com.apress.prospring5.ch12.entities.Singer;

import java.util.List;

public interface SingerService {

    List<Singer> findAll();
    List<Singer> findByFirstName(String firstName);
    Singer findById(Long id);
    Singer save(Singer singer);
    void delete(Singer singer);
}

这些方法应该是不言自明的。因为我们将使用 Spring Data JPA 的存储库支持,所以我们实现了SingerRepository接口,如下所示:

package com.apress.prospring5.ch12.repos;

import com.apress.prospring5.ch12.entities.Singer;
import org.springframework.data.repository.CrudRepository;

import java.util.List;

public interface SingerRepository extends CrudRepository<Singer, Long> {
    List<Singer> findByFirstName(String firstName);
}

通过扩展CrudRepository<T,ID extends Serializable>接口,对于SingerService中的方法,我们只需要显式声明findByFirstName()方法。

下一个代码片段显示了SingerService接口的实现类:

package com.apress.prospring5.ch12.services;

import com.apress.prospring5.ch12.entities.Singer;
import com.apress.prospring5.ch12.repos.SingerRepository;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service("singerService")
@Transactional
public class SingerServiceImpl implements SingerService {

    @Autowired
    private SingerRepository singerRepository;

    @Override
    @Transactional(readOnly = true)
    public List<Singer> findAll() {
        return Lists.newArrayList(singerRepository.findAll());
    }

    @Override
    @Transactional(readOnly = true)
    public List<Singer> findByFirstName(String firstName) {
        return singerRepository.findByFirstName(firstName);
    }

    @Override
    @Transactional(readOnly = true)
    public Singer findById(Long id) {
        return singerRepository.findById(id).get();
    }

    @Override
    public Singer save(Singer singer) {
        return singerRepository.save(singer);
    }

    @Override
    public void delete(Singer singer) {
        singerRepository.delete(singer);
    }
}

实现基本完成,下一步是在 web 项目内配置 Spring 的ApplicationContext中的服务,这将在下一节讨论。

正在配置 SingerService

对于数据访问和事务,您可以使用一个简单的 Java 配置类,如前面介绍的和这里显示的:

package com.apress.prospring5.ch12.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;

@Configuration

@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch12.repos"})
@ComponentScan(basePackages  = {"com.apress.prospring5.ch12"} )
public class DataServiceConfig {

    private static Logger logger =
        LoggerFactory.getLogger(DataServiceConfig.class);

    @Bean
    public DataSource dataSource() {
        try {
            EmbeddedDatabaseBuilder dbBuilder =
               new EmbeddedDatabaseBuilder();
            return dbBuilder.setType(EmbeddedDatabaseType.H2).build();
        } catch (Exception e) {
            logger.error("Embedded DataSource bean cannot be created!", e);
            return null;
        }
    }

    @Bean
    public Properties hibernateProperties() {
        Properties hibernateProp = new Properties();
        hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
        hibernateProp.put("hibernate.show_sql", true);
        hibernateProp.put("hibernate.max_fetch_depth", 3);
        hibernateProp.put("hibernate.jdbc.batch_size", 10);
        hibernateProp.put("hibernate.jdbc.fetch_size", 50);
        return hibernateProp;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new JpaTransactionManager(entityManagerFactory());
    }

    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Bean
    public EntityManagerFactory entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean factoryBean =
           new LocalContainerEntityManagerFactoryBean();
        factoryBean.setPackagesToScan("com.apress.prospring5.ch12.entities");
        factoryBean.setDataSource(dataSource());
        factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        factoryBean.setJpaProperties(hibernateProperties());
        factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
        factoryBean.afterPropertiesSet();
        return factoryBean.getNativeEntityManagerFactory();
    }
}

因为我们通过 Spring MVC 公开 HTTP 调用程序,所以我们需要配置 web 应用。要在不使用任何 XML 的情况下配置 Spring Web MVC 应用,需要两个配置类,如下所示:

  • 一个配置类实现了WebMvcConfigurer接口。这个接口是在 Spring 3.1 中引入的,它定义了回调方法来为使用@EnableWebMvc启用的 Spring MVC 定制基于 Java 的配置。因为我们只需要公开一个 HTTP 服务(不需要 web 接口),所以在这种情况下一个空的实现就足够了。用@EnableWebMvc注释的这个接口的实现使用mvc名称空间代替了 Spring XML 配置。更复杂的示例配置将在第十六章中详细介绍。

    package com.apress.prospring5.ch12.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.EnableWebMvc;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    @EnableWebMvc
    public class WebConfig implements WebMvcConfigurer {
    
    }
    
    
  • 另一个配置类实现了org.springframework.web.WebApplicationInitializer或者扩展了一个现成的 Spring 实现。这个接口需要在 Spring 3.0+环境中实现,以编程方式配置ServletContext。这消除了提供一个web.xml文件来配置 web 应用的必要性。该类导入数据访问和事务的配置,并基于它创建根应用上下文。web 应用上下文是使用WebConfig类和配置类创建的,配置类定义了 HTTP invoker 服务的配置。这些类也可以合并成一个类,但是使用 Spring 的良好实践是将定制服务和基础设施 beans 保存在不同的类中。

    package com.apress.prospring5.ch12.config;
    
    import org.springframework.web.servlet.support.
        AbstractAnnotationConfigDispatcherServletInitializer;
    
    public class WebInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {
    
        @Override
        protected Class<?>[] getRootConfigClasses() {
            return new Class<?>[]{
                    DataServiceConfig.class
            };
        }
    
        @Override
        protected Class<?>[] getServletConfigClasses() {
            return new Class<?>[]{
                    HttpInvokerConfig.class, WebConfig.class
            };
        }
    
        @Override
        protected String[] getServletMappings() {
            return new String[]{"/invoker/*"};
        }
    }
    
    

因为这是一个 Spring MVC web 应用,所以需要创建一个 WAR 文件并将其部署到 servlet 容器中。有多种方法可以做到这一点,例如,独立的容器(如 Tomcat)、IDE 启动的 Tomcat 实例,或者与构建工具(如 Maven)一起运行的嵌入式 Tomcat 实例。您选择哪个选项取决于您的需求,但是对于本地开发环境,建议从您的构建工具或直接从您的 IDE 启动嵌入式实例。在本书的代码中,我们使用 Tomcat Server version 9.x,并在 Intellij IDEA 中设置了一个启动器来启动 web 应用。更多细节请参见本书源代码。此时,您应该构建 web 应用,并通过您选择的方法进行部署。如果您尝试在浏览器中加载http://localhost:8080/ URL,您会看到以下消息:

Spring Remoting: Simplifying Development of Distributed Applications

RMI services over HTTP should be correctly exposed when this page is visible.

这意味着 web 应用已经被正确部署,现在应该可以在http://localhost:8080/invoker/httpInvoker/singerService URL 访问到singerService bean 了。

公开服务

如果您要与之通信的应用也是 Spring 支持的,那么使用 Spring HTTP invoker 是一个不错的选择。它提供了一种非常简单的方法,将 Spring WebApplicationContext中的服务公开给远程客户端,远程客户端也使用 Spring HTTP invoker 来调用服务。公开和访问服务的过程将在下面的章节中详细介绍。HttpInvokerConfig类包含一个用于公开 HTTP invoker 服务的 bean。

package com.apress.prospring5.ch12.config;

import com.apress.prospring5.ch12.services.SingerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter;

@Configuration
public class HttpInvokerConfig {

    @Autowired
    SingerService singerService;

    @Bean(name = "/httpInvoker/singerService")
    public HttpInvokerServiceExporter httpInvokerServiceExporter() {
        HttpInvokerServiceExporter invokerService =
             new HttpInvokerServiceExporter();
        invokerService.setService(singerService);
        invokerService.setServiceInterface(SingerService.class);
        return invokerService;
    }

}

HttpInvokerServiceExporter类定义了一个httpInvokerServiceExporter bean,用于通过 HTTP invoker 将任何 Spring bean 作为服务导出。在 bean 中,定义了两个属性。第一个是service属性,表示提供服务的 bean。对于这个属性,注入了singerService bean。第二个属性是要公开的接口类型,即com.apress.prospring5.ch12.serviced.SingerService接口。

现在,服务层已经完成,可以公开给远程客户端使用了。

调用服务

通过 Spring HTTP invoker 调用服务很简单。首先我们配置一个 SpringApplicationContext,如下面的配置类所示:

package com.apress.prospring5.ch12.config;

import com.apress.prospring5.ch12.services.SingerService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean;

@Configuration
public class RmiClientConfig {

    @Bean
    public SingerService singerService() {
        HttpInvokerProxyFactoryBean factoryBean =
            new HttpInvokerProxyFactoryBean();
        factoryBean.setServiceInterface(SingerService.class);
        factoryBean.setServiceUrl(
            "http://localhost:8080/invoker/httpInvoker/singerService");
        factoryBean.afterPropertiesSet();
        return (SingerService) factoryBean.getObject();
    }
}

如前面客户端所示,声明了一个类型为HttpInvokerProxyFactoryBean的 bean。设置了两个属性。serviceUrl指定远程服务的位置,即http://localhost:8080/invoker/httpInvoker/singerService。第二个属性是服务的接口(即SingerService)。如果您正在为客户端开发另一个项目,您需要在客户端应用的类路径中拥有SingerService接口和Singer实体类。

下面的代码片段显示了一个调用远程服务的测试类。我们正在使用一个测试类,它使用RmiClientConfig类来创建一个测试上下文。SpringRunner类是在 Spring 上下文中运行 Junit 测试所需的SpringJUnit4ClassRunner的别名。你会在第十三章中了解到更多。

package com.apress.prospring5.ch12;

import com.apress.prospring5.ch12.config.RmiClientConfig;
import com.apress.prospring5.ch12.entities.Singer;
import com.apress.prospring5.ch12.services.DBInitializer;
import com.apress.prospring5.ch12.services.SingerService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

import static org.junit.Assert.assertEquals;

@ContextConfiguration(classes = RmiClientConfig.class)
@RunWith(SpringRunner.class)
public class RmiTests {
    private Logger logger = LoggerFactory.getLogger(RmiTests.class);

    @Autowired
    private SingerService singerService;

    @Test
    public void testRmiAll() {
        List<Singer> singers = singerService.findAll();
        assertEquals(3, singers.size());
        listSingers(singers);
    }

    @Test
    public void testRmiJohn() {
        List<Singer> singers = singerService.findByFirstName("John");
        assertEquals(2, singers.size());
        listSingers(singers);
    }

    private void listSingers(List<Singer> singers){
        singers.forEach(s -> logger.info(s.toString()));
    }
}

测试类应该在部署 web 应用之后执行。测试应该通过,并列出由singerService bean 返回的Singer实例。预期输出如下所示:

//testRmiAll
INFO  c.a.p.c.RmiTests - Singer - Id: 1, First name: John, Last name: Mayer,
   Birthday: 1977-10-16
INFO  c.a.p.c.RmiTests - Singer - Id: 2, First name: Eric, Last name: Clapton,
   Birthday: 1945-03-30
INFO  c.a.p.c.RmiTests - Singer - Id: 3, First name: John, Last name: Butler,
   Birthday: 1975-04-01
//testRmiJohn - all singers with firstName='John'
INFO  c.a.p.c.RmiTests - Singer - Id: 1, First name: John, Last name: Mayer,
   Birthday: 1977-10-16
INFO  c.a.p.c.RmiTests - Singer - Id: 3, First name: John, Last name: Butler,
   Birthday: 1975-04-01

在 Spring 中使用 JMS

使用面向消息的中间件(通常称为 MQ 服务器)是支持应用间通信的另一种流行方式。消息队列(MQ)服务器的主要优点是它为应用集成提供了一种异步和松散耦合的方式。在 Java 世界中,JMS 是连接到 MQ 服务器发送或接收消息的标准。MQ 服务器维护一个队列和主题列表,应用可以连接到这些队列和主题,并发送和接收消息。以下是对队列和主题之间的区别的简要描述:

  • 队列:队列用于支持点对点消息交换模型。当生产者向队列发送消息时,MQ 服务器将消息保存在队列中,并在下次消费者连接时将消息传递给一个(且仅一个)消费者。
  • 主题:主题用于支持发布-订阅模型。任何数量的客户端都可以订阅主题中的消息。当针对该主题的消息到达时,MQ 服务器将它传递给订阅了该消息的所有客户机。当您有多个对同一条信息感兴趣的应用(例如,一个新闻提要)时,这种模型特别有用。

在 JMS 中,生产者连接到 MQ 服务器,并向队列或主题发送消息。消费者还连接到 MQ 服务器,并监听队列或感兴趣的消息主题。在 JMS 1.1 中,API 是统一的,因此生产者和消费者不需要处理不同的 API 来与队列和主题进行交互。在本节中,我们将重点关注使用队列的点对点方式,这是企业中更常用的模式。

从 Spring Framework 4.0 开始,已经实现了对 JMS 2.0 的支持。JMS 2.0 的功能可以通过在类路径中包含 JMS 2.0 JAR 来使用,同时保留对 1.x 的向后兼容性。因此,在本书中,示例将只与 JMS 2.x 相关。

在撰写本文时,ActiveMQ 不支持 JMS 2.0 因此,我们将利用 HornetQ(包含从 2.4.0.Final 开始的 JMS 2.0 支持)作为这个示例中的消息代理,并将使用一个独立的服务器。下载和安装 HornetQ 不在本书讨论范围之内;请参考 http://docs.jboss.org/hornetq/2.4.0.Final/docs/quickstart-guide/html/index.html 的文档。 2

需要几个新的依赖项,这里显示了所需的梯度配置:

//pro-spring-15/build.gradle
ext {
   jmsVersion = '2.0'
   hornetqVersion = '2.4.0.Final'

   spring = [
    ...
    jms            : "org.springframework:spring-jms:$springVersion"
   ]

   misc = [
      ...
      Hornetq      : "org.hornetq:hornetq-jms-client:$hornetqVersion"
   ]
...
}
//chapter12/jms-hornetq/build.gradle
dependencies {
    compile spring.jms, misc.jms
}

安装服务器后,我们需要在 HornetQ JMS 配置文件中创建一个队列。该文件位于您提取 HornetQ 的目录下。文件的位置是config/stand-alone/non-clustered hornetq-jms.xml,我们需要添加队列定义,如下图所示:

<configuration 
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="urn:hornetq /schema/hornetq-jms.xsd">
            ...

      <queue name="prospring5">
              <entry name="/queue/prospring5"/>
      </queue>
</configuration>

现在,通过运行run.sh脚本(取决于您的操作系统)启动 HornetQ 服务器,并确保服务器启动时没有任何错误。只要确保您在日志中看到类似的东西,没有例外,并确保下划线行在那里。

...
00:36:21,171 INFO  [org.hornetq.core.server] HQ221035: Live Server Obtained live lock
00:36:21,841 INFO  [org.hornetq.core.server] HQ221003:
     trying to deploy queue jms.queue.DLQ
00:36:21,852 INFO  [org.hornetq.core.server] HQ221003:
// the queue configured in the previous configuration sample
     trying to deploy queue jms.queue.prospring5

00:36:21,853 INFO  [org.hornetq.core.server] HQ221003:
     trying to deploy queue jms.queue.ExpiryQueue
00:36:21,993 INFO  [org.hornetq.core.server] HQ221020:
     Started Netty Acceptor version 4.0.13.Final localhost:5455
00:36:21,996 INFO  [org.hornetq.core.server] HQ221020:
     Started Netty Acceptor version 4.0.13.Final localhost:5445
00:36:21,997 INFO  [org.hornetq.core.server] HQ221007: Server is now live

现在必须提供一个 Spring 配置来连接到这个服务器并访问之前配置的prospring5队列。通常,应该有两个配置类,一个用于消息发送者,一个用于消息监听器,但是因为使用配置类的 Spring JMS 配置非常实用,并且不需要很多 beans,所以我们将所有配置放在一个类中,如下所示:

package com.apress.prospring5.ch12.config;

import org.hornetq.api.core.TransportConfiguration;
import org.hornetq.core.remoting.impl.netty.NettyConnectorFactory;
import org.hornetq.core.remoting.impl.netty.TransportConstants;
import org.hornetq.jms.client.HornetQJMSConnectionFactory;
import org.hornetq.jms.client.HornetQQueue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.listener.DefaultMessageListenerContainer;

import javax.jms.ConnectionFactory;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableJms
@ComponentScan("com.apress.prospring5.ch12")
public class AppConfig {

    @Bean HornetQQueue prospring5() {
       return new HornetQQueue("prospring5");
    }

    @Bean ConnectionFactory connectionFactory() {
       Map<String, Object> connDetails = new HashMap<>();
       connDetails.put(TransportConstants.HOST_PROP_NAME, "127.0.0.1");
       connDetails.put(TransportConstants.PORT_PROP_NAME, "5445");
       TransportConfiguration transportConfiguration = new TransportConfiguration(
             NettyConnectorFactory.class.getName(), connDetails);
       return new HornetQJMSConnectionFactory(false, transportConfiguration);
    }

    @Bean
    public JmsListenerContainerFactory<DefaultMessageListenerContainer>
            jmsListenerContainerFactory() {
       DefaultJmsListenerContainerFactory factory =
           new DefaultJmsListenerContainerFactory();
       factory.setConnectionFactory(connectionFactory());
       factory.setConcurrency("3-5");
       return factory;
    }

    @Bean JmsTemplate jmsTemplate() {
       JmsTemplate jmsTemplate = new JmsTemplate(connectionFactory());
       jmsTemplate.setDefaultDestination(prospring5());
       return jmsTemplate;
    }
}

javax.jms.ConnectionFactory接口实现由 HornetQ Java 库(HornetQJMSConnectionFactory类)提供,用于创建与 JMS 提供者的连接。然后,声明一个类型为JmsListenerContainerFactory的 bean,它将创建使用普通 JMS 客户机 API 接收 JMS 消息的消息监听器容器。jmsTemplate bean 将用于向prospring5队列发送 JMS 消息。

要接收 JMS 消息,必须声明一个消息监听器组件,提供目的地(即prospring5队列)和 JMS 容器工厂jmsListenerContainerFactory

在 Spring 中实现 JMS 侦听器

在 Spring 4.1 之前,为了开发一个消息监听器,我们需要创建一个实现javax.jms.MessageListener接口并实现其onMessage()方法的类。在 Spring 4.1 中添加了@JmsListener注释。该注释用于 bean 方法,以将它们标记为指定目的地(队列或主题)上的 JMS 消息侦听器的目标。下面的代码片段描述了SimpleMessageListener类和 bean 声明:

package com.apress.prospring5.ch12;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.TextMessage;

@Component("messageListener")
public class SimpleMessageListener{
    private static final Logger logger =
        LoggerFactory.getLogger(SimpleMessageListener.class);

    @JmsListener(destination = "prospring5", containerFactory =
      "jmsListenerContainerFactory")
    public void onMessage(Message message) {
        TextMessage textMessage = (TextMessage) message;

        try {
            logger.info(">>> Received: " + textMessage.getText());
        } catch (JMSException ex) {
            logger.error("JMS error", ex);
        }
    }
}

这里将该方法命名为onMessage,以使其目的更明显。在用@JmsListener方法注释的onMessage()中,javax.jms.Message接口的一个实例将在消息到达时被传递。在该方法中,消息被强制转换为javax.jms.TextMessage接口的实例,并且使用TextMessage.getText()方法检索文本形式的消息体。有关可能的消息格式列表,请参考当前的 JEE 在线文档。

通过在配置类上使用@EnableJms或者通过使用等价的 XML 元素声明(即<jms:annotation-driven/>)来完成对@JmsListener注释的处理。

现在让我们看看如何向propring5队列发送消息。

在 Spring 中发送 JMS 消息

让我们看看如何在 Spring 中使用 JMS 发送消息。为此,我们将使用类型为org.springframework.jms.core.JmsTemplate的便捷 bean jmsTemplate。首先我们将开发一个MessageSender接口及其实现类SimpleMessageSender。下面的代码片段分别显示了接口和类:

//MessageSender.java
package com.apress.prospring5.ch12;

public interface MessageSender {
    void sendMessage(String message);
}

//SimpleMessageSender.java
package com.apress.prospring5.ch12;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.springframework.stereotype.Component;

@Component("messageSender")
public class SimpleMessageSender implements MessageSender {
    private static final Logger logger =
             LoggerFactory.getLogger(SimpleMessageSender.class);
    @Autowired
    private JmsTemplate jmsTemplate;

    @Override
    public void sendMessage(final String message) {
        jmsTemplate.setDeliveryDelay(5000L);

        this.jmsTemplate.send(new MessageCreator() {
            @Override
            public Message createMessage(Session session)
                    throws JMSException {
                TextMessage jmsMessage = session.createTextMessage(message);
                logger.info(">>> Sending: " + jmsMessage.getText());
                return jmsMessage;
            }
        });
    }
}

如您所见,注入了一个JmsTemplate实例。在sendMessage()方法中,我们调用了JmsTemplate.send()方法,并就地构造了org.springframework.jms.core.MessageCreator接口的一个实例。在MessageCreator实例中,实现了createMessage()方法来创建一个新的TextMessage实例,该实例将被发送到 HornetQ。

消息侦听器和发送方 bean 声明都是使用组件扫描来获取的。

现在,让我们将发送和接收结合起来,看看 JMS 的运行情况。以下代码片段显示了发送消息和接收消息的主要测试程序:

package com.apress.prospring5.ch12;

import com.apress.prospring5.ch12.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

import java.util.Arrays;

public class JmsHornetQSample {
    public static void main(String... args) throws Exception{
        GenericApplicationContext ctx =
             new AnnotationConfigApplicationContext(AppConfig.class);

        MessageSender messageSender =
             ctx.getBean("messageSender", MessageSender.class);

        for(int i=0; i < 10; ++i) {
            messageSender.sendMessage("Test message: " + i);
        }

        System.in.read();
        ctx.close();
    }
}

程序很简单。运行程序将消息发送到队列。SimpleMessageListener类接收这些消息,您可以在控制台中看到以下输出:

INFO  c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 0
INFO  c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 1
INFO  c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 2
INFO  c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 3
INFO  c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 4
INFO  c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 5
INFO  c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 6
INFO  c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 7
INFO  c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 8
INFO  c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 9
INFO  c.a.p.c.SimpleMessageListener - >>> Received: Test message: 0
INFO  c.a.p.c.SimpleMessageListener - >>> Received: Test message: 1
INFO  c.a.p.c.SimpleMessageListener - >>> Received: Test message: 2
INFO  c.a.p.c.SimpleMessageListener - >>> Received: Test message: 3
INFO  c.a.p.c.SimpleMessageListener - >>> Received: Test message: 4
INFO  c.a.p.c.SimpleMessageListener - >>> Received: Test message: 5
INFO  c.a.p.c.SimpleMessageListener - >>> Received: Test message: 6
INFO  c.a.p.c.SimpleMessageListener - >>> Received: Test message: 7
INFO  c.a.p.c.SimpleMessageListener - >>> Received: Test message: 8
INFO  c.a.p.c.SimpleMessageListener - >>> Received: Test message: 9

Spring Boot 阿尔特弥斯发酵剂

使用 Spring Boot 使 JMS 应用的开发更加实用的可能性在第九章中有所暗示,其中介绍了一个涉及数据库和队列的分布式事务示例。

当 Spring Boot 检测到 Artemis 在类路径中可用时,它可以自动配置一个javax.jms.ConnectionFactory bean。Am 嵌入式 JMS 代理是自动启动和配置的。Artemis 可用于多种模式,可使用特殊的 Artemis 属性进行配置,这些属性可在application.properties文件中设置。

Artemis 可以在native模式下使用,与代理的连接由netty协议提供。application.properties文件可以是这样的:

spring.artemis.mode=native

spring.artemis.host=0.0.0.0
spring.artemis.port=61617
spring.artemis.user=prospring5
spring.artemis.password=prospring5

使用 Spring Boot 和 Artemis 创建 JMS 应用的最简单方法是使用嵌入式服务器;所需要的只是保存消息的队列的名称。因此,application.properties文件看起来像这样:

spring.artemis.mode=embedded

spring.artemis.embedded.queues=prospring5

这是将在本节的源代码中使用的方法,因为它需要最少的配置定制。使用这种方法,所需要的就是application.properties配置文件和Application类,如下所示:

package com.apress.prospring5.ch12;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.
      jms.DefaultJmsListenerContainerFactoryConfigurer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.core.JmsTemplate;

import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.TextMessage;

@SpringBootApplication
public class Application {

    private static Logger logger =
          LoggerFactory.getLogger(Application.class);

    @Bean
    public JmsListenerContainerFactory<?>
        connectionFactory(ConnectionFactory connectionFactory,
            DefaultJmsListenerContainerFactoryConfigurer configurer) {
        DefaultJmsListenerContainerFactory factory =
            new DefaultJmsListenerContainerFactory();
        configurer.configure(factory, connectionFactory);
        return factory;
    }

    public static void main(String... args) throws Exception {
        ConfigurableApplicationContext ctx =
            SpringApplication.run(Application.class, args);
        JmsTemplate jmsTemplate = ctx.getBean(JmsTemplate.class);
        jmsTemplate.setDeliveryDelay(5000L);
        for (int i = 0; i < 10; ++i) {
            logger.info(">>> Sending: Test message: " + i);
            jmsTemplate.convertAndSend("prospring5", "Test message: " + i);
        }

        System.in.read();
        ctx.close();
    }

    @JmsListener(destination = "prospring5", containerFactory = "connectionFactory")
    public void onMessage(Message message) {
        TextMessage textMessage = (TextMessage) message;

        try {
            logger.info(">>> Received: " + textMessage.getText());
        } catch (JMSException ex) {
            logger.error("JMS error", ex);
        }
    }
}

当然,要实现这一点,必须将 Spring Boot JMS 启动器库用作依赖项,并且 Artemis 服务器必须位于类路径中。梯度配置如下所示:

//pro-spring-15/build.gradle
ext {
    bootVersion = '2.0.0.M1'
    artemisVersion = '2.1.0'

    boot = [
            ...
            starterJms      :
               "org.springframework.boot:spring-boot-starter-artemis:$bootVersion"
    ]

    testing = [
            junit: "junit:junit:$junitVersion"
    ]

    misc = [
            ...
            artemisServer      :
               "org.apache.activemq:artemis-jms-server:$artemisVersion"

    ]
...
}
//boot-jms/build.gradle
buildscript {
    repositories {
        ...
     }

    dependencies {
        classpath boot.springBootPlugin
    }
}

apply plugin: 'org.springframework.boot'

dependencies {
    compile boot.starterJms, misc.artemisServer
}

spring-boot-starter- artemis声明为依赖项消除了使用@EnableJms处理用@JmsListener注释的方法的需要。jmsTemplate bean 是由 Spring Boot 创建的,默认配置由application.properties文件中设置的属性提供,它不仅可以发送消息,还可以使用receive()方法接收消息,但是这是同步完成的,这意味着jmsTemplate将会阻塞。这就是为什么使用一个显式配置的JmsListenerContainerFactory bean 来创建一个DefaultMessageListenerContainer,它将异步地使用消息,并具有最大的连接效率。

如果你运行Application类,你会在控制台中得到一些输出,和 HornetQ 的输出真的很像。

在 Spring 中使用 RESTful-WS

如今,RESTful-WS 可能是远程访问中使用最广泛的技术。从通过 HTTP 的远程服务调用到支持 Ajax 风格的交互式 web 前端,RESTful-WS 正被广泛采用。RESTful web 服务流行有几个原因。

  • 容易理解:RESTful web 服务是围绕 HTTP 设计的。URL 和 HTTP 方法一起指定了请求的意图。例如,带有 GET 的 HTTP 方法的 URL http://somedomain.com/restful/customer/1 意味着客户端想要检索客户信息,其中客户 ID 等于 1。
  • 轻量级:与基于 SOAP 的 web 服务相比,RESTful 要轻量级得多,基于 SOAP 的 web 服务包含大量元数据来描述客户端想要调用的服务。对于 RESTful 请求和响应,它只是一个 HTTP 请求和响应,就像任何其他 web 应用一样。
  • 防火墙友好:因为 RESTful web 服务被设计为可以通过 HTTP(或 HTTPS)访问,所以应用变得更加防火墙友好,并且容易被远程客户端访问。

在本节中,我们将介绍 RESTful-WS 的基本概念以及 Spring 通过其 Spring MVC 模块对 RESTful-WS 的支持。

RESTful Web 服务简介

RESTful-WS 中的 REST 是具象状态转移的简称,是一种架构风格。REST 定义了一组架构约束,它们共同描述了访问资源的统一接口。这个统一接口的主要概念包括资源的识别和通过表示对资源的操作。对于资源的标识,应该可以通过统一资源标识符(URI)访问一条信息。比如 URL http://somedomain.com/api/singer/1 是一个代表资源的 URI,是一段歌手信息,标识符为 1。如果标识符为 1 的歌手不存在,客户端将得到一个 404 HTTP 错误,就像网站上的“page not found”错误一样。另一个例子, http://somedomain.com/api/singers ,是代表歌手信息列表的资源的 URI。那些可识别的资源可以通过各种表示来管理,如表 12-1 所示。

表 12-1。

Representations for Manipulating Resources

| HTTP 方法 | 描述 | | --- | --- | | 得到 | 获取资源的表示形式。 | | 头 | 与 GET 相同,没有响应体。通常用于获取标题。 | | 邮政 | POST 创建一个新资源。 | | 放 | 将更新放入资源。 | | 删除 | 删除删除资源。 | | 选择 | 选项检索允许的 HTTP 方法。 |

对于 RESTful web 服务的详细描述,我们推荐 Ajax 和 REST 食谱:克里斯蒂安·格罗斯的问题解决方法(Apress,2006)。

为样本添加必需的依赖项

为了构建 Spring REST 应用,我们需要添加一些新的依赖项。因为我们将把对象从服务器发送到客户机,所以我们需要一个库来序列化和反序列化它们。我们还想向您展示,在同一个应用中可以使用多种类型的序列化,在本例中是 XML 和 JSON,因此需要它们各自的库。表 12-2 列出了依赖关系及其用途。

表 12-2。

Dependencies for RESTful Web Services

| GroupId:ModuleId | 版本 | 目的 | | --- | --- | --- | | `org.springframework:spring-` `oxm` | RC1 | Spring 对象到 XML 映射模块。 | | `org.codehaus.jackson:jacksonDatabind` | pr3 | 杰克逊 JSON 处理器支持 JSON 格式的数据。 | | `org.codehaus.castor:castor-xml` | 1.4.1 | Castor XML 库将用于 XML 数据的编组和解组。 | | `org.apache.httpcomponents:httpclient` | 4.5.3 | Apache HTTP 组件项目。HTTP 客户端库将用于 RESTful-WS 调用。 |

设计 Singer RESTful Web 服务

在开发 RESTful-WS 应用时,第一步是设计服务结构,包括将支持哪些 HTTP 方法,以及不同操作的目标 URL。对于 singer RESTful web 服务,我们希望支持查询、创建、更新和删除操作。对于查询,我们希望支持通过 ID 检索所有歌手或单个歌手。

这些服务将被实现为 Spring MVC 控制器。名字是SingerController类,在包com.apress.prospring5.ch12下。表 12-3 显示了 URL 模式、HTTP 方法、描述和相应的控制器方法。对于 URL,所有的都是相对于http://localhost:8080的。数据格式方面,XML 和 JSON 都支持。将根据客户端 HTTP 请求头的接受媒体类型提供相应的格式。

表 12-3。

XMLHttpRequest Methods and Properties Table

| 统一资源定位器 | HTTP 方法 | 描述 | 控制器方法 | | --- | --- | --- | --- | | `/singer/listdata` | 得到 | 检索到所有歌手 | `listData` | | `/singer/id` | 得到 | 通过 id 检索歌手 | `findBySingerId(...)` | | `/singer` | 邮政 | 创造一个新的歌手 | `create(...)` | | `/singer/id` | 放 | 按 ID 更新歌手 | `update(...)` | | `/singer/id` | 删除 | 按 ID 删除歌手 | `delete(...)` |

使用 Spring MVC 公开 RESTful Web 服务

在这一节中,我们将向您展示如何使用 Spring MVC 将 singer 服务公开为 RESTful web 服务,正如上一节中所设计的那样。这个示例建立在 Spring HTTP invoker 示例中使用的一些SingerService类之上。

你已经熟悉了Singer类,这里就不再展示代码了。但是要序列化和反序列化歌手列表,我们需要将它封装在一个容器中。 3 这里可以看到Singers类。它只有一个属性,那就是一个Singer对象的列表。目的是支持将歌手列表(由SingerController类中的listData()方法返回)转换成 XML 或 JSON 格式。

package com.apress.prospring5.ch12;

import com.apress.prospring5.ch12.entities.Singer;
import java.io.Serializable;
import java.util.List;

public class Singers implements Serializable {
    private List<Singer> singers;

    public Singers() {
    }

    public Singers(List<Singer> singers) {
        this.singers = singers;
    }

    public List<Singer> getSingers() {
        return singers;
    }

    public void setSingers(List<Singer> singers) {
        this.singers = singers;
    }
}

配置 Castor XML

为了支持将返回的歌手信息转换成 XML 格式,我们将使用 Castor XML 库( http://castor.codehaus.org )。Castor 支持 POJO 和 XML 转换之间的几种模式,在这个示例中,我们使用一个 XML 文件来定义映射。下面的 XML 片段显示了映射文件(oxm-mapping.xml):

<mapping>
    <class name="com.apress.prospring5.ch12.Singers">
        <field name="singers"
            type="com.apress.prospring5.ch12.entities.Singer"
              collection="arraylist">
            <bind-xml name="singer"/>
        </field>
    </class>

    <class name="com.apress.prospring5.ch12.entities.Singer"
        identity="id">
        <map-to xml="singer" />

        <field name="id" type="long">
            <bind-xml name="id" node="element"/>
        </field>
        <field name="firstName" type="string">
            <bind-xml name="firstName" node="element" />
        </field>
        <field name="lastName" type="string">
            <bind-xml name="lastName" node="element" />
        </field>
        <field name="birthDate" type="string" handler="dateHandler">
            <bind-xml name="birthDate" node="element" />
        </field>
        <field name="version" type="integer">
            <bind-xml name="version" node="element" />
        </field>
    </class>

    <field-handler name="dateHandler"
        class="com.apress.prospring5.ch12.DateTimeFieldHandler">
        <param name="date-format" value="yyyy-MM-dd"/>
    </field-handler>
</mapping>

定义了两个映射。第一个<class>标签映射了Singers类,在这个类中,它的 singers 属性(singers对象的一个List)是使用<bind-xml name="singer"/>标签映射的。然后映射Singer对象(第二个<class>标签内有<map-to xml="singer" />标签)。此外,为了支持从java.util.Date类型(对于SingerbirthDate属性)的转换,我们实现了一个定制的 Castor 字段处理程序。以下代码片段显示了字段处理程序:

package com.apress.prospring5.ch12;

import org.exolab.castor.mapping.GeneralizedFieldHandler;
import org.exolab.castor.mapping.ValidityException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;

public class DateTimeFieldHandler extends GeneralizedFieldHandler {

    private static Logger logger =
        LoggerFactory.getLogger(DateTimeFieldHandler.class);

    private static String dateFormatPattern;

    @Override
    public void setConfiguration(Properties config) throws ValidityException {
       dateFormatPattern = config.getProperty("date-format");
    }

    @Override
    public Object convertUponGet(Object value) {
       Date dateTime = (Date) value;
       return format(dateTime);
    }

    @Override
    public Object convertUponSet(Object value) {
       String dateTimeString = (String) value;
       return parse(dateTimeString);
    }

    @Override
    public Class<Date> getFieldType() {
       return Date.class;
    }

    protected static String format(final Date  dateTime) {
       String dateTimeString = "";
       if (dateTime != null) {
         SimpleDateFormat sdf =
             new SimpleDateFormat(dateFormatPattern);
         dateTimeString = sdf.format(dateTime);
       }
       return dateTimeString;
    }

    protected static Date  parse(final String dateTimeString) {
       Date dateTime = new Date();
       if (dateTimeString != null) {
         SimpleDateFormat sdf =
            new SimpleDateFormat(dateFormatPattern);
         try {
            dateTime = sdf.parse(dateTimeString);
         } catch (ParseException e) {
            logger.error("Not a valida date:" + dateTimeString, e);
         }
       }
       return dateTime;
    }
}

我们扩展了 Castor 的org.exolab.castor.mapping.GeneralizedFieldHandler类,并实现了convertUponGet()convertUponSet()getFieldType()方法。在这些方法中,我们实现了逻辑来执行由 Castor 使用的DateString之间的转换。

此外,我们还定义了一个用于 Castor 的属性文件。以下代码片段显示了文件(castor.properties)的内容:

org.exolab.castor.indent=true

该属性指示 Castor 生成带缩进的 XML,这样在测试时更容易阅读。

实现单控制器

下一步是实现控制器类SingerController。以下代码片段显示了该类的内容,该类实现了表 12-3 中的所有方法:

package com.apress.prospring5.ch12;

import com.apress.prospring5.ch12.entities.Singer;
import com.apress.prospring5.ch12.services.SingerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping(value="/singer")
public class SingerController {
    final Logger logger =
       LoggerFactory.getLogger(SingerController.class);

    @Autowired
    private SingerService singerService;

    @ResponseStatus(HttpStatus.OK)
    @RequestMapping(value = "/listdata", method = RequestMethod.GET)
    @ResponseBody
    public Singers listData() {
        return new Singers(singerService.findAll());
    }

    @RequestMapping(value="/{id}", method=RequestMethod.GET)
    @ResponseBody
    public Singer findSingerById(@PathVariable Long id) {
        return singerService.findById(id);
    }

    @RequestMapping(value="/", method=RequestMethod.POST)
    @ResponseBody
    public Singer create(@RequestBody Singer singer) {
        logger.info("Creating singer: " + singer);
        singerService.save(singer);
        logger.info("Singer created successfully with info: " + singer);
        return singer;
    }

    @RequestMapping(value="/{id}", method=RequestMethod.PUT)
    @ResponseBody
    public void update(@RequestBody Singer singer,
                       @PathVariable Long id) {
        logger.info("Updating singer: " + singer);
        singerService.save(singer);
        logger.info("Singer updated successfully with info: " + singer);
    }

    @RequestMapping(value="/{id}", method=RequestMethod.DELETE)
    @ResponseBody
    public void delete(@PathVariable Long id) {
        logger.info("Deleting singer with id: " + id);
        Singer singer = singerService.findById(id);
        singerService.delete(singer);
        logger.info("Singer deleted successfully");
    }
}

上节课的要点如下:

  • 该类用@Controller进行了注释,表明它是一个 Spring MVC 控制器。
  • 类级注释@RequestMapping(value="/singer")表明这个控制器将被映射到主 web 上下文下的所有 URL。在这个例子中,http://localhost : 8080/singer下的所有 URL 都将由这个控制器处理。
  • 本章前面实现的服务层中的SingerService被自动连接到控制器中。
  • 每个方法的@RequestMapping注释指示 URL 模式和它将被映射到的相应 HTTP 方法。例如,listData()方法将被映射到http://localhost:8080/singer/listdata URL,并带有一个 HTTP GET 方法。对于update()方法,它将被映射到 URL http://localhost:8080/singer/\protect\T1\textbraceleftid\ protect\T1\textbraceright,使用 HTTP PUT 方法。
  • @ResponseBody注释适用于所有方法。这指示方法的所有返回值都应该直接写入 HTTP 响应流,而不是与视图匹配。
  • 对于接受路径变量的方法(例如,findSingerById()方法),路径变量用@PathVariable标注。这指示 Spring MVC 将 URL 中的 path 变量(例如,http://localhost:8080/singer/1)绑定到findSingerById()方法的id参数中。注意对于id参数,类型是Long,而Spring的类型转换系统会自动为我们处理从StringLong的转换。
  • 对于create()update()方法,Singer参数被标注为@RequestBody。这指示 Spring 自动将 HTTP 请求体中的内容绑定到Singer域对象中。转换将由支持格式的HttpMessageConverter<Object>接口的声明实例(在包org.springframework.http.converter下)完成,这将在本章后面讨论。

A315511_5_En_12_Figa_HTML.jpg从 Spring 4.0 开始,引入了专用于 REST 的控制器注释@RestController。这是一个方便的注释,它本身用@Controller@ResponseBody进行了注释。当用在控制器类上时,所有用@RequestMapping标注的方法都会自动用@ResponseBody标注。使用该注释编写的SingerController版本将在本章后面介绍。

配置 Spring Web 应用

需要一个 Spring web 应用来解析客户端发送的 REST 请求,因此需要对它进行配置。在本章的前面,我们介绍了一个简单的 web 应用配置。现在,必须用 HTTP message converter beans for XML 和 JSON 来丰富这个配置。

Spring web 应用遵循前端控制器设计模式, 4 ,其中所有请求由单个控制器接收,该控制器随后将它们分派给适当的处理程序(控制器类)。这个中央调度程序是org.springframework.web.servlet.DispatcherServlet的一个实例,由一个AbstractAnnotationConfigDispatcherServletInitializer类注册,这个类需要扩展来替换web.xml配置。在本节的示例中,执行此操作的类WebInitializer如下所示:

package com.apress.prospring5.ch12.init;

import com.apress.prospring5.ch12.config.DataServiceConfig;
import org.springframework.web.servlet.support.
    AbstractAnnotationConfigDispatcherServletInitializer;

public class WebInitializer extends
   AbstractAnnotationConfigDispatcherServletInitializer {

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

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{
                WebConfig.class
        };
    }

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

在 Spring MVC 中,每个DispatchServlet都有自己的WebApplicationContext(然而,在DataServiceConfig.class中定义的所有服务层 beans,称为根WebApplicationContext,也可以用于每个 servlet 自己的WebApplicationContext)。

getServletMappings()方法指示 web 容器(例如,Tomcat)模式/(例如,http://localhost:8080/singer)下的所有 URL 都将由 RESTful servlet 处理。当然,我们可以在那里添加一个上下文,比如/ch12,但是对于本节中需要的示例,我们希望保持 URL 尽可能的短并且目的尽可能的明显。

带有 HTTP 消息转换器的 Spring MVC 配置类(WebConfig类)如下所示:

package com.apress.prospring5.ch12.init;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.
    MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MarshallingHttpMessageConverter;
import org.springframework.oxm.castor.CastorMarshaller;
import org.springframework.web.servlet.config.annotation.
    DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.apress.prospring5.ch12"})
public class WebConfig extends WebMvcConfigurer {

   @Autowired ApplicationContext ctx;

   @Bean
   public MappingJackson2HttpMessageConverter
        mappingJackson2HttpMessageConverter() {
      MappingJackson2HttpMessageConverter
          mappingJackson2HttpMessageConverter =
           new MappingJackson2HttpMessageConverter();
      mappingJackson2HttpMessageConverter.setObjectMapper(objectMapper());
      return mappingJackson2HttpMessageConverter;
   }

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

   @Bean
   public ObjectMapper objectMapper() {
      ObjectMapper objMapper = new ObjectMapper();
      objMapper.enable(SerializationFeature.INDENT_OUTPUT);
      objMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
      DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
      objMapper.setDateFormat(df);
      return objMapper;
   }

   @Override
   public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
      converters.add(mappingJackson2HttpMessageConverter());
      converters.add(singerMessageConverter());
   }

   @Bean MarshallingHttpMessageConverter singerMessageConverter() {
      MarshallingHttpMessageConverter mc = new MarshallingHttpMessageConverter();
      mc.setMarshaller(castorMarshaller());
      mc.setUnmarshaller(castorMarshaller());
      List<MediaType> mediaTypes = new ArrayList<>();
      MediaType mt = new MediaType("application", "xml");
      mediaTypes.add(mt);
      mc.setSupportedMediaTypes(mediaTypes);
      return mc;
   }

   @Bean CastorMarshaller castorMarshaller() {
      CastorMarshaller castorMarshaller = new CastorMarshaller();
      castorMarshaller.setMappingLocation(
         ctx.getResource( "classpath:spring/oxm-mapping.xml"));
      return castorMarshaller;
   }
}

前一节课的重点如下:

  • @EnableWebMvc注释 5 启用对 Spring MVC 的注释支持(即@Controller注释),并注册 Spring 的类型转换和格式化系统。此外,在该注释的定义下,启用了 JSR-303 验证支持。
  • configureMessageConverters(...)方法 6 声明了将用于支持格式的媒体转换的HttpMessageConverter实例。因为我们将同时支持 JSON 和 XML 作为数据格式,所以声明了两个转换器。第一个是MappingJackson2HttpMessageConverter,它是 Spring 对 Jackson JSON 库的支持。 7 另一个是MarshallingHttpMessageConverter,由spring-oxm模块提供,用于 XML 的编组/解组。在MarshallingHttpMessageConverter中,我们需要定义要使用的编组器和解组器,在本例中是由 Castor 提供的。
  • 对于castorMarshaller bean,我们使用 Spring 提供的类org.springframework.oxm.castor.CastorMarshaller,它与 Castor 集成在一起,我们提供 Castor 处理所需的映射位置。
  • @ComponentScan annotation 8 指示 Spring 扫描控制器类的指定包。

现在,服务器端服务完成了。此时,您应该构建包含 web 应用的 WAR 文件,或者如果您使用 IntelliJ IDEA 或 STS 之类的 IDE,启动 Tomcat 实例。

使用 curl 测试 RESTful-WS

让我们对我们实现的 RESTful web 服务做一个快速测试。一个简单的方法是使用curl、、 9 、,这是一个使用 URL 语法传输数据的命令行工具。要使用该工具,只需从网站下载并解压到您的计算机上。

例如,要测试对所有歌手的检索,请在 Windows 中打开命令提示符,或者在 Unix/Linux 中打开终端,将 WAR 部署到服务器上,并启动以下命令:

$ curl -v -H "Accept: application/json" http://localhost:8080/singer/listdata
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /singer/listdata HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: application/json
>
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Sat, 17 Jun 2017 17:16:43 GMT
<
{
  "singers" : [ {
    "id" : 1,
    "version" : 0,
    "firstName" : "John",
    "lastName" : "Mayer",
    "birthDate" : "1977-10-16"
  }, {
    "id" : 2,
    "version" : 0,
    "firstName" : "Eric",
    "lastName" : "Clapton",
    "birthDate" : "1945-03-30"
  }, {
    "id" : 3,
    "version" : 0,
    "firstName" : "John",
    "lastName" : "Butler",
    "birthDate" : "1975-04-01"
  } ]
* Connection #0 to host localhost left intact

此命令向服务器的 RESTful web 服务发送 HTTP 请求;在这种情况下,它调用SingerController中的listData()方法来检索并返回所有歌手信息。另外,-H选项声明了一个 HTTP header 属性,这意味着客户机希望接收 JSON 格式的数据。运行该命令会为返回的初始填充的 singer 信息生成 JSON 格式的输出。现在让我们来看看 XML 格式;命令和结果如下所示:

$ curl -v -H "Accept: application/xml" http://localhost:8080/singer/listdata
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /singer/listdata HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: application/xml
>
< HTTP/1.1 200
< Content-Type: application/xml
< Transfer-Encoding: chunked
< Date: Sat, 17 Jun 2017 17:18:22 GMT
<
<?xml  version="1.0" encoding="UTF-8"?>
<singers>
    <singer>
        <id>1</id>
        <firstName>John</firstName>
        <lastName>Mayer</lastName>
        <birthDate>1977-10-16</birthDate>
        <version>0</version>
    </singer>
    <singer>
        <id>2</id>
        <firstName>Eric</firstName>
        <lastName>Clapton</lastName>
        <birthDate>1945-03-30</birthDate>
        <version>0</version>
    </singer>
    <singer>
        <id>3</id>
        <firstName>John</firstName>
        <lastName>Butler</lastName>
        <birthDate>1975-04-01</birthDate>
        <version>0</version>
    </singer>
</singers>
* Connection #0 to host localhost left intact

如你所见,这两个样品只有一个不同之处。接受媒体已从 JSON 更改为 XML。运行该命令会产生 XML 输出。这是因为在 RESTful servlet 的WebApplicationContext中定义了HttpMessageConverterbean,而 Spring MVC 将基于客户机的 HTTP 头的接受媒体信息调用相应的消息转换器,并相应地写入 HTTP 响应。

使用 RestTemplate 访问 RESTful-WS

对于基于 Spring 的应用,RestTemplate类被设计用来访问 RESTful web 服务。在这一节中,我们将展示如何使用类来访问服务器上的 singer 服务。首先让我们看看 Spring 的RestTemplate的基本ApplicationContext配置,如下面的代码片段所示:

package com.apress.prosring5.ch12;

import com.apress.prospring5.ch12.CustomCredentialsProvider;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.MarshallingHttpMessageConverter;
import org.springframework.oxm.castor.CastorMarshaller;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class RestClientConfig {

   @Autowired ApplicationContext ctx;

   @Bean
   public HttpComponentsClientHttpRequestFactory httpRequestFactory() {
      HttpComponentsClientHttpRequestFactory httpRequestFactory =
         new HttpComponentsClientHttpRequestFactory();
      HttpClient httpClient = HttpClientBuilder.create().build();
      httpRequestFactory.setHttpClient(httpClient);
      return httpRequestFactory;
   }

   @Bean
   public RestTemplate restTemplate() {
      RestTemplate restTemplate = new RestTemplate(httpRequestFactory());
      List<HttpMessageConverter<?>> mcvs = new ArrayList<>();
      mcvs.add(singerMessageConverter());
      restTemplate.setMessageConverters(mcvs);
      return restTemplate;
   }

   @Bean MarshallingHttpMessageConverter singerMessageConverter() {
      MarshallingHttpMessageConverter mc =
          new MarshallingHttpMessageConverter();
      mc.setMarshaller(castorMarshaller());
      mc.setUnmarshaller(castorMarshaller());
      List<MediaType> mediaTypes = new ArrayList<>();
      MediaType mt = new MediaType("application", "xml");
      mediaTypes.add(mt);
      mc.setSupportedMediaTypes(mediaTypes);
      return mc;
   }

   @Bean CastorMarshaller castorMarshaller() {
      CastorMarshaller castorMarshaller = new CastorMarshaller();
      castorMarshaller.setMappingLocation(
         ctx.getResource( "classpath:spring/oxm-mapping.xml"));
      return castorMarshaller;
   }
}

您使用RestTemplate类声明了一个restTemplate bean。该类使用 Castor 注入属性messageConverters和一个MarshallingHttpMessageConverter实例,与服务器端的相同。映射文件将在服务器端和客户端之间共享。此外,对于restTemplate bean,在匿名类MarshallingHttpMessageConverter中,属性supportedMediaTypes被注入了一个MediaType实例,表明唯一支持的媒体是 XML。因此,客户端总是期望 XML 作为返回数据格式,Castor 将帮助执行 POJO 和 XML 之间的转换。

要测试 web 应用支持的所有 REST URLs,JUnit 类更合适,在由RestClientConfig定义的 Spring 应用上下文中执行。下面显示了代码,在 IntelliJ IDEA 或 STS 等智能编辑器中,每种方法都可以单独执行:

package com.apress.prosring5.ch12.test;

import com.apress.prospring5.ch12.Singers;
import com.apress.prospring5.ch12.entities.Singer;
import com.apress.prosring5.ch12.RestClientConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;

import java.util.Date;
import java.util.GregorianCalendar;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RestClientConfig.class})
public class RestClientTest {

   final Logger logger = LoggerFactory.getLogger(RestClientTest.class);
   private static final String URL_GET_ALL_SINGERS =
       "http://localhost:8080/singer/listdata";
   private static final String URL_GET_SINGER_BY_ID =
       "http://localhost:8080/singer/{id}";
   private static final String URL_CREATE_SINGER =
       "http://localhost:8080/singer/";
   private static final String URL_UPDATE_SINGER =
       "http://localhost:8080/singer/{id}";
   private static final String URL_DELETE_SINGER =
       "http://localhost:8080/singer/{id}";

   @Autowired RestTemplate restTemplate;

   @Before
   public void setUp() {
      assertNotNull(restTemplate);
   }

   @Test
   public void testFindAll() {
      logger.info("--> Testing retrieve all singers");
      Singers singers = restTemplate.getForObject(URL_GET_ALL_SINGERS,
         Singers.class);
      assertTrue(singers.getSingers().size() == 3);
      listSingers(singers);
   }

   @Test
   public void testFindbyId() {
      logger.info("--> Testing retrieve a singer by id : 1");
      Singer singer = restTemplate.getForObject(URL_GET_SINGER_BY_ID,
         Singer.class, 1);
      assertNotNull(singer);
      logger.info(singer.toString());
   }

   @Test
   public void testUpdate() {
      logger.info("--> Testing update singer by id : 1");
      Singer singer = restTemplate.getForObject(URL_UPDATE_SINGER,
         Singer.class, 1);
      singer.setFirstName("John Clayton");
      restTemplate.put(URL_UPDATE_SINGER, singer, 1);
      logger.info("Singer update successfully: " + singer);
   }

   @Test
   public void testDelete() {
      logger.info("--> Testing delete singer by id : 3");
      restTemplate.delete(URL_DELETE_SINGER, 3);
      Singers singers = restTemplate.getForObject(URL_GET_ALL_SINGERS,
         Singers.class);
      Boolean found = false;
      for(Singer s: singers.getSingers()) {
         if(s.getId() == 3) {
            found = true;
         }
      };
      assertFalse(found);
      listSingers(singers);
   }

   @Test
   public void testCreate() {
      logger.info("--> Testing create singer");
      Singer singerNew = new Singer();
      singerNew.setFirstName("BB");
      singerNew.setLastName("King");
      singerNew.setBirthDate(new Date(
             (new GregorianCalendar(1940, 8, 16)).getTime().getTime()));
      singerNew = restTemplate.postForObject(URL_CREATE_SINGER,
         singerNew, Singer.class);
      logger.info("Singer created successfully: " + singerNew);

      logger.info("Singer created successfully: " + singerNew);

      Singers singers = restTemplate.getForObject(URL_GET_ALL_SINGERS,
         Singers.class);
      listSingers(singers);
   }

   private void listSingers(Singers singers) {
       singers.getSingers().forEach(s -> logger.info(s.toString()));
   }
}

声明了用于访问各种操作的 URL,这些 URL 将在后面的示例中使用。注入RestTemplate的实例,然后在testFindAll方法中调用RestTemplate.getForObject()方法(对应于 HTTP GET 方法),传入 URL 和预期的返回类型,这是包含完整歌手列表的Singers类。

确保应用服务器正在运行。运行testFindAll测试方法,测试应该通过并产生以下输出:

INFO  c.a.p.c.t.RestClientTest - --> Testing retrieve all singers
INFO  c.a.p.c.t.RestClientTest - Singer - Id: 1, First name: John, Last name: Mayer,
   Birthday: Sun Oct 16 00:00:00 EET 1977
INFO  c.a.p.c.t.RestClientTest - Singer - Id: 2, First name: Eric, Last name: Clapton,
   Birthday: Fri Mar 30 00:00:00 EET 1945
INFO  c.a.p.c.t.RestClientTest - Singer - Id: 3, First name: John, Last name: Butler,
   Birthday: Tue Apr 01 00:00:00 EET 1975

如您所见,在RestTemplate中注册的MarshallingHttpMessageConverter bean 自动将消息转换成 POJO。接下来,让我们尝试通过 ID 检索歌手。在这个方法中,我们使用了一个RestTemplate.getForObject()方法的变体,它也传入我们想要检索的歌手的 ID 作为 URL 中的路径变量(在URL_GET_CONTACT_BY_ID中的{id}路径变量)。如果 URL 有多个路径变量,您可以使用一个实例Map<String,Object>或者使用该方法的 varargs 支持来传入路径变量。对于 varargs,您需要遵循 URL 中声明的路径变量的顺序。运行testFindbyId()测试方法。测试应该通过,您应该会看到以下输出:

INFO  c.a.p.c.t.RestClientTest - --> Testing retrieve a singer by id : 1
INFO  c.a.p.c.t.RestClientTest - Singer - Id: 1, First name: John, Last name: Mayer,
   Birthday: Sun Oct 16 00:00:00 EET 1977

如您所见,检索到了正确的歌手。现在轮到update了。首先,我们检索想要更新的歌手。singer 对象更新后,我们使用对应于 HTTP PUT 方法的RestTemplate.put()方法,传入更新 URL、更新的 singer 对象和要更新的 singer 的 ID。运行testUpdate()产生以下输出(其他输出已被省略):

INFO  c.a.p.c.t.RestClientTest - --> Testing update singer by id : 1
INFO  c.a.p.c.t.RestClientTest - Singer update successfully: Singer - Id: 1,
  First name: John Clayton,
  Last name: Mayer, Birthday: Sun Oct 16 00:00:00 EET 1977

接下来是删除操作。调用RestTemplate.delete()方法,它对应于 HTTP DELETE方法,传入 URL 和 ID。然后,检索所有歌手并再次显示以验证删除。运行testDelete()测试方法会产生以下输出(其他输出已被省略):

INFO  c.a.p.c.t.RestClientTest - --> Testing delete singer by id : 3
INFO  c.a.p.c.t.RestClientTest - Singer - Id: 1,
  First name: John Clayton,
  Last name: Mayer, Birthday: Sun Oct 16 00:00:00 EET 1977
INFO  c.a.p.c.t.RestClientTest - Singer - Id: 2, First name: Eric,
  Last name: Clapton, Birthday: Fri Mar 30 00:00:00 EET 1945

如您所见,ID 为 3 的歌手被删除。最后,我们来试试插入操作。构建了一个Singer对象的新实例。然后调用RestTemplate.postForObject()方法,它对应于 HTTP POST 方法,传入 URL、我们想要创建的Singer对象和类类型。再次运行该程序会产生以下输出:

INFO  c.a.p.c.t.RestClientTest - --> Testing create singer
INFO  c.a.p.c.t.RestClientTest - Singer created successfully: Singer - Id: 4,
  First name: BB, Last name: King, Birthday: Mon Sep 16 00:00:00 EET 1940
 //listing all singers
INFO  c.a.p.c.t.RestClientTest - Singer - Id: 1,
 First name: John Clayton, Last name: Mayer,
 Birthday: Sun Oct 16 00:00:00 EET 1977
INFO  c.a.p.c.t.RestClientTest - Singer - Id: 2, First name: Eric,
  Last name: Clapton, Birthday: Fri Mar 30 00:00:00 EET 1945
INFO  c.a.p.c.t.RestClientTest - Singer - Id: 4, First name: BB,
  Last name: King, Birthday: Mon Sep 16 00:00:00 EET 1940

服务器在服务器上创建并返回给客户机。

用 Spring Security 保护 RESTful-WS

任何远程处理服务都需要安全性来限制未授权方访问服务和检索业务信息或对其进行操作。RESTful-WS 也不例外。在这一节中,我们将演示如何使用 Spring Security 项目来保护服务器上的 RESTful-WS。在这个例子中,我们使用的是 Spring Security 5.0.0.M2(撰写本文时的最新稳定版本),它为 RESTful-WS 提供了一些有用的支持。

使用 Spring Security 来保护 RESTful-WS 是一个三步的过程。首先,在 web 应用部署描述符(web.xml)中,需要添加一个名为springSecurityFilterChain的安全过滤器,但是因为我们没有使用 XML 配置应用,所以过滤器被一个扩展了AbstractSecurityWebApplicationInitializer的类所取代。该类注册DelegatingFilterProxy以在任何其他注册Filter之前使用springSecurityFilterChain。这里显示了实现,该类是空的,因为我们没有对它进行任何定制:

package com.apress.prospring5.ch12.init;

import org.springframework.security.web.
    context.AbstractSecurityWebApplicationInitializer;

public class SecurityWebApplicationInitializer
   extends AbstractSecurityWebApplicationInitializer {
}

为了安全起见,我们现在需要添加一个 Spring 配置类,我们将在其中声明谁可以访问应用以及他们可以做什么。在这个应用中,事情很简单:我们出于教学目的使用内存认证,所以添加一个名为prospring5的用户,密码为prospring5,角色为REMOTE

package com.apress.prospring5.ch12.init;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.
   builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.
  EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.
   WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   private static Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

   @Autowired
   protected void configureGlobal(AuthenticationManagerBuilder auth)
       throws Exception {
      try {
         auth.inMemoryAuthentication()
            .withUser("prospring5")
            .password("prospring5")
            .roles("REMOTE");
      } catch (Exception e) {
         logger.error("Could not configure authentication!", e);
      }
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
      http
             .sessionManagement()
       .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/**").permitAll()
            .antMatchers("/rest/**").hasRole("REMOTE").anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .httpBasic()
       .and()
            .csrf().disable();
   }
}

该类用@EnableWebSecurity注释进行了注释,以支持 Spring web 应用中的安全行为。在configure(...)方法中,我们声明 URL /rest/**下的资源应该受到保护。sessionCreationPolicy()方法用于允许我们配置是否在认证时创建 HTTP 会话。由于我们使用的 RESTful-WS 是无状态的,我们将值设置为SessionCreationPolicy.STATELESS,这指示 Spring Security 不要为所有 RESTful 请求创建 HTTP 会话。这有助于提高 RESTful 服务的性能。

接下来,在antMatchers("/rest/**")中,我们设置只有分配了REMOTE角色的用户才能访问 RESTful 服务。httpBasic()方法指定 RESTful 服务只支持 HTTP 基本认证。

configureGlobal(AuthenticationManagerBuilder auth)方法定义了认证信息。这里我们定义了一个简单的身份验证提供者,它具有硬编码的用户和密码(都设置为remote),并分配了REMOTE角色。在企业环境中,很可能通过数据库或 LDAP 查找来完成身份验证。

.formLogin()方法用于告诉 Spring 生成一个基本的登录表单,该表单可用于测试应用是否得到了正确的保护。登录表单可在http://localhost:8080/login访问。

过滤器springSecurityFilterChain用于让 Spring Security 拦截 HTTP 请求,以进行身份验证和授权检查。因为我们只想保护 RESTful-WS,所以过滤器只应用于 URL 模式/rest/*(参见antMatchers(...)方法)。我们希望保护所有的 REST URLs,但允许用户看到应用的主页(一个简单的 HTML 文件,当在浏览器中访问http://localhost:8080/时显示),所以这是您添加rest应用上下文的时刻,除了将SecurityConfig添加到根上下文应用。

package com.apress.prospring5.ch12.init;

import com.apress.prospring5.ch12.config.DataServiceConfig;
import org.springframework.web.servlet.support.
     AbstractAnnotationConfigDispatcherServletInitializer;

public class WebInitializer extends
   AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[]{
                DataServiceConfig.class, SecurityConfig.class
        };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{
                WebConfig.class
        };
    }

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

现在安全设置完成了。如果您重新部署项目并运行RestClientTest下的任何测试方法,您将得到以下输出(其他输出已被省略):

Exception in thread "main" org.springframework.web.cient.HttpClientErrorException:
    401 Unauthorized

您将获得 HTTP 状态代码 401,这意味着您无权访问该服务。现在让我们配置客户机的RestTemplate来向服务器提供凭证信息。

package com.apress.prosring5.ch12;

import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.MarshallingHttpMessageConverter;
import org.springframework.oxm.castor.CastorMarshaller;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class RestClientConfig {

   @Autowired ApplicationContext ctx;

   @Bean Credentials credentials(){
      return new UsernamePasswordCredentials("prospring5", "prospring5");
   }

   @Bean
   CredentialsProvider provider() {
      BasicCredentialsProvider provider =
          new BasicCredentialsProvider();
      provider.setCredentials( AuthScope.ANY, credentials());
      return provider;
   }

   @Bean
   public HttpComponentsClientHttpRequestFactory httpRequestFactory() {
      CloseableHttpClient client = HttpClients.custom()
            .setDefaultCredentialsProvider(provider()).build();
      return new HttpComponentsClientHttpRequestFactory(client);
   }

   @Bean
   public RestTemplate restTemplate() {
      RestTemplate restTemplate = new RestTemplate();
      restTemplate.setRequestFactory(httpRequestFactory());
      List<HttpMessageConverter<?>> mcvs = new ArrayList<>();
      mcvs.add(singerMessageConverter());
      restTemplate.setMessageConverters(mcvs);
      return restTemplate;
   }

   @Bean MarshallingHttpMessageConverter singerMessageConverter() {
      MarshallingHttpMessageConverter mc = n
           ew MarshallingHttpMessageConverter();
      mc.setMarshaller(castorMarshaller());
      mc.setUnmarshaller(castorMarshaller());
      List<MediaType> mediaTypes = new ArrayList<>();
      MediaType mt = new MediaType("application", "xml");
      mediaTypes.add(mt);
      mc.setSupportedMediaTypes(mediaTypes);
      return mc;
   }

   @Bean CastorMarshaller castorMarshaller() {
      CastorMarshaller castorMarshaller = new CastorMarshaller();
      castorMarshaller.setMappingLocation(ctx.getResource(
         "classpath:spring/oxm-mapping.xml"));
      return castorMarshaller;
   }
}

restTemplate bean 中,注入了一个引用了httpRequestFactory bean 的构造函数参数。对于httpRequestFactory bean,使用了HttpComponentsClientHttpRequestFactory类,这是 Spring 对 Apache HttpComponents HttpClient 库的支持,我们需要这个库来构建一个CloseableHttpClient的实例,为我们的客户端存储凭证。为了支持凭证的注入,您创建了一个简单的类型为UsernamePasswordCredentials的 bean。UsernamePasswordCredentials类是用prospring5用户名和密码构建的。随着httpRequestFactory被构造并注入到RestTemplate中,所有使用该模板触发的 RESTful 请求都将携带所提供的凭证。现在我们可以简单地再次运行RestClientTest类中的测试方法,您将看到服务像往常一样被调用。

与 Spring Boot 共度 Spring

因为 Spring Boot 让一切都变得更容易开发,所以我们需要添加一个部分,介绍 Spring Boot 如何让 Spring RESTful 服务的开发变得更容易。Singer实体、存储库和服务类与之前相同;没必要改变什么。为了简单起见,并尽可能多地使用默认的 Spring Boot 默认配置,XML 序列化也将被移除。默认情况下支持 JSON 序列化。由于该应用是一个 web 应用,其配置与前面介绍的 Spring Boot web 应用相同,因此我们不再赘述。Spring Boot 应用的Application类和入口点非常简单,如下所示:

package com.apress.prospring5.ch12;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.io.IOException;

@SpringBootApplication(scanBasePackages = "com.apress.prospring5.ch12")
public class Application {
   private static Logger logger = LoggerFactory.getLogger(Application.class);

   public static void main(String args) throws IOException {
      ConfigurableApplicationContext ctx =
         SpringApplication.run(Application.class, args);
      assert (ctx != null);
      logger.info("Application Started ...");

      System.in.read();
      ctx.close();
   }
}

正如之前所承诺的,这里是 Spring 4.3 中引入的使用@RestController和 HTTP 方法重写的新的和改进的SingerController:

package com.apress.prospring5.ch12.controller;

import com.apress.prospring5.ch12.entities.Singer;
import com.apress.prospring5.ch12.services.SingerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController

@RequestMapping(value = "/singer")
public class SingerController {

   final Logger logger =
       LoggerFactory.getLogger(SingerController.class);

   @Autowired
   private SingerService singerService;

   @ResponseStatus(HttpStatus.OK)
   @GetMapping(value = "/listdata")
   public List<Singer> listData() {
      return singerService.findAll();
   }

   @ResponseStatus(HttpStatus.OK)
   @GetMapping(value = "/{id}")
   public Singer findSingerById(@PathVariable Long id) {
      return singerService.findById(id);
   }

   @ResponseStatus(HttpStatus.CREATED)
   @PostMapping(value="/")
   public Singer create(@RequestBody Singer singer) {
      logger.info("Creating singer: " + singer);
      singerService.save(singer);
      logger.info("Singer created successfully with info: " + singer);
      return singer;
   }

   @ResponseStatus(HttpStatus.OK)
    @PutMapping(value="/{id}")
   public void update(@RequestBody Singer singer,
         @PathVariable Long id) {
      logger.info("Updating singer: " + singer);
      singerService.save(singer);
      logger.info("Singer updated successfully with info: " + singer);
   }

   @ResponseStatus(HttpStatus.NO_CONTENT)
    @DeleteMapping(value="/{id}")
   public void delete(@PathVariable Long id) {
      logger.info("Deleting singer with id: " + id);
      Singer singer = singerService.findById(id);
      singerService.delete(singer);
      logger.info("Singer deleted successfully");
   }
}

Spring 版本引入了一些与基本 HTTP 方法匹配的@RequestMapping注释的定制。表 12-4 列出了新注释和旧样式@RequestMapping之间的等价关系。

表 12-4。

Annotations for Mapping HTTP Method Requests onto Specific Handler Methods Introduced in Spring 4-3

| 注释 | 旧式等价物 | | --- | --- | | `@GetMapping` | `@RequestMapping(method = RequestMethod.GET)` | | `@PostMapping` | `@RequestMapping(method = RequestMethod.POST)` | | `@PutMapping` | `@RequestMapping(method = RequestMethod.PUT)` | | `@DeleteMapping` | `@RequestMapping(method = RequestMethod.DELETE)` |

此外,因为我们使用 JSON,它支持列表和数组,所以不再需要类Singers

测试应用很简单,因为RestTemplate不需要任何配置。通过调用默认构造函数来创建一个RestTemplate实例所需要的一切。测试方法与之前相同。

package com.apress.prosring5.ch12.test;

import com.apress.prospring5.ch12.entities.Singer;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;
import java.util.Date;
import java.util.GregorianCalendar;

import static org.junit.Assert.*;

public class RestClientTest {

   final Logger logger =
      LoggerFactory.getLogger(RestClientTest.class);

   private static final String URL_GET_ALL_SINGERS =
      "http://localhost:8080/singer/listdata";
   ...
   RestTemplate restTemplate;

   @Before
   public void setUp() {
     restTemplate = new RestTemplate();

   }

   @Test
   public void testFindAll() {
      logger.info("--> Testing retrieve all singers");
      Singer singers = restTemplate.getForObject(
          URL_GET_ALL_SINGERS, Singer.class);
      assertTrue(singers.length == 3);
      listSingers(singers);
   }
   ...
}

只需运行Application类,然后逐个执行测试方法。

如果您想确保应用实际工作并且 singer 实例使用 JSON 格式序列化,您可以使用curl来测试这个 Spring Boot 应用。

curl -v -H "Accept: application/json" http://localhost:8080/singer/listdata
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /singer/listdata HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: application/json
>
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Sun,  18 Jun 2017 11:14:17 GMT
<
* Connection #0 to host localhost left intact
[{"id":1,"version":1,"firstName":"John Clayton","lastName":"Mayer",
"birthDate":245797200000},{"id":2,"version":0,"firstName":"Eric", "lastName":"Clapton","birthDate":-781326000000},{"id":4, "version":0,"firstName":"BB","lastName":"King",
"birthDate":-924404400000}]

如果输出让您感到麻烦,请记住,如果没有声明显式的 JSON 消息转换器,Date字段将显示为数字,并且响应不会被格式化。

使用 Spring Boot,你也可以很容易地获得资源,但这是一个将在第十六章详细讨论的主题。

Spring 使用 AMQP

远程处理也可以通过使用高级消息队列协议(AMQP)作为传输方式的远程过程调用(RPC)通信来完成。AMQP 是实现面向消息的中间件(MOM)的开放标准协议。

JMS 应用可以在任何操作系统环境中工作,但是它只支持 Java 平台。因此,所有通信应用都必须用 Java 开发。AMQP 标准可用于开发易于交流的多语言应用。

与使用 JMS 类似,AMQP 也使用消息代理通过。在这个例子中,我们使用 RabbitMQ 10 作为 AMQP 服务器。Spring 本身并没有在核心框架中提供远程功能。相反,它们是由一个名为 Spring AMQP 的姊妹项目来处理的, 11 ,我们使用它作为底层的通信 API。Spring AMQP 项目围绕 AMQP 提供了一个基础抽象,并实现了与 RabbitMQ 的通信。在这一章中,我们不会涵盖 AMQP 或 Spring AMQP 的所有特性,只是通过 RPC 通信的远程功能。

Spring AMQP 项目由两部分组成:spring-amqp是基础抽象,spring-rabbit是 RabbitMQ 实现。编写本文时 Spring AMQP 的稳定版本是 2.0.0.M4

首先,你需要从 www.rabbitmq.com/download.html 获取 RabbitMQ 并启动服务器。RabbitMQ 开箱即可满足我们的需求,不需要进行任何配置更改。一旦 RabbitMQ 开始运行,我们需要做的下一件事就是创建一个服务接口。在这个例子中,我们创建了一个简单的天气服务,它返回所提供的州代码的预报。让我们从创建如下所示的WeatherService接口开始:

package com.apress.prospring5.ch12;

public interface WeatherService {
    String getForecast(String stateCode);
}

接下来,让我们创建一个WeatherService的实现,它将简单地回复所提供的州的天气预报,或者如果没有可用的预报,则回复不可用的消息,如下所示:

package com.apress.prospring5.ch12;
import org.springframework.stereotype.Component;

@Component
public class WeatherServiceImpl implements WeatherService {
    @Override
    public String getForecast(String stateCode) {
        if ("FL".equals(stateCode)) {
            return "Hot";
        } else if ("MA".equals(stateCode)) {
            return "Cold";
        }

        return "Not available at this time";
    }
}

天气服务代码就绪后,让我们构建配置文件(amqp-rpc-app-context.xml),该文件将配置 AMQP 连接并公开WeatherService,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans 
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/rabbit
        http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <rabbit:connection-factory id="connectionFactory" host="localhost" />

    <rabbit:template id="amqpTemplate" connection-factory="connectionFactory"
                     reply-timeout="2000" routing-key="forecasts"
                     exchange="weather" />

    <rabbit:admin connection-factory="connectionFactory" />

    <rabbit:queue name="forecasts" />

    <rabbit:direct-exchange name="weather">
        <rabbit:bindings>
            <rabbit:binding queue="forecasts" key="forecasts" />
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <bean id="weatherServiceProxy"
          class="org.springframework.amqp.remoting.client.AmqpProxyFactoryBean">
        <property name="amqpTemplate" ref="amqpTemplate" />
        <property name="serviceInterface"
               value="com.apress.prospring5.ch12.WeatherService" />
    </bean>

    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener ref="weatherServiceExporter" queue-names="forecasts" />
    </rabbit:listener-container>

    <bean id="weatherServiceExporter"
          class="org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter">
        <property name="amqpTemplate" ref="amqpTemplate" />
        <property name="serviceInterface"
            value="com.apress.prospring5.ch12.WeatherService" />
        <property name="service">
            <bean class="com.apress.prospring5.ch12.WeatherServiceImpl"/>
        </property>
    </bean>
</beans>

我们配置 RabbitMQ 连接以及交换和队列信息。然后,我们通过使用AmqpProxyFactoryBean类创建一个 bean,客户端使用它作为代理来发出 RPC 请求。对于响应,我们使用AmqpInvokerServiceExporter类,它被连接到一个监听器容器中。侦听器容器负责侦听 AMQP 消息,并将它们传递给气象服务。如您所见,在连接、队列、侦听器容器等方面,配置与 JMS 相似。虽然在配置上相似,但 JMS 和 AMQP 是非常不同的传输协议,建议您访问 AMQP 网站 12 了解关于该协议的全部细节。

配置就绪后,让我们创建一个示例类来执行 RPC 调用。

package com.apress.prospring5.ch12;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.GenericXmlApplicationContext;

public class AmqpRpcDemo {
    private static Logger logger = LoggerFactory.getLogger(AmqpRpcDemo.class);
    public static void main(String... args) {
        GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
        ctx.load("classpath:spring/amqp-rpc-app-context.xml");
        ctx.refresh();

        WeatherService weatherService = ctx.getBean(WeatherService.class);
        logger.info("Forecast for FL: " + weatherService.getForecast("FL"));
        logger.info("Forecast for MA: " + weatherService.getForecast("MA"));
        logger.info("Forecast for CA: " + weatherService.getForecast("CA"));

        ctx.close();
    }
}

现在让我们运行示例,您应该会得到以下输出:

INFO  c.a.p.c.AmqpRpcDemo - Forecast for FL: Hot
INFO  c.a.p.c.AmqpRpcDemo - Forecast for MA: Cold
INFO  c.a.p.c.AmqpRpcDemo - Forecast for CA: Not available at this time

当然,XML 配置可以很容易地转换成 Java 配置类。但是其他相关的类也需要做一些修改。WeatherServiceImpl不再需要实现接口,因为它将只声明一个监听器方法,该方法将监听写在forecasts队列上的消息。

package com.apress.prospring5.ch12;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

@Service
public class WeatherServiceImpl {

   private static Logger logger =
       LoggerFactory.getLogger(WeatherServiceImpl.class);

   @RabbitListener(containerFactory="rabbitListenerContainerFactory",
        queues="forecasts")
   public void getForecast(String stateCode) {
      if ("FL".equals(stateCode)) {
         logger.info("Hot");
      } else if ("MA".equals(stateCode)) {
         logger.info("Cold");
      } else {
    logger.info("Not available at this time");
      }
   }
}

rabbitListenerContainerFactory bean 的类型为RabbitListenerContainerFactory,用于创建常规的SimpleMessageListenerContainer。但是让我们看看完整的 Java 配置。

package com.apress.prospring5.ch12.config;

import com.apress.prospring5.ch12.WeatherService;
import com.apress.prospring5.ch12.WeatherServiceImpl;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.remoting.client.AmqpProxyFactoryBean;
import org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.apress.prospring5.ch12")
@EnableRabbit
public class RabbitMQConfig {

   final static String queueName = "forecasts";
   final static String exchangeName = "weather";

   @Bean CachingConnectionFactory connectionFactory() {
      return new CachingConnectionFactory("127.0.0.1");
   }

   @Bean RabbitTemplate amqpTemplate() {
      RabbitTemplate rabbitTemplate = new RabbitTemplate();
      rabbitTemplate.setConnectionFactory(connectionFactory());
      rabbitTemplate.setReplyTimeout(2000); rabbitTemplate.setRoutingKey(queueName);
      rabbitTemplate.setExchange(exchangeName);
      return rabbitTemplate;
   }

   @Bean Queue forecasts() {
      return new Queue(queueName, true);
   }

   @Bean Binding dataBinding(DirectExchange directExchange, Queue queue) {
      return BindingBuilder.bind(queue).to(directExchange).with(queueName);
   }

   @Bean RabbitAdmin admin() {
      RabbitAdmin admin = new RabbitAdmin(connectionFactory());
      admin.declareQueue(forecasts());
      admin.declareBinding(dataBinding(weather(), forecasts()));
      return admin;
   }

   @Bean DirectExchange weather() {
      return new DirectExchange(exchangeName, true, false);
   }

   @Bean
   public SimpleRabbitListenerContainerFactory
          rabbitListenerContainerFactory() {
      SimpleRabbitListenerContainerFactory factory =
             new SimpleRabbitListenerContainerFactory();
      factory.setConnectionFactory(connectionFactory());
      factory.setMaxConcurrentConsumers(5);
      return factory;
   }

}

前面配置中的所有 beans 都可以很容易地与它们的 XML 对应物匹配。新元素是@EnableRabbit注释。当用在用@Configuration注释的类上时,它支持由RabbitListenerContainerFactory bean 在幕后创建的兔子监听器注释端点。

为了测试新的天气服务,我们还必须修改测试程序,amqpTemplate用于向forecasts队列发送消息,在那里WeatherServiceImpl.getForecast(...)将读取这些消息并打印出预测输出。

package com.apress.prospring5.ch12;

import com.apress.prospring5.ch12.config.RabbitMQConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

public class AmqpRpcDemo {

   public static void main(String... args) throws Exception {
      GenericApplicationContext ctx =
           new AnnotationConfigApplicationContext(RabbitMQConfig.class);
      RabbitTemplate rabbitTemplate = ctx.getBean(RabbitTemplate.class);
      rabbitTemplate.convertAndSend("FL");
      rabbitTemplate.convertAndSend("MA");
      rabbitTemplate.convertAndSend("CA");

      System.in.read();
      ctx.close();
    }
}

如果您运行前面的程序,并且 RabbitMQ 服务器已经启动,您将看到以下输出:

[SimpleAsyncTaskExecutor-1] INFO  c.a.p.c.WeatherServiceImpl - Hot
[SimpleAsyncTaskExecutor-1] INFO  c.a.p.c.WeatherServiceImpl - Cold
[SimpleAsyncTaskExecutor-1] INFO  c.a.p.c.WeatherServiceImpl - Not available at this time

利用 AMQP 和 Spring Boot

Spring Boot 也帮助你开发 AMQP 应用;它的首发神器spring-boot-starter-amqp就是为了这个。配置被简化了很多。您不再需要定义RabbitTemplateRabbitAdminSimpleRabbitListenerContainerFactorybean,因为这些 bean 是由 Spring Boot 自动配置和创建的。WeatherServiceImpl的实现变化不大,但是由于SimpleRabbitListenerContainerFactory bean 是由 Spring Boot 处理的,所以不再需要将它作为一个值添加到@RabbitListener注释中。

package com.apress.prospring5.ch12;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

@Service
public class WeatherServiceImpl {

   private static Logger logger =
        LoggerFactory.getLogger(WeatherServiceImpl.class);

   @RabbitListener(queues="forecasts")
   public void getForecast(String stateCode) {
      if ("FL".equals(stateCode)) {
         logger.info("Hot");
      } else if ("MA".equals(stateCode)) {
         logger.info("Cold");
      } else {
         logger.info("Not available at this time");
      }
   }
}

@SpringBootApplication标注的Application类也被用作配置类和运行器类。

package com.apress.prospring5.ch12;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {
   final static String queueName = "forecasts";
   final static String exchangeName = "weather";

   @Bean Queue forecasts() {
      return new Queue(queueName, true);
   }

   @Bean DirectExchange weather() {
      return new DirectExchange(exchangeName, true, false);
   }

   @Bean Binding dataBinding(DirectExchange directExchange, Queue queue) {
      return BindingBuilder.bind(queue).to(directExchange).with(queueName);
   }

   @Bean CachingConnectionFactory connectionFactory() {
      return new CachingConnectionFactory("127.0.0.1");
   }

   @Bean
   SimpleMessageListenerContainer messageListenerContainer() {
      SimpleMessageListenerContainer container =
          new SimpleMessageListenerContainer();
      container.setConnectionFactory(connectionFactory());
      container.setQueueNames(queueName);
      return container;
   }

   public static void main(String... args) throws java.lang.Exception {
      ConfigurableApplicationContext ctx =
          SpringApplication.run(Application.class, args);

      RabbitTemplate rabbitTemplate = ctx.getBean(RabbitTemplate.class);
      rabbitTemplate.convertAndSend(Application.queueName, "FL");
      rabbitTemplate.convertAndSend(Application.queueName, "MA");
      rabbitTemplate.convertAndSend(Application.queueName, "CA");

      System.in.read();
      ctx.close();
   }
}

正如您所看到的,也不需要@EnableRabbit注释,尽管配置没有减少多少,但这仍然是一个进步。如果您运行前面的类,您将得到类似的结果,正如您在前面的示例中看到的那样。

DEBUG c.a.p.c.Application - Running with Spring Boot  v2.0.0.M1, Spring v5.0.0.RC1
INFO  c.a.p.c.Application - No active profile set, falling back  to default profiles: default
INFO  c.a.p.c.Application - Started Application in 2.211 seconds JVM running for 2.801
[SimpleAsyncTaskExecutor-1] INFO  c.a.p.c.WeatherServiceImpl - Cold
[SimpleAsyncTaskExecutor-1] INFO  c.a.p.c.WeatherServiceImpl - Hot
[SimpleAsyncTaskExecutor-1] INFO  c.a.p.c.WeatherServiceImpl - Not available at this time

摘要

在本章中,我们讨论了基于 Spring 的应用中最常用的远程技术。

如果两个应用都是用 Spring 构建的,那么使用 Spring HTTP invoker 是一个可行的选择。如果需要异步模式或松散耦合模式的集成,JMS 是一种常用的方法。我们讨论了如何在 Spring 中使用 RESTful-WS 来公开服务或使用RestTemplate类访问服务。最后,我们讨论了如何通过 RabbitMQ 使用 Spring AMQP 进行 RPC 风格的远程处理。

每个技术的 Spring Boot,远程、REST、JMS 等都被覆盖,这是你应该寻找的东西。

在下一章,我们将讨论使用 Spring 测试应用;是时候我们阐述一些测试技术来让你的生活变得更容易了。

Footnotes 1

当 Spring 4 发布时,提到了 Burlap 不再处于积极开发中,并且将来会完全停止支持。

  2

如果您喜欢 Apache 产品,Apache ActiveMQ Artemis 是一个 JMS 2.0 实现,具有非阻塞架构。它提供了出色的性能。更多详情请点击 https://activemq.apache.org/artemis/ 。此外,代码示例中提供了一个使用 Artemis ActiveMQ 的项目。

  3

这是 XML 序列化所需要的;JSON 不需要容器类也能工作。

  4

你可以在这里找到这个设计模式的很好的解释: http://www.oracle.com/technetwork/java/frontcontroller-135648.html

  5

这相当于<mvc:annotation-driven>标签/。

  6

这相当于 Spring 3.1 中引入的<mvc:message-converters>标签。

  7

你可以在 http://jackson.codehaus.org 找到杰克森 JSON 库官方网站。

  8

这相当于<context:component-scan>标签。

  9

http://curl.haxx.se

  10

www.rabbitmq.org

  11

http://projects.spring.io/spring-amqp

  12

参见 AMQP 网站 www.amqp.org