Spring Cloud Stream 对 RabbitMQ 的支持
依赖
- 引入 Spring Cloud - spring-cloud-starter-stream-rabbit 依赖
- 底层使用了 Spring Boot - spring-boot-starter-amqp
配置
- spring.cloud.stream.rabbit.binder.*
binder 抽象的配置 - spring.cloud.stream.rabbit.bindings.<channelName>.consumer.*
binder 的配置 需要指定绑定的q的名字 - spring.rabbitmq.*
与rabbitmq服务相关的配置
通过 Docker 启动 RabbitMQ
官方指引
- https://hub.docker.com/_/rabbitmq
获取镜像
- docker pull rabbitmq
- docker pull rabbitmq:3.7-management
一般 拉 docker pull rabbitmq:3.7-management 其中包括 rabbitmq 还有它相关的管理界面
运行 RabbitMQ 镜像
- docker run --name rabbitmq -d -p 5672:5672 -p 15672:15672
-e RABBITMQ_DEFAULT_USER=spring -e RABBITMQ_DEFAULT_PASS=spring
rabbitmq:3.7-management
5672:RabbitMQ 的服务端口
15672:RabbitMQ 的管理界面
用户名和密码指定为spring
实例代码
由两部分组成:rabbitmq-barista-service 和 rabbitmq-waiter-service
rabbitmq-barista-service
@Component
@Slf4j
@Transactional
public class OrderListener {
@Autowired
private CoffeeOrderRepository orderRepository;
@Autowired
@Qualifier(Waiter.FINISHED_ORDERS)//注入这个Channel
private MessageChannel finishedOrdersMessageChannel;
@Value("${order.barista-prefix}${random.uuid}")
private String barista;
@StreamListener(Waiter.NEW_ORDERS)
public void processNewOrder(Long id) {//收到消息之后 从数据库取出一个coffeeorder
CoffeeOrder o = orderRepository.getOne(id);
if (o == null) {
log.warn("Order id {} is NOT valid.", id);//打印一个错误日志
return;
}
log.info("Receive a new Order {}. Waiter: {}. Customer: {}",
id, o.getWaiter(), o.getCustomer());//打印一个接收日志
o.setState(OrderState.BREWED);//更改状态
o.setBarista(barista);//设置barista
orderRepository.save(o);//保存
log.info("Order {} is READY.", id);
finishedOrdersMessageChannel.send(MessageBuilder.withPayload(id).build());//发送出去
}
}
public interface Waiter {
String NEW_ORDERS = "newOrders";
String FINISHED_ORDERS = "finishedOrders";
@Input(NEW_ORDERS)
//监听
SubscribableChannel newOrders();
@Output(FINISHED_ORDERS)
//发送
MessageChannel finishedOrders();
}
@Entity
@Table(name = "T_ORDER")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CoffeeOrder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String customer;
private String waiter;
private String barista;
@Enumerated
@Column(nullable = false)
private OrderState state;
@Column(updatable = false)
@CreationTimestamp
private Date createTime;
@UpdateTimestamp
private Date updateTime;
}
public enum OrderState {
INIT, PAID, BREWING, BREWED, TAKEN, CANCELLED
}
public interface CoffeeOrderRepository extends JpaRepository<CoffeeOrder, Long> {
}
@EnableJpaRepositories
@SpringBootApplication
@EnableBinding(Waiter.class)
public class BaristaServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BaristaServiceApplication.class, args);
}
}
application.properties
spring.application.name=barista-service
order.barista-prefix=springbucks-
server.port=0
spring.output.ansi.enabled=ALWAYS
#NEVER:禁用ANSI-colored输出(默认项)
#DETECT:会检查终端是否支持ANSI,是的话就采用彩色输出(推荐项)
#ALWAYS:总是使用ANSI-colored格式输出,若终端不支持的时候,会有很多干扰信息,不推荐使用
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.datasource.url=jdbc:mysql://localhost/springbucks
spring.datasource.username=root
spring.datasource.password=123456
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=spring
spring.rabbitmq.password=spring
spring.cloud.stream.bindings.newOrders.group=barista-service
#监听的newOrders队列 如果启动多个barista-service它的一个消息就会被当中的一个实例收到
rabbitmq-waiter-service
需要修改的代码
CoffeeOrderController
@RestController
@RequestMapping("/order")
@Slf4j
public class CoffeeOrderController {
@Autowired
private CoffeeOrderService orderService;
@Autowired
private CoffeeService coffeeService;
private RateLimiter rateLimiter;
public CoffeeOrderController(RateLimiterRegistry rateLimiterRegistry) {
rateLimiter = rateLimiterRegistry.rateLimiter("order");
}
@GetMapping("/{id}")
public CoffeeOrder getOrder(@PathVariable("id") Long id) {
CoffeeOrder order = null;
try {
order = rateLimiter.executeSupplier(() -> orderService.get(id));
log.info("Get Order: {}", order);
} catch(RequestNotPermitted e) {
log.warn("Request Not Permitted! {}", e.getMessage());
}
return order;
}
@PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseStatus(HttpStatus.CREATED)
@io.github.resilience4j.ratelimiter.annotation.RateLimiter(name = "order")
public CoffeeOrder create(@RequestBody NewOrderRequest newOrder) {
log.info("Receive new Order {}", newOrder);
Coffee[] coffeeList = coffeeService.getCoffeeByName(newOrder.getItems())
.toArray(new Coffee[] {});
return orderService.createOrder(newOrder.getCustomer(), coffeeList);
}
@PutMapping("/{id}")
public CoffeeOrder updateState(@PathVariable("id") Long id,
@RequestBody OrderStateRequest orderState) {
//订单状态的更新
log.info("Update order state: {}", orderState);
CoffeeOrder order = orderService.get(id);
orderService.updateState(order, orderState.getState());
return order;
}
}
Barista
public interface Barista {
String NEW_ORDERS = "newOrders";
String FINISHED_ORDERS = "finishedOrders";
@Input
//订阅的消息
SubscribableChannel finishedOrders();
@Output
//接收的消息
MessageChannel newOrders();
}
OrderListener
@Component
@Slf4j
public class OrderListener {
@StreamListener(Barista.FINISHED_ORDERS)//监听FINISHED_ORDERS这个消息
public void listenFinishedOrders(Long id) {
log.info("We've finished an order [{}].", id);
}//是Barista发出的
}
CoffeeOrderService
@Service
@Transactional
@Slf4j
public class CoffeeOrderService implements MeterBinder {
@Autowired
private CoffeeOrderRepository orderRepository;
@Autowired
private OrderProperties orderProperties;
@Autowired
private Barista barista;
private String waiterId = UUID.randomUUID().toString();
private Counter orderCounter = null;
public CoffeeOrder get(Long id) {
return orderRepository.getOne(id);
}
public CoffeeOrder createOrder(String customer, Coffee...coffee) {
CoffeeOrder order = CoffeeOrder.builder()
.customer(customer)
.items(new ArrayList<>(Arrays.asList(coffee)))
.discount(orderProperties.getDiscount())
.total(calcTotal(coffee))
.state(OrderState.INIT)
.waiter(orderProperties.getWaiterPrefix() + waiterId)
.build();
CoffeeOrder saved = orderRepository.save(order);
log.info("New Order: {}", saved);
orderCounter.increment();
return saved;
}
public boolean updateState(CoffeeOrder order, OrderState state) {
if (order == null) {
log.warn("Can not find order.");
return false;
}
if (state.compareTo(order.getState()) <= 0) {
//判断State State只能往一个方向去状态的控制 从未支付到支付
log.warn("Wrong State order: {}, {}", state, order.getState());
return false;
}
order.setState(state);
orderRepository.save(order);
log.info("Updated Order: {}", order);
if (state == OrderState.PAID) {
// 有返回值,如果要关注发送结果,则判断返回值
// 一般消息体不会这么简单
// 如果是支付消息 我们就做一个消息的发送
barista.newOrders().send(MessageBuilder.withPayload(order.getId()).build());//传入订单的id
}
return true;
}
@Override
public void bindTo(MeterRegistry meterRegistry) {
this.orderCounter = meterRegistry.counter("order.count");
}
private Money calcTotal(Coffee...coffee) {
List<Money> items = Stream.of(coffee).map(c -> c.getPrice())
.collect(Collectors.toList());
return Money.total(items).multipliedBy(orderProperties.getDiscount())
.dividedBy(100, RoundingMode.HALF_UP);
}
}
WaiterServiceApplication
@SpringBootApplication
@EnableJpaRepositories
@EnableCaching
@EnableDiscoveryClient
@EnableBinding(Barista.class)
//告诉我的应用程序,去EnableBinding哪些接口
public class WaiterServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(WaiterServiceApplication.class, args);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new PerformanceInteceptor())
.addPathPatterns("/coffee/**").addPathPatterns("/order/**");
}
@Bean
public Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonBuilderCustomizer() {
return builder -> {
builder.indentOutput(true);
builder.timeZone(TimeZone.getTimeZone("Asia/Shanghai"));
};
}
}
application.properties
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.show_sql=false
spring.jpa.properties.hibernate.format_sql=false
# 运行过一次后,如果不想清空数据库就注释掉下面这行 不管是什么数据库
spring.datasource.initialization-mode=always
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
info.app.author=DigitalSonic
info.app.encoding=@project.build.sourceEncoding@
server.port=8080
spring.datasource.url=jdbc:mysql://localhost/springbucks
spring.datasource.username=root
spring.datasource.password=123456
order.discount=95
resilience4j.ratelimiter.limiters.coffee.limit-for-period=5
resilience4j.ratelimiter.limiters.coffee.limit-refresh-period-in-millis=30000
resilience4j.ratelimiter.limiters.coffee.timeout-in-millis=5000
resilience4j.ratelimiter.limiters.coffee.subscribe-for-events=true
resilience4j.ratelimiter.limiters.coffee.register-health-indicator=true
resilience4j.ratelimiter.limiters.order.limit-for-period=3
resilience4j.ratelimiter.limiters.order.limit-refresh-period-in-millis=30000
resilience4j.ratelimiter.limiters.order.timeout-in-millis=1000
resilience4j.ratelimiter.limiters.order.subscribe-for-events=true
resilience4j.ratelimiter.limiters.order.register-health-indicator=true
#配置rabbitmq
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=spring
spring.rabbitmq.password=spring
spring.cloud.stream.bindings.finishedOrders.group=waiter-service
#监听的 finishedOrders 队列 如果启动多个waiter-service它的一个消息就会被当中的一个实例收到
另外 在CoffeeOrder中 增加了private String barista;属性
结果分析
输入 http://localhost:15672/ 进入RabbitMQ
进入 之后 我们可以看到 finishedOrders 和 newOrders 两个队列
finishedOrders 绑定了 waiter-service
输入 http://localhost:8080/actuator/bindings
可以 看到 对应的 binding 的信息
使用 postman 进行测试
创建一个订单 显示 未支付
进行 支付操作 状态变更完毕
这个时候 查询订单 会发现 订单已经完成 制作完毕