Python实现3D建模工具(下)

#Python实现3D建模工具
###用户接口

我们希望与场景实现两种交互,一种是你可以操纵场景从而能够从不同的角度观察模型,一种是你拥有添加与操作修改模型对象的能力。为了实现交互,我们需要得到键盘与鼠标的输入,GLUT允许我们在键盘或鼠标事件上注册对应的回调函数。

新建interaction.py文件,用户接口在Interaction类中实现。

导入需要的库

from collections import defaultdict
from OpenGL.GLUT import glutGet, glutKeyboardFunc, glutMotionFunc, glutMouseFunc, glutPassiveMotionFunc, \glutPostRedisplay, glutSpecialFunc
from OpenGL.GLUT import GLUT_LEFT_BUTTON, GLUT_RIGHT_BUTTON, GLUT_MIDDLE_BUTTON, \GLUT_WINDOW_HEIGHT, GLUT_WINDOW_WIDTH, \GLUT_DOWN, GLUT_KEY_UP, GLUT_KEY_DOWN, GLUT_KEY_LEFT, GLUT_KEY_RIGHT
import trackball

初始化Interaction类,注册glut的事件回调函数。

class Interaction(object):def __init__(self):""" 处理用户接口 """#被按下的键self.pressed = None#轨迹球,会在之后进行说明self.trackball = trackball.Trackball(theta = -25, distance=15)#当前鼠标位置self.mouse_loc = None#回调函数词典self.callbacks = defaultdict(list)self.register()def register(self):""" 注册glut的事件回调函数 """glutMouseFunc(self.handle_mouse_button)glutMotionFunc(self.handle_mouse_move)glutKeyboardFunc(self.handle_keystroke)glutSpecialFunc(self.handle_keystroke)

回调函数的实现:

def handle_mouse_button(self, button, mode, x, y):""" 当鼠标按键被点击或者释放的时候调用 """xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)y = ySize - y  # OpenGL原点在窗口左下角,窗口原点在左上角,所以需要这种转换。self.mouse_loc = (x, y)if mode == GLUT_DOWN:#鼠标按键按下的时候self.pressed = buttonif button == GLUT_RIGHT_BUTTON:passelif button == GLUT_LEFT_BUTTON:  self.trigger('pick', x, y)else:  # 鼠标按键被释放的时候self.pressed = None#标记当前窗口需要重新绘制glutPostRedisplay()def handle_mouse_move(self, x, screen_y):""" 鼠标移动时调用 """xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)y = ySize - screen_y if self.pressed is not None:dx = x - self.mouse_loc[0]dy = y - self.mouse_loc[1]if self.pressed == GLUT_RIGHT_BUTTON and self.trackball is not None:# 变化场景的角度self.trackball.drag_to(self.mouse_loc[0], self.mouse_loc[1], dx, dy)elif self.pressed == GLUT_LEFT_BUTTON:self.trigger('move', x, y)elif self.pressed == GLUT_MIDDLE_BUTTON:self.translate(dx/60.0, dy/60.0, 0)else:passglutPostRedisplay()self.mouse_loc = (x, y)def handle_keystroke(self, key, x, screen_y):""" 键盘输入时调用 """xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)y = ySize - screen_yif key == 's':self.trigger('place', 'sphere', x, y)elif key == 'c':self.trigger('place', 'cube', x, y)elif key == GLUT_KEY_UP:self.trigger('scale', up=True)elif key == GLUT_KEY_DOWN:self.trigger('scale', up=False)elif key == GLUT_KEY_LEFT:self.trigger('rotate_color', forward=True)elif key == GLUT_KEY_RIGHT:self.trigger('rotate_color', forward=False)glutPostRedisplay()

###内部回调

针对用户行为会调用self.trigger方法,它的第一个参数指明行为期望的效果,后续参数为该效果的参数,trigger的实现如下:

def trigger(self, name, *args, **kwargs):for func in self.callbacks[name]:func(*args, **kwargs)

从代码可以看出trigger会取得callbacks词典下该效果对应的所有方法逐一调用。

那么如何将方法添加进callbacks呢?我们需要实现一个注册回调函数的方法:

def register_callback(self, name, func):self.callbacks[name].append(func)

还记得Viewer中未实现的self.init_interaction()吗,我们就是在这里注册回调函数的,下面补完init_interaction.

