《优化接口设计的思路》系列:第三篇—留下用户调用接口的痕迹

在这里插入图片描述
系列文章导航
《优化接口设计的思路》系列:第一篇—接口参数的一些弯弯绕绕
《优化接口设计的思路》系列:第二篇—接口用户上下文的设计与实现
《优化接口设计的思路》系列:第三篇—留下用户调用接口的痕迹

前言

大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。

接口设计是整个系统设计中非常重要的一环,其中包括限流、权限、入参出参、切面等方面。设计一个好的接口可以帮助我们省去很多不必要的麻烦,从而提升整个系统的稳定性和可扩展性。作为接口设计经验分享的第三篇,我想分享一下如何在用户使用过程中留下操作痕迹。在实际开发中,我会采取一些手段来记录用户操作,例如使用日志记录用户行为,或者在数据库中保存用户操作记录。这些痕迹可以帮助我们快速定位和解决问题,同时也可以为后续数据分析和优化提供有价值的参考。

方法一、将接口的参数和结果打印在日志文件中

日志文件是我们记录用户使用痕迹的第一个地方,我之前写过一篇SpringBoot项目如何配置logback.xml的文章来实现系统日志输出,有兴趣的同学可以去看看。
这里我主要讲一下怎么方便将所有接口的出入参打印出来。

1、使用aop监控接口

依赖如下

<!-- aspectj -->
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.5</version>
</dependency>

如果有同学不知道aspectj是啥的,可以看我这篇文章SpringBoot整合aspectj实现面向切面编程(即AOP)

关键代码如下

