ASM 字节码插桩:隐私合规方法检测!
1.前言
近两年来工信部对于应用的隐私合规安全问题愈加重视,对 Android 平台的管控程度也要比 IOS 平台严格很多,很多不合规的应用也先后被下架要求整改。笔者就曾遇到过加班整改隐私合规的问题,隐私合规问题主要针对两个方面。
在用户同意隐私协议之前不能收集用户隐私数据,例如 IMEI、AndroidId、MAC 等
在用户同意隐私协议之后,收集用户数据行为在对应场景不能超频。比如一分钟不能超过 3 次获取 IMEI
针对上述两个方面,有以下措施来针对
通过静态扫描,收集项目中(自有代码 + 三方 sdk)使用隐私合规相关 api 的相关代码
通过 ASM 插桩,在调用隐私合规 api 之前插入代码,记录运行时的方法调用链和当前时间
hook 隐私合规 api,替换字节码指令将调用链指向工具类,在未同意隐私协议之前,不调用相关的 api
2.实现
2.1 注解和工具类
通过定义注解和工具类,用来定义要处理哪些隐私合规相关的方法,目前笔者已经处理了大部分,请放心食用
2.1.1 注解
/*** 收集和注解匹配的方法* visitMethodInsn(int opcode, String owner, String name,String desc)** ======如果 originName 和 originDesc 传"",逻辑会在插件中处理=====*/
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD})
public @interface AsmMethodReplace {/*** 指令操作码*/int targetMethodOpcode();/*** 方法所有者类*/String targetClass();/*** 方法名称*/String targetName() default "";/*** 方法描述符*/String targetDesc() default "";/*** 是否进行 hook*/boolean hook() default false;}
该注解用来匹配调用隐私合规 api 的字节码指令,例如通过 ASM 调用 getImei 的字节码指令为methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "android/telephony/TelephonyManager", "getImei", "()Ljava/lang/String;", false);。targetName 属性和 targetDesc 可以不赋值,这里做了取巧的处理,会根据工具类的 method name 和method descriptor 推断出调用隐私合规方法的字节码指令,这块后面插件会处理。最后的 hook 属性表示是否 hook 掉原始的调用链,将调用指向工具类中的方法。
2.1.2 工具类
上述注解可以用在任何地方,笔者将常用的隐私合规的方法聚合起来,方法工具类中统一处理,例如处理 IMEI 的逻辑如下:
@RequiresApi(api = Build.VERSION_CODES.O)
@AsmMethodReplace(targetMethodOpcode = OPCODE_INVOKEVIRTUAL, targetClass = CLASS_NAME_TELEPHONYMANAGER,hook = true)
public static String getImei(TelephonyManager telephonyManager) {if (!checkAgreePrivacy("getImei")) {Log.e(TAG, TIP);return "";}if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {Log.i(TAG, "getImei-SDK_INT above android Q");return "";}return telephonyManager.getImei();
}
如果还没有同意隐私协议,直接 return “”,否者走正常的调用方法。同时,通过工具类
@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base);ConfigGlobal.getInstance().setStoreDirectory(base.getExternalCacheDir().getAbsolutePath());}
设置存储调用堆栈和时间的文件路径。
2.2 插件处理
gradle 插件的基本使用在这里就不赘述了,这里主要是两方面的处理
编译时扫描代码,处理有定义特定注解的方法,分析字节码指令,收集所有需要处理的隐私合规相关 api 相关的信息
再次扫描,根据第一次扫描收集到的信息,判断当前类是否含有调用隐私合规 api 的字节码指令,如果有,在该类中注入一个写文件方法及在隐私合规 api 调用指令之前插入写文件的字节码指令,用来记录调用堆栈和频次。
2.2.1 模版代码
下面这块代码是我们在自定义 gradle 插件时常用的模版代码,供大家使用
abstract class BaseTransform extends Transform {AbstractExecutorService executorService = ForkJoinPool.commonPool()private List<Callable<Void>> taskList = new ArrayList<>()protected Project projectBaseTransform(Project project) {this.project = project}@OverrideString getName() {return getClass().simpleName}@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS}@OverrideSet<? super QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT}@Overrideboolean isIncremental() {return true}@Overridevoid transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {super.transform(transformInvocation)println("transform start--------------->")if (firstTransform()) {printCopyRight()}onTransformStart(transformInvocation)def startTime = System.currentTimeMillis()def inputs = transformInvocation.inputsdef outputProvider = transformInvocation.outputProviderdef context = transformInvocation.contextdef isIncremental = transformInvocation.isIncremental()if (!isIncremental) {outputProvider.deleteAll()}//1
// inputs.each { input ->
// input.jarInputs.each { JarInput jarInput ->
// forEachJar(jarInput, outputProvider, context, isIncremental)
// }
//
// input.directoryInputs.each { DirectoryInput dirInput ->
// forEachDir(dirInput, outputProvider, context, isIncremental)
// }
// }//3inputs.each { input ->input.jarInputs.each { jarInput ->submitTask(new Runnable() {@Overridevoid run() {forEachJar(jarInput, outputProvider, context, isIncremental)}})}input.directoryInputs.each { DirectoryInput dirInput ->submitTask(new Runnable() {@Overridevoid run() {forEachDir(dirInput, outputProvider, context, isIncremental)}})}}def futures = executorService.invokeAll(taskList)futures.each { it ->it.get()}onTransformEnd(transformInvocation)println(getName() + "transform end--------------->" + "duration : " + (System.currentTimeMillis() - startTime) + " ms")}void submitTask(Runnable runnable) {taskList.add(new Callable<Void>() {@OverrideVoid call() throws Exception {runnable.run()return null}})}void forEachDir(DirectoryInput directoryInput, TransformOutputProvider outputProvider, Context context, boolean isIncremental) {def inputDir = directoryInput.fileFile dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY)println "directoryInputPath:" + directoryInput.file.absolutePathprintln "destPath:" + dest.absolutePathdef srcDirPath = inputDir.absolutePathdef destDirPath = dest.absolutePathdef temporaryDir = context.temporaryDirFileUtils.forceMkdir(dest)Logger.info("srcDirPath:${srcDirPath}, destDirPath:${destDirPath}")if (isIncremental) {directoryInput.getChangedFiles().each { entry ->def classFile = entry.keyswitch (entry.value) {case Status.NOTCHANGED:Logger.info("处理 class: " + classFile.absoluteFile + " NOTCHANGED")breakcase Status.REMOVED:Logger.info("处理 class: " + classFile.absoluteFile + " REMOVED")//最终文件应该存放的路径def destFilePath = classFile.absolutePath.replace(srcDirPath, destDirPath)def destFile = File(destFilePath)if (destFile.exists()) {destFile.delete()}breakcase Status.ADDED:case Status.CHANGED:Logger.info("处理 class: " + classFile.absoluteFile + " ADDED or CHANGED")modifyClassFile(classFile, srcDirPath, destDirPath, temporaryDir)breakdefault:break}}} else {com.android.utils.FileUtils.getAllFiles(inputDir).each { File file ->modifyClassFile(file, srcDirPath, destDirPath, temporaryDir)}}}void modifyClassFile(classFile, srcDirPath, destDirPath, temporaryDir) {Logger.info("处理 class: " + classFile.absoluteFile)//目标路径def destFilePath = classFile.absolutePath.replace(srcDirPath, destDirPath)def destFile = new File(destFilePath)if (destFile.exists()) {destFile.delete()}Logger.info("处理 class:destFile" + destFile.absoluteFile)String className = CommonUtil.path2ClassName(classFile.absolutePath.replace(srcDirPath + File.separator, ""))Logger.info("处理 className: " + className)File modifyFile = nullif (CommonUtil.isLegalClass(classFile) && shouldHookClass(className)) {modifyFile = getModifyFile(classFile, temporaryDir, className)}if (modifyFile == null) {modifyFile = classFile}FileUtils.copyFile(modifyFile, destFile)}File getModifyFile(File classFile, File temporaryDir, String className) {byte[] sourceBytes = IOUtils.toByteArray(new FileInputStream(classFile))def tempFile = new File(temporaryDir, CommonUtil.generateClassFileName(classFile))if (tempFile.exists()) {FileUtils.forceDelete(tempFile)}def modifyBytes = modifyClass(className, sourceBytes)if (modifyBytes == null) {modifyBytes = sourceBytes}tempFile.createNewFile()def fos = new FileOutputStream(tempFile)fos.write(modifyBytes)fos.flush()IOUtils.closeQuietly(fos)return tempFile}void forEachJar(JarInput jarInput, TransformOutputProvider outputProvider, Context context, boolean isIncremental) {Logger.info("jarInput:" + jarInput.file)File destFile = outputProvider.getContentLocation(//防止同名被覆盖CommonUtil.generateJarFileName(jarInput.file), jarInput.contentTypes, jarInput.scopes, Format.JAR)//增量编译处理if (isIncremental) {Status status = jarInput.statusswitch (status) {case Status.NOTCHANGED:Logger.info("处理 jar: " + jarInput.file.absoluteFile + " NotChanged")//Do nothingreturncase Status.REMOVED:Logger.info("处理 jar: " + jarInput.file.absoluteFile + " REMOVED")if (destFile.exists()) {FileUtils.forceDelete(destFile)}returncase Status.ADDED:case Status.CHANGED:Logger.info("处理 jar: " + jarInput.file.absoluteFile + " ADDED or CHANGED")break}}if (destFile.exists()) {FileUtils.forceDelete(destFile)}CommonUtil.isLegalJar(jarInput.file) ? transformJar(jarInput.file, context.getTemporaryDir(), destFile): FileUtils.copyFile(jarInput.file, destFile)}def transformJar(File jarFile, File temporaryDir, File destFile) {Logger.info("处理 jar: " + jarFile.absoluteFile)File tempOutputJarFile = new File(temporaryDir, CommonUtil.generateJarFileName(jarFile))if (tempOutputJarFile.exists()) {FileUtils.forceDelete(tempOutputJarFile)}JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempOutputJarFile))JarFile inputJarFile = new JarFile(jarFile, false)try {def entries = inputJarFile.entries()while (entries.hasMoreElements()) {def jarEntry = entries.nextElement()def entryName = jarEntry.getName()def inputStream = inputJarFile.getInputStream(jarEntry)try {byte[] sourceByteArray = IOUtils.toByteArray(inputStream)def modifiedByteArray = nullif (!jarEntry.isDirectory() && CommonUtil.isLegalClass(entryName)) {String className = CommonUtil.path2ClassName(entryName)if (shouldHookClass(className)) {modifiedByteArray = modifyClass(className, sourceByteArray)}}if (modifiedByteArray == null) {modifiedByteArray = sourceByteArray}jarOutputStream.putNextEntry(new JarEntry(entryName))jarOutputStream.write(modifiedByteArray)jarOutputStream.closeEntry()} finally {IOUtils.closeQuietly(inputStream)}}} finally {jarOutputStream.flush()IOUtils.closeQuietly(jarOutputStream)IOUtils.closeQuietly(inputJarFile)}FileUtils.copyFile(tempOutputJarFile, destFile)}private byte[] modifyClass(String className, byte[] sourceBytes) {byte[] classBytesCodetry {classBytesCode = hookClassInner(className, sourceBytes)} catch (Throwable e) {e.printStackTrace()classBytesCode = nullprintln "throw exception when modify class ${className}"}return classBytesCode}/*** 打印日志信息*/static void printCopyRight() {println()println '#######################################################################'println '########## 'println '########## 欢迎使用隐私合规处理插件'println '########## 'println '#######################################################################'println '########## 'println '########## 插件配置参数 'println '########## 'println '########## -isDebug: ' + PrivacyGlobalConfig.isDebugprintln '########## -handleAnnotationName: ' + PrivacyGlobalConfig.handleAnnotationNameprintln '########## -exclude: ' + PrivacyGlobalConfig.excludeprintln '########## 'println '########## 'println '########## 'println '#######################################################################'println()}protected boolean firstTransform() {return false}boolean shouldHookClass(String className) {def excludes = PrivacyGlobalConfig.excludeif (excludes != null) {for (String string : excludes) {if (className.startsWith(string)) {return false}}}return shouldHookClassInner(className)}protected abstract boolean shouldHookClassInner(String className)protected abstract byte[] hookClassInner(String className, byte[] bytes)protected abstract void onTransformStart(TransformInvocation transformInvocation)protected abstract void onTransformEnd(TransformInvocation transformInvocation)
}
2.2.2 注解处理 transform
收集和处理具有特定注解的字节码指令,给下一个 transform 使用
@Override
byte[] hookClassInner(String className, byte[] bytes) {ClassReader cr = new ClassReader(bytes)ClassNode classNode = new ClassNode()cr.accept(classNode, 0)classNode.methods.each { methodNode ->//编译期注解methodNode.invisibleAnnotations.each { annotationNode ->if (PrivacyGlobalConfig.getHandleAnnotationName() == annotationNode.desc) {collectPrivacyMethod(annotationNode, methodNode, cr.className)}}}return bytes
}/*** 收集注解和注解关联的方法* @param annotationNode 注解信息* @param methodNode 方法信息*/
static collectPrivacyMethod(AnnotationNode annotationNode, MethodNode methodNode, String className) {List<Object> values = annotationNode.valuesLogger.info("annotation values : ${values}")MethodReplaceItem item = new MethodReplaceItem(values, methodNode, CommonUtil.getClassInternalName(className))PrivacyGlobalConfig.methodReplaceItemList.offer(item)Logger.info("collectPrivacyMethod success: ${item}")println("collectPrivacyMethod success: ${item}")
}
MethodReplaceItem中封装了收集到的字节码属性,同时会根据注解关联方法的字节码指令推断出想要处理的隐私合规 api 的字节码指令。
MethodReplaceItem(List<Object> annotationPair, MethodNode methodNode, String owner) {replaceOpcode = Opcodes.INVOKESTATICreplaceClass = ownerreplaceMethod = methodNode.namereplaceDesc = methodNode.descfor (int i = 0; i < annotationPair.size(); i = i + 2) {def key = annotationPair[i]def value = annotationPair[i + 1]if (key == "targetMethodOpcode") {targetOpcode = value} else if (key == "targetClass") {targetOwner = value} else if (key == "targetName") {targetMethod = value} else if (key == "targetDesc") {targetDesc = value}else if(key == "hook"){willHook = value}}Logger.info("=====targetOpcode:${targetOpcode},targetOwner:${targetOwner} , replaceDesc${replaceDesc}")if (isEmpty(targetMethod)) {targetMethod = replaceMethod}if (isEmpty(targetDesc)) {//静态方法,oriDesc 跟 targetDesc 一样if (targetOpcode == Opcodes.INVOKESTATIC) {targetDesc = replaceDesc} else {//非静态方法,约定第一个参数是实例类名,oriDesc 比 targetDesc 少一个参数,处理一下// (Landroid/telephony/TelephonyManager;)Ljava/lang/String -> ()Ljava/lang/Stringdef param = replaceDesc.split('\)')[0] + ")"def result = replaceDesc.split('\)')[1]def index = replaceDesc.indexOf(targetOwner)if (index != -1) {param = "(" + param.substring(index + targetOwner.length() + 1)}Logger.info("index::: ${index}")targetDesc = param + result}}
}
2.2.3 合规方法处理 transform
再次扫描整个项目,根据在上一个 transform 中收集到的要处理的隐私合规的 api,遍历字节码指令,当匹配上时,在当前的类中注入写文件的方法,同时在调用隐私合规的字节码指令前插入写文件的字节码指令,用来记录。
@Override
byte[] hookClassInner(String className, byte[] bytes) {Logger.info("${getName()} modifyClassInner--------------->")def findHookPoint = falseMap<MethodNode, InsertInsnPoint> collectMap = new HashMap<>()ClassReader cr = new ClassReader(bytes)ClassNode classNode = new ClassNode()cr.accept(classNode, ClassReader.EXPAND_FRAMES)classNode.methods.each { methodNode ->//过滤掉含有特定注解的方法if (isNotHookMethod(cr.className, methodNode)) {methodNode.instructions.each { insnNode ->//判断字节码能否匹配def methodReplaceItem = searchHookPoint(insnNode)if (methodReplaceItem != null) {//判断是否需要 hook 掉当前指令def inject = methodReplaceItem.willHook//记录隐私合规 api 所在的类及方法logHookPoint(classNode.name, methodReplaceItem, methodNode, insnNode.opcode, insnNode.owner, insnNode.name, insnNode.desc, inject)if (inject) {//hookinjectInsn(insnNode, methodReplaceItem)}//插入写文件方法指令,收集调用隐私方法的堆栈collectInsertInsn(insnNode, methodNode, classNode, collectMap, inject)findHookPoint = true}}}}if (!collectMap.isEmpty() && findHookPoint) {//插入写文件指令,用来展示堆栈信息collectMap.each { key, value ->key.instructions.insert(value.hookInsnNode, value.instList)}//插入 writeToFile 方法ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)classNode.accept(cw)insertWriteToFileMethod(cw)return cw.toByteArray()}return bytes
}
在 collectInsertInsn 方法中,通过 throwable 来收集当前的堆栈
/*** 收集 在调用特定的方法前插入调用写入文件的方法的指令* @param insnNode* @param methodNode* @param classNode* @param collectMap*/
static void collectInsertInsn(insnNode, methodNode, classNode, collectMap, Inject) {def className = classNode.namedef methodName = methodNode.namedef methodDesc = methodNode.descdef owner = nulldef name = nulldef desc = nullif (insnNode instanceof MethodInsnNode) {owner = insnNode.ownername = insnNode.namedesc = insnNode.desc}//------logStringBuilder lintLog = new StringBuilder()lintLog.append(className)lintLog.append(" -> ")lintLog.append(methodName)lintLog.append(" -> ")lintLog.append(methodDesc)lintLog.append("\r\n")lintLog.append(owner)lintLog.append(" -> ")lintLog.append(name)lintLog.append(" -> ")lintLog.append(desc)//------要插入字节码指令lintLog.append("\r\n")InsnList insnList = new InsnList()insnList.add(new LdcInsnNode(lintLog.toString()))insnList.add(new TypeInsnNode(Opcodes.NEW, "java/lang/Throwable"))insnList.add(new InsnNode(Opcodes.DUP))insnList.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/Throwable", "<init>", "()V", false))insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, className, writeToFileMethodName, writeToFileMethodDesc))println "插入指令完成 =---------->"collectMap.put(methodNode, new InsertInsnPoint(insnList, insnNode))
}
最终,在项目编译完成之后,会在项目的根目录下生成 replaceInsn.txt 文件,记录包含隐私合规 api 的类和相关方法。
当项目运行起来之后,会在设置的路径中(笔者设置在 getExternalCacheDir 中)生成 privacy_log.txt 文件,里面会记录隐私合规 api 的调用堆栈和时间,根据该调用链,我们就可以快速定位是哪一块业务执行了敏感操作。
3.总结
通过 ASM + gradle plugin ,能够排查出大部分的隐私合规问题。有什么不足之处,也请读者多多提意见和建议。
源码地址
https://github.com/season-max/asm_hook.git
相关文章:
ASM 字节码插桩:隐私合规方法检测!
1.前言近两年来工信部对于应用的隐私合规安全问题愈加重视,对 Android 平台的管控程度也要比 IOS 平台严格很多,很多不合规的应用也先后被下架要求整改。笔者就曾遇到过加班整改隐私合规的问题,隐私合规问题主要针对两个方面。在用户同意隐私…...
spring data jpa使用流式查询
思路 调用org.hibernate.query.Query.stream方法查询数据 代码样例 import static org.hibernate.annotations.QueryHints.READ_ONLY; import static org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE; import org.hibernate.query.Query;使用HQL查询 Query<MyEntity> …...

Golang实现RabbitMQ中死信队列各个情况
下面这段教程针对是你已经有一些基本的MQ的知识,比如说能够很清楚的理解queue、exchange等概念,如果你还不是很理解,我建议你先访问官网查看基本的教程。 文章目录1、造成死信队列的主要原因2、操作逻辑图3、代码实战3.1 针对原因1࿱…...

react源码分析:组件的创建和更新
这一章节就来讲讲ReactDOM.render()方法的内部实现与流程吧。 因为初始化的源码文件部分所涵盖的内容很多,包括创建渲染、更新渲染、Fiber树的创建与diff,element的创建与插入,还包括一些优化算法,所以我就整个的React执行流程画了…...

Android Lmkd 低内存终止守护程序
一、低内存终止守护程序 Android 低内存终止守护程序 (lmkd) 进程可监控运行中的 Android 系统的内存状态,并通过终止最不必要的进程来应对内存压力大的问题,使系统以可接受的性能水平运行。 所有应用进程都是从zygote孵化出来的,记录在AMS…...
快速掌握 Flutter 图片开发核心技能
大家好,我是 17。 在 Flutter 中使用图片是最基础能力之一。17 做了精心准备,满满的都是干货!本文介绍如何在 Flutter 中使用图片,尽量详细,示例完整,包会! 使用网络图片 使用网络图片超级简…...
复习使用git(二)
删除远程分支 git push origin --delete 分支名 撤销修改 撤销工作区的修改 已修改,但尚未添加(add),使用 git restore 文件名 撤销工作区的修改。 Note: “git checkout – 文件名”,checkout 检出的意思&#x…...
魔兽世界335服务端架设对外网开放的步骤
警告:在没有网络安全防护措施或基础知识的情况下,开放端口可能造成被黑客入侵、流量攻击、破坏数据、资料泄露等情况的发生。在你选择开放端口时,视为已经充分了解可能发生的后果、危害,清楚自己在做什么,并且自己将对…...

华为OD机试模拟题 用 C++ 实现 - 通信误码(2023.Q1)
最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 最多获得的短信条数(2023.Q1)) 文章目录 最近更新的博客使用说明通信误码题目输入输出示例一输入输出说明示例二输入输出说明Code使用说明 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,...

Vue 核心
文章目录Vue 核心一,Vue 简介(一)官网(二)介绍与描述(三)Vue 的特点(四)与其它 JS 框架的关联(五)Vue 周边库二,初识 Vue三࿰…...

Kylin V10桌面版arm3568 源码安装redis
上传redis-5.0.14.tar.gz到/home/kylin/下载;解压kylinkylin:~/下载$ tar -zxvf redis-5.0.14.tar.gz/opt下新建redis目录,并将上面解压的文件夹移到此处kylinkylin:~/下载$ sudo mv redis-5.0.14 /opt/redis/编译:kylinkylin:/opt/redis/red…...

【ICCV2022】 CAPAO:一种高效的单阶段人体姿态估计模型
CAPAO:一种高效的单阶段人体姿态估计模型 重新思考关键点表示:将关键点和姿态建模作为多人姿态估计的对象(Rethinking Keypoint Representations: Modeling Keypoints and Poses as Objects for Multi-Person Human Pose Estimation…...

ROS1学习笔记:ROS中的坐标管理系统(ubuntu20.04)
参考B站古月居ROS入门21讲:ROS中的坐标系管理系统 基于VMware Ubuntu 20.04 Noetic版本的环境 文章目录一、机器人中的坐标变换二、TF功能包三、小海龟跟随实验3.1 启动实验3.2 查看当前的TF树3.3 坐标相对位置可视化3.3.1 tf_echo3.3.2 rviz一、机器人中的坐标变换…...

requests---(2)session简介与自动写博客
目录:导读 session简介 session登录 自动写博客 获取登录cookies 抓取写博客接口 requests自动写博客 写在最后 http协议是无状态的,也就是每个请求都是独立的。那么登录后的一系列动作,都需要用cookie来验证身份是否是登录状态&#…...

基于 HAProxy + Keepalived 搭建 RabbitMQ 高可用集群
RabbitMQ 集群 通常情况下,在集群中我们把每一个服务称之为一个节点,在 RabbitMQ 集群中,节点类型可以分为两种: 内存节点:元数据存放于内存中。为了重启后能同步数据,内存节点会将磁盘节点的地址存放于磁…...

基于51单片机和proteus的智能调速风扇设计
此智能风扇是基于51单片机和proteus的仿真设计,功能如下: 1. Timer0 PWM控制电机转速 2. DHT11采集温湿度 3. LCD1602显示温湿度及电机状态 4. 按键控制电机加减速启停等 5. 串口控制电机加减速启停等 功能框图如下: Proteus仿真界面如下…...

SQL Server开启CDC的完整操作过程
这里写自定义目录标题写在前面SQL Server开启CDC1. 将指定库的实例先开启CDC2. 开启需要开启CDC的表3. 关闭CDC功能更详细信息参照官网写在前面 鉴于老旧数据的结构和项目都在sqlserver上存储,且迁移成本巨大,当下要为sqlserver的存储过程减负。要将一部…...

【Spring Cloud Alibaba】008-Sentinel
【Spring Cloud Alibaba】008-Sentinel 文章目录【Spring Cloud Alibaba】008-Sentinel一、服务雪崩1、概述2、解决方案常见的容错机制二、Sentinel:分布式系统的流量防卫兵1、**Sentinel** 概述简介特性Sentinel 的开源生态Sentinel 的历史2、Sentinel 基本概念资源…...
解读CRC校验计算
个人随笔 (Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu) 参考:http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html 参考:https://en.wikipedia.org/wiki/Cyclic_redundancy_check 参考:https://www.cnblogs.com/…...

深入理解Spring MVC下
上一篇博客从理论概念上来梳理Spring MVC相关知识,此篇博客将通过spring官网提供showcase代码为例子,详细介绍showcase代码中包含的各个例子是如何实现的。官网的showcase代码包含的主要例子包括,Demo地址:Mapping Requests&#…...

K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...

为什么需要建设工程项目管理?工程项目管理有哪些亮点功能?
在建筑行业,项目管理的重要性不言而喻。随着工程规模的扩大、技术复杂度的提升,传统的管理模式已经难以满足现代工程的需求。过去,许多企业依赖手工记录、口头沟通和分散的信息管理,导致效率低下、成本失控、风险频发。例如&#…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...

基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一)
宇树机器人多姿态起立控制强化学习框架论文解析 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一) 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化…...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍
文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结: 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析: 实际业务去理解体会统一注…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...

技术栈RabbitMq的介绍和使用
目录 1. 什么是消息队列?2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...

【C++进阶篇】智能指针
C内存管理终极指南:智能指针从入门到源码剖析 一. 智能指针1.1 auto_ptr1.2 unique_ptr1.3 shared_ptr1.4 make_shared 二. 原理三. shared_ptr循环引用问题三. 线程安全问题四. 内存泄漏4.1 什么是内存泄漏4.2 危害4.3 避免内存泄漏 五. 最后 一. 智能指针 智能指…...

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配置的颜色主题,无需引入,直接可…...