实现任意系统下载office文件的域控

       一.背景

        最近用户提出需求:某个系统A下载的excel文档需要进行权限控制,比如只能下载文档的用户(即文档owner)查看或者编辑,其他人想要查看或者编辑,需要文档owner进行手动设置,当然也可以手动取消权限控制,如下图所示。

        该功能的核心逻辑就是对 windows的 office文件实现域控,这个一般是购买第三方的服务,比如WPS等产品。下载文档的系统A是供应商提供的,在公司已有域控服务的前提下,有2个方案。一是供应商做二开,在应用程序里面调用我们的域控接口,二是自研,在nginx上修改下载请求逻辑。如果要求供应商二开,后续的需求变更就会比较麻烦,过程也比较冗长。因此基于nginx+lua的自研方案就比较合适,并且以后可以对接任意系统,稍微改几行代码,就可以实现对任意应用系统(基于http协议)下载文档的域控功能。

二.方案设计

        我们需要实现拦截器的功能,可以在openresty的 access_by_lua_block阶段,拦截用户文档下载请求/download,使用ngx.socket.tcp主动发起/download请求,将返回的结果作为域控请求/upload的参数,再次使用ngx.socket.tcp发起域控请求,然后将结果返回给浏览器即可。

        需要注意的是,如果文档很大或者下载并发比较高的话,可能会占用nginx服务器的内存,因此我们使用ngx.socket.tcp实现流式传输,内存里面只存很小一部分数据,对nginx服务器造成的压力很小。

三.代码demo

