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 的知识点

参考:WebApi 和 WebService 的比较

写法:SpringMVC 的 webApi 写法

3 项目使用目录结构

image-20220802171033937

WebApi 分为服务端、客户端;

中建表视图通过专用类去调用第二数据源;

4 服务端

4.1 服务端 WebApi 接口方式:

1> Servlet 方式

自定义类继承 HttpServlet,类中重写 doGet 和 doPost 方法。

将类注册到 web.xml 中。

并且跳过鉴权。

2> SpringMVC 方式

自定义类,注解为 RestController.

自定义方法,加 RequestMapping 注解。可定义参数类型、返回值类型。

并且跳过鉴权。

比较和结论:

Servlet 方式注册繁琐,且一个类只能定义一个方法,写法复杂,没有 SpringMVC 方式方便。故最终使用 SpringMVC 方式。

有一个地方要注意,访问路径 url:

4.2 服务端核心代码

image-20220802171802427

用到了平台的接口工具,在服务端的方法上加 @InterfaceLog 的注解,进行切面拦截写入日志,在 InterfaceLogAspect 类的 around 方法中需要根据项目接口协议去定义状态成功与否是如何判别的。

image-20220802172139449

全信项目是 state=1 表示成功,其他为失败;并记录服务端标识;

4.3 服务端业务代码

image-20220802171725037

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();  
 }  
}

另外,需要设置跳过鉴权:
image-20230315152150529

5 客户端

image-20220802171149670

5.1 客户端主要步骤:

CappClient 类写调用客户端的方法,里面主要步骤:

  1. 定义接口日志的一些变量

  2. 准备请求头数据

  3. 创建连接

  4. 调用接口,接收数据

  5. 处理接收到的数据 –如果有后续处理逻辑注意记录方法

  6. 接口调用日志记录

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 日志查看和手动重发功能

image-20220802171232605

6.1 页面效果:

image-20220811101643176

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;  
}

7 附相关代码文件

WebApi 接口日志和重发(服务端和客户端).rar