package com.summo.aspect;import java.util.Objects;import javax.servlet.http.HttpServletRequest;import com.alibaba.druid.util.StringUtils;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;@Aspect
@Component
@Slf4j
public class ControllerLoggingAspect {/*** 拦截所有controller包下的方法*/@Pointcut("execution(* com.summo.controller..*.*(..))")private void controllerMethod() {}@Around("controllerMethod()")public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {long startTime = System.currentTimeMillis();//获取本次接口的唯一码String token = java.util.UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();MDC.put("requestId", token);//获取HttpServletRequestRequestAttributes ra = RequestContextHolder.getRequestAttributes();ServletRequestAttributes sra = (ServletRequestAttributes)ra;HttpServletRequest request = sra.getRequest();// 获取请求相关信息String url = request.getRequestURL().toString();String method = request.getMethod();String uri = request.getRequestURI();String params = request.getQueryString();if (StringUtils.isEmpty(params) && StringUtils.equals("POST", method)) {if (Objects.nonNull(joinPoint.getArgs())) {for (Object arg : joinPoint.getArgs()) {params += arg;}}}// 获取调用方法相信Signature signature = joinPoint.getSignature();String className = signature.getDeclaringTypeName();String methodName = signature.getName();log.info("@http请求开始, {}#{}() URI: {}, method: {}, URL: {}, params: {}",className, methodName, uri, method, url, params);//result的值就是被拦截方法的返回值try {//proceed方法是调用实际所拦截的controller中的方法,这里的result为调用方法后的返回值Object result = joinPoint.proceed();long endTime = System.currentTimeMillis();//定义请求结束时的返回数据,包括调用时间、返回值结果等log.info("@http请求结束, {}#{}(), URI: {}, method: {}, URL: {}, time: {}ms ",className, methodName, uri, method, url, (endTime - startTime));return result;} catch (Exception e) {long endTime = System.currentTimeMillis();log.error("@http请求出错, {}#{}(), URI: {}, method: {}, URL: {}, time: {}ms",className, methodName, uri, method, url, (endTime - startTime), e);throw e;} finally {MDC.remove("requestId");}}
}

2、增加requestId

由于接口的调用都是异步的,所以一旦QPS上来,那么接口的调用就会很混乱,不加一个标识的话,就不知道哪个返回值属于那个请求的了。
这个时候我们则需要加一个requestId(或者叫traceId)用来标识一个请求。

也即这段代码

//获取本次接口的唯一码
String token = java.util.UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
MDC.put("requestId", token);... ... 
MDC.remove("requestId");

同时logback.xml中也需要加一下requestId的打印,在logback.xml中可以使用%X{requestId}获取到MDC中添加的遍历。
完整的logback.xml配置文件如下:

<configuration><!-- 默认的一些配置 --><include resource="org/springframework/boot/logging/logback/defaults.xml"/><!-- 定义应用名称,区分应用 --><property name="APP_NAME" value="monitor-test"/><!-- 定义日志文件的输出路径 --><property name="LOG_PATH" value="${user.home}/logs/${APP_NAME}"/><!-- 定义日志文件名称和路径 --><property name="LOG_FILE" value="${LOG_PATH}/application.log"/><!-- 定义警告级别日志文件名称和路径 --><property name="WARN_LOG_FILE" value="${LOG_PATH}/warn.log"/><!-- 定义错误级别日志文件名称和路径 --><property name="ERROR_LOG_FILE" value="${LOG_PATH}/error.log"/><!-- 自定义控制台打印格式 --><property name="FILE_LOG_PATTERN" value="%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%blue(requestId: %X{requestId})] [%highlight(%thread)] ${PID:- } %logger{36} %-5level - %msg%n"/><!-- 将日志滚动输出到application.log文件中 --><appender name="APPLICATION"class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 输出文件目的地 --><file>${LOG_FILE}</file><encoder><pattern>${FILE_LOG_PATTERN}</pattern><charset>utf8</charset></encoder><!-- 设置 RollingPolicy 属性,用于配置文件大小限制,保留天数、文件名格式 --><rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!-- 文件命名格式 --><fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i.log</fileNamePattern><!-- 文件保留最大天数 --><maxHistory>7</maxHistory><!-- 文件大小限制 --><maxFileSize>50MB</maxFileSize><!-- 文件总大小 --><totalSizeCap>500MB</totalSizeCap></rollingPolicy></appender><!-- 摘取出WARN级别日志输出到warn.log中 --><appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${WARN_LOG_FILE}</file><encoder><!-- 使用默认的输出格式打印 --><pattern>${CONSOLE_LOG_PATTERN}</pattern><charset>utf8</charset></encoder><!-- 设置 RollingPolicy 属性,用于配置文件大小限制,保留天数、文件名格式 --><rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!-- 文件命名格式 --><fileNamePattern>${LOG_PATH}/warn.%d{yyyy-MM-dd}.%i.log</fileNamePattern><!-- 文件保留最大天数 --><maxHistory>7</maxHistory><!-- 文件大小限制 --><maxFileSize>50MB</maxFileSize><!-- 文件总大小 --><totalSizeCap>500MB</totalSizeCap></rollingPolicy><!-- 日志过滤器,将WARN相关日志过滤出来 --><filter class="ch.qos.logback.classic.filter.ThresholdFilter"><level>WARN</level></filter></appender><!-- 摘取出ERROR级别日志输出到error.log中 --><appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${ERROR_LOG_FILE}</file><encoder><!-- 使用默认的输出格式打印 --><pattern>${CONSOLE_LOG_PATTERN}</pattern><charset>utf8</charset></encoder><!-- 设置 RollingPolicy 属性,用于配置文件大小限制,保留天数、文件名格式 --><rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!-- 文件命名格式 --><fileNamePattern>${LOG_PATH}/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern><!-- 文件保留最大天数 --><maxHistory>7</maxHistory><!-- 文件大小限制 --><maxFileSize>50MB</maxFileSize><!-- 文件总大小 --><totalSizeCap>500MB</totalSizeCap></rollingPolicy><!-- 日志过滤器,将ERROR相关日志过滤出来 --><filter class="ch.qos.logback.classic.filter.ThresholdFilter"><level>ERROR</level></filter></appender><!-- 配置控制台输出 --><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>${FILE_LOG_PATTERN}</pattern><charset>utf8</charset></encoder></appender><!-- 配置输出级别 --><root level="INFO"><!-- 加入控制台输出 --><appender-ref ref="CONSOLE"/><!-- 加入APPLICATION输出 --><appender-ref ref="APPLICATION"/><!-- 加入WARN日志输出 --><appender-ref ref="WARN"/><!-- 加入ERROR日志输出 --><appender-ref ref="ERROR"/></root>
</configuration>

3、效果如下图

4、接口监控遇到的一些坑

返回值数据量很大会刷屏,尽量不要打印返回值。
文件上传接口会直接挂掉,所以上传的接口一般不会加入监控。

方法二、将风险高的操作保存到数据库中

虽然方法一能够记录每个接口的日志,但这些日志只存在于服务器上,并且有大小和时间限制,到期后就会消失。这种做法对所有请求或操作都一视同仁,不会对风险较高的请求进行特殊处理。为了解决危险操作带来的风险,我们需要将其持久化,以便在出现问题时能够快速找到原因。最常见的做法是将风险高的操作保存到数据库中。
实现原理还是使用方法一种的切面,不过这里使用的是注解切面,具体做法请见下文。

1、新建一张log表,存储风险操作

表结构如下:

建表语句我也贴出来

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for user_oper_log
-- ----------------------------
DROP TABLE IF EXISTS `user_oper_log`;
CREATE TABLE `user_oper_log` (`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '物理主键',`operation` varchar(64) DEFAULT NULL COMMENT '操作内容',`time` bigint DEFAULT NULL COMMENT '耗时',`method` text COMMENT '操作方法',`params` text COMMENT '参数内容',`ip` varchar(64) DEFAULT NULL COMMENT 'IP',`location` varchar(64) DEFAULT NULL COMMENT '操作地点',`response_code` varchar(32) DEFAULT NULL COMMENT '应答码',`response_text` text COMMENT '应答内容',`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',`gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',`creator_id` bigint DEFAULT NULL COMMENT '创建人',`modifier_id` bigint DEFAULT NULL COMMENT '更新人',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4  COMMENT='用户操作日志表';SET FOREIGN_KEY_CHECKS = 1;

核心字段为操作方法、参数内容、IP、操作地点、应答码、应答内容、创建人这些,其中IP和操作地址这两个是推算的,不一定很准。这些字段也不是非常全面,如果大家还有自己想记录的字段信息也可以加进来。

2、新建@Log注解和切面处理类LogAspect

注解类

package com.summo.log;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {/*** 接口功能描述** @return*/String methodDesc() default "";
}

切面处理类

package com.summo.log;import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import java.util.Set;import javax.servlet.http.HttpServletRequest;import com.alibaba.fastjson.JSONObject;import com.summo.entity.UserOperInfoDO;
import com.summo.repository.UserOperInfoRepository;
import com.summo.util.HttpContextUtil;
import com.summo.util.IPUtil;
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.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;@Slf4j
@Aspect
@Component
public class LogAspect {@Autowiredprivate UserOperInfoRepository userOperInfoRepository;@Pointcut("@annotation(com.summo.log.Log)")public void pointcut() {// do nothing}@Around("pointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {Object result = null;//默认操作对象为-1LMethodSignature signature = (MethodSignature)joinPoint.getSignature();Method method = signature.getMethod();Log logAnnotation = method.getAnnotation(Log.class);UserOperInfoDO log = new UserOperInfoDO();if (logAnnotation != null) {// 注解上的描述log.setOperation(logAnnotation.methodDesc());}// 请求的类名String className = joinPoint.getTarget().getClass().getName();// 请求的方法名String methodName = signature.getName();log.setMethod(className + "." + methodName + "()");// 请求的方法参数值Object[] args = joinPoint.getArgs();// 请求的方法参数名称LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();String[] paramNames = u.getParameterNames(method);if (args != null && paramNames != null) {StringBuilder params = new StringBuilder();params = handleParams(params, args, Arrays.asList(paramNames));log.setParams(params.toString());}log.setGmtCreate(Calendar.getInstance().getTime());long beginTime = System.currentTimeMillis();// 执行方法result = joinPoint.proceed();// 执行时长(毫秒)long time = System.currentTimeMillis() - beginTime;HttpServletRequest request = HttpContextUtil.getHttpServletRequest();// 设置 IP 地址String ip = IPUtil.getIpAddr(request);log.setIp(ip);log.setTime(time);//保存操作记录到数据库中userOperInfoRepository.save(log);return result;}/*** 参数打印合理化** @param params     参数字符串* @param args       参数列表* @param paramNames 参数名* @return*/private StringBuilder handleParams(StringBuilder params, Object[] args, List paramNames) {for (int i = 0; i < args.length; i++) {if (args[i] instanceof Map) {Set set = ((Map)args[i]).keySet();List<Object> list = new ArrayList<>();List<Object> paramList = new ArrayList<>();for (Object key : set) {list.add(((Map)args[i]).get(key));paramList.add(key);}return handleParams(params, list.toArray(), paramList);} else {if (args[i] instanceof Serializable) {Class<?> aClass = args[i].getClass();try {aClass.getDeclaredMethod("toString", new Class[] {null});// 如果不抛出 NoSuchMethodException 异常则存在 toString 方法 ,安全的 writeValueAsString ,否则 走 Object的// toString方法params.append(" ").append(paramNames.get(i)).append(": ").append(JSONObject.toJSONString(args[i]));} catch (NoSuchMethodException e) {params.append(" ").append(paramNames.get(i)).append(": ").append(JSONObject.toJSONString(args[i].toString()));}} else if (args[i] instanceof MultipartFile) {MultipartFile file = (MultipartFile)args[i];params.append(" ").append(paramNames.get(i)).append(": ").append(file.getName());} else {params.append(" ").append(paramNames.get(i)).append(": ").append(args[i]);}}}return params;}
}

3、使用方法

在需要监控的接口方法上加上@Log注解

@PostMapping("/saveRel")
@Log(methodDesc = "添加记录")
public Boolean saveRel(@RequestBody SaveRelReq saveRelReq) {return userRoleRelService.saveRel(saveRelReq);
}@DeleteMapping("/delRel")
@Log(methodDesc = "删除记录")
public Boolean delRel(Long relId) {return userRoleRelService.delRel(relId);
}

调用一下测试功能

数据库中保存的记录

这里可以看到已经有记录保存在数据库中了,包括两次添加操作、一次删除操作,并且记录了操作人的IP地址(这里我使用的是localhost所以IP是127.0.0.1)和操作时间。但是这里有一个问题:没有记录操作人的ID,也即creator_id字段为空,如果不知道这条记录是谁的,那这个功能就没有意义了,所以在方法三我将会说一下如何记录每一行数据的创建者和修改者。

方法三、记录每一行数据的创建者和修改者

这个功能的实现需要用到一个非常关键的东西:用户上下文。如何实现请看:《优化接口设计的思路》系列:第二篇—接口用户上下文的设计与实现。

那么现在假设我已经有了GlobalUserContext.getUserContext()方法可以获取到用户上下文信息,如何使用呢?
方法二没有记录操作人的ID,现在可以可以通过下面这种方法获取当前操作人的ID:

log.setCreatorId(GlobalUserContext.getUserContext().getUserId());

但是!!!我这里的标题是:记录每一行数据的创建者和修改者,可不仅仅是只操作user_oper_log的每一行数据,而是系统中的每一张表的每一行数据!那现在问题来了,如何实现这个需求?
最笨的办法就是在每个新增、更新的代码下都加上setCreatorId和setModifierId这些代码,实现是可以实现,但是感觉太low了,所以我这里提供一个思路和一个例子来优化这些代码。

1、统一字段名和类型

在每张表中都加入gmt_create(datetime 创建时间)、gmt_modified(datetime 更新时间)、creator_id(bigint 创建人ID)、modifier_id(bigint 更新人ID),我们将所有表中的这些辅助字段统一命名、统一类型,这样给我们统一处理提供了基础。

2、将这些字段集成到一个抽象类中

这样做的好处有两个:

  • 其他表的DO类继承这个抽象类,那么DO中就不需要再定义以上4个字段
  • 统一处理的类只有抽象类一个了

tips:非常建议使用mybatis-plus来实现这个功能,maven依赖如下:

 <!-- mybatis-plus -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.2</version>
</dependency>
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-extension</artifactId><version>3.3.2</version>
</dependency>

类名定义和代码如下
AbstractBaseDO.java

package com.summo.entity;import java.io.Serializable;
import java.util.Date;import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Getter;
import lombok.Setter;@Getter
@Setter
public class AbstractBaseDO<T extends Model<T>> extends Model<T> implements Serializable {/*** 创建时间*/@TableField(fill = FieldFill.INSERT)private Date gmtCreate;/*** 修改时间*/@TableField(fill = FieldFill.INSERT_UPDATE)private Date gmtModified;/*** 创建人ID*/@TableField(fill = FieldFill.INSERT)private Long creatorId;/*** 修改人ID*/@TableField(fill = FieldFill.INSERT_UPDATE)private Long modifierId;}

3、使用mybatis-plus的MetaObjectHandler全局拦截insert和update操作

自定义MetaObjectHandlerConfig继承MetaObjectHandler,代码如下
MetaObjectHandlerConfig.java

package com.summo.entity;import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;@Configuration
public class MetaObjectHandlerConfig implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {}@Overridepublic void updateFill(MetaObject metaObject) {}
}

逻辑补全的代码如下

package com.summo.entity;import java.util.Calendar;
import java.util.Date;import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.summo.context.GlobalUserContext;
import com.summo.context.UserContext;
import org.apache.ibatis.reflection.MetaObject;@Configuration
public class MetaObjectHandlerConfig implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {//获取用户上下文UserContext userContext = GlobalUserContext.getUserContext();//获取创建时间Date date = Calendar.getInstance().getTime();//设置gmtCreatethis.fillStrategy(metaObject, "gmtCreate", date);//设置gmtModifiedthis.fillStrategy(metaObject, "gmtModified", date);//设置creatorIdthis.fillStrategy(metaObject, "creatorId", userContext.getUserId());//设置modifierIdthis.fillStrategy(metaObject, "modifierId", userContext.getUserId());}@Overridepublic void updateFill(MetaObject metaObject) {//获取用户上下文UserContext userContext = GlobalUserContext.getUserContext();//获取更新时间Date date = Calendar.getInstance().getTime();//更新操作修改gmtModifiedthis.setFieldValByName("gmtModified", date, metaObject);//更新操作修改modifierIdthis.setFieldValByName("modifierId", userContext.getUserId(), metaObject);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/82426.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

阿里云交互式建模(PAI-DSW)训练并微调推理ChatGLM模型

参考内容为《轻量微调和推理ChatGLM模型实践》 点击“交互式建模&#xff08;DSW&#xff09;”&#xff0c;然后选择“创建实例” 写上实例名称&#xff0c;然后选择GPU规格&#xff0c;选择“ecs.gn6v-c8g1.2xlarge(8 vCPU&#xff0c;32GB)” 页面往下拉选择“pytorch:…

Leetcode162. 寻找峰值

力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 峰值元素是指其值严格大于左右相邻值的元素。 给你一个整数数组 nums&#xff0c;找到峰值元素并返回其索引。数组可能包含多个峰值&#xff0c;在这种情况下&#xff0c;返回 任何一个峰值 所在位置即…

物理内存分配

目录 内核物理内存分配接口 内存分配行为&#xff08;物理上&#xff09; 内存分配的行为操作 内存 三个水位线 水线计算 水位线影响内存分配行为 内存分配核心__alloc_pages 释放页 1、内核物理内存分配接口 struct page *alloc_pages(gfp_t gfp, unsigned int ord…

Redis 三种特殊的数据类型 - Geospatial地理位置 - Hyperloglog基数统计的算法 - Bitmaps位图(位存储)

目录 Redis 三种特殊的数据类型&#xff1a; Geospatial&#xff1a;地理位置 Geospatial类型常用的命令&#xff1a; GEOADD&#xff1a;添加地理位置 GEOPOS&#xff1a;获取地理位置 GEODIST&#xff1a;返回两个给定位置之间的距离 GEORADIUS&#xff1a;以给定的经纬…

Seata 源码篇之AT模式启动流程 - 上 - 02

Seata 源码篇之AT模式启动流程 - 02 自动配置两个关键点 初始化初始化TM初始化RM初始化TC 全局事务执行流程TM 发起全局事务GlobalTransactional 注解处理全局事务的开启 TM 和 RM 执行分支事务IntroductionDelegatingIntroductionInterceptorDelegatePerTargetObjectIntroduct…

在华为云服务器上CentOS 7安装单机版Redis

https://redis.io/是官网地址。 点击右上角的Download。 可以进入https://redis.io/download/——Redis官网下载最新版的网址。 然后在https://redis.io/download/页面往下拉&#xff0c;点击下图超链接这里。 进入https://download.redis.io/releases/下载自己需要的安装…

【C语言】自定义类型:结构体【结构体内存具详细】,枚举,联合

目录 一、结构体 1.结构的声明 2.特殊的声明 3.结构的自引用 4.结构体变量的定义和初始化 5.结构体内存对齐&#xff08;重点来了&#xff09; 6.为什么会存在内存对齐 7.修改默认对齐数 8.结构体传参 二、位段 1.什么是位段 2.位段的内存分配 3.位段的跨平台问题…

ajax day4

1、promise链式调用 /*** 目标&#xff1a;把回调函数嵌套代码&#xff0c;改成Promise链式调用结构* 需求&#xff1a;获取默认第一个省&#xff0c;第一个市&#xff0c;第一个地区并展示在下拉菜单中*/let pname axios({url: http://hmajax.itheima.net/api/province,}).t…

21天学会C++:Day11----运算符重载

CSDN的uu们&#xff0c;大家好。这里是C入门的第十一讲。 座右铭&#xff1a;前路坎坷&#xff0c;披荆斩棘&#xff0c;扶摇直上。 博客主页&#xff1a; 姬如祎 收录专栏&#xff1a;C专题 目录 1. 知识引入 2. 运算符重载 2.1 operator<() 2.2 operator() 2.3 o…

jvm中对象创建、内存布局以及访问定位

对象创建 Java语言层面&#xff0c;创建对象通常&#xff08;例外&#xff1a;复制、反序列化&#xff09;仅仅是一个new关键字即可&#xff0c;而在虚拟机中&#xff0c;对象&#xff08;限于普通Java对象&#xff0c;不包括数组和Class对象等&#xff09;的创建又是怎样一个过…

小米华为,化干戈为玉帛!

近日来&#xff0c;手机圈又掀起了各大厂家推出新品的高潮。首先是华为Mate60的推出&#xff0c;其自研的麒麟9000S芯片瞬间点燃了国内手机市场&#xff0c;得到了国内甚至国外业界人士的认可和好评。 而近日网上盛传的小米创始人雷军的“愿意加入华为技术生态圈”的邀请&…

Redis缓存实现及其常见问题解决方案

随着互联网技术的发展&#xff0c;数据处理的速度和效率成为了衡量一个系统性能的重要指标。在众多的数据处理技术中&#xff0c;缓存技术以其出色的性能优化效果&#xff0c;成为了不可或缺的一环。而在众多的缓存技术中&#xff0c;Redis 以其出色的性能和丰富的功能&#xf…

JDK jps命令复习

之前写过jdk命令工具的博文&#xff0c;下面复习jps命令&#xff1b; jps 是 Java Process Status Tool 的简称,它的作用是为了列出所有正在运行中的 Java 虚拟机进程和相关信息&#xff1b; jps 命令参数 -q 只输出进程 ID,省略主类的名称 -m 输出虚拟机进程启动时传递…

AG35学习笔记(一):debug串口抓取模组log、debug串口测试AT指令、echo命令通过串口发送16进制数据

目录 一、概述二、抓取模组log2.1 硬件接口2.2 用户登录2.3 相关指令 三、测试AT指令3.1 查看端口3.2 进入模式 四、串口发16进制echo使用 一、概述 二、抓取模组log 在之前记录了通过USB&#xff0c;使用移远工具Qwinlog来抓取log&#xff08;3.3 抓取模组log&#xff09;。…

【Java】第一个Servlet程序

第一个Servlet程序 创建项目引入依赖手动创建必要的目录/文件编写代码打包程序部署验证程序是否正常工作 创建项目 选中maven 创建好项目后,观察左侧项目结构 引入依赖 当权代码需要使用servlet开发,而Java标准库中并没有servlet,此时就需要让maven能够把servlet的依赖获取…

子网的划分

强化计算机网络发现王道没有这一块的内容&#xff0c;导致做题稀里糊涂。于是个人调研补充。 子网划分是将一个大型IP网络划分成更小的子网&#xff0c;以实现更有效的网络管理和资源分配。 原因&#xff1a; 提高网络性能&#xff1a;子网划分可以减少广播域的大小&#xff…

成集云 | 用友NC集成旺店通ERP(旺店通主管库存)| 解决方案

源系统成集云目标系统 方案介绍 用友NC是用友NC产品的全新系列&#xff0c;是面向集团企业的世界级高端管理软件。它以“全球化集团管控、行业化解决方案、全程化电子商务、平台化应用集成”的管理业务理念而设计&#xff0c;采用J2EE架构和先进开放的集团级开发平台…

bootstrap柵格

.col-xs- 超小屏幕 手机 (<768px) .col-sm- 小屏幕 平板 (≥768px) .col-md- 中等屏幕 桌面显示器 (≥992px) .col-lg- 大屏幕 大桌面显示器 (≥1200px) 分为12个格子 -后面的1代表占12分子1也就是一份 1.中等屏幕 <div class"container-fluid a">&l…

Autojs 小游戏实践-潮玩宇宙开扭蛋

概述 最近在玩潮流宇宙&#xff0c;里面有扭蛋兔的一个玩法&#xff0c;开始有很多蛋&#xff0c;需要我们一个个点开&#xff0c;然后根据装备品质替换分解&#xff0c;潮流提供了自动开扭蛋功能&#xff0c;但是开到品质比自己装备好的时候回暂停&#xff0c;由于个人懒得看…

在Kubernetes上安装和配置Istio:逐步指南,展示如何在Kubernetes集群中安装和配置Istio服务网格

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…