from interaction import Interaction
...
class Viewer(object):...def init_interaction(self):self.interaction = Interaction()self.interaction.register_callback('pick', self.pick)self.interaction.register_callback('move', self.move)self.interaction.register_callback('place', self.place)self.interaction.register_callback('rotate_color', self.rotate_color)self.interaction.register_callback('scale', self.scale)def pick(self, x, y):""" 鼠标选中一个节点 """passdef move(self, x, y):""" 移动当前选中的节点 """passdef place(self, shape, x, y):""" 在鼠标的位置上新放置一个节点 """passdef rotate_color(self, forward):""" 更改选中节点的颜色 """passdef scale(self, up):""" 改变选中节点的大小 """pass

pickmove 等函数的说明如下表所示

回调函数参数说明
pickx:number, y:number鼠标选中一个节点
movex:number, y:number移动当前选中的节点
placeshape:string, x:number, y:number在鼠标的位置上新放置一个节点
rotate_colorforward:boolean更改选中节点的颜色
scaleup:boolean改变选中节点的大小

我们将在之后实现这些函数。

Interaction类抽象出了应用层级别的用户输入接口,这意味着当我们希望将glut更换为别的工具库的时候,只要照着抽象出来的接口重新实现一遍底层工具的调用就行了,也就是说仅需改动Interaction类内的代码,实现了模块与模块之间的低耦合。

这个简单的回调系统已满足了我们的项目所需。在真实的生产环境中,用户接口对象常常是动态生成和销毁的,所以真实生产中还需要实现解除注册的方法,我们这里就不用啦。

###与场景交互

####旋转场景
在这个项目中摄像机是固定的,我们主要靠移动场景来观察不同角度下的3d模型。摄像机固定在距离原点15个单位的位置,面对世界坐标系的原点。感观上是这样,但其实这种说法不准确,真实情况是在世界坐标系里摄像机是在原点的,但在摄像机坐标系中,摄像机后退了15个单位,这就等价于前者说的那种情况了。

####使用轨迹球
我们使用轨迹球算法来完成场景的旋转,旋转的方法理解起来很简单,想象一个可以向任意角度围绕球心旋转的地球仪,你的视线是不变的,但是通过你的手在拨这个球,你可以想看哪里拨哪里。在我们的项目中,这个拨球的手就是鼠标右键,你点着右键拖动就能实现这个旋转场景的效果了。

想要更多的理解轨迹球可以参考OpenGL Wiki,在这个项目中,我们使用Glumpy中轨迹球的实现。

下载trackball.py文件,并将其置于工作目录下:

$ wget  http://labfile.oss.aliyuncs.com/courses/561/trackball.py

drag_to方法实现与轨迹球的交互,它会比对之前的鼠标位置和移动后的鼠标位置来更新旋转矩阵。

self.trackball.drag_to(self.mouse_loc[0], self.mouse_loc[1], dx, dy)

得到的旋转矩阵保存在viewer的trackball.matrix中。

更新viewer.py下的ModelView矩阵

class Viewer(object):...def render(self):self.init_view()glEnable(GL_LIGHTING)glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)# 将ModelView矩阵设为轨迹球的旋转矩阵glMatrixMode(GL_MODELVIEW)glPushMatrix()glLoadIdentity()glMultMatrixf(self.interaction.trackball.matrix)# 存储ModelView矩阵与其逆矩阵之后做坐标系转换用currentModelView = numpy.array(glGetFloatv(GL_MODELVIEW_MATRIX))self.modelView = numpy.transpose(currentModelView)self.inverseModelView = inv(numpy.transpose(currentModelView))self.scene.render()glDisable(GL_LIGHTING)glCallList(G_OBJ_PLANE)glPopMatrix()glFlush()

运行代码:

####选择场景中的对象

既然要操作场景中的对象,那么必然得先能够选中对象,要怎么才能选中呢?想象你有一只指哪打哪的激光笔,当激光与对象相交时就相当于选中了对象。

我们如何判定激光穿透了对象呢?

想要真正实现对复杂形状物体进行选择判定是非常考验算法和性能的,所以在这里我们简化问题,对对象使用包围盒(axis-aligned bounding box, 简称AABB),包围盒可以想象成一个为对象量身定做的盒子,你刚刚好能将模型放进去。这样做的好处就是对于不同形状的对象你都可以使用同一段代码处理选中判定,并能保证较好的性能。

新建aabb.py,编写包围盒类:

