Fegin介绍
Fegin是一个非常好用的HTTP客户端
Feign很大程度上简化了HTTP调用方式
Fegin能做什么
Feign包含了多种HTTP的调用形式
(1 、Spring MVC: @RequestMapping @RequestParam @Pathvariable, @RequestHeader, @RequestBody)
Feign可以整合Ribbon和Hystrix
Feign提供了多种HTTP底层支持(底层支持Apache HttpClient,OKHttp,RestTemplate等)
Feign特性
Feign实现了可插拔注解支持,包括Feign和JAX-RS注解
Feign支持可插拔的HTTP编码器和解码器
Feign支持HTTP请求和响应的压缩
传统服务间的调用方式: 比如服务A调用服务B,服务A要跟服务B建立网络连接,然后构造一个复杂的请求,最后对返回的响应结果再写一大堆代码来处理。
Feign为我们提供了优雅的解决方案: 用注解定义一个FeignClient接口。没有底层建立连接,构造请求,解析响应的代码。Fegin底层会根据注解,和你指定的服务建立连接,构造请求,获取响应,解析响应等等。
Feign原理: Fegin的一个关键机制是使用了动态代理,如下图: 参考 https://blog.csdn.net/Anbang713/article/details/85370080
首先,如果你对某个接口定义了 @FeignClient注解,Fegin就会针对这个接口创建一个动态代理
接着你要调用哪个接口,本质就是调用Fegi创建的动态代理,这是核心的核心
Fegin的动态代理会根据你在接口上的 @RequestMmapping等注解,来动态构造你要请求的服务地址
最后针对这个地址,发起请求、解析响应
Feign实践
通过Order工程调用Product工程的接口
一、Feign实现应用间的通信
声明式REST客户端(伪RPC),采用基于接口的注解。本质上是Http客户端,Http远程调用。
1、 在Order工程中的pom文件增加
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
2、增加注解@EnableFeignClients
3、声明要调用的接口
package com.example.demo.client; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; /** * 需要在Product服务中要调的接口 */ @FeignClient(name = "product") //product代表访问product应用下的msg接口 public interface ProductClient { @GetMapping("/msg") // String productMsg(); }
4、在Order应用调用
二、Order查询调用Product来查询商品信息
第一步:Product工程
1、增加 List<ProductInfo> findByProductIdIn(List<String> productIdList);方法
package com.example.product.repository; import com.example.product.dataobject.ProductInfo; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface ProductInfoRepository extends JpaRepository<ProductInfo, String> { //查询所有在架的商品 List<ProductInfo> findByProductStatus(Integer productStatus); List<ProductInfo> findByProductIdIn(List<String> productIdList); }
2、Server层
@Service public class ProductServiceImpl implements ProductService{ @Autowired private ProductInfoRepository productInfoRepository; /** * 查询商品列表 * * @param productIdList */ @Override public List<ProductInfo> findList(List<String> productIdList) { return productInfoRepository.findByProductIdIn(productIdList); } }
接口
public interface ProductService { /** * 查询商品列表 * @param productIdList */ List<ProductInfo> findList(List<String> productIdList); }
3、Controller层
@RestController @RequestMapping("/product") @Slf4j public class ProductController { @Autowired private ProductService productService; /** * 获取商品列表(给订单服务用) * @param productIdList * @return */ @PostMapping("/listForOrder") public List<ProductInfo> listForOrder(@RequestBody List<String> productIdList){ return productService.findList(productIdList); } }
第二步、Order工程
1.在接口中定义product中的方法
/** * 需要在Product服务中要调的接口 */ @FeignClient(name = "product") //product代表访问product应用下的msg接口 public interface ProductClient { @PostMapping("/product/listForOrder") List<ProductInfo> listForOrder(@RequestBody List<String> productIdList); }
2、Controller层调用上一步定义的方法。
@RestController @Slf4j public class ClientController { @Autowired private ProductClient productClient; @GetMapping("/getProductList") public String getProductList(){ List<ProductInfo> productInfoList = productClient.listForOrder(Arrays.asList("164103465734242707")); log.info("response={}",productInfoList); return "ok"; } }
三、扣库存的实现
第一步:Product工程
1、创建ResultEnum 信息提示
@Getter public enum ResultEnum { PRODUCT_NOT_EXIST(1,"商品不存在"), PRODUCT_STOCK_ERROR(2,"库存有误"), ; private Integer code; private String message; ResultEnum(Integer code, String message){ this.code = code; this.message = message; } }
2、创建购物车类CartDTO
/** * 购物车 */ @Data public class CartDTO { /** * 商品id */ private String productId; /** * 商品数量 */ private Integer productQuantity; public CartDTO() { } public CartDTO(String productId, Integer productQuantity) { this.productId = productId; this.productQuantity = productQuantity; } }
3、异常类
public class ProductException extends RuntimeException { private Integer code; public ProductException(Integer code, String message){ super(message); this.code = code; } public ProductException(ResultEnum resultEnum){ super(resultEnum.getMessage()); this.code = resultEnum.getCode(); } }
4、Service层扣库存
/** * 扣库存 * * @param cartDTOList */ @Override @Transactional //由于扣库存是list操作,所以需要事务操作 public void decreaseStock(List<CartDTO> cartDTOList) { for(CartDTO cartDTO : cartDTOList){ Optional<ProductInfo> productInfoOptional = productInfoRepository.findById(cartDTO.getProductId()); //判断商品是否存在 if(!productInfoOptional.isPresent()){ throw new ProductException(ResultEnum.PRODUCT_NOT_EXIST); } ProductInfo productInfo = productInfoOptional.get(); //库存是否足够 数据库里的库存-购物车中的数量 Integer result = productInfo.getProductStock() - cartDTO.getProductQuantity(); if(result <= 0){ throw new ProductException(ResultEnum.PRODUCT_STOCK_ERROR); } productInfo.setProductStock(result); productInfoRepository.save(productInfo); } }
5、Controller层
@PostMapping("/decreaseStock") public void decreaseStock(@RequestBody List<CartDTO> cartDTOList){ productService.decreaseStock(cartDTOList); }
第二步 Order工程
1、增加方法定义
/** * 需要在Product服务中要调的接口 */ @FeignClient(name = "product") //product代表访问product应用下的msg接口 public interface ProductClient { @PostMapping("/product/decreaseStock") void decreaseStock(@RequestBody List<CartDTO> cartDTOList); }
2、增加测试Controller
@GetMapping("/productDecreaseStock") public String productDecreaseStock(){ productClient.decreaseStock(Arrays.asList(new CartDTO("164103465734242707",3))); return "ok"; }
3、测试调用
然后查看数据库是否扣库存成功。
四、完善下单接口
1、服务层代码
@Autowired private ProductClient productClient; /** * 创建订单 * * @param orderDTO * @return */ @Override public OrderDTO create(OrderDTO orderDTO) { String orderId= KeyUtil.genUniqueKey(); // 查询商品信息(调用商品服务) List<String> productIdList = orderDTO.getOrderDetailList().stream() .map(OrderDetail::getProductId) .collect(Collectors.toList()); List<ProductInfo> productInfoList = productClient.listForOrder(productIdList); Date date = new Date(); //计算总价 BigDecimal orderAmount = new BigDecimal(0); for(OrderDetail orderDetail: orderDTO.getOrderDetailList()){ for(ProductInfo productInfo : productInfoList){ if (productInfo.getProductId().equals(orderDetail.getProductId())){ //单价 * 数量 orderAmount = productInfo.getProductPrice() .multiply( new BigDecimal(orderDetail.getProductQuantity())) .add(orderAmount); //这种方式copy,值为null也会copy过去 BeanUtils.copyProperties(productInfo, orderDetail); orderDetail.setOrderId(orderId); orderDetail.setDetailId(KeyUtil.genUniqueKey()); orderDetail.setCreateTime(date); orderDetail.setUpdateTime(date); //订单详情入口 orderDetailRepository.save(orderDetail); } } } //扣库存(调用商品服务) List<CartDTO> cartDTOList = orderDTO.getOrderDetailList().stream() .map(e -> new CartDTO(e.getProductId(), e.getProductQuantity())) .collect(Collectors.toList()); productClient.decreaseStock(cartDTOList); // 5、订单入口 OrderMaster orderMaster = new OrderMaster(); orderDTO.setOrderId(orderId); BeanUtils.copyProperties(orderDTO, orderMaster); orderMaster.setOrderAmount(orderAmount); orderMaster.setOrderStatus(OrderStatusEnum.New.getCode()); orderMaster.setPayStatus(PayStatusEnum.WAIT.getCode()); orderMaster.setCreateTime(date); orderMaster.setUpdateTime(date); orderMasterResponsibility.save(orderMaster); return orderDTO; }
2、Controller层代码
@RequestMapping("/create") public ResultVO<Map<String, String>> create(@Valid OrderForm orderForm, BindingResult bindingResult){ if(bindingResult.hasErrors()){ log.error("【创建订单】参数不正确, orderForm={}", orderForm); throw new OrderException(ResultEnum.PARAM_ERROR.getCode(), bindingResult.getFieldError().getDefaultMessage()); } // orderForm -> orderDTO OrderDTO orderDTO = OrderForm2OrderDTOConverter.convert(orderForm); if (CollectionUtils.isEmpty(orderDTO.getOrderDetailList())) { log.error("【创建订单】购物车信息为空"); throw new OrderException(ResultEnum.CART_EMPTY); } OrderDTO result = orderService.create(orderDTO); Map<String, String> map = new HashMap<>(); map.put("orderId", result.getOrderId()); return ResultVOUtil.success(map); }
3、验证下单接口,使用postman工具
Feign实践缺点:
里面介绍到Order工程调用Product工程里的方法,定义了FeignClient的/product/listForOrder方法,如果还有一个工程要调用/product/listForOrder这个方法,又要重复定义一次。
/
解决方法:
可以把Feign的接口定义统一到一个工程中,其它接口如果想调用,继承这个接口即可。
举例:
定义了一个Backend_API工程
然后在要使用这个接口的地方继承该接口