【Mybatis】基于Mybatis插件+注解,实现敏感数据自动加解密

一、介绍

业务场景中经常会遇到诸如用户手机号,身份证号,银行卡号,邮箱,地址,密码等等信息,属于敏感信息,需要保存在数据库中。而很多公司会会要求对数据库中的此类数据进行加密存储。

敏感数据脱敏需要处理的两个问题:

  1. 查询操作,需要对查询的关键字进行加密,同时也要对从库中查到的数据进行解密
  2. 插入和更新操作,需要对插入或者更新的数据进行加密,然后保存到数据库。

二、解决思路

  1. 最简单的方式:对代码中涉及的敏感数据接口在查询和插入、更新时进行加解密。缺点是工作量大,代码侵入多
  2. 在mybatis中进行统一拦截,上层业务调用不需要再考虑敏感数据的加解密问题。

通过查阅资料,发现思路二目前普遍有两种处理方式:

  1. 采用mybatis插件在mybatis SQL执行和查询结果填充操作上进行切入
  2. 使用mybatis框架提供的TypeHandler来实现在持久层处理数据

本文介绍第一种方式,即使用Mybatis的插件,通过拦截器实现敏感数据加解密

三、mybatis插件原理

Mybatis的插件,是采用责任链机制,通过JDK动态代理来实现的。默认情况下,Mybatis允许使用插件来拦截四个对象:

  • Executor:执行CURD操作;
  • StatementHandler:处理sql语句预编译,设置参数等相关工作;
  • ParameterHandler:设置预编译参数用的;
  • ResultSetHandler:处理结果集。
    在这里插入图片描述
    编写插件需要标识拦截方法和实现拦截逻辑。
    标识拦截拦截方法是通过注解org.apache.ibatis.plugin.Intercepts和注解org.apache.ibatis.plugin.Signature实现的。

四、实现

  1. 设置参数时对参数中含有敏感字段的数据进行加密
  2. 对查询返回的结果进行解密处理
    基于上面两种要求,我们只需要对ParameterHandlerResultSetHandler进行切入。

定义特定注解,在切入时只需要检查字段中是否包含该注解来决定是否加解密

加解密注解

包含两个注解:

  1. SensitiveData注解:用在实体类上,表示此类有些字段需要加密,需要结合@EncryptTransaction一起使用
  2. EncryptTransaction注解:用在类的字段上或者方法的参数上,表示该字段或参数需要加密
1. 定义SensitiveData注解
package com.zsx.annotation;import java.lang.annotation.*;
/*** 该注解定义在类上* 插件通过扫描类对象是否包含这个注解来决定是否继续扫描其中的字段注解* 这个注解要配合EncryptTransaction注解* @author zhousx* @create 2023/10/01-22:45**/
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {}
2. 定义EncryptTransaction注解
package com.zsx.annotation;import java.lang.annotation.*;
/*** 该注解有两种使用方式* ①:配合@SensitiveData加在类中的字段上* ②:直接在Mapper中的方法参数上使用* @author zhousx* @create 2023/10/01-22:45**/
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTransaction {}

插件实现

1. 参数插件ParameterInterceptor

切入mybatis设置参数时对敏感数据进行加密
Mybatis插件的使用就是通过实现Mybatis中的Interceptor接口
再配合@Intercepts注解

// 使用mybatis插件时需要定义签名
// type标识需要切入的Handler
// method表示要要切入的方法
// args表示要切入的方法的参数
@Intercepts({
@Signature(type = ParameterHandler.class, method = “setParameters”, args = PreparedStatement.class),
})

上面这个签名就表示:切入ParameterHandler类的setParameters(PreparedStatement preparedStatement)方法

