上一篇:字节跳动面试经验总结,已顺利拿到offer!

背景

最近项目进入联调阶段,服务层的接口需要和协议层进行交互,协议层需要将入参 [json字符串] 组装成服务层所需的 json 字符串,组装的过程中很容易出错。

入参出错导致接口调试失败问题在联调中出现很多次,因此就想写一个请求日志切面把入参信息打印一下,同时协议层调用服务层接口名称对不上也出现了几次,通过请求日志切面就可以知道上层是否有没有发起调用,方便前后端甩锅还能拿出证据。

本篇文章是实战性的,对于切面的原理不会讲解,只会简单介绍一下切面的知识点。

切面介绍

面向切面编程是一种编程范式,它作为 OOP 面向对象编程的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、权限控制、缓存控制、日志打印等等。

AOP 把软件的功能模块分为两个部分:

  • 核心关注点

  • 横切关注点

业务处理的主要功能为核心关注点,而非核心、需要拓展的功能为横切关注点。AOP 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点进行分离。

使用切面有以下好处:

  • 集中处理某一关注点/横切逻辑

  • 可以很方便的添加/删除关注点

  • 侵入性少,增强代码可读性及可维护性

因此当想打印请求日志时很容易想到切面,对控制层代码 0 侵入。

切面的使用【基于注解】

@Aspect => 声明该类为一个注解类

切点注解:

  • @Pointcut => 定义一个切点,可以简化代码

通知注解:

  • @Before => 在切点之前执行代码

  • @After => 在切点之后执行代码

  • @AfterReturning => 切点返回内容后执行代码,可以对切点的返回值进行封装

  • @AfterThrowing => 切点抛出异常后执行

  • @Around => 环绕,在切点前后执行代码

动手写一个请求日志切面

使用 @Pointcut 定义切点:

@Pointcut("execution(* your_package.controller..*(..))")
public void requestServer() {
}

@Pointcut 定义了一个切点,因为是请求日志切边,因此切点定义的是 Controller 包下的所有类下的方法。定义切点以后在通知注解中直接使用 requestServer 方法名就可以了。

使用 @Before 再切点前执行:

@Before("requestServer()")
public void doBefore(JoinPoint joinPoint) {ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();LOGGER.info("===============================Start========================");LOGGER.info("IP                 : {}", request.getRemoteAddr());LOGGER.info("URL                : {}", request.getRequestURL().toString());LOGGER.info("HTTP Method        : {}", request.getMethod());LOGGER.info("Class Method       : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
}

在进入 Controller 方法前,打印出调用方 IP、请求 URL、HTTP 请求类型、调用的方法名。

使用 @Around 打印进入控制层的入参:

@Around("requestServer()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {long start = System.currentTimeMillis();Object result = proceedingJoinPoint.proceed();LOGGER.info("Request Params       : {}", getRequestParams(proceedingJoinPoint));LOGGER.info("Result               : {}", result);LOGGER.info("Time Cost            : {} ms", System.currentTimeMillis() - start);return result;
}

打印了入参、结果以及耗时。

getRquestParams 方法:

private Map<String, Object> getRequestParams(ProceedingJoinPoint proceedingJoinPoint) {Map<String, Object> requestParams = new HashMap<>();//参数名String[] paramNames = ((MethodSignature)proceedingJoinPoint.getSignature()).getParameterNames();//参数值Object[] paramValues = proceedingJoinPoint.getArgs();for (int i = 0; i < paramNames.length; i++) {Object value = paramValues[i];//如果是文件对象if (value instanceof MultipartFile) {MultipartFile file = (MultipartFile) value;value = file.getOriginalFilename();  //获取文件名}requestParams.put(paramNames[i], value);}return requestParams;}

通过 @PathVariable 以及 @RequestParam 注解传递的参数无法打印出参数名,因此需要手动拼接一下参数名,同时对文件对象进行了特殊处理,只需获取文件名即可。

@After 方法调用后执行:

@After("requestServer()")
public void doAfter(JoinPoint joinPoint) {LOGGER.info("===============================End========================");
}

没有业务逻辑只是打印了 End。

完整切面代码:

@Component
@Aspect
public class RequestLogAspect {private final static Logger LOGGER = LoggerFactory.getLogger(RequestLogAspect.class);@Pointcut("execution(* your_package.controller..*(..))")public void requestServer() {}@Before("requestServer()")public void doBefore(JoinPoint joinPoint) {ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();LOGGER.info("===============================Start========================");LOGGER.info("IP                 : {}", request.getRemoteAddr());LOGGER.info("URL                : {}", request.getRequestURL().toString());LOGGER.info("HTTP Method        : {}", request.getMethod());LOGGER.info("Class Method       : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());}@Around("requestServer()")public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {long start = System.currentTimeMillis();Object result = proceedingJoinPoint.proceed();LOGGER.info("Request Params     : {}", getRequestParams(proceedingJoinPoint));LOGGER.info("Result               : {}", result);LOGGER.info("Time Cost            : {} ms", System.currentTimeMillis() - start);return result;}@After("requestServer()")public void doAfter(JoinPoint joinPoint) {LOGGER.info("===============================End========================");}/*** 获取入参* @param proceedingJoinPoint** @return* */private Map<String, Object> getRequestParams(ProceedingJoinPoint proceedingJoinPoint) {Map<String, Object> requestParams = new HashMap<>();//参数名String[] paramNames =
((MethodSignature)proceedingJoinPoint.getSignature()).getParameterNames();//参数值Object[] paramValues = proceedingJoinPoint.getArgs();for (int i = 0; i < paramNames.length; i++) {Object value = paramValues[i];//如果是文件对象if (value instanceof MultipartFile) {MultipartFile file = (MultipartFile) value;value = file.getOriginalFilename();  //获取文件名}requestParams.put(paramNames[i], value);}return requestParams;}
}

高并发下请求日志切面

写完以后对自己的代码很满意,但是想着可能还有完善的地方就和朋友交流了一下。

果然还有继续优化的地方 每个信息都打印一行,在高并发请求下确实会出现请求之间打印日志串行的问题,因为测试阶段请求数量较少没有出现串行的情况。

果然生产环境才是第一发展力,能够遇到更多 bug,写更健壮的代码,解决日志串行的问题只要将多行打印信息合并为一行就可以了,因此构造一个对象。

RequestInfo.java:

@Data
public class RequestInfo {private String ip;private String url;private String httpMethod;private String classMethod;private Object requestParams;private Object result;private Long timeCost;
}

环绕通知方法体:

@Around("requestServer()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {long start = System.currentTimeMillis();ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();Object result = proceedingJoinPoint.proceed();RequestInfo requestInfo = new RequestInfo();requestInfo.setIp(request.getRemoteAddr());requestInfo.setUrl(request.getRequestURL().toString());requestInfo.setHttpMethod(request.getMethod());requestInfo.setClassMethod(String.format("%s.%s", proceedingJoinPoint.getSignature().getDeclaringTypeName(),proceedingJoinPoint.getSignature().getName()));requestInfo.setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint));requestInfo.setResult(result);requestInfo.setTimeCost(System.currentTimeMillis() - start);LOGGER.info("Request Info      : {}", JSON.toJSONString(requestInfo));return result;
}

将 url、http request 这些信息组装成 RequestInfo 对象,再序列化打印对象,打印序列化对象结果而不是直接打印对象是因为序列化有更直观、更清晰,同时可以借助在线解析工具对结果进行解析。另外,搜索公众号互联网架构师后台回复“2T”,获取一份惊喜礼包。

是不是还不错?在解决高并发下请求串行问题的同时添加了对异常请求信息的打印,通过使用 @AfterThrowing 注解对抛出异常的方法进行处理。

RequestErrorInfo.java:

@Data
public class RequestErrorInfo {private String ip;private String url;private String httpMethod;private String classMethod;private Object requestParams;private RuntimeException exception;
}

异常通知环绕体:

@AfterThrowing(pointcut = "requestServer()", throwing = "e")
public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();RequestErrorInfo requestErrorInfo = new RequestErrorInfo();requestErrorInfo.setIp(request.getRemoteAddr());requestErrorInfo.setUrl(request.getRequestURL().toString());requestErrorInfo.setHttpMethod(request.getMethod());requestErrorInfo.setClassMethod(String.format("%s.%s", joinPoint.getSignature().getDeclaringTypeName(),joinPoint.getSignature().getName()));requestErrorInfo.setRequestParams(getRequestParamsByJoinPoint(joinPoint));requestErrorInfo.setException(e);LOGGER.info("Error Request Info      : {}", JSON.toJSONString(requestErrorInfo));
}

对于异常,耗时是没有意义的,因此不统计耗时,而是添加了异常的打印。

最后放一下完整日志请求切面代码:

@Component
@Aspect
public class RequestLogAspect {private final static Logger LOGGER = LoggerFactory.getLogger(RequestLogAspect.class);@Pointcut("execution(* your_package.controller..*(..))")public void requestServer() {}@Around("requestServer()")public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {long start = System.currentTimeMillis();ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();Object result = proceedingJoinPoint.proceed();RequestInfo requestInfo = new RequestInfo();requestInfo.setIp(request.getRemoteAddr());requestInfo.setUrl(request.getRequestURL().toString());requestInfo.setHttpMethod(request.getMethod());requestInfo.setClassMethod(String.format("%s.%s", proceedingJoinPoint.getSignature().getDeclaringTypeName(),proceedingJoinPoint.getSignature().getName()));requestInfo.setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint));requestInfo.setResult(result);requestInfo.setTimeCost(System.currentTimeMillis() - start);LOGGER.info("Request Info      : {}", JSON.toJSONString(requestInfo));return result;}@AfterThrowing(pointcut = "requestServer()", throwing = "e")public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();RequestErrorInfo requestErrorInfo = new RequestErrorInfo();requestErrorInfo.setIp(request.getRemoteAddr());requestErrorInfo.setUrl(request.getRequestURL().toString());requestErrorInfo.setHttpMethod(request.getMethod());requestErrorInfo.setClassMethod(String.format("%s.%s", joinPoint.getSignature().getDeclaringTypeName(),joinPoint.getSignature().getName()));requestErrorInfo.setRequestParams(getRequestParamsByJoinPoint(joinPoint));requestErrorInfo.setException(e);LOGGER.info("Error Request Info      : {}", JSON.toJSONString(requestErrorInfo));}/*** 获取入参* @param proceedingJoinPoint** @return* */private Map<String, Object> getRequestParamsByProceedingJoinPoint(ProceedingJoinPoint proceedingJoinPoint) {//参数名String[] paramNames = ((MethodSignature)proceedingJoinPoint.getSignature()).getParameterNames();//参数值Object[] paramValues = proceedingJoinPoint.getArgs();return buildRequestParam(paramNames, paramValues);}private Map<String, Object> getRequestParamsByJoinPoint(JoinPoint joinPoint) {//参数名String[] paramNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();//参数值Object[] paramValues = joinPoint.getArgs();return buildRequestParam(paramNames, paramValues);}private Map<String, Object> buildRequestParam(String[] paramNames, Object[] paramValues) {Map<String, Object> requestParams = new HashMap<>();for (int i = 0; i < paramNames.length; i++) {Object value = paramValues[i];//如果是文件对象if (value instanceof MultipartFile) {MultipartFile file = (MultipartFile) value;value = file.getOriginalFilename();  //获取文件名}requestParams.put(paramNames[i], value);}return requestParams;}@Datapublic class RequestInfo {private String ip;private String url;private String httpMethod;private String classMethod;private Object requestParams;private Object result;private Long timeCost;}@Datapublic class RequestErrorInfo {private String ip;private String url;private String httpMethod;private String classMethod;private Object requestParams;private RuntimeException exception;}
}

赶紧给你们的应用加上吧【如果没加的话】,没有日志的话,总怀疑上层出错,但是却拿不出证据。

地藏 Kelvin 的评论:关于 traceId 跟踪定位,可以根据 traceId 跟踪整条调用链,以 log4j2 为例介绍如何加入 traceId。

添加拦截器:

public class LogInterceptor implements HandlerInterceptor {private final static String TRACE_ID = "traceId";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String traceId = java.util.UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();ThreadContext.put("traceId", traceId);return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {        ThreadContext. remove(TRACE_ID);}
}

在调用前通过 ThreadContext 加入 traceId,调用完成后移除。

修改日志配置文件:在原来的日志格式中添加 traceId 的占位符:

<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>

执行效果:

日志跟踪更方便,DMC 是配置 logback 和 log4j 使用的,使用方式和 ThreadContext 差不多,将 ThreadContext.put 替换为 MDC.put 即可,同时修改日志配置文件。

推荐使用 log4j2,log4j2 也是可以配合 MDC 一起使用的:

MDC 是 slf4j 包下的,其具体使用哪个日志框架与我们的依赖有关。

热心网友评论:if(value instanceof MultipartFile)这种流处理方式?但是如果参数碰到 List files 呢?这种处理方式好像就不行了哈。

确实代码有欠缺考虑的地方,解决方案如下:

//如果是批量文件上传
if (value instanceof List) {try {List<MultipartFile> multipartFiles = castList(value, MultipartFile.class);if (multipartFiles!= null) {List<String> fileNames = new ArrayList<>();for (MultipartFile file : multipartFiles) {fileNames.add(file.getOriginalFilename());}requestParams.put(paramNames[i], fileNames);break;}} catch (ClassCastException e) {//忽略不是文件类型的List}
}

对 List 的类型做一个判断,如果是文件 List,就进行遍历获取文件名。

其中 castList 方法为:

public static <T> List<T> castList(Object obj, Class<T> clazz) {List<T> result = new ArrayList<T>();if (obj instanceof List<?>) {for (Object o : (List<?>) obj) {result.add(clazz.cast(o));}return result;}return null;
}

修改后完整的代码为:

@Component
@Aspect
public class RequestLogAspect {private final static Logger LOGGER = LoggerFactory.getLogger(RequestLogAspect.class);@Pointcut("execution(* com.hikvision.trainplatform.controller..*(..))")public void requestServer() {}@Around("requestServer()")public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {long start = System.currentTimeMillis();ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();Object result = proceedingJoinPoint.proceed();RequestInfo requestInfo = new RequestInfo();requestInfo.setIp(request.getRemoteAddr());requestInfo.setUrl(request.getRequestURL().toString());requestInfo.setHttpMethod(request.getMethod());requestInfo.setClassMethod(String.format("%s.%s", proceedingJoinPoint.getSignature().getDeclaringTypeName(),proceedingJoinPoint.getSignature().getName()));requestInfo.setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint));requestInfo.setResult(result);requestInfo.setTimeCost(System.currentTimeMillis() - start);LOGGER.info("Request Info      : {}", JSON.toJSONString(requestInfo));return result;}@AfterThrowing(pointcut = "requestServer()", throwing = "e")public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();RequestErrorInfo requestErrorInfo = new RequestErrorInfo();requestErrorInfo.setIp(request.getRemoteAddr());requestErrorInfo.setUrl(request.getRequestURL().toString());requestErrorInfo.setHttpMethod(request.getMethod());requestErrorInfo.setClassMethod(String.format("%s.%s", joinPoint.getSignature().getDeclaringTypeName(),joinPoint.getSignature().getName()));requestErrorInfo.setRequestParams(getRequestParamsByJoinPoint(joinPoint));requestErrorInfo.setException(e);LOGGER.info("Error Request Info      : {}", JSON.toJSONString(requestErrorInfo));}/*** 获取入参* @param proceedingJoinPoint** @return* */private Map<String, Object> getRequestParamsByProceedingJoinPoint(ProceedingJoinPoint proceedingJoinPoint) {//参数名String[] paramNames = ((MethodSignature)proceedingJoinPoint.getSignature()).getParameterNames();//参数值Object[] paramValues = proceedingJoinPoint.getArgs();return buildRequestParam(paramNames, paramValues);}private Map<String, Object> getRequestParamsByJoinPoint(JoinPoint joinPoint) {//参数名String[] paramNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();//参数值Object[] paramValues = joinPoint.getArgs();return buildRequestParam(paramNames, paramValues);}private Map<String, Object> buildRequestParam(String[] paramNames, Object[] paramValues) {Map<String, Object> requestParams = new HashMap<>();for (int i = 0; i < paramNames.length; i++) {Object value = paramValues[i];//如果是文件对象if (value instanceof MultipartFile) {MultipartFile file = (MultipartFile) value;value = file.getOriginalFilename();  //获取文件名}//如果是批量文件上传if (value instanceof List) {System.out.println("Yes...");try {List<MultipartFile> multipartFiles = castList(value, MultipartFile.class);if (multipartFiles!= null) {List<String> fileNames = new ArrayList<>();for (MultipartFile file : multipartFiles) {fileNames.add(file.getOriginalFilename());}requestParams.put(paramNames[i], fileNames);break;}} catch (ClassCastException e) {//忽略不是文件类型的List}}requestParams.put(paramNames[i], value);}return requestParams;}public static <T> List<T> castList(Object obj, Class<T> clazz) {List<T> result = new ArrayList<T>();if (obj instanceof List<?>) {for (Object o : (List<?>) obj) {result.add(clazz.cast(o));}return result;}return null;}@Datapublic class RequestInfo {private String ip;private String url;private String httpMethod;private String classMethod;private Object requestParams;private Object result;private Long timeCost;}@Datapublic class RequestErrorInfo {private String ip;private String url;private String httpMethod;private String classMethod;private Object requestParams;private RuntimeException exception;}
}

