跬步1 Parking Code Review Demo
程序员有三大美德
- 懒惰(Laziness)
- 急躁(Impatience)
- 傲慢(Hubris)
quoted from Larry Wall
20190131 Parking team进行了code review
记录一下自己的一些感想,欢迎拍砖
1 Builder
1.1 Code
/**
* 构建ETCPBaseRequestVo
* @param object
* @return ETCPBaseRequestVo
* @throws JsonProcessingException
*/
public ETCPBaseRequestVo buildETCPBaseRequestVo(Object object) throws JsonProcessingException{
String result = JsonParseUtil.objectToJSON(object);
String timestamp = DateUtil.getYYYYMMddHHmmss();
String singedData = DataEncryptionUtil.getSignedData(result, timestamp, merchantConfig.getKey());
ETCPBaseRequestVo etcpBaseRequestVo = new ETCPBaseRequestVo();
etcpBaseRequestVo.setMerchant_no(merchantConfig.getNumber());
etcpBaseRequestVo.setTime_stamp(timestamp);
etcpBaseRequestVo.setSign(singedData);
etcpBaseRequestVo.setData(result);
return etcpBaseRequestVo;
}
1.2 What’s the point
当我们构造一个类的实例的时候,拢共分几步?
三步,和把大象装冰箱一样哎!
1调用构造函数(打开冰箱门)
ETCPBaseRequestVo etcpBaseRequestVo = new ETCPBaseRequestVo();
2一个个set参数(把大象放进去)
etcpBaseRequestVo.setTime_stamp(timestamp);
etcpBaseRequestVo.setSign(singedData);
etcpBaseRequestVo.setData(result);
3return(把冰箱门带上)
return etcpBaseRequestVo;
懒惰的我发现第二步,我要写好多遍实例名etcpBaseRequestVo,好烦!
怎么能少写点code?
低糖的Java加糖用Lombok.
1.3 Demo
return etcpBaseRequestVo.Builder()
.time_stamp(timestamp)
.sign(singedData);
.data(result)
.build();
POJO类加上@Builder注解就可以如上使用了
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Builder
public class ETCPBaseRequestVo implements Serializable {
/**
* 商户号
*/
private String merchant_no;
/**
* 时间戳
*/
private String time_stamp;
/**
* 签名
*/
private String sign;
/**
* 业务数据参数
*/
private String data;
}
2 If and Else
2.1 Code
/**
* 请求 ETCP 获取Token
* @param mobilePhoneNo
* @return String
* @throws JsonProcessingException
*/
public String requestUsersigin(String mobilePhoneNo) throws JsonProcessingException{
String token = "";
//联合登录获取Token Vo
UsersiginRequestVo usersiginRequestVo = new UsersiginRequestVo();
usersiginRequestVo.setAppId(merchantConfig.getAppId());
usersiginRequestVo.setMobilePhone(mobilePhoneNo);
//构建ETCPBaseRequestVo
ETCPBaseRequestVo usersiginBaseRequestVo = buildETCPBaseRequestVo(usersiginRequestVo);
//call ETCP API[usersigin]
UsersiginResponseVo usersiginResponseVo = etcpFeignClient.userSignIn(usersiginBaseRequestVo);
if(null != usersiginResponseVo && null != usersiginResponseVo.getCode()){
if(usersiginResponseVo.getCode().equals(ETCPBusinessCodeEnum.SUCCESS.getCode())){
token = usersiginResponseVo.getData().getToken();
}else{
log.error(usersiginResponseVo.getCode() + ":" + usersiginResponseVo.getMessage());
}
}else{
log.error("call ETCP API[usersigin] response is null");
}
return token;
}
2.2 What’s the point
当我看到一堆if嵌套引起的缩进的时候,我是懵逼的(不知道游标卡尺master python程序员怎么想).
其实这个还好,只有两层,不过当分支block过大或嵌套更深时,仍然非常可怕
因为缩进某种意义是个栈结构,我理解大多数人的栈深度并不大
我承认我脑子转的不够快,但能不能写一些对我这样的迟钝者友好些的代码呢?
答案是可以,提前return就好了
如果有一个if/else,处理逻辑一个长一个短,把短的提前判断出来,然后return
2.3 Demo
/**
* 请求 ETCP 获取Token
* @param mobilePhoneNo
* @return String
* @throws JsonProcessingException
*/
public String requestUsersigin(String mobilePhoneNo) throws JsonProcessingException{
String token = "";
//联合登录获取Token Vo
UsersiginRequestVo usersiginRequestVo = new UsersiginRequestVo();
usersiginRequestVo.setAppId(merchantConfig.getAppId());
usersiginRequestVo.setMobilePhone(mobilePhoneNo);
//构建ETCPBaseRequestVo
ETCPBaseRequestVo usersiginBaseRequestVo = buildETCPBaseRequestVo(usersiginRequestVo);
//call ETCP API[usersigin]
UsersiginResponseVo usersiginResponseVo = etcpFeignClient.userSignIn(usersiginBaseRequestVo);
if(Objects.isNull(usersiginResponseVo) || Objects.isNull(usersiginResponseVo.getCode())) {
log.error("call ETCP API[usersigin] response is null");
return token;
}
if(usersiginResponseVo.getCode().equals(ETCPBusinessCodeEnum.SUCCESS.getCode())){
return usersiginResponseVo.getData().getToken();
}
log.error(usersiginResponseVo.getCode() + ":" + usersiginResponseVo.getMessage());
return token;
}
3 Equals
3.1 Code
/**
* 请求 ETCP 获取Token
* @param mobilePhoneNo
* @return String
* @throws JsonProcessingException
*/
public String requestUsersigin(String mobilePhoneNo) throws JsonProcessingException{
String token = "";
//联合登录获取Token Vo
UsersiginRequestVo usersiginRequestVo = new UsersiginRequestVo();
usersiginRequestVo.setAppId(merchantConfig.getAppId());
usersiginRequestVo.setMobilePhone(mobilePhoneNo);
//构建ETCPBaseRequestVo
ETCPBaseRequestVo usersiginBaseRequestVo = buildETCPBaseRequestVo(usersiginRequestVo);
//call ETCP API[usersigin]
UsersiginResponseVo usersiginResponseVo = etcpFeignClient.userSignIn(usersiginBaseRequestVo);
if(null != usersiginResponseVo && null != usersiginResponseVo.getCode()){
if(usersiginResponseVo.getCode().equals(ETCPBusinessCodeEnum.SUCCESS.getCode())){
token = usersiginResponseVo.getData().getToken();
}else{
log.error(usersiginResponseVo.getCode() + ":" + usersiginResponseVo.getMessage());
}
}else{
log.error("call ETCP API[usersigin] response is null");
}
return token;
}
3.2 What’s the point
if(usersiginResponseVo.getCode().equals(ETCPBusinessCodeEnum.SUCCESS.getCode()))
这段equals是可能抛空指针的
如果你装了alibaba的编码规约插件,你可能会看到warning,让你把非空变量作为equals函数调用方
另一种办法是使用java built-in的Objects.equals()
3.3 Demo
if(Objects.equals(usersiginResponseVo.getCode(), ETCPBusinessCodeEnum.SUCCESS.getCode())
4 Let it crash
4.1 Code
/**
* 请求 ETCP 获取Token
* @param mobilePhoneNo
* @return String
* @throws JsonProcessingException
*/
public String requestUsersigin(String mobilePhoneNo) throws JsonProcessingException{
String token = "";
//联合登录获取Token Vo
UsersiginRequestVo usersiginRequestVo = new UsersiginRequestVo();
usersiginRequestVo.setAppId(merchantConfig.getAppId());
usersiginRequestVo.setMobilePhone(mobilePhoneNo);
//构建ETCPBaseRequestVo
ETCPBaseRequestVo usersiginBaseRequestVo = buildETCPBaseRequestVo(usersiginRequestVo);
//call ETCP API[usersigin]
UsersiginResponseVo usersiginResponseVo = etcpFeignClient.userSignIn(usersiginBaseRequestVo);
if(null != usersiginResponseVo && null != usersiginResponseVo.getCode()){
if(usersiginResponseVo.getCode().equals(ETCPBusinessCodeEnum.SUCCESS.getCode())){
token = usersiginResponseVo.getData().getToken();
}else{
log.error(usersiginResponseVo.getCode() + ":" + usersiginResponseVo.getMessage());
}
}else{
log.error("call ETCP API[usersigin] response is null");
}
return token;
}
4.2 What’s the point
这段在遇到异常的情况下返回是空字符串””
但是如果token是空的情况,业务肯定失败,这时应该直接抛出Exception
有问题请直接失败,这就是失败策略之fail-fast
4.3 Demo
public String requestUsersigin(String mobilePhoneNo) throws JsonProcessingException{
String token;
//联合登录获取Token Vo
UsersiginRequestVo usersiginRequestVo = new UsersiginRequestVo();
usersiginRequestVo.setAppId(merchantConfig.getAppId());
usersiginRequestVo.setMobilePhone(mobilePhoneNo);
//构建ETCPBaseRequestVo
ETCPBaseRequestVo usersiginBaseRequestVo = buildETCPBaseRequestVo(usersiginRequestVo);
//call ETCP API[usersigin]
UsersiginResponseVo usersiginResponseVo = etcpFeignClient.userSignIn(usersiginBaseRequestVo);
if(Objects.isNull(usersiginResponseVo) || Objects.isNull(usersiginResponseVo.getCode())) {
throw new RuntimeException("call ETCP API[usersigin] response is null");
}
if(usersiginResponseVo.getCode().equals(ETCPBusinessCodeEnum.SUCCESS.getCode())){
return usersiginResponseVo.getData().getToken();
}
throw new RuntimeException(usersiginResponseVo.getCode() + ":" + usersiginResponseVo.getMessage());
}
5 Expire token
5.1 Code
/**
* 绑定车牌
* @param vehiclePlateInfoRequestVo
* @return
*/
public ServiceResponseVo bindVehiclePlate(VehiclePlateInfoRequestVo vehiclePlateInfoRequestVo) throws JsonProcessingException {
//ServiceResponseVo serviceResponseVo = null;
//获取手机号
String mobilePhoneNo = getMobilePhoneNo(vehiclePlateInfoRequestVo.getUserId());
//获取用户Id
String userId = vehiclePlateInfoRequestVo.getUserId();
//获取token
String token = getToken(mobilePhoneNo, userId);
//绑定车牌请求Vo
BindcarRequestVo bindcarRequestVo = new BindcarRequestVo();
bindcarRequestVo.setToken(token);
bindcarRequestVo.setPlateNumber(vehiclePlateInfoRequestVo.getPlateNo());
//call ETCP API[bindcar]
BindcarResponseVo bindcarResponseVo = requestBindVehiclePlate(bindcarRequestVo);
//响应包处理
ServiceResponseVo serviceResponseVo = bindVehiclePlateResponseHandle(bindcarResponseVo);
//token 失效
if(serviceResponseVo.getRespCode().equals(ETCPBusinessCodeEnum.TOKEN_INVALID.getCode())){
//call ETCP API[bindcar]
bindcarResponseVo = requestBindVehiclePlate(bindcarRequestVo);
//响应包处理
serviceResponseVo = bindVehiclePlateResponseHandle(bindcarResponseVo);
}
return serviceResponseVo;
}
5.2 What’s the point
这是绑定车牌的逻辑,需要从缓存获取token
token可能过期,过期时应该更新token
这个实现有一个问题就是没有在刷新token时缓存失效
可能会导致业务在token过期的间隔中持续失败
5.3 Demo
这个是payment的充电接入层实现
@SneakyThrows
public <Req, Resp> Resp call(String method, Req data, Class<Resp> clazz) {
return call(method, data, clazz, getToken());
}
private <Req, Resp> Resp call(@NonNull String method, @NonNull Req data, @NonNull Class<Resp> clazz, String token) throws CommonException {
String url = properties.getBaseUrl() + method;
CommonResponse response = restTemplate.postForObject(url, new HttpEntity<>(getRequestDatagram(data), getHeaders(token)), CommonResponse.class);
if (Objects.isNull(response)) {throw new CommonException(CommonErrorEnum.DOWNSTREAM_SYSTEM_ERROR);}
CommonErrorEnum errorEnum = EnumUtil.map(response.getCode(), EvcsErrorEnum.class, null, CommonErrorEnum.class, CommonErrorEnum.DOWNSTREAM_SYSTEM_ERROR, true);
if (Objects.equals(CommonErrorEnum.TOKEN_INVALID, errorEnum)) {
redisClient.delete(properties.getTokenKey());
return call(method, data, clazz);
}
if (Objects.equals(CommonErrorEnum.SUCCESS, errorEnum)) {
byte[] resp = decrypt(response.getData());
try {
return JsonUtil.readValue(resp, clazz);
} catch (Exception e) {
log.error("can not deserialize to {}, response data is , request is ", clazz.getSimpleName(), new String(resp), data);
throw e;
}
}
log.error("evcs response error: , and request is {}, url is {}", response, JsonUtil.writeValueAsString(data), url);
throw new CommonException(errorEnum, StringUtils.defaultIfBlank(response.getMessage(), errorEnum.getShowMessage()));
}
public String getToken() throws CommonException {
String token = redisClient.get(properties.getTokenKey());
if (StringUtils.isNotBlank(token)) {return token;}
TokenResponse tokenResponse = call(EvcsProperties.TOKEN_API, new TokenRequest(properties.getLocalOperatorId(), properties.getServerOperatorSecret()), TokenResponse.class, null);
if (!EvcsErrorEnum.SUCCESS.getValue().equals(tokenResponse.getErrorMessage())) {throw new CommonException(CommonErrorEnum.DOWNSTREAM_SYSTEM_ERROR);}
redisClient.put(properties.getTokenKey(), tokenResponse.getToken(), tokenResponse.getAvailableTime(), TimeUnit.SECONDS);
return tokenResponse.getToken();
}
6 Why not use JsonAlias
6.1 Code
/**
* 查询在场车辆的停车费用 响应包处理
* @param orderunpayResponseVo
* @return List<ParkFeeInfoResponseVo>
*/
public List<ParkFeeInfoResponseVo> unPayParkFeeInfoResponseHandle(OrderunpayResponseVo orderunpayResponseVo) throws ParseException{
List<ParkFeeInfoResponseVo> parkFeeInfoResponseVoList= new ArrayList<ParkFeeInfoResponseVo>();
if(null != orderunpayResponseVo && null != orderunpayResponseVo.getCode()){
if(orderunpayResponseVo.getCode() == 0){
OrderunpayDataVo [] orderunpayDataVo = orderunpayResponseVo.getData();
for (OrderunpayDataVo orderunpayDataVoTemp : orderunpayDataVo) {
ParkFeeInfoResponseVo parkFeeInfoResponseVo = new ParkFeeInfoResponseVo();
parkFeeInfoResponseVo.setOrderId(orderunpayDataVoTemp.getOrderId());
parkFeeInfoResponseVo.setEntranceTime(orderunpayDataVoTemp.getEntranceTime());
Long parkingTime = DateUtil.getDiffTime(orderunpayDataVoTemp.getEntranceTime(),orderunpayDataVoTemp.getExitTime());
parkFeeInfoResponseVo.setParkingTime(parkingTime.toString());
parkFeeInfoResponseVo.setTotalParkingFee(orderunpayDataVoTemp.getParkingFee());
parkFeeInfoResponseVo.setPaidParkingFee(orderunpayDataVoTemp.getPaidFee());
parkFeeInfoResponseVo.setRemainingParkingFee(orderunpayDataVoTemp.getRemainingParkFee());
parkFeeInfoResponseVo.setRemainingFee(orderunpayDataVoTemp.getRemainingFee());
parkFeeInfoResponseVo.setPaidServiceFee(orderunpayDataVoTemp.getPaidServiceFee());
parkFeeInfoResponseVo.setRemainingServiceFee(orderunpayDataVoTemp.getRemainingServiceFee());
parkFeeInfoResponseVo.setCoupon(orderunpayDataVoTemp.getDiscountAmount().toString());
parkFeeInfoResponseVo.setPlateNo(orderunpayDataVoTemp.getCarNumber());
parkFeeInfoResponseVo.setParkId(orderunpayDataVoTemp.getParkingId());
parkFeeInfoResponseVo.setParkName(orderunpayDataVoTemp.getParkingName());
parkFeeInfoResponseVoList.add(parkFeeInfoResponseVo);
}
}else if(orderunpayResponseVo.getCode().equals(ETCPBusinessCodeEnum.TOKEN_INVALID.getCode())){
log.error(orderunpayResponseVo.getCode() + ":" + orderunpayResponseVo.getMessage());
}else{
log.error(orderunpayResponseVo.getCode() + ":" + orderunpayResponseVo.getMessage());
}
}else{
log.error("call ETCP API[orderunpay] response is null");
}
return parkFeeInfoResponseVoList;
}
6.2 What’s the point
这段的逻辑是把etcp的返回结果包装成我们自己的内部逻辑处理类
那么问题来了,为啥不直接用内部逻辑类作为etcp的返回结果?
这里有一个问题,两边的字段名不一致怎么办?
可能有人会说,内部类的命名是我们可以控制的,我们匹配etcp的命名不就好了?
那么如果停简单的字段名和etcp不一致(事实上真的不一致,再退一步,如果以后接别的CP/SP怎么办)怎么办?
我们需要更通用的方法
6.3 Demo
public class InternalOrder {
@JsonAlias({"OrderId", "order_id"})
String orderId;
}
这样不论是
{"OrderId":"testOrderId"}
还是
{"order_id":"testOrderId"}
都可以反序列化成功 By the way 这个例子嵌套导致的缩进同样很多,可以用前述方式2 If and Else来实现
7 Global excpetion handler
7.1 Code
/**
* 绑定车牌
* @param vehiclePlateInfoRequestVo
* @return MAResponse<VoidVo>
*/
@PostMapping(value = "/etcp/bindvehicleplate")
public MAResponse<VoidVo> bindVehiclePlate(@RequestBody VehiclePlateInfoRequestVo vehiclePlateInfoRequestVo){
MAResponse<VoidVo> maResponse = null;
try{
log.info("call /etcp/bindvehicleplate begin");
ServiceResponseVo serviceResponseVo = etcpAdaptService.bindVehiclePlate(vehiclePlateInfoRequestVo);
maResponse = MAResponse.buildMAResponse(serviceResponseVo.getRespCode(), serviceResponseVo.getRespMsg());
log.info("call /etcp/bindvehicleplate end");
} catch (Exception e){
log.error("call /etcp/bindvehicleplate error", e);
maResponse = MAResponse.buildMAResponse(ServiceResponseType.ERROR.getRespCode(), ServiceResponseType.ERROR.getMessage());
}
return maResponse;
}
7.2 What’s the point
每个操作都要try/catch和打日志,好烦
可以用注解来打日志,用global excpetion handler来处理异常
7.3 Demo
/**
* 绑定车牌
* @param vehiclePlateInfoRequestVo
* @return MAResponse<VoidVo>
*/
@Stalker
@PostMapping(value = "/etcp/bindvehicleplate")
public MAResponse<VoidVo> bindVehiclePlate(@RequestBody VehiclePlateInfoRequestVo vehiclePlateInfoRequestVo){
MAResponse<VoidVo> maResponse = null;
log.info("call /etcp/bindvehicleplate begin");
ServiceResponseVo serviceResponseVo = etcpAdaptService.bindVehiclePlate(vehiclePlateInfoRequestVo);
maResponse = MAResponse.buildMAResponse(serviceResponseVo.getRespCode(), serviceResponseVo.getRespMsg());
log.info("call /etcp/bindvehicleplate end");
return maResponse;
}
环绕日志注解,包括出入参和运行时间
@Aspect
@Component
@Slf4j
public class AspectConfig {
@Around("@annotation(com.ezia.coreservices.common.annotation.Stalker)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
try {
long startTime = System.nanoTime();
Object result = joinPoint.proceed();
long costTime = System.nanoTime() - startTime;
log.info("class {} method {} costs {} with request and result ",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
costTime,
Arrays.toString(joinPoint.getArgs()),
result);
return result;
} catch (Exception e) {
log.info("class {} method {} error with request ",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
Arrays.toString(joinPoint.getArgs()),
e);
throw e;
}
}
}
全局异常handler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
private static ApiResult<?> handle(Exception e, ErrorEnum errorEnum) {
return handle(e, errorEnum, null);
}
private static ApiResult<?> handle(Exception e, ErrorEnum errorEnum, String errMsg) {
log.error(e.getMessage(), e);
return ApiResult.error(errorEnum, errMsg);
}
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResult<?> bindException(BindException e) {
return handle(e, PARAM_INVALID,
e.getBindingResult().getFieldErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.findFirst().orElse(PARAM_INVALID.getShowMessage()));
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResult<?> constraintViolationException(ConstraintViolationException e) {
return handle(e, PARAM_INVALID, e.getMessage());
}
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResult<?> missingServletRequestParameterException(Exception e) {
return handle(e, CommonErrorEnum.PARAM_ABSENT, e.getMessage());
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ApiResult<?> httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { return handle(e, CommonErrorEnum.HTTP_REQUEST_METHOD_INVALID); }
@ExceptionHandler(CommonException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiResult<?> commonException(CommonException e) { return handle(e, e.getErrorEnum()); }
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiResult<?> internalServerError(Exception e) { return handle(e, CommonErrorEnum.UNKNOWN_ERROR); }
}
文档信息
- 本文作者:Wit
- 本文链接:https://abbywitmia.github.io/2019/01/31/parking-code-review/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)