from OpenGL.GL import glCallList, glMatrixMode, glPolygonMode, glPopMatrix, glPushMatrix, glTranslated, \GL_FILL, GL_FRONT_AND_BACK, GL_LINE, GL_MODELVIEW
from primitive import G_OBJ_CUBE
import numpy
import math#判断误差
EPSILON = 0.000001class AABB(object):def __init__(self, center, size):self.center = numpy.array(center)self.size = numpy.array(size)def scale(self, scale):self.size *= scaledef ray_hit(self, origin, direction, modelmatrix):""" 返回真则表示激光射中了包盒参数说明:  origin, distance -> 激光源点与方向modelmatrix      -> 世界坐标到局部对象坐标的转换矩阵 """aabb_min = self.center - self.sizeaabb_max = self.center + self.sizetmin = 0.0tmax = 100000.0obb_pos_worldspace = numpy.array([modelmatrix[0, 3], modelmatrix[1, 3], modelmatrix[2, 3]])delta = (obb_pos_worldspace - origin)# test intersection with 2 planes perpendicular to OBB's x-axisxaxis = numpy.array((modelmatrix[0, 0], modelmatrix[0, 1], modelmatrix[0, 2]))e = numpy.dot(xaxis, delta)f = numpy.dot(direction, xaxis)if math.fabs(f) > 0.0 + EPSILON:t1 = (e + aabb_min[0])/ft2 = (e + aabb_max[0])/fif t1 > t2:t1, t2 = t2, t1if t2 < tmax:tmax = t2if t1 > tmin:tmin = t1if tmax < tmin:return (False, 0)else:if (-e + aabb_min[0] > 0.0 + EPSILON) or (-e+aabb_max[0] < 0.0 - EPSILON):return False, 0yaxis = numpy.array((modelmatrix[1, 0], modelmatrix[1, 1], modelmatrix[1, 2]))e = numpy.dot(yaxis, delta)f = numpy.dot(direction, yaxis)# intersection in yif math.fabs(f) > 0.0 + EPSILON:t1 = (e + aabb_min[1])/ft2 = (e + aabb_max[1])/fif t1 > t2:t1, t2 = t2, t1if t2 < tmax:tmax = t2if t1 > tmin:tmin = t1if tmax < tmin:return (False, 0)else:if (-e + aabb_min[1] > 0.0 + EPSILON) or (-e+aabb_max[1] < 0.0 - EPSILON):return False, 0# intersection in zzaxis = numpy.array((modelmatrix[2, 0], modelmatrix[2, 1], modelmatrix[2, 2]))e = numpy.dot(zaxis, delta)f = numpy.dot(direction, zaxis)if math.fabs(f) > 0.0 + EPSILON:t1 = (e + aabb_min[2])/ft2 = (e + aabb_max[2])/fif t1 > t2:t1, t2 = t2, t1if t2 < tmax:tmax = t2if t1 > tmin:tmin = t1if tmax < tmin:return (False, 0)else:if (-e + aabb_min[2] > 0.0 + EPSILON) or (-e+aabb_max[2] < 0.0 - EPSILON):return False, 0return True, tmindef render(self):""" 渲染显示包围盒,可在调试的时候使用 """glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)glMatrixMode(GL_MODELVIEW)glPushMatrix()glTranslated(self.center[0], self.center[1], self.center[2])glCallList(G_OBJ_CUBE)glPopMatrix()glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)

更新Node类与Scene类,加入与选中节点有关的内容

更新Node类:

from aabb import AABB
...
class Node(object):def __init__(self):self.color_index = random.randint(color.MIN_COLOR, color.MAX_COLOR)self.aabb = AABB([0.0, 0.0, 0.0], [0.5, 0.5, 0.5])self.translation_matrix = numpy.identity(4)self.scaling_matrix = numpy.identity(4)self.selected = False...def render(self):glPushMatrix()glMultMatrixf(numpy.transpose(self.translation_matrix))glMultMatrixf(self.scaling_matrix)cur_color = color.COLORS[self.color_index]glColor3f(cur_color[0], cur_color[1], cur_color[2])if self.selected:  # 选中的对象会发光glMaterialfv(GL_FRONT, GL_EMISSION, [0.3, 0.3, 0.3])self.render_self()if self.selected:glMaterialfv(GL_FRONT, GL_EMISSION, [0.0, 0.0, 0.0])glPopMatrix()def select(self, select=None):if select is not None:self.selected = selectelse:self.selected = not self.selected

