添加15章示例

master
DigitalSonic 6 years ago
parent c11b5cc7db
commit 7118be6d3d

@ -0,0 +1,26 @@
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring.springbucks</groupId>
<artifactId>waiter-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>waiter-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>0.14.1</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>6.0.1.GA</version>
</dependency>
<!-- 增加Jackson的Hibernate类型支持 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate5</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,49 @@
package geektime.spring.springbucks.waiter;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import geektime.spring.springbucks.waiter.controller.PerformanceInteceptor;
import geektime.spring.springbucks.waiter.integration.Barista;
import geektime.spring.springbucks.waiter.integration.Customer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.TimeZone;
@SpringBootApplication
@EnableJpaRepositories
@EnableCaching
@EnableDiscoveryClient
@EnableBinding({ Barista.class, Customer.class })
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"));
};
}
}

@ -0,0 +1,96 @@
package geektime.spring.springbucks.waiter.controller;
import geektime.spring.springbucks.waiter.controller.request.NewCoffeeRequest;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.service.CoffeeService;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/coffee")
@RateLimiter(name = "coffee")
@Slf4j
public class CoffeeController {
@Autowired
private CoffeeService coffeeService;
@PostMapping(path = "/", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public Coffee addCoffeeWithoutBindingResult(@Valid NewCoffeeRequest newCoffee) {
return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
}
@PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public Coffee addJsonCoffeeWithoutBindingResult(@Valid @RequestBody NewCoffeeRequest newCoffee) {
return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
}
@PostMapping(path = "/", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public List<Coffee> batchAddCoffee(@RequestParam("file") MultipartFile file) {
List<Coffee> coffees = new ArrayList<>();
if (!file.isEmpty()) {
BufferedReader reader = null;
try {
reader = new BufferedReader(
new InputStreamReader(file.getInputStream()));
String str;
while ((str = reader.readLine()) != null) {
String[] arr = StringUtils.split(str, " ");
if (arr != null && arr.length == 2) {
coffees.add(coffeeService.saveCoffee(arr[0],
Money.of(CurrencyUnit.of("CNY"),
NumberUtils.createBigDecimal(arr[1]))));
}
}
} catch (IOException e) {
log.error("exception", e);
} finally {
IOUtils.closeQuietly(reader);
}
}
return coffees;
}
@GetMapping(path = "/", params = "!name")
public List<Coffee> getAll() {
return coffeeService.getAllCoffee();
}
@GetMapping("/{id}")
public Coffee getById(@PathVariable Long id) {
Coffee coffee = coffeeService.getCoffee(id);
log.info("Coffee {}:", coffee);
return coffee;
}
@GetMapping(path = "/", params = "name")
public Coffee getByName(@RequestParam String name) {
return coffeeService.getCoffee(name);
}
}

@ -0,0 +1,70 @@
package geektime.spring.springbucks.waiter.controller;
import geektime.spring.springbucks.waiter.controller.request.NewOrderRequest;
import geektime.spring.springbucks.waiter.controller.request.OrderStateRequest;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.model.CoffeeOrder;
import geektime.spring.springbucks.waiter.service.CoffeeOrderService;
import geektime.spring.springbucks.waiter.service.CoffeeService;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@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 {} with state {}", id, orderState);
CoffeeOrder order = orderService.get(id);
orderService.updateState(order, orderState.getState());
return order;
}
}

@ -0,0 +1,46 @@
package geektime.spring.springbucks.waiter.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class PerformanceInteceptor implements HandlerInterceptor {
private ThreadLocal<StopWatch> stopWatch = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
StopWatch sw = new StopWatch();
stopWatch.set(sw);
sw.start();
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
stopWatch.get().stop();
stopWatch.get().start();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
StopWatch sw = stopWatch.get();
sw.stop();
String method = handler.getClass().getSimpleName();
if (handler instanceof HandlerMethod) {
String beanType = ((HandlerMethod) handler).getBeanType().getName();
String methodName = ((HandlerMethod) handler).getMethod().getName();
method = beanType + "." + methodName;
}
log.info("{};{};{};{};{}ms;{}ms;{}ms", request.getRequestURI(), method,
response.getStatus(), ex == null ? "-" : ex.getClass().getSimpleName(),
sw.getTotalTimeMillis(), sw.getTotalTimeMillis() - sw.getLastTaskTimeMillis(),
sw.getLastTaskTimeMillis());
stopWatch.remove();
}
}

@ -0,0 +1,19 @@
package geektime.spring.springbucks.waiter.controller.request;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.joda.money.Money;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Getter
@Setter
@ToString
public class NewCoffeeRequest {
@NotEmpty
private String name;
@NotNull
private Money price;
}

@ -0,0 +1,15 @@
package geektime.spring.springbucks.waiter.controller.request;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@Setter
@ToString
public class NewOrderRequest {
private String customer;
private List<String> items;
}

@ -0,0 +1,13 @@
package geektime.spring.springbucks.waiter.controller.request;
import geektime.spring.springbucks.waiter.model.OrderState;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class OrderStateRequest {
private OrderState state;
}

@ -0,0 +1,17 @@
package geektime.spring.springbucks.waiter.integration;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface Barista {
String NEW_ORDERS = "newOrders";
String FINISHED_ORDERS = "finishedOrders";
@Input
SubscribableChannel finishedOrders();
@Output
MessageChannel newOrders();
}

@ -0,0 +1,11 @@
package geektime.spring.springbucks.waiter.integration;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
public interface Customer {
String NOTIFY_ORDERS = "notifyOrders";
@Output(NOTIFY_ORDERS)
MessageChannel notification();
}

