SpringCloud Alibaba无人售货柜实战(三):基础公共模块搭建——统一返回、异常、工具类、常量
写代码最怕什么?一百个接口一百种返回格式。今天把公共模块搭好,后面七个微服务直接引依赖就行。
一、公共模块的意义
微服务项目最容易出现的问题:每个服务各写各的返回格式、各搞各的异常处理、各造各的工具类。结果就是——改一个返回字段,要翻七个代码仓库。
公共模块common解决三个问题:
- 代码复用:Result、工具类、常量写一次,到处用
- 统一规范:所有接口返回格式一致,前端不用猜
- 减少重复:异常处理、Feign配置等公共逻辑集中管理
二、项目工程结构
采用 Maven 多模块结构,父POM统一管理版本:
vending-machine/ ├── pom.xml # 父POM,管版本 ├── vending-common/ # 公共模块 │ ├── pom.xml │ └── src/main/java/com/heipiao/common/ │ ├── result/ # 统一返回 │ │ ├── Result.java │ │ └── PageResult.java │ ├── exception/ # 异常体系 │ │ ├── BusinessException.java │ │ ├── SystemException.java │ │ └── GlobalExceptionHandler.java │ ├── errorcode/ # 错误码 │ │ ├── ErrorCode.java │ │ ├── SystemErrorCode.java │ │ ├── ProductErrorCode.java │ │ └── OrderErrorCode.java │ ├── constant/ # 常量枚举 │ │ ├── OrderStatus.java │ │ ├── PayStatus.java │ │ └── DeviceStatus.java │ ├── context/ # 请求上下文 │ │ ├── UserContext.java │ │ └── UserContextHolder.java │ ├── utils/ # 工具类 │ │ ├── JwtUtils.java │ │ ├── RedisUtils.java │ │ ├── MqttUtils.java │ │ └── DateUtils.java │ └── feign/ # Feign配置 │ ├── FeignAuthInterceptor.java │ └── FeignConfig.java ├── vending-gateway/ # 网关 ├── vending-device-service/ # 设备服务 ├── vending-product-service/ # 商品服务 ├── vending-order-service/ # 订单服务 └── ...其他服务三、统一返回封装
3.1 Result类
所有接口统一返回Result<T>,前端只需要判断code == 200。
@Data@Schema(description="统一返回结果")publicclassResult<T>implementsSerializable{@Schema(description="状态码")privateIntegercode;@Schema(description="提示信息")privateStringmessage;@Schema(description="数据")privateTdata;@Schema(description="时间戳")privateLongtimestamp;privateResult(){this.timestamp=System.currentTimeMillis();}publicstatic<T>Result<T>success(){returnsuccess(null);}publicstatic<T>Result<T>success(Tdata){Result<T>result=newResult<>();result.setCode(200);result.setMessage("success");result.setData(data);returnresult;}publicstatic<T>Result<T>success(Tdata,Stringmessage){Result<T>result=newResult<>();result.setCode(200);result.setMessage(message);result.setData(data);returnresult;}publicstatic<T>Result<T>fail(Integercode,Stringmessage){Result<T>result=newResult<>();result.setCode(code);result.setMessage(message);returnresult;}publicstatic<T>Result<T>fail(ErrorCodeerrorCode){returnfail(errorCode.getCode(),errorCode.getMessage());}}3.2 PageResult 分页返回
@Data@Schema(description="分页返回结果")publicclassPageResult<T>implementsSerializable{@Schema(description="当前页")privateLongpageNum;@Schema(description="每页条数")privateLongpageSize;@Schema(description="总条数")privateLongtotal;@Schema(description="总页数")privateLongpages;@Schema(description="数据列表")privateList<T>list;publicstatic<T>PageResult<T>of(List<T>list,Longtotal,LongpageNum,LongpageSize){PageResult<T>result=newPageResult<>();result.setList(list);result.setTotal(total);result.setPageNum(pageNum);result.setPageSize(pageSize);result.setPages((total+pageSize-1)/pageSize);returnresult;}}四、统一错误码体系
错误码不是随便编的,要有规律。格式:模块(2位) + 类型(2位) + 序号(3位)。
publicinterfaceErrorCode{IntegergetCode();StringgetMessage();}@Getter@AllArgsConstructorpublicenumSystemErrorCodeimplementsErrorCode{SUCCESS(20000,"操作成功"),UNKNOWN_ERROR(50000,"系统未知异常"),PARAM_INVALID(40001,"参数校验失败"),UNAUTHORIZED(40100,"未登录或token已过期"),FORBIDDEN(40300,"无权限访问"),SERVICE_UNAVAILABLE(50300,"服务暂时不可用"),GATEWAY_TIMEOUT(50400,"网关请求超时");privatefinalIntegercode;privatefinalStringmessage;}@Getter@AllArgsConstructorpublicenumOrderErrorCodeimplementsErrorCode{ORDER_NOT_FOUND(30001,"订单不存在"),ORDER_STATUS_INVALID(30002,"订单状态不允许此操作"),ORDER_ALREADY_PAID(30003,"订单已支付,请勿重复操作"),DEVICE_BUSY(30004,"设备正忙,请稍后再试"),STOCK_NOT_ENOUGH(30005,"库存不足"),PAY_FAILED(30006,"支付失败");privatefinalIntegercode;privatefinalStringmessage;}模块编号:系统=20/50,商品=10,订单=30,支付=40,设备=50,用户=60。一眼看错误码就知道是哪个服务出了问题。
五、全局异常处理
5.1 自定义异常
@GetterpublicclassBusinessExceptionextendsRuntimeException{privatefinalIntegercode;publicBusinessException(ErrorCodeerrorCode){super(errorCode.getMessage());this.code=errorCode.getCode();}publicBusinessException(ErrorCodeerrorCode,Stringmessage){super(message);this.code=errorCode.getCode();}}@GetterpublicclassSystemExceptionextendsRuntimeException{privatefinalIntegercode;publicSystemException(ErrorCodeerrorCode){super(errorCode.getMessage());this.code=errorCode.getCode();}}区分原则:
BusinessException是用户操作导致的(比如库存不足),SystemException是系统内部错误(比如数据库连接失败)。
5.2 全局异常处理器
@RestControllerAdvice@Slf4jpublicclassGlobalExceptionHandler{@ExceptionHandler(BusinessException.class)publicResult<Void>handleBusiness(BusinessExceptione){log.warn("业务异常: code={}, msg={}",e.getCode(),e.getMessage());returnResult.fail(e.getCode(),e.getMessage());}@ExceptionHandler(SystemException.class)publicResult<Void>handleSystem(SystemExceptione){log.error("系统异常: code={}, msg={}",e.getCode(),e.getMessage());returnResult.fail(e.getCode(),e.getMessage());}@ExceptionHandler(MethodArgumentNotValidException.class)publicResult<Void>handleValidation(MethodArgumentNotValidExceptione){Stringmsg=e.getBindingResult().getFieldErrors().stream().map(error->error.getField()+": "+error.getDefaultMessage()).collect(Collectors.joining("; "));returnResult.fail(SystemErrorCode.PARAM_INVALID.getCode(),msg);}@ExceptionHandler(Exception.class)publicResult<Void>handleUnknown(Exceptione){log.error("未知异常",e);returnResult.fail(SystemErrorCode.UNKNOWN_ERROR);}}六、常量枚举
@Getter@AllArgsConstructorpublicenumOrderStatus{WAITING_OPEN(0,"待开柜"),OPENED(1,"已开柜"),WAITING_SETTLE(2,"已关门待结算"),PAID(3,"已支付"),CANCELLED(4,"已取消"),PAY_FAILED(5,"支付失败"),REFUNDED(6,"已退款");privatefinalIntegercode;privatefinalStringdesc;publicstaticOrderStatusof(Integercode){for(OrderStatuss:values()){if(s.code.equals(code)){returns;}}thrownewBusinessException(SystemErrorCode.PARAM_INVALID,"无效的订单状态: "+code);}}@Getter@AllArgsConstructorpublicenumDeviceStatus{OFFLINE(0,"离线"),ONLINE(1,"在线"),FAULT(2,"故障"),MAINTENANCE(3,"维护中");}七、请求上下文 UserContext
网关解析JWT后,把用户信息通过HTTP Header透传到下游服务。下游服务用拦截器取出,存入ThreadLocal。
@DatapublicclassUserContext{privateLonguserId;privateStringopenid;privateStringnickname;privateStringrole;privatestaticfinalThreadLocal<UserContext>HOLDER=newThreadLocal<>();publicstaticvoidset(UserContextcontext){HOLDER.set(context);}publicstaticUserContextget(){returnHOLDER.get();}publicstaticLonggetUserId(){UserContextctx=HOLDER.get();returnctx!=null?ctx.getUserId():null;}publicstaticvoidclear(){HOLDER.remove();}}配合拦截器使用:
@Component@Slf4jpublicclassUserContextInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){StringuserId=request.getHeader("X-User-Id");Stringopenid=request.getHeader("X-User-Openid");Stringrole=request.getHeader("X-User-Role");if(StrUtil.isNotBlank(userId)){UserContextcontext=newUserContext();context.setUserId(Long.valueOf(userId));context.setOpenid(openid);context.setRole(role);UserContext.set(context);}returntrue;}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex){UserContext.clear();}}八、Feign公共配置
微服务间Feign调用时,需要把当前用户信息透传下去,否则下游服务拿不到用户ID。
@ComponentpublicclassFeignAuthInterceptorimplementsRequestInterceptor{@Overridepublicvoidapply(RequestTemplatetemplate){UserContextcontext=UserContext.get();if(context!=null){if(context.getUserId()!=null){template.header("X-User-Id",String.valueOf(context.getUserId()));}if(StrUtil.isNotBlank(context.getOpenid())){template.header("X-User-Openid",context.getOpenid());}if(StrUtil.isNotBlank(context.getRole())){template.header("X-User-Role",context.getRole());}}}}这段代码解决了微服务调用链上的用户身份传递问题。A服务调B服务时,Feign拦截器自动把用户Header带过去,B服务的拦截器再解析存入ThreadLocal。
九、父POM依赖管理
父POM用dependencyManagement统一版本,子模块引依赖不写version:
<properties><java.version>17</java.version><spring-boot.version>3.2.0</spring-boot.version><spring-cloud.version>2023.0.0</spring-cloud.version><spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version><mybatis-plus.version>3.5.5</mybatis-plus.version><hutool.version>5.8.25</hutool.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${spring-cloud-alibaba.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>十、小结
公共模块看着不起眼,但它是整个项目的"基础设施"。Result统一了返回格式,ErrorCode统一了错误体系,GlobalExceptionHandler统一了异常处理,UserContext+ Feign拦截器打通了用户身份传递。这些搭好了,后面写业务代码就像搭积木一样——引依赖、写Controller、调Service,干净利落。