更新Scene类:

class Scene(object):def __init__(self):self.node_list = list()self.selected_node = None

Viewer类中实现通过鼠标位置获取激光的函数以及pick函数

    # class Viewerdef get_ray(self, x, y):""" 返回光源和激光方向"""self.init_view()glMatrixMode(GL_MODELVIEW)glLoadIdentity()# 得到激光的起始点start = numpy.array(gluUnProject(x, y, 0.001))end = numpy.array(gluUnProject(x, y, 0.999))# 得到激光的方向direction = end - startdirection = direction / norm(direction)return (start, direction)def pick(self, x, y):""" 是否被选中以及哪一个被选中交由Scene下的pick处理 """start, direction = self.get_ray(x, y)self.scene.pick(start, direction, self.modelView)

为了确定是哪个对象被选中,我们会遍历场景下的所有对象,检查激光是否与该对象相交,取离摄像机最近的对象为选中对象。

# Scene 下实现
def pick(self, start, direction, mat):""" 参数中的mat为当前ModelView的逆矩阵,作用是计算激光在局部(对象)坐标系中的坐标"""import sysif self.selected_node is not None:self.selected_node.select(False)self.selected_node = None# 找出激光击中的最近的节点。mindist = sys.maxintclosest_node = Nonefor node in self.node_list:hit, distance = node.pick(start, direction, mat)if hit and distance < mindist:mindist, closest_node = distance, node# 如果找到了,选中它if closest_node is not None:closest_node.select()closest_node.depth = mindistclosest_node.selected_loc = start + direction * mindistself.selected_node = closest_node# Node下的实现
def pick(self, start, direction, mat):# 将modelview矩阵乘上节点的变换矩阵newmat = numpy.dot(numpy.dot(mat, self.translation_matrix), numpy.linalg.inv(self.scaling_matrix))results = self.aabb.ray_hit(start, direction, newmat)return results

运行代码(蓝立方体被选中):

检测包围盒也有其缺点,如下图所示,我们希望能点中球背后的立方体,然而却选中了立方体前的球体,因为我们的激光射中了球体的包围盒。为了效率我们牺牲了这部分功能。在性能,代码复杂度与功能准确度之间之间进行衡量与抉择是在计算机图形学与软件工程中常常会遇见的。

####操作场景中的对象

对对象的操作主要包括在场景中加入新对象, 移动对象、改变对象的颜色与改变对象的大小。因为这部分的实现较为简单,所以仅实现加入新对象与移动对象的操作.

加入新对象的代码如下:

# Viewer下的实现
def place(self, shape, x, y):start, direction = self.get_ray(x, y)self.scene.place(shape, start, direction, self.inverseModelView)# Scene下的实现
import numpy
from node import Sphere, Cube, SnowFigure
...
def place(self, shape, start, direction, inv_modelview):new_node = Noneif shape == 'sphere': new_node = Sphere()elif shape == 'cube': new_node = Cube()elif shape == 'figure': new_node = SnowFigure()self.add_node(new_node)# 得到在摄像机坐标系中的坐标translation = (start + direction * self.PLACE_DEPTH)# 转换到世界坐标系pre_tran = numpy.array([translation[0], translation[1], translation[2], 1])translation = inv_modelview.dot(pre_tran)new_node.translate(translation[0], translation[1], translation[2])

效果如下,按C键创建立方体,按S键创建球体。

移动目标对象的代码如下:

# Viewer下的实现
def move(self, x, y):start, direction = self.get_ray(x, y)self.scene.move_selected(start, direction, self.inverseModelView)# Scene下的实现
def move_selected(self, start, direction, inv_modelview):if self.selected_node is None: return# 找到选中节点的坐标与深度(距离)node = self.selected_nodedepth = node.deptholdloc = node.selected_loc# 新坐标的深度保持不变newloc = (start + direction * depth)# 得到世界坐标系中的移动坐标差translation = newloc - oldlocpre_tran = numpy.array([translation[0], translation[1], translation[2], 0])translation = inv_modelview.dot(pre_tran)# 节点做平移变换node.translate(translation[0], translation[1], translation[2])node.selected_loc = newloc

移动了一下立方体:

##五、一些探索

