最近在开发过程中,需要访问一个第三方系统的部分接口。但是第三方系统只提供生产环境供我们调用。由于项目在迭代过程中涉及到大量地方的测试并且第三方系统不允许将测试数据推送到对方的生产环境,我们采用了挡板的方式来保障测试的进行。
在我方项目调用第三方的系统,我方采用的是feignClient 调用的方式。当前spring微服务在各个系统中应用的范围越来越广,当前挡板适用于feign调用。
使用方式是
1.在使用的feign调用方法上加上@HttpBaffle注释。
2.将json文件放在apollo配置的挡板路径里,使用的json文件必须是UTF-8编码,如果不使用读取的汉字会是乱码。
注意:测试环境apollo里的挡板开关配置为开,生产环境apollo里的挡板开关配置为关。
拷贝我的代码帮忙点个赞
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.2</version>
</dependency>
</dependencies>
package com.changshin.baffle;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HttpBaffle {
/**
* 挡板路径
*
* @return 路径
*/
String bafflePath() default "";
}
package com.changshin.baffle;
import cn.hutool.core.io.FileUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.File;
import java.lang.reflect.Method;
@Slf4j
@Aspect
@Component
public class HttpBaffleAspect {
/**
* 挡板开关
*/
@Value("${baffle.flag}")
private String baffleFlag;
/**
* 挡板名称
*/
@Value("${baffle.path}")
private String bafflePath;
@Pointcut("@annotation(com.changshin.baffle.HttpBaffle)")
public void baffle() {
//default implementation ignored
}
/**
* 执行切面
*
* @param joinPoint 连接点
* @return 对象
*/
@Around("baffle()")
public Object around(ProceedingJoinPoint joinPoint) {
Object obj = null;
try {
obj = exeBaffle(joinPoint);
} catch (Throwable e) {
log.error("挡板切面执行异常", e);
throw new RuntimeException("挡板执行异常");
}
return obj;
}
public Object exeBaffle(ProceedingJoinPoint joinPoint) throws Throwable {
Object obj = null;
if ("1".equals(baffleFlag)) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
String logInfo = getBaffleInfo(joinPoint, method);
log.info(logInfo);
//获取注解方法的返回类型
Class<?> returnType = method.getReturnType();
//挡板文件路径
HttpBaffle httpBaffle = method.getAnnotation(HttpBaffle.class);
String baffleRealPath = httpBaffle.bafflePath();
if (StringUtils.isEmpty(bafflePath)) {
baffleRealPath = bafflePath + File.separator + returnType.getSimpleName() + ".json";
}
if (!FileUtil.exist(baffleRealPath)) {
String errMsg = baffleRealPath + "挡板文件不存在";
log.info(errMsg);
throw new RuntimeException(errMsg);
}
//读取挡板内容
String jsonStr = FileUtil.readUtf8String(baffleRealPath);
JSONObject jsonObject = JSONUtil.parseObj(jsonStr);
//JSON转为注解方法的返回对象
obj = JSONUtil.toBean(jsonObject, returnType);
} else {
log.info("挡板开关为关闭状态,执行FEIGN调用");
obj = joinPoint.proceed();
}
return obj;
}
/**
* 获取挡板信息
*
* @param joinPoint 连接点
* @param method 方法
* @return 挡板信息
*/
private String getBaffleInfo(ProceedingJoinPoint joinPoint, Method method) {
StringBuffer sb = new StringBuffer("[");
sb.append(joinPoint.getSignature().getDeclaringType().getSimpleName());//类名
sb.append("#");
sb.append(method.getName());
sb.append("]");
sb.append("方法使用注解挡板,不执行HTTP请求");
return sb.toString();
}
}
网上的feignclient挡板采用的方法是通过写一个feign调用的继承类,并使用@primary注解。这种方式存在的问题是每一个挡板都要写一个实现方法,最重要的是上线前必须要注释@Primary @Component。如果没有注释会导致上线后访问的还是挡板,造成事故。
import cn.seifon.example.feignstubmock.dto.YunxunSmsReqDto;
import cn.seifon.example.feignstubmock.dto.YunxunSmsRespDto;
import cn.seifon.example.feignstubmock.feign.YunxunSmsFeign;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @Author: Seifon
* @Description:
* @Date: Created in 10:24 2019/1/7
*/
@Primary //注意:需要在原Feign接口@FeignClient注解加入primary = false 属性
@Component
@ConditionalOnProperty(name = "feign-stub.yunxun.sms.mode", havingValue = "stub")
public class YunxunSmsFeignStub implements YunxunSmsFeign {
private static final Logger LOG = LoggerFactory.getLogger(YunxunSmsFeignStub.class);
@Override
public YunxunSmsRespDto send(YunxunSmsReqDto request) {
YunxunSmsRespDto yunxunSmsRespDto = new YunxunSmsRespDto();
//模拟正常响应结果
yunxunSmsRespDto.setCode("0");
yunxunSmsRespDto.setFailNum("0");
yunxunSmsRespDto.setSuccessNum("1");
yunxunSmsRespDto.setMsgId(String.valueOf(RandomUtils.nextLong(19000000000000000L, 19999999999999999L)));
yunxunSmsRespDto.setTime(DateFormatUtils.format(new Date(), "yyyyMMddHHmmss"));
yunxunSmsRespDto.setErrorMsg("");
String params = request.getParams();
String[] paramSplit = StringUtils.split(params, ",");
if (paramSplit[0].length() != 11) {
//模拟错误响应结果
yunxunSmsRespDto.setCode("107");
yunxunSmsRespDto.setMsgId("");
yunxunSmsRespDto.setErrorMsg("手机号码格式错误");
}
return yunxunSmsRespDto;
}
}