文章来源:https://c1n.cn/D5opM

感谢您的阅读,也欢迎您发表关于这篇文章的任何建议,关注我,技术不迷茫!小编到你上高速。

· END ·

最后,关注公众号互联网架构师,在后台回复:2T,可以获取我整理的 Java 系列面试题和答案,非常齐全。

正文结束

推荐阅读 ↓↓↓

1.救救大龄码农!45岁程序员在国务院网站求助总理!央媒网评来了...

2.如何才能成为优秀的架构师?

3.从零开始搭建创业公司后台技术栈

4.程序员一般可以从什么平台接私活?

5.37岁程序员被裁,120天没找到工作,无奈去小公司,结果懵了...

6.IntelliJ IDEA 2019.3 首个最新访问版本发布,新特性抢先看

7.这封“领导痛批95后下属”的邮件,句句扎心!

8.15张图看懂瞎忙和高效的区别!

最近,前端开发把我恶心着了,为了甩锅,我写了个牛逼的日志切面!相关推荐

  1. 8年前端开发的知识点沉淀(不知道会多少字,一直写下去吧...)

    先啰嗦几句 2011年开始从事前端,从一个页面的切图仔到如今还算合格的前端工程师,一路走来,我很清楚要学的很多,其中也学了很多,学的同时也丢了很多(身陷边学边丢,边丢边学的状态).尤其这3年,前端领域 ...

  2. 8年前端开发的知识点沉淀(不知道会多少字,一直写下去吧,)

    先啰嗦几句 2011年开始从事前端,从一个页面的切图仔到如今还算合格的前端工程师,一路走来,我很清楚要学的很多,其中也学了很多,学的同时也丢了很多(身陷边学边丢,边丢边学的状态).尤其这3年,前端领域 ...

  3. 做前端开发拿 30W 年薪很难吗?

    之前在知乎上看到有不少的前端工程师们一直在讨论这么一个问题 好的web前端年薪会有多少?         底下不少前辈高人们放出了自己的工资截图,也有新人出来表示瞻仰.高的4,50w往上走都有(更高的 ...

  4. web前端开发七武器

    http://blog.sina.com.cn/s/blog_40e5679c010119pf.html 武器一:前端开发IDE 最佳前端开发IDE:IntelliJ IDEA 推荐指数:   凭什么 ...

  5. Web前端开发工程师到底是干什么的?

    Web前端开发工程师其实是查BUG,改BUG,写BUG.哈哈,以上内容纯属开玩笑.事实上,Web前端开发工程师简单地说,就是使用HTML.CSS.JavaScript等技术来实现客户端(手机和电脑)上 ...

  6. 如何学习才能成为优秀的Web前端开发工程师?

    随着Web前端技术的广泛应用,Web前端开发工程师也成为了热门的岗位之一,并且吸引了很多人想要转行成为Web前端开发工程师.那么如何学习才能成为Web前端开发工程师呢? 如何学习才能成为优秀的Web前 ...

  7. 【微信小程序】零基础搭建微信小程序 前端开发

    已经用微信开发者工具做完了基础页面,正在思索如何搭建后台._为了理清思路边写博客边思考. 前端开发 小程序的框架 页面设置 在app.json里写这个小程序的所有页面,写在第一个的编译时默认第一个展示 ...

  8. 移动平台WEB前端开发技巧汇总

    原名<移动平台3G手机网站前端开发布局技巧汇总>,由武方博整理的,让我们了解下移动设备上的WEB站点开发的基础知识,多些时间和精力去优化其他细节,我这里对原文的标签格式做了细微的调整,阅读 ...

  9. 2019年前端开发10大战略性技术蓝图

    2010年的你,如果能学会Android开发,现在的你,薪资不会低于年薪50万-- 2015年的你,如果能熟练使用react,现在的你,薪资不会低于月薪30K-- 看到这两个数据,也许有人会反驳:技术 ...

  10. 总结2019大前端开发十大战略性技术布局

    2010年,如果你能学会Android开发到目前,你的薪资不会低于年薪50万 2015年,如果你能熟练使用react到目前,你的薪资不会低于月薪30K-- 看到这两个数据,也许有人会说,首先技术刚出来 ...

最新文章

  1. 【 FPGA 】超声波测距小实验(一)
  2. 考前自学系列·计算机组成原理·补码定点加减运算和溢出判断,浮点数的加减运算,原码的乘法
  3. [UE4]使用蓝图关闭对象的碰撞SetActorEnableCollision
  4. mysql海量数据存储
  5. jQuery07源码 (3803 , 4299) attr() prop() val() addClass()等 : 对元素属性的操作
  6. 用WM_COPYDATA消息来实现两个进程之间传递数据
  7. MySQL启动关闭服务巨慢,这样解决!
  8. 安卓案例:View动画 - 弹球碰壁
  9. gcc和arm-linux-gcc区别
  10. MVC中使用T4模板
  11. DataFrame和RDD互操作的两种方式:
  12. django 自定义标签
  13. 关于大型网站技术演进的思考(十三)--网站静态化处理—CSI(5)
  14. 在线摩尔斯密码加密解密工具
  15. go语言发送手机短信 - 互亿无线
  16. 【小程序】小游戏开发工具详解(下)
  17. 芯鼎盛LED恒流驱动芯片TX6128设计的DC-DC开关电源降压恒流DEMO说明
  18. 开发Web版一对一远程直播教室只需30分钟 - 使用face2face网络教室
  19. python自动化介绍
  20. window系统 安装 nvm 详细步骤

热门文章

  1. iOS 不同数据类型存入可变数组
  2. ORACLE关联查询
  3. 【数据库】sql连表查询
  4. 革新—决定磁带未来的最大挑战
  5. 西门子PLC学习笔记七-(位逻辑指令)
  6. 「leetcode」77.组合【回溯算法】详解!
  7. 如何在Mac上访问 USB 驱动器?
  8. Ps提示“脚本错误-50出现一般Photoshop错误,如何解决?
  9. 苹果Mac数据库管理开发工具:JetBrains DataGrip
  10. 如何使用 AirDrop 将 MAC 中的照片整理好,并上传到 iPhone 的相册中?