到这里我们就已经实现了一个简单的3D建模工具了,想一下这个程序还能在什么地方进行改进,或是增加一些新的功能?比如说:

  • 编写新的节点类,支持三角形网格能够组合成任意形状。
  • 增加一个撤销栈,支持撤销命令功能。
  • 能够保存/加载3d设计,比如保存为 DXF 3D 文件格式
  • 改进程序,选中目标更精准。

你也可以从开源的3d建模软件汲取灵感,学习他人的技巧,比如参考三维动画制作软件Blender的建模部分,或是三维建模工具OpenSCAD。

##六、参考资料与延伸阅读

  • A 3D Modeller
  • A 3D Modeller 源代码
  • Real Time Rendering
  • OpenGL学习脚印: 坐标变换过程(vertex transformation)
  • OpenGL学习脚印: 坐标和变换的数学基础(math-coordinates and transformations)

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

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

相关文章

IoTDB 入门教程 基础篇⑥——数据库SQL操作 | 数据库管理和数据读写

文章目录 一、前文二、数据库管理2.1 创建数据库2.2 查询数据库2.3 删除数据库 三、数据读写3.1 查询数据3.2 新增数据3.3 修改数据3.4 删除数据 四、参考 一、前文 IoTDB入门教程——导读 本博文主要讲述数据库管理和数据读写 二、数据库管理 2.1 创建数据库 CREATE DATABASE…

【JVM】类加载机制及双亲委派模型

目录 一、类加载过程 1. 加载 2. 连接 a. 验证 b. 准备 c. 解析 3. 初始化 二、双亲委派模型 类加载器 双亲委派模型的工作过程 双亲委派模型的优点 一、类加载过程 JVM的类加载机制是JVM在运行时&#xff0c;将 .class 文件加载到内存中并转换为Java类的过程。它…

全面升级企业网络安全 迈入SASE新时代

随着数字化业务、云计算、物联网和人工智能等技术的飞速发展&#xff0c;企业的业务部署环境日渐多样化&#xff0c;企业数据的存储由传统的数据中心向云端和SaaS迁移。远程移动设备办公模式的普及&#xff0c;企业多分支机构的加速设立&#xff0c;也使得企业业务系统的用户范…

第IV章-Ⅱ Vue3中的插槽使用

第IV章-Ⅱ Vue3中的插槽使用 基本插槽默认内容 具名插槽作用域插槽 在 Vue 3 中&#xff0c;插槽&#xff08;slots&#xff09;是一种强大的模式&#xff0c;用于将模板代码从父组件注入到子组件中&#xff0c;使得子组件的内容可以在使用时被自定义。Vue 3 中的插槽用法包括基…

# 代码随想录算法训练营Day31 | 理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和

代码随想录算法训练营Day31 | 理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和 LeetCode 455.分发饼干 题目链接&#xff1a;LeetCode 455.分发饼干 思路&#xff1a; 分别排序&#xff0c;最大的饼干给胃口最大的孩子&#xff0c;充分利用。 注意索引需要大于0 class…

神器:jQuery一键转换为纯净JavaScript代码

我的新书《Android App开发入门与实战》已于2020年8月由人民邮电出版社出版&#xff0c;欢迎购买。点击进入详情 该工具将 jQuery 代码转换为现代、高效的 JavaScript。它允许您用纯 JavaScript 替换 jQuery&#xff0c;同时保持原始代码不变。 虽然 jQuery 一直是 Web 开发中…

【自动驾驶|毫米波雷达】初识毫米波雷达射频前端硬件

第一次更新&#xff1a;2024/5/4 目录 整体概述 混频器&#xff08;MIXER&#xff09; 低通滤波器&#xff08;LPF&#xff1a;Low-Pass filter&#xff09; 数模转换器&#xff08;ADC&#xff1a;Analog to Digital Converter&#xff09; 毫米波雷达功能框图 整体概述 完…

ctfshow web入门 sql注入 web224--web233

web224 扫描后台&#xff0c;发现robots.txt&#xff0c;访问发现/pwdreset.php &#xff0c;再访问可以重置密码 &#xff0c;登录之后发现上传文件 检查发现没有限制诶 上传txt,png,zip发现文件错误了 后面知道群里有个文件能上传 <? _$GET[1]_?>就是0x3c3f3d60245…

echars 的一些运用

基础图形 // 支架压力数据量统计 let splitLine { // 网格线设置show:true,lineStyle:{color:#E2E7EC,width:1,}, } let itemStyle { // 柱形图颜色color:#5B9BD5, } let axisLine { // 轴线样式lineStyle:{color:#E2E7EC,}, } let axisLabel { // 轴刻度字-样式fontSize:…