@ -0,0 +1,30 @@
package geektime.spring.springbucks.waiter.integration;
import geektime.spring.springbucks.waiter.model.CoffeeOrder;
import geektime.spring.springbucks.waiter.service.CoffeeOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class OrderListener {
@Autowired
private Customer customer;
@Autowired
private CoffeeOrderService orderService;
@StreamListener(Barista.FINISHED_ORDERS)
public void listenFinishedOrders(Long id) {
log.info("We've finished an order [{}].", id);
CoffeeOrder order = orderService.get(id);
Message<Long> message = MessageBuilder.withPayload(id)
.setHeader("customer", order.getCustomer())
.build();
log.info("Notify the customer: {}", order.getCustomer());
customer.notification().send(message);
}
}

@ -0,0 +1,32 @@
package geektime.spring.springbucks.waiter.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import java.io.Serializable;
import java.util.Date;
@MappedSuperclass
@Data
@NoArgsConstructor
@AllArgsConstructor
// 增加了jackson-datatype-hibernate5就不需要这个Ignore了
//@JsonIgnoreProperties(value = {"hibernateLazyInitializer"})
public class BaseEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(updatable = false)
@CreationTimestamp
private Date createTime;
@UpdateTimestamp
private Date updateTime;
}

@ -0,0 +1,29 @@
package geektime.spring.springbucks.waiter.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.hibernate.annotations.Type;
import org.joda.money.Money;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "T_COFFEE")
@Builder
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class Coffee extends BaseEntity implements Serializable {
private String name;
@Type(type = "org.jadira.usertype.moneyandcurrency.joda.PersistentMoneyMinorAmount",
parameters = {@org.hibernate.annotations.Parameter(name = "currencyCode", value = "CNY")})
private Money price;
}

@ -0,0 +1,45 @@
package geektime.spring.springbucks.waiter.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.hibernate.annotations.Type;
import org.joda.money.Money;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.List;
@Entity
@Table(name = "T_ORDER")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CoffeeOrder extends BaseEntity implements Serializable {
private String customer;
@ManyToMany
@JoinTable(name = "T_ORDER_COFFEE")
@OrderBy("id")
private List<Coffee> items;
@Enumerated
@Column(nullable = false)
private OrderState state;
private Integer discount;
@Type(type = "org.jadira.usertype.moneyandcurrency.joda.PersistentMoneyMinorAmount",
parameters = {@org.hibernate.annotations.Parameter(name = "currencyCode", value = "CNY")})
private Money total;
private String waiter;
private String barista;
}

@ -0,0 +1,5 @@
package geektime.spring.springbucks.waiter.model;
public enum OrderState {
INIT, PAID, BREWING, BREWED, TAKEN, CANCELLED
}

@ -0,0 +1,7 @@
package geektime.spring.springbucks.waiter.repository;
import geektime.spring.springbucks.waiter.model.CoffeeOrder;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CoffeeOrderRepository extends JpaRepository<CoffeeOrder, Long> {
}

@ -0,0 +1,11 @@
package geektime.spring.springbucks.waiter.repository;
import geektime.spring.springbucks.waiter.model.Coffee;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CoffeeRepository extends JpaRepository<Coffee, Long> {
List<Coffee> findByNameInOrderById(List<String> list);
Coffee findByName(String name);
}

@ -0,0 +1,92 @@
package geektime.spring.springbucks.waiter.service;
import geektime.spring.springbucks.waiter.integration.Barista;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.model.CoffeeOrder;
import geektime.spring.springbucks.waiter.model.OrderState;
import geektime.spring.springbucks.waiter.repository.CoffeeOrderRepository;
import geektime.spring.springbucks.waiter.support.OrderProperties;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.MeterBinder;
import lombok.extern.slf4j.Slf4j;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@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) {
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());
}
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);
}
}

@ -0,0 +1,51 @@
package geektime.spring.springbucks.waiter.service;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.repository.CoffeeRepository;
import lombok.extern.slf4j.Slf4j;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
@Slf4j
@CacheConfig(cacheNames = "CoffeeCache")
public class CoffeeService {
@Autowired
private CoffeeRepository coffeeRepository;
public Coffee saveCoffee(String name, Money price) {
return coffeeRepository.save(Coffee.builder().name(name).price(price).build());
}
@Cacheable
public List<Coffee> getAllCoffee() {
return coffeeRepository.findAll(Sort.by("id"));
}
public Coffee getCoffee(Long id) {
// return coffeeRepository.findById(id).get();
return coffeeRepository.getOne(id);
}
public long getCoffeeCount() {
return coffeeRepository.count();
}
public Coffee getCoffee(String name) {
return coffeeRepository.findByName(name);
}
public List<Coffee> getCoffeeByName(List<String> names) {
return coffeeRepository.findByNameInOrderById(names);
}
}

@ -0,0 +1,31 @@
package geektime.spring.springbucks.waiter.support;
import geektime.spring.springbucks.waiter.service.CoffeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class CoffeeIndicator implements HealthIndicator {
@Autowired
private CoffeeService coffeeService;
@Override
public Health health() {
long count = coffeeService.getCoffeeCount();
Health health;
if (count > 0) {
health = Health.up()
.withDetail("count", count)
.withDetail("message", "We have enough coffee.")
.build();
} else {
health = Health.down()
.withDetail("count", 0)
.withDetail("message", "We are out of coffee.")
.build();
}
return health;
}
}

@ -0,0 +1,23 @@
package geektime.spring.springbucks.waiter.support;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneyDeserializer extends StdDeserializer<Money> {
protected MoneyDeserializer() {
super(Money.class);
}
@Override
public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
}
}

