Spring Boot 接口加解密
1. 介绍
在我们日常的Java开发中,免不了和其他系统的业务交互,或者微服务之间的接口调用
如果我们想保证数据传输的安全,对接口出参加密,入参解密。
但是不想写重复代码,我们可以提供一个通用starter,提供通用加密解密功能
2. 前置知识
2.1 hutool-crypto加密解密工具
hutool-crypto提供了很多加密解密工具,包括对称加密,非对称加密,摘要加密等等,这不做详细介绍。
2.2 request流只能读取一次的问题
2.2.1 问题:
在接口调用链中,request的请求流只能调用一次,处理之后,如果之后还需要用到请求流获取数据,就会发现数据为空。
比如使用了filter或者aop在接口处理之前,获取了request中的数据,对参数进行了校验,那么之后就不能在获取request请求流了。
2.2.2 解决办法
继承HttpServletRequestWrapper,将请求中的流copy一份,复写getInputStream和getReader方法供外部使用。每次调用后的getInputStream方法都是从复制出来的二进制数组中进行获取,这个二进制数组在对象存在期间一致存在。
使用Filter过滤器,在一开始,替换request为自己定义的可以多次读取流的request。
这样就实现了流的重复获取
InputStreamHttpServletRequestWrapper
package xyz.hlh.cryptotest.utils;import org.apache.commons.io.IOUtils;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;/*** 请求流支持多次获取*/
public class InputStreamHttpServletRequestWrapper extends HttpServletRequestWrapper {/*** 用于缓存输入流*/private ByteArrayOutputStream cachedBytes;public InputStreamHttpServletRequestWrapper(HttpServletRequest request) {super(request);}@Overridepublic ServletInputStream getInputStream() throws IOException {if (cachedBytes == null) {// 首次获取流时,将流放入 缓存输入流 中cacheInputStream();}// 从 缓存输入流 中获取流并返回return new CachedServletInputStream(cachedBytes.toByteArray());}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}/*** 首次获取流时,将流放入 缓存输入流 中*/private void cacheInputStream() throws IOException {// 缓存输入流以便多次读取。为了方便, 我使用 org.apache.commons IOUtilscachedBytes = new ByteArrayOutputStream();IOUtils.copy(super.getInputStream(), cachedBytes);}/*** 读取缓存的请求正文的输入流* <p>* 用于根据 缓存输入流 创建一个可返回的*/public static class CachedServletInputStream extends ServletInputStream {private final ByteArrayInputStream input;public CachedServletInputStream(byte[] buf) {// 从缓存的请求正文创建一个新的输入流input = new ByteArrayInputStream(buf);}@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener listener) {}@Overridepublic int read() throws IOException {return input.read();}}}
HttpServletRequestInputStreamFilter
package xyz.hlh.cryptotest.filter;import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import xyz.hlh.cryptotest.utils.InputStreamHttpServletRequestWrapper;import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;/*** @author HLH* @description:* 请求流转换为多次读取的请求流 过滤器* @email 17703595860@163.com* @date : Created in 2022/2/4 9:58*/
@Component
@Order(HIGHEST_PRECEDENCE + 1) // 优先级最高
public class HttpServletRequestInputStreamFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {// 转换为可以多次获取流的requestHttpServletRequest httpServletRequest = (HttpServletRequest) request;InputStreamHttpServletRequestWrapper inputStreamHttpServletRequestWrapper = new InputStreamHttpServletRequestWrapper(httpServletRequest);// 放行chain.doFilter(inputStreamHttpServletRequestWrapper, response);}
}
2.3 SpringBoot的参数校验validation
为了减少接口中,业务代码之前的大量冗余的参数校验代码
SpringBoot-validation提供了优雅的参数校验,入参都是实体类,在实体类字段上加上对应注解,就可以在进入方法之前,进行参数校验,如果参数错误,会抛出错误BindException,是不会进入方法的。
这种方法,必须要求在接口参数上加注解@Validated或者是@Valid
但是很多清空下,我们希望在代码中调用某个实体类的校验功能,所以需要如下工具类。
ParamException
package xyz.hlh.cryptotest.exception;import lombok.Getter;import java.util.List;/*** @author HLH* @description 自定义参数异常* @email 17703595860@163.com* @date Created in 2021/8/10 下午10:56*/
@Getter
public class ParamException extends Exception {private final List<String> fieldList;private final List<String> msgList;public ParamException(List<String> fieldList, List<String> msgList) {this.fieldList = fieldList;this.msgList = msgList;}
}
ValidationUtils
package xyz.hlh.cryptotest.utils;import xyz.hlh.cryptotest.exception.CustomizeException;
import xyz.hlh.cryptotest.exception.ParamException;import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;/*** @author HLH* @description 验证工具类* @email 17703595860@163.com* @date Created in 2021/8/10 下午10:56*/
public class ValidationUtils {private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();/*** 验证数据* @param object 数据*/public static void validate(Object object) throws CustomizeException {Set<ConstraintViolation<Object>> validate = VALIDATOR.validate(object);// 验证结果异常throwParamException(validate);}/*** 验证数据(分组)* @param object 数据* @param groups 所在组*/public static void validate(Object object, Class<?> ... groups) throws CustomizeException {Set<ConstraintViolation<Object>> validate = VALIDATOR.validate(object, groups);// 验证结果异常throwParamException(validate);}/*** 验证数据中的某个字段(分组)* @param object 数据* @param propertyName 字段名称*/public static void validate(Object object, String propertyName) throws CustomizeException {Set<ConstraintViolation<Object>> validate = VALIDATOR.validateProperty(object, propertyName);// 验证结果异常throwParamException(validate);}/*** 验证数据中的某个字段(分组)* @param object 数据* @param propertyName 字段名称* @param groups 所在组*/public static void validate(Object object, String propertyName, Class<?> ... groups) throws CustomizeException {Set<ConstraintViolation<Object>> validate = VALIDATOR.validateProperty(object, propertyName, groups);// 验证结果异常throwParamException(validate);}/*** 验证结果异常* @param validate 验证结果*/private static void throwParamException(Set<ConstraintViolation<Object>> validate) throws CustomizeException {if (validate.size() > 0) {List<String> fieldList = new LinkedList<>();List<String> msgList = new LinkedList<>();for (ConstraintViolation<Object> next : validate) {fieldList.add(next.getPropertyPath().toString());msgList.add(next.getMessage());}throw new ParamException(fieldList, msgList);}}}
2.5 自定义starter
自定义starter步骤
-
创建工厂,编写功能代码
-
声明自动配置类,把需要对外提供的对象创建好,通过配置类统一向外暴露
-
在resource目录下准备一个名为
spring/spring.factories的文件,以org.springframework.boot.autoconfigure.EnableAutoConfiguration为key,自动配置类为value列表,进行注册
2.6 RequestBodyAdvice和ResponseBodyAdvice
-
RequestBodyAdvice是对请求的json串进行处理, 一般使用环境是处理接口参数的自动解密 -
ResponseBodyAdvice是对请求相应的jsoin传进行处理,一般用于相应结果的加密
3. 功能介绍
接口相应数据的时候,返回的是加密之后的数据 接口入参的时候,接收的是解密之后的数据,但是在进入接口之前,会自动解密,取得对应的数据
4. 功能细节
加密解密使用对称加密的AES算法,使用hutool-crypto模块进行实现
所有的实体类提取一个公共父类,包含属性时间戳,用于加密数据返回之后的实效性,如果超过60分钟,那么其他接口将不进行处理。
如果接口加了加密注解EncryptionAnnotation,并且返回统一的json数据Result类,则自动对数据进行加密。如果是继承了统一父类RequestBase的数据,自动注入时间戳,确保数据的时效性
如果接口加了解密注解DecryptionAnnotation,并且参数使用RequestBody注解标注,传入json使用统一格式RequestData类,并且内容是继承了包含时间长的父类RequestBase,则自动解密,并且转为对应的数据类型
功能提供Springboot的starter,实现开箱即用
5. 代码实现
5.1 项目结构

5.2 crypto-common
5.2.1 结构

5.3 crypto-spring-boot-starter
5.3.1 接口

5.3.2 重要代码
crypto.properties AES需要的参数配置
# 模式 cn.hutool.crypto.Mode
crypto.mode=CTS
# 补码方式 cn.hutool.crypto.Mode
crypto.padding=PKCS5Padding
# 秘钥
crypto.key=testkey123456789
# 盐
crypto.iv=testiv1234567890
spring.factories 自动配置文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
xyz.hlh.crypto.config.AppConfig
CryptConfig AES需要的配置参数
package xyz.hlh.crypto.config;import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;import java.io.Serializable;/*** @author HLH* @description: AES需要的配置参数* @email 17703595860@163.com* @date : Created in 2022/2/4 13:16*/
@Configuration
@ConfigurationProperties(prefix = "crypto")
@PropertySource("classpath:crypto.properties")
@Data
@EqualsAndHashCode
@Getter
public class CryptConfig implements Serializable {private Mode mode;private Padding padding;private String key;private String iv;}
AppConfig 自动配置类
package xyz.hlh.crypto.config;import cn.hutool.crypto.symmetric.AES;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;/*** @author HLH* @description: 自动配置类* @email 17703595860@163.com* @date : Created in 2022/2/4 13:12*/
@Configuration
public class AppConfig {@Resourceprivate CryptConfig cryptConfig;@Beanpublic AES aes() {return new AES(cryptConfig.getMode(), cryptConfig.getPadding(), cryptConfig.getKey().getBytes(StandardCharsets.UTF_8), cryptConfig.getIv().getBytes(StandardCharsets.UTF_8));}}
DecryptRequestBodyAdvice 请求自动解密
package xyz.hlh.crypto.advice;import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import xyz.hlh.crypto.annotation.DecryptionAnnotation;
import xyz.hlh.crypto.common.exception.ParamException;
import xyz.hlh.crypto.constant.CryptoConstant;
import xyz.hlh.crypto.entity.RequestBase;
import xyz.hlh.crypto.entity.RequestData;
import xyz.hlh.crypto.util.AESUtil;import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Type;/*** @author HLH* @description: requestBody 自动解密* @email 17703595860@163.com* @date : Created in 2022/2/4 15:12*/
@ControllerAdvice
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;/*** 方法上有DecryptionAnnotation注解的,进入此拦截器* @param methodParameter 方法参数对象* @param targetType 参数的类型* @param converterType 消息转换器* @return true,进入,false,跳过*/@Overridepublic boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {return methodParameter.hasMethodAnnotation(DecryptionAnnotation.class);}@Overridepublic HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {return inputMessage;}/*** 转换之后,执行此方法,解密,赋值* @param body spring解析完的参数* @param inputMessage 输入参数* @param parameter 参数对象* @param targetType 参数类型* @param converterType 消息转换类型* @return 真实的参数*/@SneakyThrows@Overridepublic Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {// 获取requestRequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;if (servletRequestAttributes == null) {throw new ParamException("request错误");}HttpServletRequest request = servletRequestAttributes.getRequest();// 获取数据ServletInputStream inputStream = request.getInputStream();RequestData requestData = objectMapper.readValue(inputStream, RequestData.class);if (requestData == null || StringUtils.isBlank(requestData.getText())) {throw new ParamException("参数错误");}// 获取加密的数据String text = requestData.getText();// 放入解密之前的数据request.setAttribute(CryptoConstant.INPUT_ORIGINAL_DATA, text);// 解密String decryptText = null;try {decryptText = AESUtil.decrypt(text);} catch (Exception e) {throw new ParamException("解密失败");}if (StringUtils.isBlank(decryptText)) {throw new ParamException("解密失败");}// 放入解密之后的数据request.setAttribute(CryptoConstant.INPUT_DECRYPT_DATA, decryptText);// 获取结果Object result = objectMapper.readValue(decryptText, body.getClass());// 强制所有实体类必须继承RequestBase类,设置时间戳if (result instanceof RequestBase) {// 获取时间戳Long currentTimeMillis = ((RequestBase) result).getCurrentTimeMillis();// 有效期 60秒long effective = 60*1000;// 时间差long expire = System.currentTimeMillis() - currentTimeMillis;// 是否在有效期内if (Math.abs(expire) > effective) {throw new ParamException("时间戳不合法");}// 返回解密之后的数据return result;} else {throw new ParamException(String.format("请求参数类型:%s 未继承:%s", result.getClass().getName(), RequestBase.class.getName()));}}/*** 如果body为空,转为空对象* @param body spring解析完的参数* @param inputMessage 输入参数* @param parameter 参数对象* @param targetType 参数类型* @param converterType 消息转换类型* @return 真实的参数*/@SneakyThrows@Overridepublic Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {String typeName = targetType.getTypeName();Class<?> bodyClass = Class.forName(typeName);return bodyClass.newInstance();}
}
EncryptResponseBodyAdvice 相应自动加密
package xyz.hlh.crypto.advice;import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
import xyz.hlh.crypto.annotation.EncryptionAnnotation;
import xyz.hlh.crypto.common.entity.Result;
import xyz.hlh.crypto.common.exception.CryptoException;
import xyz.hlh.crypto.entity.RequestBase;
import xyz.hlh.crypto.util.AESUtil;import java.lang.reflect.Type;/*** @author HLH* @description:* @email 17703595860@163.com* @date : Created in 2022/2/4 15:12*/
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Result<?>> {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {ParameterizedTypeImpl genericParameterType = (ParameterizedTypeImpl)returnType.getGenericParameterType();// 如果直接是Result,则返回if (genericParameterType.getRawType() == Result.class && returnType.hasMethodAnnotation(EncryptionAnnotation.class)) {return true;}if (genericParameterType.getRawType() != ResponseEntity.class) {return false;}// 如果是ResponseEntity<Result>for (Type type : genericParameterType.getActualTypeArguments()) {if (((ParameterizedTypeImpl) type).getRawType() == Result.class && returnType.hasMethodAnnotation(EncryptionAnnotation.class)) {return true;}}return false;}@SneakyThrows@Overridepublic Result<?> beforeBodyWrite(Result<?> body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 加密Object data = body.getData();// 如果data为空,直接返回if (data == null) {return body;}// 如果是实体,并且继承了Request,则放入时间戳if (data instanceof RequestBase) {((RequestBase)data).setCurrentTimeMillis(System.currentTimeMillis());}String dataText = JSONUtil.toJsonStr(data);// 如果data为空,直接返回if (StringUtils.isBlank(dataText)) {return body;}// 如果位数小于16,报错if (dataText.length() < 16) {throw new CryptoException("加密失败,数据小于16位");}String encryptText = AESUtil.encryptHex(dataText);return Result.builder().status(body.getStatus()).data(encryptText).message(body.getMessage()).build();}
}
5.4 crypto-test
5.4.1 结构

5.4.2 重要代码
application.yml 配置文件
spring:mvc:format:date-time: yyyy-MM-dd HH:mm:ssdate: yyyy-MM-dd# 日期格式化jackson:date-format: yyyy-MM-dd HH:mm:ss
Teacher 实体类
package xyz.hlh.crypto.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Range;import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;/*** @author HLH* @description: Teacher实体类,使用SpringBoot的validation校验* @email 17703595860@163.com* @date : Created in 2022/2/4 10:21*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Teacher extends RequestBase implements Serializable {@NotBlank(message = "姓名不能为空")private String name;@NotNull(message = "年龄不能为空")@Range(min = 0, max = 150, message = "年龄不合法")private Integer age;@NotNull(message = "生日不能为空")private Date birthday;}
TestController 测试Controller
package xyz.hlh.crypto.controller;import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import xyz.hlh.crypto.annotation.DecryptionAnnotation;
import xyz.hlh.crypto.annotation.EncryptionAnnotation;
import xyz.hlh.crypto.common.entity.Result;
import xyz.hlh.crypto.common.entity.ResultBuilder;
import xyz.hlh.crypto.entity.Teacher;/*** @author HLH* @description: 测试Controller* @email 17703595860@163.com* @date : Created in 2022/2/4 9:16*/
@RestController
public class TestController implements ResultBuilder {/*** 直接返回对象,不加密* @param teacher Teacher对象* @return 不加密的对象*/@PostMapping("/get")public ResponseEntity<Result<?>> get(@Validated @RequestBody Teacher teacher) {return success(teacher);}/*** 返回加密后的数据* @param teacher Teacher对象* @return 返回加密后的数据 ResponseBody<Result>格式*/@PostMapping("/encrypt")@EncryptionAnnotationpublic ResponseEntity<Result<?>> encrypt(@Validated @RequestBody Teacher teacher) {return success(teacher);}/*** 返回加密后的数据* @param teacher Teacher对象* @return 返回加密后的数据 Result格式*/@PostMapping("/encrypt1")@EncryptionAnnotationpublic Result<?> encrypt1(@Validated @RequestBody Teacher teacher) {return success(teacher).getBody();}/*** 返回解密后的数据* @param teacher Teacher对象* @return 返回解密后的数据*/@PostMapping("/decrypt")@DecryptionAnnotationpublic ResponseEntity<Result<?>> decrypt(@Validated @RequestBody Teacher teacher) {return success(teacher);}}
相关文章:
Spring Boot 接口加解密
1. 介绍 在我们日常的Java开发中,免不了和其他系统的业务交互,或者微服务之间的接口调用 如果我们想保证数据传输的安全,对接口出参加密,入参解密。 但是不想写重复代码,我们可以提供一个通用starter,提…...
大公司为什么禁止SpringBoot项目使用Tomcat?
前言 在SpringBoot框架中,我们使用最多的是Tomcat,这是SpringBoot默认的容器技术,而且是内嵌式的Tomcat。同时,SpringBoot也支持Undertow容器,我们可以很方便的用Undertow替换Tomcat,而Undertow的性能和内…...
2023年第十三届MathorCup高校数学建模挑战赛|A题|量子计算机在信用评分卡组合优化中的应用
目录 题目详情 最终收入 贷款利息收入 - 坏账损失 赛题说明 1:流程简化及示例 赛题说明 2:QUBO 模型简介 赛题说明 3:赛题数据 问题 题目详情 在银行信用卡或相关的贷款等业务中,对客户授信之前,需…...
linux下搭建Hbase分布式数据库
文章目录 Hbase概念1.安装Hbase1.jdk的配置2.安装hbase 2.启动和操作1.启动服务2 **web-ui访问地址:http://node01:16010/master-status** 3.简单的操作1.连接 HBase2.帮助命令3.创建一张表 create a table4.使用查看表是否存在5.describe 查看表描述6.put命令插入数据到表7. s…...
unity,射手游戏
文章目录 介绍一,制作玩家具体函数脚本PlayerCharacter三、 制作玩家控制脚本 PlayerController,调用上面的函数方法四、 制作子弹脚本 shell五、 给玩家挂载脚本六、 制作坦克脚本七、 给坦克添加组件八、 开始游戏,播放动画九、 下载 介绍 …...
摒弃单一变现手段,开拓多元商业模式,破解场景单一APP盈利难题!
工具类APP已成为人们生活、工作中不可或缺的一部分,包括天气服务、搜索、日历等细分领域,在用户中存在巨大的市场需求。但是,这类APP也面临着一些难以避免的问题。 比如功能单一、用户停留时间较短、可替代性强等,这些问题会影响…...
JavaEE-轻松了解网络原理之TCP协议
目录 TCP协议TCP协议数据格式TCP原理确认应答超时重传连接管理三次握手四次挥手 滑动窗口流量控制拥塞控制延迟应答捎带应答面向字节流异常问题 TCP协议 TCP,即Transmission Control Protocol,传输控制协议. TCP协议数据格式 16位源端口号与16位目的端…...
薪资17K是一个怎样的水平?来看看98年测试工程师的面试全过程…
我的情况 大概介绍一下个人情况,男,本科,三年多测试工作经验,懂python,会写脚本,会selenium,会性能,然而到今天都没有收到一份offer!从年后就开始准备简历,年…...
OpenCV3 和 Qt5 计算机视觉:11~12
原文:Computer Vision with OpenCV 3 and Qt5 协议:CC BY-NC-SA 4.0 译者:飞龙 本文来自【ApacheCN 计算机视觉 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。 当别人说你没有底线的时候,…...
R包编写流程
文章目录 所需工具Step 1: 创建R项目Step 2: 在R文件夹中添加函数Step 3: 编辑元数据Step 4: 文档化Step 5: 检查包Step 6: 打包重要参考: 所需工具 R包的编写需要的工具包有:devtools,Rtools Step 1: 创建R项目 devtools::create_package…...
试验GPT写文章书
试验GPT写文章书 写一本名叫《寻找人生目标的十种方法》 回答 2023/4/22 16:12:31 很高兴为您提供以下内容,这是一本关于寻找人生目标的十种方法的建议和思考。 《寻找人生目标的十种方法》 第一章:明确自己的价值观 了解自己内心真正想要追求的东…...
class与typename的异同
一、class与typename的相同点 typename关键字常用于函数模板,这里首先引入函数模板的概念:函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定 类型版本 //函数模板格式…...
OpenCV 图像处理学习手册:6~7
原文:Learning Image Processing with OpenCV 协议:CC BY-NC-SA 4.0 译者:飞龙 本文来自【ApacheCN 计算机视觉 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。 当别人说你没有底线的时候,…...
Java中的序列化与反序列化(一)
1、概述 大家好,我是欧阳方超。今天来看一下Java序列化与反序列化的问题。 2、序列化与反序列化 2.1、序列化与反序列化的概念 在Java中,序列化是将对象转换为可存储或传输的格式(一般为字节流)的过程,序列化后的字…...
3.函数、结构体、包
一、函数定义和调用 package mainimport ("fmt" )func test() {fmt.Println("hello world") } func main() {test() }二、函数的参数 1.单个参数 func test(n int) {fmt.Println("传递进来的参数是", n) } func main() {test(10) }2.多个参数…...
科普丨关于 A/B 测试的十问十答
你想知道的,都在这里!本文是神策数据「十问十答」科普系列文章的第二期,围绕 A/B 测试展开。 1 Q:什么是 A/B 测试? A:A/B 测试作为互联网企业的核心增长手段之一,其价值已在实际应用中被多次验…...
尚融宝——整合OpenFeign与Sentinel实现兜底方法——验证手机号码是否注册功能
一、整合过程 在项目添加依赖:添加位置 <!--服务调用--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency> 在需要的服务中添加启动注…...
几种常见的激活函数
文章目录 常见的激活函数介绍Sigmoid函数ReLU函数LeakyReLU函数Tanh函数Softmax函数总结 常见的激活函数介绍 激活函数是神经网络中的重要组成部分,它决定了神经元的输出。在神经网络的前向传播中,输入数据被传递给神经元,经过加权和和激活函…...
MySQL-数据库,数据表的基本操作
数据库,数据表的基本操作 1 数据库的基本操作1.1 创建数据库1.2 删除数据库 2 数据表的基本操作2.1 创建数据表2.1.1 主键约束2.1.2 外键约束2.1.3 非空约束2.1.4 唯一性约束2.1.5 默认约束2.1.6 自增列 2.2 查看数据表结构2.2.1 查看表基本结构语句DESC2.2.2 查看表…...
IC-14W网络IC卡读写器_银河麒麟桌面操作系统V10适配测试报告
银河麒麟操作系统产品NeoCertify 认证测试报告 系统版本:银河麒麟桌面操作系统V10 厂商名称: 广州荣士电子有限公司 认证产品:IC-14W网络IC卡读写器 测试日期: 2022-11-04 …...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以? 在 Golang 的面试中,map 类型的使用是一个常见的考点,其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...
中南大学无人机智能体的全面评估!BEDI:用于评估无人机上具身智能体的综合性基准测试
作者:Mingning Guo, Mengwei Wu, Jiarun He, Shaoxian Li, Haifeng Li, Chao Tao单位:中南大学地球科学与信息物理学院论文标题:BEDI: A Comprehensive Benchmark for Evaluating Embodied Agents on UAVs论文链接:https://arxiv.…...
376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...
Python爬虫(二):爬虫完整流程
爬虫完整流程详解(7大核心步骤实战技巧) 一、爬虫完整工作流程 以下是爬虫开发的完整流程,我将结合具体技术点和实战经验展开说明: 1. 目标分析与前期准备 网站技术分析: 使用浏览器开发者工具(F12&…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP
编辑-虚拟网络编辑器-更改设置 选择桥接模式,然后找到相应的网卡(可以查看自己本机的网络连接) windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置,选择刚才配置的桥接模式 静态ip设置: 我用的ubuntu24桌…...
iview框架主题色的应用
1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题,无需引入,直接可…...
JDK 17 序列化是怎么回事
如何序列化?其实很简单,就是根据每个类型,用工厂类调用。逐个完成。 没什么漂亮的代码,只有有效、稳定的代码。 代码中调用toJson toJson 代码 mapper.writeValueAsString ObjectMapper DefaultSerializerProvider 一堆实…...
归并排序:分治思想的高效排序
目录 基本原理 流程图解 实现方法 递归实现 非递归实现 演示过程 时间复杂度 基本原理 归并排序(Merge Sort)是一种基于分治思想的排序算法,由约翰冯诺伊曼在1945年提出。其核心思想包括: 分割(Divide):将待排序数组递归地分成两个子…...