代码随想录leetcode200题之数组

目录 1 介绍2 训练3 参考 1 介绍 本博客用来记录代码随想录leetcode200题中数组部分的题目。 2 训练 题目1&#xff1a;704二分查找 C代码如下&#xff0c; class Solution { public:int search(vector<int>& nums, int target) {int res -1;int l 0, r nums…

STM32快速入门(串口传输之USART)

STM32快速入门&#xff08;串口传输之USART&#xff09; 前言 USART串口传输能实现信息在设备之间的点对点传输&#xff0c;支持单工、半双工、全全双工&#xff0c;一般是有三个引脚&#xff1a;TX、RX、SW_RX&#xff08;共地&#xff09;。不需要一根线来同步时钟。最大优…

停车场收费管理系统winfrom

停车场收费管理系统winfrom 管理员登陆后可以费用设置 修改密码 开户 充值 注销 入库 出库 退出 本项目通过dat文件格式存储数据 public static void Load() { string path Application.StartupPath "\data\data2.dat"; if (File.Exists(path)) …

4diacIDE同时编译不同版本踩坑记录

4diac不同版本依赖插件版本及jdk版本是不同的&#xff0c;当你需要搭建不同版本4diacIDE开发环境时&#xff0c;就会出现各种问题。最近一个月github上项目提交记录比较多&#xff0c;出现了不少坑。以下记录下此背景下的解决方法&#xff1a; 1、首先由于.target依赖的eclipse…

探索大语言模型在信息提取中的应用与前景

随着人工智能技术的快速发展&#xff0c;大语言模型&#xff08;LLMs&#xff09;在自然语言处理&#xff08;NLP&#xff09;领域取得了显著的进展。特别是在信息提取&#xff08;IE&#xff09;任务中&#xff0c;LLMs展现出了前所未有的潜力和优势。信息提取是从非结构化文本…

【Linux】网络接口绑定和组合的操作实例

网络接口绑定和组合的操作实例 &#xff08;一&#xff09;网卡1. 增2. 查3. 激活——设置网络接口 &#xff08;二&#xff09;网络接口绑定1. 概述2. 实验操作3. 删除绑定 &#xff08;三&#xff09;网络接口组合1. 概述2. 实验操作 &#xff08;一&#xff09;网卡 1. 增 …

Java_从入门到JavaEE_10

一、继承 概念&#xff1a;子类继承父类所有的属性和方法应用场景&#xff1a;多个类似的类&#xff0c;有相同的属性和方法&#xff0c;就可以把相同属性和方法抽取到父类继承优缺点&#xff1a; 优点&#xff1a;解决了代码的冗余缺点&#xff1a;增加了类与类之间的关联性、…

122. Kafka问题与解决实践

文章目录 前言顺序问题1. 为什么要保证消息的顺序&#xff1f;2.如何保证消息顺序&#xff1f;3.出现意外4.解决过程 消息积压1. 消息体过大2. 路由规则不合理3. 批量操作引起的连锁反应4. 表过大 主键冲突数据库主从延迟重复消费多环境消费问题后记 前言 假如有家公司是做餐饮…

041.数据流中的移动平均值

刷算法题&#xff1a; 第一遍&#xff1a;1.看5分钟&#xff0c;没思路看题解 2.通过题解改进自己的解法&#xff0c;并且要写每行的注释以及自己的思路。 3.思考自己做到了题解的哪一步&#xff0c;下次怎么才能做对(总结方法) 4.整理到自己的自媒体平台。 5.再刷重复的类…

web安全day03

MYSQL注入&#xff1a; SQL 注入的原理、危害及防御措施 SQL 注入的原理&#xff1a;原本的 SQL 语句在与用户可控的参数经过了如拼接、替换等字符串操作后&#xff0c;得到一个新的 SQL 语句并被数据库解析执行&#xff0c;从而达到非预期的效果。 SQL 注入的危害&#xff…

docker安装elasticsearch:7.17.21

docker安装elasticsearch:7.17.21 下载对应版本的docker镜像 docker pull docker.elastic.co/elasticsearch/elasticsearch:7.17.21启动容器 docker run --name elasticsearch-test -p 9200:9200 -p 9300:9300 -e "discovery.typesingle-node" -t docker.elastic.…