@ -0,0 +1,42 @@
package geektime.spring.springbucks.waiter.support;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.format.Formatter;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.util.Locale;
@Component
public class MoneyFormatter implements Formatter<Money> {
/**
* CNY 10.00 / 10.00
*
*/
@Override
public Money parse(String text, Locale locale) throws ParseException {
if (NumberUtils.isParsable(text)) {
return Money.of(CurrencyUnit.of("CNY"), NumberUtils.createBigDecimal(text));
} else if (StringUtils.isNotEmpty(text)) {
String[] split = StringUtils.split(text, " ");
if (split != null && split.length == 2 && NumberUtils.isParsable(split[1])) {
return Money.of(CurrencyUnit.of(split[0]),
NumberUtils.createBigDecimal(split[1]));
} else {
throw new ParseException(text, 0);
}
}
throw new ParseException(text, 0);
}
@Override
public String print(Money money, Locale locale) {
if (money == null) {
return null;
}
return money.getCurrencyUnit().getCode() + " " + money.getAmount();
}
}

@ -0,0 +1,21 @@
package geektime.spring.springbucks.waiter.support;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneySerializer extends StdSerializer<Money> {
protected MoneySerializer() {
super(Money.class);
}
@Override
public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumber(money.getAmount());
}
}

@ -0,0 +1,15 @@
package geektime.spring.springbucks.waiter.support;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@ConfigurationProperties("order")
@RefreshScope
@Data
@Component
public class OrderProperties {
private Integer discount = 100;
private String waiterPrefix = "springbucks-";
}

@ -0,0 +1,44 @@
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.show_sql=false
spring.jpa.properties.hibernate.format_sql=false
# 用来解决 LazyInitializationException: no Session
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
# 运行过一次后,如果不想清空数据库就注释掉下面这行
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@
spring.output.ansi.enabled=ALWAYS
server.port=0
spring.datasource.url=jdbc:mysql://localhost/springbucks
spring.datasource.username=springbucks
spring.datasource.password=springbucks
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
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=spring
spring.rabbitmq.password=spring
spring.cloud.stream.bindings.finishedOrders.group=waiter-service
spring.cloud.stream.rabbit.bindings.notifyOrders.producer.routing-key-expression=headers.customer

@ -0,0 +1,8 @@
spring.application.name=waiter-service
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.prefer-ip-address=true
spring.cloud.consul.config.enabled=true
spring.cloud.consul.config.format=yaml

@ -0,0 +1,5 @@
insert into t_coffee (name, price, create_time, update_time) values ('espresso', 2000, now(), now());
insert into t_coffee (name, price, create_time, update_time) values ('latte', 2500, now(), now());
insert into t_coffee (name, price, create_time, update_time) values ('capuccino', 2500, now(), now());
insert into t_coffee (name, price, create_time, update_time) values ('mocha', 3000, now(), now());
insert into t_coffee (name, price, create_time, update_time) values ('macchiato', 3000, now(), now());

@ -0,0 +1,33 @@
# 注意 MySQL 与 H2 的语法差异
# H2: drop table tbl if exists;
# MySQL: drop table if exists tbl;
drop table if exists t_coffee;
drop table if exists t_order;
drop table if exists t_order_coffee;
create table t_coffee (
id bigint auto_increment,
create_time timestamp,
update_time timestamp,
name varchar(255),
price bigint,
primary key (id)
);
create table t_order (
id bigint auto_increment,
create_time timestamp,
update_time timestamp,
customer varchar(255),
waiter varchar(255),
barista varchar(255),
discount integer,
total bigint,
state integer,
primary key (id)
);
create table t_order_coffee (
coffee_order_id bigint not null,
items_id bigint not null
);

@ -0,0 +1,16 @@
package geektime.spring.springbucks.waiter;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class WaiterServiceApplicationTests {
@Test
public void contextLoads() {
}
}

@ -0,0 +1,29 @@
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/

