WebApi 的接口开发、日志记录、查看和重发 -YB
WebApi 的接口开发、日志记录、查看和重发 -YB
@acted by yuanbao at 20220707
WebApi 的接口开发、日志记录、查看和重发 1 项目背景 2 WebApi 的知识点 3 项目使用目录结构 4 服务端 4.1 服务端 WebApi 接口方式:1> Servlet 方式 2> SpringMVC 方式比较和结论:4.2 服务端核心代码 4.3 服务端业务代码 5 客户端 5.1 客户端主要步骤:5.2 客户端 DEMO 代码:6 日志查看和手动重发功能 6.1 页面效果:6.2 手动重发的实现:7 附相关代码文件
1 项目背景
全信项目中需要与 CAPP、NC 集成,集成方式有一部分数据采用 WebApi 接口方式,一部分数据采用中间表视图的方式。
基于以往接口的开发经验,由于接口涉及到与外部系统的集成,所以要考虑通过记录日志进行责任划分。平台提供的接口工具只能对服务端进行日志记录,缺乏客户端日志,且手动重发的功能待完善。
2 WebApi 的知识点
3 项目使用目录结构
WebApi 分为服务端、客户端;
中建表视图通过专用类去调用第二数据源;
4 服务端
4.1 服务端 WebApi 接口方式:
1> Servlet 方式
自定义类继承 HttpServlet,类中重写 doGet 和 doPost 方法。
将类注册到 web.xml 中。
并且跳过鉴权。
2> SpringMVC 方式
自定义类,注解为 RestController.
自定义方法,加 RequestMapping 注解。可定义参数类型、返回值类型。
并且跳过鉴权。
比较和结论:
Servlet 方式注册繁琐,且一个类只能定义一个方法,写法复杂,没有 SpringMVC 方式方便。故最终使用 SpringMVC 方式。
有一个地方要注意,访问路径 url:
-
V4.5:UniMax 的 SpringMVC 的 webApi 接口需要写成.do 后缀的方式,才可正常解析请求。如:http://localhost:8080/prj-qxdl/api/test6.do
-
V5.2:直接用.do 存在问题,后来樊辉的 url 调整为:http://localhost:8080/prj-v5-web/ext/api/test6 这样可行。
-
- 1、类需要放在 com.epichust.mestar.rest 路径下
-
- 2、不需要加鉴权
-
- 3、路径:在 api 前需要加个 ext/
4.2 服务端核心代码
用到了平台的接口工具,在服务端的方法上加 @InterfaceLog 的注解,进行切面拦截写入日志,在 InterfaceLogAspect 类的 around 方法中需要根据项目接口协议去定义状态成功与否是如何判别的。
全信项目是 state=1 表示成功,其他为失败;并记录服务端标识;
4.3 服务端业务代码
NcService.java 类暴露接口,该接口方法的 url 为:http://localhost:29010/prj-qxdl/api/test6.do
/**
* @ClassName: NcDemoService
* @Description: MES作WebApi服务端,提供接口给NC系统调用。DEMO,其他接口参考此方式。 <br>
* @Author: yuanbao
* @Date: 2022/6/24
**/
@RestController
@RequestMapping("/api")
public class NcService
{
@Autowired
UmppOrderProcess umppOrderProcess;
/**
* @MethodName: test6
* @Description: 传递jsonObject,返回jsonObject。增加日志记录。
* @Param request
* @Param response
* @Return void
* @Author: yuanbao
* @Date: 2022/6/27
**/
@InterfaceLog(wsCode="nc-test6", wsName="同步物料", dataType="NC->MES")
@RequestMapping(value = "/test6", method = RequestMethod.POST, produces = "text/html;charset=utf-8")
public String test6(HttpServletRequest request, HttpServletResponse response, @RequestBody JSONObject jsonObject)
{
MestarLogger.info("test6 api");
MestarLogger.info("req info:" + jsonObject.toJSONString());
JSONObject ret = new JSONObject();
int retCode = 1;
String retMessage = "成功";
try
{
umppOrderProcess.test6(jsonObject); // 后续逻辑的处理
} catch (Exception e)
{
MestarLogger.error("接口处理失败:" + e.getMessage());
retCode = 0;
retMessage = e.getMessage();
}
ret.put("code", retCode);
ret.put("message", retMessage);
MestarLogger.info("res info:" + ret.toJSONString());
return ret.toJSONString();
}
}
另外,需要设置跳过鉴权:
5 客户端
5.1 客户端主要步骤:
CappClient 类写调用客户端的方法,里面主要步骤:
-
定义接口日志的一些变量
-
准备请求头数据
-
创建连接
-
调用接口,接收数据
-
处理接收到的数据 –如果有后续处理逻辑注意记录方法
-
接口调用日志记录
5.2 客户端 DEMO 代码:
/**
* @ClassName: CappClient
* @Description: CAPP接口客户端-基于WebApi。MES从ERP获取工艺数据 <br>
* @Author: yuanbao
* @Date: 2022/6/9
**/
@Service
public class CappClient
{
@Autowired
ISysParamManager sysParamManager;
@Autowired
CappClientProcess cappClientProcess;
@Autowired
MbbInterfaceService mbbInterfaceService;
/**
* @MethodName: syncCraftData
* @Description: 同步工艺参数数据
* @Param mrlCode 物料号
* @Param bomVer bom版本
* @Return void
* @Author: yuanbao
* @Date: 2022/6/9
**/
public void syncCraftData(String mrlCode, String bomVer)
{
MestarLogger.info("CAPP接口调用 start");
StopWatch sw = new StopWatch("CAPP接口调用");
// 0. 定义接口日志的一些变量
JSONObject recordMap = new JSONObject();
String paramsJson = null; // 请求参数信息
String respContent = null; // 返回结果信息
boolean status = true; // 接口调用最终状态 true表示成功
String exceptionInfo = ""; // 异常信息
long start = System.currentTimeMillis();
long end = 0L;
// 接口编码
String wsCode = "capp-syncCraftData"; // todo 自定义
// 接口名称
String wsName = "同步工艺参数数据"; // todo 自定义
// 数据流向
String dataFlow = "CAPP->MES"; // todo 自定义
// 类名
String targetName = this.getClass().getName(); // 获取当前类名
// 方法名
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName(); // 获取当前方法名
recordMap.put("wsCode", wsCode);
recordMap.put("wsName", wsName);
recordMap.put("dataFlow", dataFlow);
recordMap.put("targetName", targetName);
recordMap.put("methodName", methodName);
try (CloseableHttpClient client = HttpClients.createDefault();)
{
// 1. 准备请求头数据
Map<String, String> params = Maps.newHashMap();
params.put("wlid", mrlCode);
params.put("bom_ver", bomVer);
paramsJson = JSONUtil.writeMap2JSON(params);
recordMap.put("reqParams", paramsJson);
// 2. 创建连接
sw.start("建立连接");
String wsURL = sysParamManager.getSysParamValByCode(ProjectConstantUtil.CAPP_WEBAPI_URL_PREFIX) + ProjectConstantUtil.CAPP_WEBAPI_URL_METHOD_PROC;
recordMap.put("reqUrl", wsURL);
HttpPost httpPost = new HttpPost(wsURL);
StringEntity entity = new StringEntity(paramsJson, "utf-8");//解决中文乱码问题
entity.setContentEncoding("UTF-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
sw.stop();
// 3. 调用接口,接收数据
sw.start("请求数据");
HttpResponse resp = client.execute(httpPost);
MestarLogger.info("状态码:" + resp.getStatusLine().getStatusCode());
sw.stop();
// 4. 处理接收到的数据
sw.start("处理数据");
HttpEntity he = resp.getEntity();
respContent = EntityUtils.toString(he, "UTF-8");
recordMap.put("resData", respContent);
if (resp.getStatusLine().getStatusCode() == 200)
{
if (StringUtils.isEmpty(respContent))
{
status = false;
exceptionInfo = "接口返回数据为空";
throw new MestarException(exceptionInfo);
}
JSONObject jsonData = JSONObject.parseObject(respContent);
// todo 自定义处理过程:如果返回值中传数据再处理,需要再定义后续处理逻辑
{
recordMap.put("processTarget", "cappClientProcess"); // todo 记录类的实例名
recordMap.put("processMethod", "syncCraftData"); // todo 记录方法名
cappClientProcess.syncCraftData(jsonData); // todo
}
if (jsonData.getIntValue("code") == 1)
{
status = true;
} else
{
status = false;
exceptionInfo = jsonData.getString("message");
}
} else
{
status = false;
exceptionInfo = "接口调用失败,状态码:" + resp.getStatusLine().getStatusCode();
throw new MestarException(exceptionInfo);
}
sw.stop();
} catch (Exception e)
{
status = false;
exceptionInfo = e.getMessage();
MestarLogger.error(e);
throw new MestarException(e.getMessage());
} finally
{
// 5. 接口调用日志记录
recordMap.put("status", status);
recordMap.put("exceptionInfo", exceptionInfo);
recordMap.put("scFlag", 1); // [QX]端标识:0服务端,1客户端
end = System.currentTimeMillis();
recordMap.put("start", start);
recordMap.put("end", end);
mbbInterfaceService.saveRecord(recordMap);
MestarLogger.info(sw.prettyPrint());
MestarLogger.info("CAPP接口调用 end");
}
}
}
6 日志查看和手动重发功能
6.1 页面效果:
6.2 手动重发的实现:
/**
* @MethodName: resendApiReq
* @Description: QX-接口日志重发,实现重新调用接口的功能,适用于客户端接口日志
* @Param miDetail 接口日志
* @Return void
* @Author: yuanbao
* @Date: 2022/7/1
**/
public void resendApiReq(MbbInterfaceDetail miDetail) throws Exception
{
MestarLogger.info("接口日志重发 start");
StopWatch sw = new StopWatch("接口日志重发");
// 0. 定义接口日志的一些变量
String paramsJson = null; // 请求参数信息
String respContent = null; // 返回结果信息
boolean status = true; // 接口调用最终状态 true表示成功
String exceptionInfo = ""; // 异常信息
long start = System.currentTimeMillis();
long end = 0L;
try (CloseableHttpClient client = HttpClients.createDefault();)
{
// 1. 准备请求头数据
paramsJson = miDetail.getToData();
// 2. 创建连接
sw.start("建立连接");
String wsURL = miDetail.getUda1();
HttpPost httpPost = new HttpPost(wsURL);
StringEntity entity = new StringEntity(paramsJson, "utf-8");//解决中文乱码问题
entity.setContentEncoding("UTF-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
sw.stop();
// 3. 调用接口,接收数据
sw.start("请求数据");
HttpResponse resp = client.execute(httpPost);
System.out.println("状态码:" + resp.getStatusLine().getStatusCode());
sw.stop();
// 4. 处理接收到的数据
sw.start("处理数据");
HttpEntity he = resp.getEntity();
respContent = EntityUtils.toString(he, "UTF-8");
if (resp.getStatusLine().getStatusCode() == 200)
{
if (StringUtils.isEmpty(respContent))
{
status = false;
exceptionInfo = "接口返回数据为空";
throw new MestarException(exceptionInfo);
}
JSONObject jsonData = JSONObject.parseObject(respContent);
// 判断是否有后续处理逻辑,有则处理
if (com.epichust.mestar.utils.lang.StringUtils.isNotEmpty(miDetail.getUda2()))
{
String processTarget = miDetail.getUda2();
String processMethod = miDetail.getUda3();
// 通过反射去执行
ProjectCommonUtils.executeMethod(processTarget, processMethod, jsonData);
}
if (jsonData.getIntValue("code") == 1)
{
status = true;
} else
{
status = false;
exceptionInfo = jsonData.getString("message");
}
} else
{
status = false;
exceptionInfo = "接口调用失败,状态码:" + resp.getStatusLine().getStatusCode();
throw new MestarException(exceptionInfo);
}
sw.stop();
} catch (Exception e)
{
status = false;
exceptionInfo = e.getMessage();
MestarLogger.error(e);
throw new MestarException(e.getMessage());
} finally
{
// 5. 接口调用日志记录
// 子记录
MbbInterfaceDetail miDetailx = new MbbInterfaceDetail();
BeanUtils.copyProperties(miDetail, miDetailx, new String[] { "id" });
// 其他数据回写
miDetailx.setState(status ? 1 : 2);
miDetailx.setExceptionInfo(exceptionInfo);
miDetailx.setResponseData(respContent);
miDetailx.setPid(miDetail.getId());
miDetailx.setOperSource("Page");
miDetailx.setIsResend("YES");
SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd HH🇲🇲ss");
end = System.currentTimeMillis();
miDetailx.setExecuteStartTime(dateformat.parse(dateformat.format(start)));
miDetailx.setExecuteEndTime(dateformat.parse(dateformat.format(end)));
miDetailx.setRunTime(end - start); // 毫秒
WsInterfaceLogger.saveWsInterfaceLog(miDetailx);
// 原记录:重发次数+1
Long resendTimes = miDetail.getResendTimes() == null ? 0L : miDetail.getResendTimes();
miDetail.setResendTimes(resendTimes + 1);
miDetail.setState(status ? 1 : 2);
WsInterfaceLogger.saveWsInterfaceLog(miDetail);
MestarLogger.info(sw.prettyPrint());
MestarLogger.info("接口日志重发 end");
}
}
反射的方法:
/**
* @methodName: executeMethod
* @description: 反射-根据类名和方法名执行方法
* @param: beanClass 类的实例名
* @param: methodName 方法名,要执行的方法
* @param: methodParams 方法的入参,可以是多个
* @author: yuanbao
* @Date: 2022/7/4
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static Object executeMethod(String beanClass, String methodName, Object... methodParams)
{
Object ret = null;
try
{
Object entity = SpringContextHolder.getBean(beanClass);
Class<?> clazz = entity.getClass();
Class<?> methodParamsClazz = methodParams.getClass();
// 获取所有方法,去匹配方法名(要求方法没重载)
Method[] methods = clazz.getMethods();
Method method = null;
for (Method m : methods)
{
if (methodName.equals(m.getName()))
{
method = m;
break;
}
}
// 调用方法,并返回结果
if (method == null)
{
throw new MestarException("类名:" + beanClass + " 方法名:" + methodName + ",方法未匹配!");
}
ret = method.invoke(entity, methodParams);
} catch (Exception e)
{
MestarLogger.error(e);
MestarLogger.error("invoke error. beanClass:" + beanClass + ", methodName:" + methodName + ". error info:" + getStackTraceInfo(e));
throw new MestarException("invoke error. beanClass:" + beanClass + ", methodName:" + methodName + ". error info:" + getStackTraceInfo(e));
}
return ret;
}