下载接口(/download)代码demo

  @GetMapping("/download")public void download(HttpServletResponse response) throws UnsupportedEncodingException {File file = new File("E:\\test\\test-excel.xlsx");if (file.exists()) { //判断文件父目录是否存在response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("UTF-8");response.setHeader("Content-Disposition", "attachment;fileName="+ java.net.URLEncoder.encode("test-excel.xlsx", "UTF-8"));byte[] buffer = new byte[1024];FileInputStream bis = null; //文件输入流
//            BufferedInputStream bis = null;OutputStream os; //输出流try {os = response.getOutputStream();bis = new FileInputStream(file);
//                bis = new BufferedInputStream(fis);int i = bis.read(buffer);while (i != -1) {os.write(buffer);i = bis.read(buffer);}} catch (Exception e) {e.printStackTrace();}try {bis.close();
//                fis.close();} catch (IOException e) {e.printStackTrace();}}System.out.println("下载结束了!");}

域控接口(/upload)代码demo 

  @PostMapping("/upload") public void doUpload(@RequestParam("file") MultipartFile file , HttpServletResponse response, @RequestParam("rights") String rights) {String fileName = "test-excel.xlsx";System.out.println("rights = " + rights);response.setHeader("Connection", "keep-alive");
//        response.setContentType("application/vnd.ms-excel");
//        response.setHeader("Content-disposition", "attachment;filename=" + fileName);FileInputStream fis;try (OutputStream os = response.getOutputStream()) {fis = (FileInputStream) file.getInputStream();byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = fis.read(buffer)) != -1) {os.write(buffer, 0, bytesRead);}os.flush();} catch (IOException e) {e.printStackTrace();}}

nginx路由配置

location /file-test {access_by_lua_block {local resp_header_param = {"content_type", "content_disposition"}local file_download = require "resty.file_download"local resp_header = file_download.finebi_file_download(resp_header_param)}
}

        这里需要注意的是,如果重新发起请求的url和初始url一样,比如都是http://abc.com/download,那么仅仅使用上面的配置会进入死循环,需要额外再加配置,比如下面,这时候主动发起的url是:http://abc.com/or-file-export/download。

# /or-file-export 是主动发起下载文档请求的路径前缀,
# 需要和正常的下载路径区分开,否则会进入死循环
location /or-file-export/ {rewrite /or-file-export(.*) $1 break;proxy_pass xxx;...
}

lua文档下载脚本

local util = require "resty.lserver.util"
local http_client = require "resty.lserver.http_client"
local cjson = require 'cjson'
local _M = {}
local exclude_header = {}
local file_export_path = "/or-file-export"local function is_include(t, value)for k, v in pairs(t) doif v == value thenreturn trueendendreturn false
endlocal function parse_bi_jwt_token(token)if token thenlocal json = cjson.decode(ngx.decode_base64(string.sub(token, ngx.re.find(token, [[(?<=\.)[^\.]+]], "jo"))))if json and json.sub thenreturn json.subendelse return nil, "用户cookie: f_auth_token不存在!"endreturn nil, "解析用户名称出错!f_auth_token = " .. token
endfunction _M.bi_file_download(resp_header_param)local session_id_name = "f_auth_token"local username = parse_bi_jwt_token(ngx.var["cookie_" .. session_id_name])if not username thenngx.log(ngx.ERR, "发生错误:下载加域权限的excel文件时,bi-user信息解析失败!")ngx.say("发生错误:下载加域权限的excel文件时,bi-user信息解析失败!")ngx.exit(500)endngx.log(ngx.ERR, "\r\nusername = " .. username .. "\r\n")local schema = "https"local url = schema .. "://" .. ngx.var.host .. file_export_path .. ngx.var.request_urilocal req_header = ngx.req.get_headers()local real_header = {}for k,v in pairs(req_header) doif not is_include(exclude_header, k) thenreal_header[k] = vendendlocal origin_client = http_client.get(url, real_header)local resp_header = origin_client["resp_header"]--[[该代码段使用原始请求的响应头,因为该请求是下载文件,因此不必要使用原始的响应头,只需要指定:Content-Type和Content-Dispositionfor key, value in pairs(resp_header) dongx.header[key] = resp_header[key]end
--]]if resp_header_param and type(resp_header_param) == "table" thenfor key, value in pairs(resp_header_param) dongx.header[value] = resp_header[value]endendlocal rights_param = "[{\"user\":\"" .. username .. "\",\"right\":\"OWNER\"}]"local map = { rights = rights_param, file = {client = origin_client, name = "file", filename = "新建仪表板1.xlsx",content_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}}local header_map = {}header_map["Transfer-Encoding"] = "chunked"local final_client = http_client.form_data("http://xx.xx.xx.xx:8082/upload", map, header_map)local response, err = http_client.receive_foreach(final_client, function (chunked)ngx.print(chunked)end)ngx.exit(200)
endreturn _M

httpclient lua脚本

local ngx_re_match = ngx.re.match
local ngx_re_find = ngx.re.find
local str_sub = string.sub
local cjson = require 'cjson'
local bit = require 'bit'
local ffi = require 'ffi'
local resty_base = require "resty.core.base"local class_name = "lserver-http-client-2.0";
local _M = {_VERSION = 0.2, _CLASS_NAME = class_name}
local mt = { __index = _M }
local is_debug = true
local method_get = 'GET'
local method_post = 'POST'local char_underline = string.byte('_')
local char_minus = string.byte('-')
local function http_upper(word, ty)local strlen = #wordlocal buffer= resty_base.get_string_buf(strlen+1)ffi.copy(buffer, word, strlen)buffer[0] = bit.band(buffer[0], -33)for i = 1, strlen-1 doif buffer[i] == char_underline or buffer[i] == char_minus thenbuffer[i] = char_minusbuffer[i + 1] = bit.band(buffer[i + 1], -33)endendlocal res = ffi.string(buffer, strlen)return res
endlocal function is_include(t, value)for k, v in pairs(t) doif v == value thenreturn trueendendreturn false
endlocal header_meta = {__newindex = function (t, k, v)if type(k) == "string" then --and (not is_include(header_whitelist, k))k = http_upper(k)endrawset(t, k, v)end,__index = function (t, k)local value = rawget(t, k)if value == nil and type(k) == "string" thenk = http_upper(k)elsereturn valueendreturn rawget(t, k)end
}local function read_head(self)local header = setmetatable({}, header_meta)local header_line_arrif is_debug thenheader_line_arr = {}endrepeatlocal header_line = self.sock:receive()if is_debug thentable.insert(header_line_arr, header_line)endif not header_line thenreturn headerendlocal i, i2 = ngx.re.find(header_line, [[\:\s*]])if i and i2 thenlocal header_name = str_sub(header_line, 1, i-1)local value = str_sub(header_line, i2 + 1)if header_name thenheader[header_name] = valueendenduntil ngx_re_find(header_line, [[^\s*$]], "jo")if is_debug thenngx.log(ngx.ERR, 'resp header:\n',table.concat(header_line_arr, '\n') .. "\n")endself.resp_header = header
endlocal function send_header(self, header_map)local head_arr = {self.method, " ", self.request_url, " HTTP/1.1\r\n"}for key, value in pairs(header_map) doself.req_header[key] = valueendlocal i = #head_arr + 1for k, v in pairs(self.req_header) dohead_arr[i] = k .. ": " .. tostring(v) .. '\r\n'i = i + 1endhead_arr[i] = '\r\n'if is_debug thenngx.log(ngx.ERR, 'reqheader\n', table.concat(head_arr))endself.sock:send(head_arr)
endlocal function read_code(self)local line = assert(self.sock:receive())if is_debug thenngx.log(ngx.ERR, "\r\n", line, "\r\n" )endlocal code = tonumber(str_sub(line, 10, 12))local reason = str_sub(line, 14)self.code = codeself.reason = reason
end---读取http响应中的一块chunk
---@param sock tcpsock
---@return nil|string chunk
---@return nil|string error
local function read_chunk(sock)local chunk_size_str, err = sock:receive()if not chunk_size_str thenreturn nil, err or 'chunk size error'endlocal chunk_size = tonumber(chunk_size_str, 16)if not chunk_size thenreturn nil, "chunk size error"endlocal chunk, err = sock:receive(chunk_size)if not chunk thenreturn nil,err or 'no chunk'endlocal chunk_end = sock:receive(2)if chunk_end ~= "\r\n" thenreturn nil, "error chunk_end"endreturn chunk
end---发送一块chunked
---@param sock tcpsock
---@return nil|string data
local function send_chunked(sock, data)data = data or ""sock:send({string.format("%x", #data), '\r\n', data, '\r\n'})
endlocal function transfer_chunk(sock, dest_sock)local chunk_size_str, err = sock:receive()if not chunk_size_str thenreturn nil, err or 'chunk size error'endlocal send_content = chunk_size_str .. "\r\n"if chunk_size_str ~= "0" thendest_sock:send(send_content)endlocal chunk_size = tonumber(chunk_size_str, 16)if not chunk_size thenif is_debug thenngx.log(ngx.ERR, "XXXXXXXXXXXXXXXXXX chunk size error XXXXXXXXXXXXXXXXXXX")endreturn nil, ""endif chunk_size == 0 thenif is_debug thenngx.log(ngx.ERR, "VVVVVVVVVVVVVVVVVV chunk receive 结束了 VVVVVVVVVVVVVVVVVV")endendlocal chunk, err = sock:receive(chunk_size)if not chunk thenif is_debug thenngx.log(ngx.ERR, "XXXXXXXXXXXXXXXXXX no chunk XXXXXXXXXXXXXXXXXXX")endreturn nil,err or 'no chunk'endif chunk ~= "" thendest_sock:send(chunk)endlocal chunk_end = sock:receive(2)if chunk_end ~= "\r\n" thenif is_debug thenngx.log(ngx.ERR, "XXXXXXXXXXXXXXXXXX error chunk_end XXXXXXXXXXXXXXXXXXX")endreturn nil, "error chunk_end"endif chunk_size ~= 0 thendest_sock:send(chunk_end)endreturn chunk
endlocal function conn_and_new(uri)local sock = ngx.socket.tcp()local schema, host, port, request_urlif string.sub(uri, 1, 5) == "unix:" thenlocal _, sock_file_end = ngx_re_find(uri, [[unix:.+?\.\w+]], 'jo')if sock_file_end thenschema = 'unix'host = str_sub(uri, 1, sock_file_end)request_url = str_sub(uri, sock_file_end+1)elseerror('unix uri error')endelselocal uriMatch = assert(ngx_re_match(uri, [[^(?:(http[s]?):)//([^:/\?]+)(?::(\d+))?(.*)]], "jo"), "http地址格式错误")schema, host, port, request_url = unpack(uriMatch)port = port and tonumber(port) or (schema=="https" and 443 or 80)endrequest_url = (request_url and request_url ~= "") and request_url or "/"if is_debug thenngx.log(ngx.ERR, "schema = " .. schema .. " host = " .. host .. " port = " .. port .. " request_url = " .. request_url  .. "\r\n")endif schema == 'unix' thenassert(sock:connect(host), "sock连接错误: "..uri)elseassert(sock:connect(host, port), "网络连接错误: "..uri) endif schema == "https" thenassert(sock:sslhandshake(nil, host, false))endreturn setmetatable({uri = uri,sock = sock,request_url = request_url,host = host,method = method_get,code = 0,req_header = setmetatable({Host = host}, header_meta),resp_header = nil,} , mt)
endfunction _M.get(uri, req_header_map)local self = conn_and_new(uri)send_header(self, req_header_map)read_code(self)read_head(self)return self
endfunction _M.post(uri, form, req_header_map)local self = conn_and_new(uri)self.method = method_postlocal body = {}for key, value in pairs(form) dotable.insert(body, ngx.escape_uri(key) .. "=" .. ngx.escape_uri(value))endlocal body_str = table.concat(body, "&")self.req_header.content_type = 'application/x-www-form-urlencoded; charset=UTF-8'self.req_header.content_length = #body_strsend_header(self, req_header_map)self.sock:send(body_str)read_code(self)read_head(self)return self
endfunction _M.post_json(uri, json_obj, req_header_map)local self = conn_and_new(uri)self.method = method_postlocal body_str = cjson.encode(json_obj)self.req_header.content_type = 'application/json'send_header(self, req_header_map)self.sock:send(body_str)read_code(self)read_head(self)return self
end---循环遍历接受的值
---@param self table
---@param callback function(chunk:string)
function _M.receive_foreach(self, callback)assert(self.code == 200, "code is " .. (self.code or 999))local sock = self.sock ---@cast sock tcpsockif type(self.resp_header) == "table" and self.resp_header.transfer_encoding == 'chunked' thenrepeatlocal chunk, err = read_chunk(self.sock)if chunk == "" thenbreakelseif chunk thencallback(chunk)enduntil not chunkelselocal content_length = 0if type(self.resp_header) == "table" and self.resp_header.content_length thencontent_length = tonumber(self.resp_header.content_length) or 0endlocal receive_len = 0repeatlocal chunk = sock:receiveany(32768)if chunk thenreceive_len = receive_len + #chunkcallback(chunk)if receive_len >= content_length thenbreakendenduntil not chunkendself.sock:close()
endfunction _M.form_data(uri, fromdata_map, req_header_map)local self = conn_and_new(uri)self.method = method_postlocal boundary = "WebKitFormBoundary" .. ngx.md5(tostring(ngx.now()))self.req_header["Content-Type"] = "multipart/form-data; boundary=----" .. boundarysend_header(self, req_header_map)for key, value in pairs(fromdata_map) doif type(value) == "string" thenlocal req_param = "------" .. boundary .. "\r\n" .."Content-Disposition: form-data; name=\"".. key .."\"\r\n\r\n" .. valueif is_debug thenngx.log(ngx.ERR, "form_data_reqparam\r\n", req_param .. "\r\n")endif self.req_header["Transfer-Encoding"] == "chunked" thensend_chunked(self.sock, req_param)send_chunked(self.sock, '\r\n')else self.sock:send(req_param)self.sock:send("\r\n")endelseif type(value) == "table" and key == "file" thenlocal req_param = "------" .. boundary .. "\r\n" .. "Content-Disposition: form-data; ".. "name=\"" .. value.name .. "\"; filename=\"" .. value.filename .. "\"\r\n".. "Content-Type: " .. value.content_type .. "\r\n\r\n"if is_debug thenngx.log(ngx.ERR, "form_data_reqparam\r\n", req_param)endif self.req_header["Transfer-Encoding"] == "chunked" thensend_chunked(self.sock, req_param)local client = value.clientclient:receive_foreach(function (chunk)send_chunked(self.sock, chunk)end)send_chunked(self.sock, '\r\n')else self.sock:send(req_param)self.sock:send(value.content)self.sock:send("\r\n")endendendlocal end_boundary = "------".. boundary .."--\r\n"if self.req_header["Transfer-Encoding"] == "chunked" thensend_chunked(self.sock, end_boundary)send_chunked(self.sock)else self.sock:send(end_boundary)endread_code(self)read_head(self)return self
endfunction _M.receive_all(self)if type(self.resp_header) == "table" and self.resp_header.transfer_encoding == 'chunked' thenif is_debug thenngx.log(ngx.ERR, "###########################receive_all using chunk###########################r\n")endlocal result = {}repeatlocal chunk, err = read_chunk(self.sock)if chunk == "" thenbreakelseif chunk thentable.insert(result, chunk)enduntil not chunkself.sock:close()return table.concat(result)endlocal receive_typereceive_type = '*a'if type(self.resp_header) == "table" and self.resp_header.content_length thenreceive_type = tonumber(self.resp_header.content_length)endlocal body, err = self.sock:receive(receive_type)self.sock:close()return body, err
endfunction _M.close(self)self.sock:close()
endreturn _M

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

初识C语言—指针

.h 头文件&#xff08;函数的声明&#xff0c;类型的声明&#xff0c;头文件的包含&#xff09; .c 源文件&#xff08;函数实现&#xff09; 浮点数的四舍五入&#xff0c;不能用你肉眼看到的数值来计算&#xff0c;因为浮点数在内存中有可能不能精确保存。 内存&#xff1a…

ICML23 - Synthetic Data for Model Selection

前言 如果你对这篇文章感兴趣&#xff0c;可以点击「【访客必读 - 指引页】一文囊括主页内所有高质量博客」&#xff0c;查看完整博客分类与对应链接。 本文关注的问题为&#xff1a;是否可以使用合成数据&#xff08;Synthetic Data&#xff09;用于模型选择&#xff1f;即不…

多输入多输出 | Matlab实现RIME-BP霜冰算法优化BP神经网络多输入多输出预测

多输入多输出 | Matlab实现RIME-BP霜冰算法优化BP神经网络多输入多输出预测 目录 多输入多输出 | Matlab实现RIME-BP霜冰算法优化BP神经网络多输入多输出预测预测效果基本介绍程序设计往期精彩参考资料 预测效果 基本介绍 多输入多输出 | Matlab实现RIME-BP霜冰算法优化BP神经网…

MySQL 外键约束 多表联查 联合查询

外键约束 外键用来让两张表的数据之间建立连接&#xff0c;从而保证数据的一致性和完整性。 有一张学生表和班级表&#xff0c;学生表通过班级表的ID引用到该班级&#xff0c;从而进行关联&#xff0c;而通过外键约束可以保证数据的一致性完整性。 如学生ID18关联到课程ID1号…

瑞吉苍穹外卖如何拓展?已经经过不同公司多轮面试。项目中会问到哪些问题?以及问题如何解决?

别催了&#xff0c;别催了&#xff0c;先收藏吧。 作者大大正在加班加点完成。 文章会尽快发布&#xff0c;关注收藏&#xff0c;尽请期待。 想要加入并查阅作者的知识库可以联系作者 不要白嫖&#xff0c;通过后&#xff0c;附上关注和收藏截图。 已有众多小伙伴加入 目前…

MySql安全加固:可信IP地址访问控制 设置密码复杂度

MySql安全加固&#xff1a;可信IP地址访问控制 & 设置密码复杂度 1.1 可信IP地址访问控制1.2 设置密码复杂度 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 1.1 可信IP地址访问控制 当您在创建用户时使用’%作为主机部分&#xff0c;…

Day20-磁盘管理

Day20-磁盘管理 1. cut 切:2. 磁盘历史和内外部物理结构介绍2.1 磁盘发展趋势和实现措施2.2 磁盘知识的体系结构2.3 机械磁盘的外部结构2.4 SSD固态硬盘的外部结构2.5 固态硬盘内部结构2.6 缓存在服务器各硬件上的速度和大小对比另类维度图解&#xff0c;从上到下由高速到低速&…

2024现代Android开发趋势

2024现代Android开发趋势 在当今的Android开发领域&#xff0c;我们看到了许多令人兴奋的技术和趋势&#xff0c;这些技术和趋势正在改变着应用程序的开发方式和用户体验。让我们一起深入探讨2024年现代Android开发的主要方向和关键技术。 无处不在的Kotlin Kotlin已经成为An…

202435读书笔记|《半小时漫画中国史》——读点经济学与历史,生活更美好,趣味烧脑土地制度、商鞅变法、华丽丽的丝绸之路这里都有

202435读书笔记|《半小时漫画中国史》——读点经济学与历史&#xff0c;生活更美好&#xff0c;趣味烧脑土地制度、商鞅变法、华丽丽的丝绸之路这里都有 1. 土地政策、度量衡及税收2. 商鞅变法3. 西汉经济4. 西汉盐铁大辩论5. 西汉丝绸之路 《半小时漫画中国史&#xff1a;经济…

吸猫毛空气净化器哪个好?推荐除猫毛效果好宠物空气净化器品牌

当下有越来越多的家庭选择养宠物&#xff01;尽管家里变得更加温馨&#xff0c;但养宠可能会带来异味和空气中的毛发增多可能会带来健康问题&#xff0c;这是一个大问题&#xff01; 不想家里弥漫着异味&#xff0c;特别是来自宠物便便的味道&#xff0c;所以需要一款能够处理…

打印100-200之间的素数

#include <stdio.h>int prime(int n){int i 1;for(i 2;i < n;i){if(n % i 0)return 0;}return 1; } //打印100-200之间的素数 int main() {int n 0;int j 100;for(j 100;j < 200;j){if(prime(j)){printf("%d是素数\n",j);n;}}printf("100-200…

【center-loss 中心损失函数】 原理及程序解释(更新中)

文章目录 前言问题引出open-set问题抛出 解决方法softmax函数、softmax-loss函数解决代码&#xff08;center_loss.py&#xff09;原理程序解释 代码运用 如何梯度更新首先了解一下基本的梯度下降算法然后 补充&#xff1a;外围知识模型 前言 学习一下&#xff1a; 中心损失函…

【刷题】位运算

消失的两个数字 消失的两个数字 “单身狗”进阶版思路 class Solution { public:vector<int> missingTwo(vector<int>& nums) {int ret 0;int n nums.size();for(int i 0; i < n; i){ret ^ (nums[i] ^ i);}ret ^ (n ^ (n 1) ^ (n 2));// 按位异或的…

94. 递归实现排列型枚举 刷题笔记

思路 依次枚举 每个位置用哪个数字 要求按照字典序最小来输出 而每次搜索下一层时i都是从1开始 也就是说 如果有小的数可以填上 那么该方案会填上这个数字 例如 当n等于3 第一次搜索 1 2 3输出后返回 返回后此时i3 第二个位置填3 1 3 2 输出后返回 此时返回到第一层…

如何用Python检查时间序列数据是否平稳?

时间序列数据通常以其时间性质为特征。这种时间性质为数据增加了趋势或季节性&#xff0c;使其与时间序列分析和预测兼容。如果时间序列数据不随时间变化或没有时间结构&#xff0c;则称其为静态数据。因此&#xff0c;检查数据是否平稳是非常必要的。在时间序列预测中&#xf…

基于粒子群优化算法的图象聚类识别matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于粒子群优化算法的图象聚类识别。通过PSO优化方法&#xff0c;将数字图片的特征进行聚类&#xff0c;从而识别出数字0~9. 2.测试软件版本以及运行结果展示 M…

智能咖啡厅助手:人形机器人 +融合大模型,行为驱动的智能咖啡厅机器人(机器人大模型与具身智能挑战赛)

智能咖啡厅助手&#xff1a;人形机器人 融合大模型&#xff0c;行为驱动的智能咖啡厅机器人(机器人大模型与具身智能挑战赛) “机器人大模型与具身智能挑战赛”的参赛作品。的目标是结合前沿的大模型技术和具身智能技术&#xff0c;开发能在模拟的咖啡厅场景中承担服务员角色并…

Flutter中的三棵树

Widget Tree&#xff1a; 页面配置信息。 Element Tree&#xff1a; Widget tree的实例化对象&#xff0c;创建出renderObject&#xff0c;并关联到element.renderobject属性上&#xff0c;最后完成RenderObject Tree的创建。 RenderObject Tree&#xff1a;完成布局和图层绘制…

自测-1 打印沙漏

文章预览&#xff1a; 题目算法代码 题目 算法 以前做过这个&#xff0c;那次是c语言写的&#xff0c;一点一点处理一层一层完成&#xff0c;这次我换了一种语言用了另一种思想使用递归去写&#xff0c;还是我们要先求出应该有多少层这个很容易&#xff0c;中间输出部分我们算…

STM32标准库——(14)I2C通信协议、MPU6050简介

1.I2C通信 I2C 通讯协议(Inter&#xff0d;Integrated Circuit)是由Phiilps公司开发的&#xff0c;由于它引脚少&#xff0c;硬件实现简单&#xff0c;可扩展性强&#xff0c; 不需要USART、CAN等通讯协议的外部收发设备&#xff0c;现在被广泛地使用在系统内多个集成电路(IC)间…