@ -0,0 +1,286 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven2 Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
# TODO classpath?
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`which java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found .mvn/wrapper/maven-wrapper.jar"
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
esac
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
if [ "$MVNW_VERBOSE" = true ]; then
echo "Downloading from: $jarUrl"
fi
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
if command -v wget > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found wget ... using wget"
fi
wget "$jarUrl" -O "$wrapperJarPath"
elif command -v curl > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found curl ... using curl"
fi
curl -o "$wrapperJarPath" "$jarUrl"
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Falling back to using Java to download"
fi
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
if [ -e "$javaClass" ]; then
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Compiling MavenWrapperDownloader.java ..."
fi
# Compiling the Java class
("$JAVA_HOME/bin/javac" "$javaClass")
fi
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
# Running the downloader
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Running MavenWrapperDownloader.java ..."
fi
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
if [ "$MVNW_VERBOSE" = true ]; then
echo $MAVEN_PROJECTBASEDIR
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

@ -0,0 +1,161 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven2 Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
echo Found %WRAPPER_JAR%
) else (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %DOWNLOAD_URL%
powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"
echo Finished downloading %WRAPPER_JAR%
)
@REM End of extension
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
exit /B %ERROR_CODE%

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring.springbucks</groupId>
<artifactId>barista-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>barista-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,18 @@
package geektime.spring.springbucks.barista;
import geektime.spring.springbucks.barista.integration.Waiter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories
@SpringBootApplication
@EnableBinding(Waiter.class)
public class BaristaServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BaristaServiceApplication.class, args);
}
}

@ -0,0 +1,44 @@
package geektime.spring.springbucks.barista.integration;
import geektime.spring.springbucks.barista.model.CoffeeOrder;
import geektime.spring.springbucks.barista.model.OrderState;
import geektime.spring.springbucks.barista.repository.CoffeeOrderRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
@Slf4j
@Transactional
public class OrderListener {
@Autowired
private CoffeeOrderRepository orderRepository;
@Autowired
@Qualifier(Waiter.FINISHED_ORDERS)
private MessageChannel finishedOrdersMessageChannel;
@Value("${order.barista-prefix}${random.uuid}")
private String barista;
@StreamListener(Waiter.NEW_ORDERS)
@SendTo(Waiter.FINISHED_ORDERS)
public Long processNewOrder(Long id) {
CoffeeOrder o = orderRepository.getOne(id);
if (o == null) {
log.warn("Order id {} is NOT valid.", id);
throw new IllegalArgumentException("Order ID is INVAILD!");
}
log.info("Receive a new Order {}. Waiter: {}. Customer: {}",
id, o.getWaiter(), o.getCustomer());
o.setState(OrderState.BREWED);
o.setBarista(barista);
orderRepository.save(o);
log.info("Order {} is READY.", id);
return id;
}
}

@ -0,0 +1,17 @@
package geektime.spring.springbucks.barista.integration;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface Waiter {
String NEW_ORDERS = "newOrders";
String FINISHED_ORDERS = "finishedOrders";
@Input(NEW_ORDERS)
SubscribableChannel newOrders();
@Output(FINISHED_ORDERS)
MessageChannel finishedOrders();
}

@ -0,0 +1,38 @@
package geektime.spring.springbucks.barista.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
@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;
}

@ -0,0 +1,5 @@
package geektime.spring.springbucks.barista.model;
public enum OrderState {
INIT, PAID, BREWING, BREWED, TAKEN, CANCELLED
}

@ -0,0 +1,7 @@
package geektime.spring.springbucks.barista.repository;
import geektime.spring.springbucks.barista.model.CoffeeOrder;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CoffeeOrderRepository extends JpaRepository<CoffeeOrder, Long> {
}

@ -0,0 +1,21 @@
spring.application.name=barista-service
order.barista-prefix=springbucks-
server.port=8070
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=springbucks
spring.datasource.password=springbucks
spring.cloud.stream.kafka.binder.brokers=localhost
spring.cloud.stream.kafka.binder.defaultBrokerPort=9092
spring.cloud.stream.bindings.newOrders.group=barista-service

@ -0,0 +1,16 @@
package geektime.spring.springbucks.barista;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class BaristaServiceApplicationTests {
@Test
public void contextLoads() {
}
}

@ -0,0 +1,26 @@
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/

@ -0,0 +1,19 @@
---
version: '2'
services:
zookeeper:
image: zookeeper:latest
kafka:
image: confluentinc/cp-kafka:latest
depends_on:
- zookeeper
ports:
- 9092:9092
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring.springbucks</groupId>
<artifactId>waiter-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>waiter-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>0.14.1</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>6.0.1.GA</version>
</dependency>
<!-- 增加Jackson的Hibernate类型支持 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate5</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,48 @@
package geektime.spring.springbucks.waiter;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import geektime.spring.springbucks.waiter.controller.PerformanceInteceptor;
import geektime.spring.springbucks.waiter.integration.Barista;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.TimeZone;
@SpringBootApplication
@EnableJpaRepositories
@EnableCaching
@EnableDiscoveryClient
@EnableBinding(Barista.class)
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"));
};
}
}

@ -0,0 +1,96 @@
package geektime.spring.springbucks.waiter.controller;
import geektime.spring.springbucks.waiter.controller.request.NewCoffeeRequest;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.service.CoffeeService;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/coffee")
@RateLimiter(name = "coffee")
@Slf4j
public class CoffeeController {
@Autowired
private CoffeeService coffeeService;
@PostMapping(path = "/", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public Coffee addCoffeeWithoutBindingResult(@Valid NewCoffeeRequest newCoffee) {
return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
}
@PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public Coffee addJsonCoffeeWithoutBindingResult(@Valid @RequestBody NewCoffeeRequest newCoffee) {
return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
}
@PostMapping(path = "/", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public List<Coffee> batchAddCoffee(@RequestParam("file") MultipartFile file) {
List<Coffee> coffees = new ArrayList<>();
if (!file.isEmpty()) {
BufferedReader reader = null;
try {
reader = new BufferedReader(
new InputStreamReader(file.getInputStream()));
String str;
while ((str = reader.readLine()) != null) {
String[] arr = StringUtils.split(str, " ");
if (arr != null && arr.length == 2) {
coffees.add(coffeeService.saveCoffee(arr[0],
Money.of(CurrencyUnit.of("CNY"),
NumberUtils.createBigDecimal(arr[1]))));
}
}
} catch (IOException e) {
log.error("exception", e);
} finally {
IOUtils.closeQuietly(reader);
}
}
return coffees;
}
@GetMapping(path = "/", params = "!name")
public List<Coffee> getAll() {
return coffeeService.getAllCoffee();
}
@GetMapping("/{id}")
public Coffee getById(@PathVariable Long id) {
Coffee coffee = coffeeService.getCoffee(id);
log.info("Coffee {}:", coffee);
return coffee;
}
@GetMapping(path = "/", params = "name")
public Coffee getByName(@RequestParam String name) {
return coffeeService.getCoffee(name);
}
}

@ -0,0 +1,70 @@
package geektime.spring.springbucks.waiter.controller;
import geektime.spring.springbucks.waiter.controller.request.NewOrderRequest;
import geektime.spring.springbucks.waiter.controller.request.OrderStateRequest;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.model.CoffeeOrder;
import geektime.spring.springbucks.waiter.service.CoffeeOrderService;
import geektime.spring.springbucks.waiter.service.CoffeeService;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@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 {} with state {}", id, orderState);
CoffeeOrder order = orderService.get(id);
orderService.updateState(order, orderState.getState());
return order;
}
}

@ -0,0 +1,46 @@
package geektime.spring.springbucks.waiter.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class PerformanceInteceptor implements HandlerInterceptor {
private ThreadLocal<StopWatch> stopWatch = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
StopWatch sw = new StopWatch();
stopWatch.set(sw);
sw.start();
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
stopWatch.get().stop();
stopWatch.get().start();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
StopWatch sw = stopWatch.get();
sw.stop();
String method = handler.getClass().getSimpleName();
if (handler instanceof HandlerMethod) {
String beanType = ((HandlerMethod) handler).getBeanType().getName();
String methodName = ((HandlerMethod) handler).getMethod().getName();
method = beanType + "." + methodName;
}
log.info("{};{};{};{};{}ms;{}ms;{}ms", request.getRequestURI(), method,
response.getStatus(), ex == null ? "-" : ex.getClass().getSimpleName(),
sw.getTotalTimeMillis(), sw.getTotalTimeMillis() - sw.getLastTaskTimeMillis(),
sw.getLastTaskTimeMillis());
stopWatch.remove();
}
}

@ -0,0 +1,19 @@
package geektime.spring.springbucks.waiter.controller.request;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.joda.money.Money;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Getter
@Setter
@ToString
public class NewCoffeeRequest {
@NotEmpty
private String name;
@NotNull
private Money price;
}

@ -0,0 +1,15 @@
package geektime.spring.springbucks.waiter.controller.request;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@Setter
@ToString
public class NewOrderRequest {
private String customer;
private List<String> items;
}

@ -0,0 +1,13 @@
package geektime.spring.springbucks.waiter.controller.request;
import geektime.spring.springbucks.waiter.model.OrderState;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class OrderStateRequest {
private OrderState state;
}

@ -0,0 +1,17 @@
package geektime.spring.springbucks.waiter.integration;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface Barista {
String NEW_ORDERS = "newOrders";
String FINISHED_ORDERS = "finishedOrders";
@Input
SubscribableChannel finishedOrders();
@Output
MessageChannel newOrders();
}

@ -0,0 +1,14 @@
package geektime.spring.springbucks.waiter.integration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class OrderListener {
@StreamListener(Barista.FINISHED_ORDERS)
public void listenFinishedOrders(Long id) {
log.info("We've finished an order [{}].", id);
}
}

@ -0,0 +1,32 @@
package geektime.spring.springbucks.waiter.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import java.io.Serializable;
import java.util.Date;
@MappedSuperclass
@Data
@NoArgsConstructor
@AllArgsConstructor
// 增加了jackson-datatype-hibernate5就不需要这个Ignore了
//@JsonIgnoreProperties(value = {"hibernateLazyInitializer"})
public class BaseEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(updatable = false)
@CreationTimestamp
private Date createTime;
@UpdateTimestamp
private Date updateTime;
}

@ -0,0 +1,29 @@
package geektime.spring.springbucks.waiter.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.hibernate.annotations.Type;
import org.joda.money.Money;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "T_COFFEE")
@Builder
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class Coffee extends BaseEntity implements Serializable {
private String name;
@Type(type = "org.jadira.usertype.moneyandcurrency.joda.PersistentMoneyMinorAmount",
parameters = {@org.hibernate.annotations.Parameter(name = "currencyCode", value = "CNY")})
private Money price;
}

@ -0,0 +1,45 @@
package geektime.spring.springbucks.waiter.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.hibernate.annotations.Type;
import org.joda.money.Money;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.List;
@Entity
@Table(name = "T_ORDER")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CoffeeOrder extends BaseEntity implements Serializable {
private String customer;
@ManyToMany
@JoinTable(name = "T_ORDER_COFFEE")
@OrderBy("id")
private List<Coffee> items;
@Enumerated
@Column(nullable = false)
private OrderState state;
private Integer discount;
@Type(type = "org.jadira.usertype.moneyandcurrency.joda.PersistentMoneyMinorAmount",
parameters = {@org.hibernate.annotations.Parameter(name = "currencyCode", value = "CNY")})
private Money total;
private String waiter;
private String barista;
}

@ -0,0 +1,5 @@
package geektime.spring.springbucks.waiter.model;
public enum OrderState {
INIT, PAID, BREWING, BREWED, TAKEN, CANCELLED
}

@ -0,0 +1,7 @@
package geektime.spring.springbucks.waiter.repository;
import geektime.spring.springbucks.waiter.model.CoffeeOrder;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CoffeeOrderRepository extends JpaRepository<CoffeeOrder, Long> {
}

@ -0,0 +1,11 @@
package geektime.spring.springbucks.waiter.repository;
import geektime.spring.springbucks.waiter.model.Coffee;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CoffeeRepository extends JpaRepository<Coffee, Long> {
List<Coffee> findByNameInOrderById(List<String> list);
Coffee findByName(String name);
}

@ -0,0 +1,92 @@
package geektime.spring.springbucks.waiter.service;
import geektime.spring.springbucks.waiter.integration.Barista;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.model.CoffeeOrder;
import geektime.spring.springbucks.waiter.model.OrderState;
import geektime.spring.springbucks.waiter.repository.CoffeeOrderRepository;
import geektime.spring.springbucks.waiter.support.OrderProperties;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.MeterBinder;
import lombok.extern.slf4j.Slf4j;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@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) {
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());
}
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);
}
}

@ -0,0 +1,51 @@
package geektime.spring.springbucks.waiter.service;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.repository.CoffeeRepository;
import lombok.extern.slf4j.Slf4j;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
@Slf4j
@CacheConfig(cacheNames = "CoffeeCache")
public class CoffeeService {
@Autowired
private CoffeeRepository coffeeRepository;
public Coffee saveCoffee(String name, Money price) {
return coffeeRepository.save(Coffee.builder().name(name).price(price).build());
}
@Cacheable
public List<Coffee> getAllCoffee() {
return coffeeRepository.findAll(Sort.by("id"));
}
public Coffee getCoffee(Long id) {
// return coffeeRepository.findById(id).get();
return coffeeRepository.getOne(id);
}
public long getCoffeeCount() {
return coffeeRepository.count();
}
public Coffee getCoffee(String name) {
return coffeeRepository.findByName(name);
}
public List<Coffee> getCoffeeByName(List<String> names) {
return coffeeRepository.findByNameInOrderById(names);
}
}

@ -0,0 +1,31 @@
package geektime.spring.springbucks.waiter.support;
import geektime.spring.springbucks.waiter.service.CoffeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class CoffeeIndicator implements HealthIndicator {
@Autowired
private CoffeeService coffeeService;
@Override
public Health health() {
long count = coffeeService.getCoffeeCount();
Health health;
if (count > 0) {
health = Health.up()
.withDetail("count", count)
.withDetail("message", "We have enough coffee.")
.build();
} else {
health = Health.down()
.withDetail("count", 0)
.withDetail("message", "We are out of coffee.")
.build();
}
return health;
}
}

@ -0,0 +1,23 @@
package geektime.spring.springbucks.waiter.support;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneyDeserializer extends StdDeserializer<Money> {
protected MoneyDeserializer() {
super(Money.class);
}
@Override
public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
}
}

@ -0,0 +1,42 @@
package geektime.spring.springbucks.waiter.support;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.format.Formatter;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.util.Locale;
@Component
public class MoneyFormatter implements Formatter<Money> {
/**
* CNY 10.00 / 10.00
*
*/
@Override
public Money parse(String text, Locale locale) throws ParseException {
if (NumberUtils.isParsable(text)) {
return Money.of(CurrencyUnit.of("CNY"), NumberUtils.createBigDecimal(text));
} else if (StringUtils.isNotEmpty(text)) {
String[] split = StringUtils.split(text, " ");
if (split != null && split.length == 2 && NumberUtils.isParsable(split[1])) {
return Money.of(CurrencyUnit.of(split[0]),
NumberUtils.createBigDecimal(split[1]));
} else {
throw new ParseException(text, 0);
}
}
throw new ParseException(text, 0);
}
@Override
public String print(Money money, Locale locale) {
if (money == null) {
return null;
}
return money.getCurrencyUnit().getCode() + " " + money.getAmount();
}
}

@ -0,0 +1,21 @@
package geektime.spring.springbucks.waiter.support;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneySerializer extends StdSerializer<Money> {
protected MoneySerializer() {
super(Money.class);
}
@Override
public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumber(money.getAmount());
}
}

@ -0,0 +1,15 @@
package geektime.spring.springbucks.waiter.support;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@ConfigurationProperties("order")
@RefreshScope
@Data
@Component
public class OrderProperties {
private Integer discount = 100;
private String waiterPrefix = "springbucks-";
}

@ -0,0 +1,37 @@
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=springbucks
spring.datasource.password=springbucks
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
spring.cloud.stream.kafka.binder.brokers=localhost
spring.cloud.stream.kafka.binder.defaultBrokerPort=9092
spring.cloud.stream.bindings.finishedOrders.group=waiter-service

@ -0,0 +1,8 @@
spring.application.name=waiter-service
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.prefer-ip-address=true
spring.cloud.consul.config.enabled=true
spring.cloud.consul.config.format=yaml

@ -0,0 +1,5 @@
insert into t_coffee (name, price, create_time, update_time) values ('espresso', 2000, now(), now());
insert into t_coffee (name, price, create_time, update_time) values ('latte', 2500, now(), now());
insert into t_coffee (name, price, create_time, update_time) values ('capuccino', 2500, now(), now());
insert into t_coffee (name, price, create_time, update_time) values ('mocha', 3000, now(), now());
insert into t_coffee (name, price, create_time, update_time) values ('macchiato', 3000, now(), now());

@ -0,0 +1,33 @@
# 注意 MySQL 与 H2 的语法差异
# H2: drop table tbl if exists;
# MySQL: drop table if exists tbl;
drop table if exists t_coffee;
drop table if exists t_order;
drop table if exists t_order_coffee;
create table t_coffee (
id bigint auto_increment,
create_time timestamp,
update_time timestamp,
name varchar(255),
price bigint,
primary key (id)
);
create table t_order (
id bigint auto_increment,
create_time timestamp,
update_time timestamp,
customer varchar(255),
waiter varchar(255),
barista varchar(255),
discount integer,
total bigint,
state integer,
primary key (id)
);
create table t_order_coffee (
coffee_order_id bigint not null,
items_id bigint not null
);

@ -0,0 +1,16 @@
package geektime.spring.springbucks.waiter;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class WaiterServiceApplicationTests {
@Test
public void contextLoads() {
}
}

@ -0,0 +1,26 @@
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring.springbucks</groupId>
<artifactId>customer-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>customer-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>0.14.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,41 @@
package geektime.spring.springbucks.customer;
import geektime.spring.springbucks.customer.integration.Waiter;
import geektime.spring.springbucks.customer.support.CustomConnectionKeepAliveStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import java.util.concurrent.TimeUnit;
@SpringBootApplication
@Slf4j
@EnableDiscoveryClient
@EnableFeignClients
@EnableAspectJAutoProxy
@EnableBinding(Waiter.class)
public class CustomerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServiceApplication.class, args);
}
@Bean
public CloseableHttpClient httpClient() {
return HttpClients.custom()
.setConnectionTimeToLive(30, TimeUnit.SECONDS)
.evictIdleConnections(30, TimeUnit.SECONDS)
.setMaxConnTotal(200)
.setMaxConnPerRoute(20)
.disableAutomaticRetries()
.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
.build();
}
}

@ -0,0 +1,75 @@
package geektime.spring.springbucks.customer.controller;
import geektime.spring.springbucks.customer.integration.CoffeeOrderService;
import geektime.spring.springbucks.customer.integration.CoffeeService;
import geektime.spring.springbucks.customer.model.Coffee;
import geektime.spring.springbucks.customer.model.CoffeeOrder;
import geektime.spring.springbucks.customer.model.NewOrderRequest;
import geektime.spring.springbucks.customer.model.OrderState;
import geektime.spring.springbucks.customer.model.OrderStateRequest;
import io.github.resilience4j.bulkhead.Bulkhead;
import io.github.resilience4j.bulkhead.BulkheadFullException;
import io.github.resilience4j.bulkhead.BulkheadRegistry;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerOpenException;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.vavr.control.Try;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@RestController
@RequestMapping("/customer")
@Slf4j
public class CustomerController {
@Autowired
private CoffeeService coffeeService;
@Autowired
private CoffeeOrderService coffeeOrderService;
@Value("${customer.name}")
private String customer;
private CircuitBreaker circuitBreaker;
private Bulkhead bulkhead;
public CustomerController(CircuitBreakerRegistry circuitBreakerRegistry,
BulkheadRegistry bulkheadRegistry) {
circuitBreaker = circuitBreakerRegistry.circuitBreaker("menu");
bulkhead = bulkheadRegistry.bulkhead("menu");
}
@GetMapping("/menu")
public List<Coffee> readMenu() {
return Try.ofSupplier(
Bulkhead.decorateSupplier(bulkhead,
CircuitBreaker.decorateSupplier(circuitBreaker,
() -> coffeeService.getAll())))
.recover(CircuitBreakerOpenException.class, Collections.emptyList())
.recover(BulkheadFullException.class, Collections.emptyList())
.get();
}
@PostMapping("/order")
@io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker(name = "order")
@io.github.resilience4j.bulkhead.annotation.Bulkhead(name = "order")
public CoffeeOrder createAndPayOrder() {
NewOrderRequest orderRequest = NewOrderRequest.builder()
.customer(customer)
.items(Arrays.asList("capuccino"))
.build();
CoffeeOrder order = coffeeOrderService.create(orderRequest);
log.info("Create order: {}", order != null ? order.getId() : "-");
order = coffeeOrderService.updateState(order.getId(),
OrderStateRequest.builder().state(OrderState.PAID).build());
log.info("Order is PAID: {}", order);
return order;
}
}

@ -0,0 +1,26 @@
package geektime.spring.springbucks.customer.integration;
import geektime.spring.springbucks.customer.model.CoffeeOrder;
import geektime.spring.springbucks.customer.model.NewOrderRequest;
import geektime.spring.springbucks.customer.model.OrderStateRequest;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(name = "waiter-service", contextId = "coffeeOrder")
public interface CoffeeOrderService {
@GetMapping("/order/{id}")
CoffeeOrder getOrder(@PathVariable("id") Long id);
@PostMapping(path = "/order/", consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
CoffeeOrder create(@RequestBody NewOrderRequest newOrder);
@PutMapping("/order/{id}")
CoffeeOrder updateState(@PathVariable("id") Long id,
@RequestBody OrderStateRequest orderState);
}

@ -0,0 +1,21 @@
package geektime.spring.springbucks.customer.integration;
import geektime.spring.springbucks.customer.model.Coffee;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient(name = "waiter-service", contextId = "coffee", path = "/coffee")
public interface CoffeeService {
@GetMapping(path = "/", params = "!name")
List<Coffee> getAll();
@GetMapping("/{id}")
Coffee getById(@PathVariable Long id);
@GetMapping(path = "/", params = "name")
Coffee getByName(@RequestParam String name);
}

@ -0,0 +1,32 @@
package geektime.spring.springbucks.customer.integration;
import geektime.spring.springbucks.customer.model.CoffeeOrder;
import geektime.spring.springbucks.customer.model.OrderState;
import geektime.spring.springbucks.customer.model.OrderStateRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class NotificationListener {
@Autowired
private CoffeeOrderService orderService;
@Value("${customer.name}")
private String customer;
@StreamListener(Waiter.NOTIFY_ORDERS)
public void takeOrder(@Payload Long id) {
CoffeeOrder order = orderService.getOrder(id);
if (OrderState.BREWED == order.getState()) {
log.info("Order {} is READY, I'll take it.", id);
orderService.updateState(id,
OrderStateRequest.builder().state(OrderState.TAKEN).build());
} else {
log.warn("Order {} is NOT READY. Why are you notify me?", id);
}
}
}

@ -0,0 +1,11 @@
package geektime.spring.springbucks.customer.integration;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
public interface Waiter {
String NOTIFY_ORDERS = "notifyOrders";
@Input(NOTIFY_ORDERS)
SubscribableChannel notification();
}

@ -0,0 +1,22 @@
package geektime.spring.springbucks.customer.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.joda.money.Money;
import java.io.Serializable;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Coffee implements Serializable {
private Long id;
private String name;
private Money price;
private Date createTime;
private Date updateTime;
}

@ -0,0 +1,25 @@
package geektime.spring.springbucks.customer.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.joda.money.Money;
import java.util.Date;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CoffeeOrder {
private Long id;
private String customer;
private List<Coffee> items;
private OrderState state;
private String waiter;
private String barista;
private Integer discount;
private Money total;
private Date createTime;
private Date updateTime;
}

@ -0,0 +1,16 @@
package geektime.spring.springbucks.customer.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Builder
@Getter
@Setter
public class NewOrderRequest {
private String customer;
private List<String> items;
}

@ -0,0 +1,5 @@
package geektime.spring.springbucks.customer.model;
public enum OrderState {
INIT, PAID, BREWING, BREWED, TAKEN, CANCELLED
}

@ -0,0 +1,14 @@
package geektime.spring.springbucks.customer.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
@Builder
public class OrderStateRequest {
private OrderState state;
}

@ -0,0 +1,25 @@
package geektime.spring.springbucks.customer.support;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.http.HttpResponse;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import java.util.Arrays;
public class CustomConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
private final long DEFAULT_SECONDS = 30;
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
return Arrays.asList(response.getHeaders(HTTP.CONN_KEEP_ALIVE))
.stream()
.filter(h -> StringUtils.equalsIgnoreCase(h.getName(), "timeout")
&& StringUtils.isNumeric(h.getValue()))
.findFirst()
.map(h -> NumberUtils.toLong(h.getValue(), DEFAULT_SECONDS))
.orElse(DEFAULT_SECONDS) * 1000;
}
}

@ -0,0 +1,23 @@
package geektime.spring.springbucks.customer.support;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneyDeserializer extends StdDeserializer<Money> {
protected MoneyDeserializer() {
super(Money.class);
}
@Override
public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
}
}

@ -0,0 +1,21 @@
package geektime.spring.springbucks.customer.support;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneySerializer extends StdSerializer<Money> {
protected MoneySerializer() {
super(Money.class);
}
@Override
public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumber(money.getAmount());
}
}

@ -0,0 +1,15 @@
package geektime.spring.springbucks.customer.support;
import geektime.spring.springbucks.customer.model.CoffeeOrder;
import lombok.Data;
import org.springframework.context.ApplicationEvent;
@Data
public class OrderWaitingEvent extends ApplicationEvent {
private CoffeeOrder order;
public OrderWaitingEvent(CoffeeOrder order) {
super(order);
this.order = order;
}
}

@ -0,0 +1,40 @@
server.port=0
customer.name=spring-${server.port}
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
spring.output.ansi.enabled=ALWAYS
feign.client.config.default.connect-timeout=500
feign.client.config.default.read-timeout=500
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.prefer-ip-address=true
resilience4j.circuitbreaker.backends.order.failure-rate-threshold=50
resilience4j.circuitbreaker.backends.order.wait-duration-in-open-state=5000
resilience4j.circuitbreaker.backends.order.ring-buffer-size-in-closed-state=5
resilience4j.circuitbreaker.backends.order.ring-buffer-size-in-half-open-state=3
resilience4j.circuitbreaker.backends.order.event-consumer-buffer-size=10
resilience4j.circuitbreaker.backends.menu.failure-rate-threshold=50
resilience4j.circuitbreaker.backends.menu.wait-duration-in-open-state=5000
resilience4j.circuitbreaker.backends.menu.ring-buffer-size-in-closed-state=5
resilience4j.circuitbreaker.backends.menu.ring-buffer-size-in-half-open-state=3
resilience4j.circuitbreaker.backends.menu.event-consumer-buffer-size=10
resilience4j.bulkhead.backends.order.max-concurrent-call=1
resilience4j.bulkhead.backends.order.max-wait-time=5
resilience4j.bulkhead.backends.menu.max-concurrent-call=5
resilience4j.bulkhead.backends.menu.max-wait-time=5
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=spring
spring.rabbitmq.password=spring
spring.cloud.stream.rabbit.bindings.notifyOrders.consumer.binding-routing-key=${customer.name}

@ -0,0 +1,16 @@
package geektime.spring.springbucks.customer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomerServiceApplicationTests {
@Test
public void contextLoads() {
}
}

@ -0,0 +1,29 @@
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring.springbucks</groupId>
<artifactId>barista-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>barista-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,18 @@
package geektime.spring.springbucks.barista;
import geektime.spring.springbucks.barista.integration.Waiter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories
@SpringBootApplication
@EnableBinding(Waiter.class)
public class BaristaServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BaristaServiceApplication.class, args);
}
}

@ -0,0 +1,44 @@
package geektime.spring.springbucks.barista.integration;
import geektime.spring.springbucks.barista.model.CoffeeOrder;
import geektime.spring.springbucks.barista.model.OrderState;
import geektime.spring.springbucks.barista.repository.CoffeeOrderRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
@Slf4j
@Transactional
public class OrderListener {
@Autowired
private CoffeeOrderRepository orderRepository;
@Autowired
@Qualifier(Waiter.FINISHED_ORDERS)
private MessageChannel finishedOrdersMessageChannel;
@Value("${order.barista-prefix}${random.uuid}")
private String barista;
@StreamListener(Waiter.NEW_ORDERS)
public void processNewOrder(Long id) {
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);
orderRepository.save(o);
log.info("Order {} is READY.", id);
finishedOrdersMessageChannel.send(MessageBuilder.withPayload(id).build());
}
}

@ -0,0 +1,17 @@
package geektime.spring.springbucks.barista.integration;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface Waiter {
String NEW_ORDERS = "newOrders";
String FINISHED_ORDERS = "finishedOrders";
@Input(NEW_ORDERS)
SubscribableChannel newOrders();
@Output(FINISHED_ORDERS)
MessageChannel finishedOrders();
}

@ -0,0 +1,38 @@
package geektime.spring.springbucks.barista.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
@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;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save