package com.zsx.intercepter;import com.baomidou.mybatisplus.core.MybatisParameterHandler;
import com.zsx.annotation.EncryptTransaction;
import com.zsx.annotation.SensitiveData;
import com.zsx.utils.DBAESUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.sql.PreparedStatement;
import java.util.*;/*** @author zhousx* @create 2023/10/01-22:45**/
@Slf4j
// 注入Spring
@Component
@Intercepts({@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class ParameterInterceptor implements Interceptor {@Autowiredprivate com.zsx.utils.IEncryptUtil IEncryptUtil;@Overridepublic Object intercept(Invocation invocation) throws Throwable {//@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler//若指定ResultSetHandler ,这里则能强转为ResultSetHandlerMybatisParameterHandler parameterHandler = (MybatisParameterHandler) invocation.getTarget();// 获取参数对像,即 mapper 中 paramsType 的实例Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");parameterField.setAccessible(true);//取出实例Object parameterObject = parameterField.get(parameterHandler);// 搜索该方法中是否有需要加密的普通字段List<String> paramNames = searchParamAnnotation(parameterHandler);if (parameterObject != null) {Class<?> parameterObjectClass = parameterObject.getClass();//对类字段进行加密//校验该实例的类是否被@SensitiveData所注解SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);if (Objects.nonNull(sensitiveData)) {//取出当前当前类所有字段,传入加密方法Field[] declaredFields = parameterObjectClass.getDeclaredFields();IEncryptUtil.encrypt(declaredFields, parameterObject);}//如果传参为List类型,对list里的对象进行加密processListParam(parameterObject);// 对普通字段进行加密if (!CollectionUtils.isEmpty(paramNames)) {// 反射获取 BoundSql 对象,此对象包含生成的sql和sql的参数map映射Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql");boundSqlField.setAccessible(true);BoundSql boundSql = (BoundSql) boundSqlField.get(parameterHandler);PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];// 改写参数processParam(parameterObject, paramNames);// 改写的参数设置到原parameterHandler对象parameterField.set(parameterHandler, parameterObject);parameterHandler.setParameters(ps);}}return invocation.proceed();}/*** 如果传参为List类型,对list里的对象判断,是否进行加密* @param parameterObject* @throws IllegalAccessException*/private void processListParam(Object parameterObject) throws IllegalAccessException {// mybatis会把list封装到一个ParamMap中if (parameterObject instanceof Map) {//1. 如果不对传参users使用@Param注解,则会在map中放入collection、list、users三个键值对,这三个指向同一个内存地址即内容相同。if (((Map) parameterObject).containsKey("list")) {Map map = (Map) parameterObject;ArrayList list = (ArrayList) map.get("list");Object element = list.get(0);Class<?> elementClass = element.getClass();SensitiveData tempSensitiveData = AnnotationUtils.findAnnotation(elementClass, SensitiveData.class);if (Objects.nonNull(tempSensitiveData)) {for (Object elementObject : list) {Field[] declaredFields = elementClass.getDeclaredFields();IEncryptUtil.encrypt(declaredFields, elementObject);}}}//2. 如果使用了@Param注解对参数重命名为users,那么map中只会放入users、param1两个键值对,这2个指向同一个内存地址即内容相同。if (((Map) parameterObject).containsKey("param1")) {Map map = (Map) parameterObject;Object param1 = map.get("param1");//如果param1是ArrayList,则转为arrayList。否则不转换if (param1 instanceof ArrayList) {ArrayList list = (ArrayList) param1;Object element = list.get(0);Class<?> elementClass = element.getClass();SensitiveData tempSensitiveData = AnnotationUtils.findAnnotation(elementClass, SensitiveData.class);if (Objects.nonNull(tempSensitiveData)) {for (Object elementObject : list) {Field[] declaredFields = elementClass.getDeclaredFields();IEncryptUtil.encrypt(declaredFields, elementObject);}}}}}}/*** 处理普通参数,对params中的String参数进行加密* @param parameterObject* @param params* @throws Exception*/private void processParam(Object parameterObject, List<String> params) throws Exception {// 处理参数对象  如果是 map 且map的key 中没有 tenantId,添加到参数map中// 如果参数是bean,反射设置值if (parameterObject instanceof Map) {@SuppressWarnings("unchecked")Map<String, String> map = ((Map<String, String>) parameterObject);for (String param : params) {String value = map.get(param);map.put(param, value == null ? null : DBAESUtil.encrypt(value));}
//            parameterObject = map;}}/*** 查找参数的注解是否是含有 @EncryptTransaction注解,如果是,则存储参数名* @param parameterHandler* @return* @throws NoSuchFieldException* @throws ClassNotFoundException* @throws IllegalAccessException*/private List<String> searchParamAnnotation(ParameterHandler parameterHandler) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException {Class<MybatisParameterHandler> handlerClass = MybatisParameterHandler.class;Field mappedStatementFiled = handlerClass.getDeclaredField("mappedStatement");mappedStatementFiled.setAccessible(true);MappedStatement mappedStatement = (MappedStatement) mappedStatementFiled.get(parameterHandler);String methodName = mappedStatement.getId();Class<?> mapperClass = Class.forName(methodName.substring(0, methodName.lastIndexOf('.')));methodName = methodName.substring(methodName.lastIndexOf('.') + 1);Method[] methods = mapperClass.getDeclaredMethods();Method method = null;for (Method m : methods) {if (m.getName().equals(methodName)) {method = m;break;}}List<String> paramNames = null;if (method != null) {Annotation[][] pa = method.getParameterAnnotations();Parameter[] parameters = method.getParameters();for (int i = 0; i < pa.length; i++) {for (Annotation annotation : pa[i]) {if (annotation instanceof EncryptTransaction) {if (paramNames == null) {paramNames = new ArrayList<>();}paramNames.add(parameters[i].getName());}}}}return paramNames;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}
2. 返回值插件ResultSetInterceptor
package com.zsx.intercepter;import com.zsx.annotation.SensitiveData;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Properties;/*** @author zhousx* @create 2023/10/01-22:45**/
@Slf4j
@Component
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class ResultSetInterceptor implements Interceptor {@Autowiredprivate com.zsx.utils.IDecryptUtil IDecryptUtil;@Overridepublic Object intercept(Invocation invocation) throws Throwable {//取出查询的结果Object resultObject = invocation.proceed();if (Objects.isNull(resultObject)) {return null;}//基于selectListif (resultObject instanceof ArrayList) {@SuppressWarnings("unchecked")ArrayList<Objects> resultList = (ArrayList<Objects>) resultObject;if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {for (Object result : resultList) {//逐一解密IDecryptUtil.decrypt(result);}}//基于selectOne} else {if (needToDecrypt(resultObject)) {IDecryptUtil.decrypt(resultObject);}}return resultObject;}private boolean needToDecrypt(Object object) {Class<?> objectClass = object.getClass();SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);return Objects.nonNull(sensitiveData);}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}

加解密工具类

1. 加密接口
package com.zsx.utils;import java.lang.reflect.Field;/*** @author zhousx* @create 2023/10/01-22:45**/
public interface IEncryptUtil {/*** 加密** @param declaredFields 加密字段* @param paramsObject   对象* @param <T>            入参类型* @return 返回加密* @throws IllegalAccessException 不可访问*/<T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException;
}
2. 解密接口
package com.zsx.utils;/*** @author zhousx* @create 2023/10/01-22:45**/
public interface IDecryptUtil {/*** 解密** @param result resultType的实例* @return T* @throws IllegalAccessException 字段不可访问异常*/<T> T decrypt(T result) throws IllegalAccessException;
}
3. 加密实现类
package com.zsx.utils;import com.fasterxml.jackson.databind.ObjectReader;
import com.zsx.annotation.EncryptTransaction;
import org.springframework.stereotype.Component;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Objects;
import java.util.Random;/*** @author zhousx* @create 2023/10/01-22:45**/
@Component
public class EncryptUtilImpl implements IEncryptUtil {@Overridepublic <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException {//取出所有被EncryptTransaction注解的字段for (Field field : declaredFields) {EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class);if (!Objects.isNull(encryptTransaction)) {field.setAccessible(true);Object object = field.get(paramsObject);//暂时只实现String类型的加密if (object instanceof String) {String value = (String) object;//加密try {field.set(paramsObject, DBAESUtil.encrypt(value));} catch (Exception e) {e.printStackTrace();}}}}return paramsObject;}
}
4. 解密实现类
package com.zsx.utils;import com.zsx.annotation.EncryptTransaction;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;
import java.util.Objects;/*** @author zhousx* @create 2023/10/01-22:45**/
@Component
public class DecryptImpl implements IDecryptUtil {/*** 解密** @param result resultType的实例*/@Overridepublic <T> T decrypt(T result) throws IllegalAccessException {//取出resultType的类Class<?> resultClass = result.getClass();Field[] declaredFields = resultClass.getDeclaredFields();for (Field field : declaredFields) {//取出所有被DecryptTransaction注解的字段EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class);if (!Objects.isNull(encryptTransaction)) {field.setAccessible(true);Object object = field.get(result);//String的解密if (object instanceof String) {String value = (String) object;//对注解的字段进行逐一解密try {field.set(result, DBAESUtil.decrypt(value));} catch (Exception e) {e.printStackTrace();}}}}return result;}
}
5. 加解密工具类
package com.zsx.utils;import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;/*** @author zhousx* @create 2023/10/01-22:45**/
public class DBAESUtil {private static final String DEFAULT_V = "6859505890402435";// 自己填写private static final String KEY = "***";private static final String ALGORITHM = "AES";private static SecretKeySpec getKey() {byte[] arrBTmp = DBAESUtil.KEY.getBytes();// 创建一个空的16位字节数组(默认值为0)byte[] arrB = new byte[16];for (int i = 0; i < arrBTmp.length && i < arrB.length; i++) {arrB[i] = arrBTmp[i];}return new SecretKeySpec(arrB, ALGORITHM);}/*** 加密*/public static String encrypt(String content) throws Exception {final Base64.Encoder encoder = Base64.getEncoder();SecretKeySpec keySpec = getKey();Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");IvParameterSpec iv = new IvParameterSpec(DEFAULT_V.getBytes());cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);byte[] encrypted = cipher.doFinal(content.getBytes());return encoder.encodeToString(encrypted);}/*** 解密*/public static String decrypt(String content) throws Exception {final Base64.Decoder decoder = Base64.getDecoder();SecretKeySpec keySpec = getKey();Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");IvParameterSpec iv = new IvParameterSpec(DEFAULT_V.getBytes());cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);byte[] base64 = decoder.decode(content);byte[] original = cipher.doFinal(base64);return new String(original);}}

使用

1. 注解在实体类上
package com.zsx.entity;import com.zsx.annotation.EncryptTransaction;
import com.zsx.annotation.SensitiveData;
import lombok.*;
import java.io.Serializable;/*** @author zhousx*/
@With
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@SensitiveData // 插件只对加了该注解的类进行扫描,只有加了这个注解的类才会生效
public class User implements Serializable {private Integer id;private String name;// 表明对该字段进行加密@EncryptTransactionprivate String email;// 表明对该字段进行加密@EncryptTransactionprivate String phone;}
2. 注解在参数上
package com.zsx.mapper;import com.zsx.annotation.EncryptTransaction;
import com.zsx.annotation.SensitiveData;
import com.zsx.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
public interface IUserDao {/*** 测试查询 普通参数加密* @param phone* @return*/List<User> getUserByPhone(@EncryptTransaction @Param("phone") String phone);/*** 测试插入 普通参数加密,多个需要加密的字段* @param name* @param email* @param phone* @return*/int insertUserByParam(@Param("name") String name, @EncryptTransaction @Param("email") String email, @EncryptTransaction @Param("phone") String phone);
}

完整测试用例

UserController.java

/*** Project Name: test-zsx* File Name: UserController* Package Name: com.zsx.controller* Date: 2023/9/13 11:21* Copyright (c) 2023 天翼数字生活科技有限公司 All Rights Reserved.*/
package com.zsx.controller;import com.alibaba.fastjson2.JSON;
import com.zsx.entity.User;
import com.zsx.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;/*** @description: 测试mybatis拦截器+注解 实现数据的自动加解密功能* @author: zhoushaoxiong* @date: 2023/9/13 11:21*/
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate IUserService userService;/*** 插入一条记录* @param user* @return*/@PostMapping("/add")public String addUser(@RequestBody User user){userService.addUser(user);return "success";}/*** 查询一条记录* @param id* @return*/@PostMapping("/get/one")public String getUser(Long id){User user = userService.getUserById(id);return user.toString();}/*** 查询全部* @return*/@PostMapping("/get/list")public String getUserAll(){List<User> users = userService.getAllUser();return JSON.toJSONString(users);}/*** 通过手机号查询* @param phone* @return*/@PostMapping("/get/phone")public String getUserByPhone(String phone){List<User> users = userService.getUserByPhone(phone);return JSON.toJSONString(users);}/*** 通过对象查询* @param phone* @return*/@PostMapping("/get/user/phone")public String getUserByUserPhone(String phone){List<User> users = userService.getUser(phone);return JSON.toJSONString(users);}/*** 批量插入* @return*/@PostMapping("/add/list")public String addUserList(){List<User> users = userService.addUserList();return JSON.toJSONString(users);}/*** 插入 dao使用@Param注解* @return*/@PostMapping("/add/user/param")public String addUserParam(){int result = userService.addUserByParam();return JSON.toJSONString(result);}
}

IUserDao.java

package com.zsx.mapper;import com.zsx.annotation.EncryptTransaction;
import com.zsx.annotation.SensitiveData;
import com.zsx.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
public interface IUserDao {/*** 测试查询 普通参数 拦截器对结果对象进行解密* @param id* @return*/User getUserById(Long id);/*** 测试插入 传参为对象,拦截器对含有加密注解的对象的属性 进行自动加密* @param user* @return*/int addUser(User user);/*** 测试查询 查询结果为list 需要对list结果里的对象属性进行解密* @return*/List<User> getAllUsers();/*** 测试查询 普通参数加密* @param phone* @return*/List<User> getUserByPhone(@EncryptTransaction @Param("phone") String phone);/*** 测试查询 传参为对象 对象中的phone参数需要拦截器进行加密才能查询* @param user* @return*/List<User> getUserByUser(User user);/*** 测试插入 对list进行加密* @param users* @return*/int insertBatch(List<User> users);/*** 测试插入 使用@Param注解 对list进行加密* @param users* @return*/int insertBatchByParam(@Param("users") List<User> users);/*** 测试插入 普通参数加密,多个需要加密的字段* @param name* @param email* @param phone* @return*/int insertUserByParam(@Param("name") String name, @EncryptTransaction @Param("email") String email, @EncryptTransaction @Param("phone") String phone);
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zsx.mapper.IUserDao"><select id="getUserById" resultType="com.zsx.entity.User">SELECT * FROM user WHERE id = #{id}</select><select id="getAllUsers" resultType="com.zsx.entity.User">select * from user</select><select id="getUserByPhone" resultType="com.zsx.entity.User">select * from user where phone = #{phone}</select><select id="getUserByUser" parameterType="com.zsx.entity.User" resultType="com.zsx.entity.User">select * from user where phone = #{phone}</select><insert id="addUser" parameterType="com.zsx.entity.User">insert into user (name, email, phone) values (#{name}, #{email}, #{phone})</insert><insert id="insertBatch" parameterType="com.zsx.entity.User">insert into user (name, email, phone) values<foreach collection="list"  separator="," item="item">(#{item.name}, #{item.email}, #{item.phone})</foreach></insert><insert id="insertBatchByParam" parameterType="com.zsx.entity.User">insert into user (name, email, phone) values<foreach collection="users"  separator="," item="item">(#{item.name}, #{item.email}, #{item.phone})</foreach></insert><insert id="insertUserByParam">insert into user (name, email, phone) values (#{name}, #{email}, #{phone})</insert></mapper>

引入依赖

    <dependencies><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.2</version></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.26</version></dependency></dependencies>

本文内容主要参考shenyang1026的博客: https://blog.csdn.net/relosy/article/details/123494036

其他参考:

  1. https://blog.csdn.net/relosy/article/details/123494036
  2. https://blog.csdn.net/wtmdcnm/article/details/115211183

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

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

相关文章

什么是大数据,大数据简介

大数据的概念通俗的说法 大数据&#xff0c;按照我的理解比较通俗易懂的是在数据量很多很大的情况下数据处理速度需要足够快&#xff0c;用我们以前传统意义上的的技术比如关系型数据库mysql没办法处理或者处理起来非常复杂&#xff0c;必须有一些新的处理技术也就是大数据处理…

深入理解强化学习——序列决策(Sequential Decision Making)

分类目录&#xff1a;《深入理解联邦学习》总目录 在本文中我们将介绍序列决策&#xff08;Sequential Decision Making&#xff09;过程中的各个过程。 智能体与环境 强化学习研究的问题是智能体与环境交互的问题&#xff0c;下图左边的智能体一直在与下图右边的环境进行交互…

微信小程序 movable-view 控制长按才触发拖动 轻轻滑动页面正常滚动效果

今天写 movable-areamovable-view遇到了个头疼的问题 那就是 movable-view 监听了用户拖拽自己 但 我们小程序 上下滚动页面靠的也是拖拽 也就是说 如果放在这里 用户拖动 movable-view部分 就会永远触发不了滚动 那么 我们先可以 加一个 bindlongpress"longpressHandler…

扫描器(xray和bp联动)

文章目录 分类主动扫描和被动扫描bp与xray联动 分类 扫描器分为对web的扫描器和对主机的扫描器 主动扫描和被动扫描 主动扫描&#xff1a; 输入某个URL&#xff0c;然后由扫描器中的爬虫模块爬取所有链接&#xff0c;对GET、POST等请求进行参数变形和污染&#xff0c;进行重放测…

什么是信创测试?信创测试工具有哪些?

信创全称是“信息技术应用创新”&#xff0c;旨在实现信息技术自主可控&#xff0c;规避外部技术制裁和风险&#xff0c;其涉及产业链包括硬件、基础软件、应用软件、云服务、数据安全等领域。 信创测试是指对信创工程项目中的产品、系统等进行测试和验证&#xff0c;以确保其…

Seata入门系列【4】undo_log、global_table、branch_table、lock_table字段及作用详解

1 客户端 1.1 undo_log 在AT模式中&#xff0c;需要在参与全局事务的数据库中&#xff0c;添加一个undo_log表&#xff0c;建表语句如下&#xff1a; SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS 0;-- ---------------------------- -- Table structure for undo_log -- --…

Go 语言中的反射

今天主要来聊聊 Go 语言中反射&#xff0c;希望对你有新的认知 虽然很多人使用 Go 语言有一定时间了&#xff0c;甚至有的使用了 1 年 2 年&#xff0c;然后对于 Go 语言中的反射还是模棱两可&#xff0c;使用起来的时候&#xff0c;心里也不是非常有底气 更有甚者&#xff0…

深度学习_3_张量运算

代码&#xff1a; import torchimport osimport pandas as pd import numpy as npx torch.tensor([[1, 2, 3], [4, 5, 6]]) print(x) sumA x.sum(dim 0) print(sumA) print(torch.sum(x, dim 0, keepdim True)) print(x/torch.sum(x, dim 0, keepdim True) )

如何在会计面试中展现自己的优势?

在会计面试中展现自己的优势是非常重要的&#xff0c;因为这将决定你是否能够脱颖而出并获得这个职位。下面是一些可以帮助你展示自己优势的方法&#xff1a; 1. 准备充分&#xff1a;在面试前&#xff0c;确保你对公司的背景和业务有所了解。研究公司的财务报告和新闻&#xf…

【LeetCode】2.两数相加

目录 1 题目2 答案2.1 我写的&#xff08;不对&#xff09;2.2 更正 3 问题 1 题目 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返…

[LitCTF 2023]导弹迷踪

这道题相较于其他的分数类型的js题有一点不一样&#xff0c;他不是像常规的有用bp多次抓包修改最后得分来获取flag的。 本题将flag藏到了他的前端文件中本身没有任何难度&#xff0c;只是为了记录一种新的做法 按照我们平常做js的思路就是先随便玩一下然后bp抓包看得分或者抓包…

各大自动化测试框架对比

自动化测试是把以人为驱动的测试行为转化为机器执行的一种过程&#xff0c;主要是编写代码、脚本&#xff0c;让软件自动运行&#xff0c;发现缺陷&#xff0c;代替部分的手工测试&#xff1b;自动化测试可分为自动化性能测试、自动化功能测试&#xff0c;自动化功能测试包括了…

C#,工业化软件与院校软件的对比及编程语言的选择建议

飞机发动之之一&#xff0c;涡轮喷气航空发动机&#xff08;JET ENGINE&#xff09; 火箭发动机之一&#xff0c;俄罗斯RD-180煤油和液氧发动机&#xff08;ROCKET ENGINE&#xff09; 1 飞机发动机与火箭发动机的简明对比 2 工业软件与院校软件的简单对比 除了以上类似的对比…

地下城堡3魂之诗食谱,地下城堡3菜谱37种

地下城堡3魂之诗食谱大全&#xff0c;让你解锁制作各种美食的方法&#xff01;不同的食材搭配不同的配方制作&#xff0c;食物效果和失效也迥异。但有时候我们可能会不知道如何制作这些食物&#xff0c;下面为您介绍地下城堡3菜谱37种。 关注【娱乐天梯】&#xff0c;获取内部福…

【Redis】Hash 哈希内部编码方式

Hash 哈希内部编码方式 哈希的内部编码有两种&#xff1a; ziplist&#xff08;压缩列表&#xff09;&#xff1a;当哈希类型元素个数⼩于hash-max-ziplist-entries配置&#xff08;默认512个&#xff09;、同时所有值都⼩于hash-max-ziplist-value配置&#xff08;默认64字节…

HDMI 基于 4 层 PCB 的布线指南

HDMI 基于 4 层 PCB 的布线指南 简介 HDMI 规范文件里面规定其差分线阻抗要求控制在 100Ω 15%&#xff0c;其中 Rev.1.3a 里面规定相对放宽了一些&#xff0c;容忍阻抗失控在 100Ω 25%范围内&#xff0c;不要超过 250ps。 通常&#xff0c;在 PCB 设计时&#xff0c;注意控…

初学vue,想自己找个中长期小型项目练练手,应该做什么?

前言 可以试着做一两个完整的后台管理项目后再去做其他的&#xff0c;下面推荐一些github上的vue后台管理的项目&#xff0c;可以自己选择性的练一下手 Vue2 1、iview-admin Star: 16.4k 基于 iview组件库开发的一款后台管理系统框架&#xff0c;提供了一系列的强大组件和基…

10、SpringBoot_测试用例

四、测试用例 1.准备工作 添加依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional></dependency><dependency><groupId>com…

RabbitMQ详细使用

工作队列 注意事项&#xff1a;一个消息只能被处理一次&#xff0c;不可以处理多次 轮询分发信息 消息应答 消费者在接收到消息并且处理该消息之后&#xff0c;告诉rabbitmq它已经处理了&#xff0c;rabbitmq可以把该消息删除了。倘若mq没有收到应答&#xff0c;mq会将消息转…

uniapp上echarts地图钻取

1: 预期效果 通过切换地图 , 实现地图的钻取效果 2: 实现原理以及核心方法/参数 一开始是想利用更换地图数据的形式进行地图钻取 , 这就意味着我们需要准备全国30多个省份的地图数据 , 由于一开始考虑需要适配小程序端 , 如此多的地图文件增加了程序的体积 , 如果使用接口调…