-
用到了wrapper,ge、le、ne、eq等的用法,及多表查询自写sql整理资料记录一下,以备后续复习。 目录------------(可点击相应目录直接跳转) 一、条件构造器关系介绍 条件构造器关系介绍 : wapper介绍 : 二、项目实例 1、根据主键或者简单的查询条件进行查询 2、MyBatis-Plus还提供了Wrapper条件构造器,具体使用看如下代码: 三、具体使用操作 1、ge、gt、le、lt、isNull、isNotNull 2、eq、ne 3、between、notBetween 4、allEq 5、like、notLike、likeLeft、likeRight 6、in、notIn、inSql、notinSql、exists、notExists 7、or、and 8、嵌套or、嵌套and 9、orderBy、orderByDesc、orderByAsc 10、last 11、指定要查询的列 12、set、setSql 四、项目中实际应用代码实例 实例1--包含 eq相等的比较方法 实例2--包含 ge le ge等比较方法,及分页查询方法 实例3--多表查询,手写sql示例,五表联查 先了解一下内外连接: SQL内连接(INNER JOIN) SQL外连接(OUTER JOIN 一、条件构造器关系介绍 条件构造器关系介绍 : 上图绿色框为抽象类abstract 蓝色框为正常class类,可new对象 黄色箭头指向为父子类关系,箭头指向为父类 wapper介绍 : Wrapper : 条件构造抽象类,最顶端父类 AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件 QueryWrapper : Entity 对象封装操作类,不是用lambda语法 UpdateWrapper : Update 条件封装,用于Entity对象更新操作 AbstractLambdaWrapper : Lambda 语法使用 Wrapper统一处理解析 lambda 获取 column。 LambdaQueryWrapper :看名称也能明白就是用于Lambda语法使用的查询Wrapper LambdaUpdateWrapper : Lambda 更新封装Wrapper 二、项目实例 1、根据主键或者简单的查询条件进行查询 /** * 通过单个ID主键进行查询 */ @Test public void selectById() { User user = userMapper.selectById(1094592041087729666L); System.out.println(user); } /** * 通过多个ID主键查询 */ @Test public void selectByList() { List<Long> longs = Arrays.asList(1094592041087729666L, 1094590409767661570L); List<User> users = userMapper.selectBatchIds(longs); users.forEach(System.out::println); } /** * 通过Map参数进行查询 */ @Test public void selectByMap() { Map<String, Object> params = new HashMap<>(); params.put("name", "张雨琪"); List<User> users = userMapper.selectByMap(params); users.forEach(System.out::println); } 2、MyBatis-Plus还提供了Wrapper条件构造器,具体使用看如下代码: /** * 名字包含雨并且年龄小于40 * <p> * WHERE name LIKE '%雨%' AND age < 40 */ @Test public void selectByWrapperOne() { QueryWrapper<User> wrapper = new QueryWrapper(); wrapper.like("name", "雨").lt("age", 40); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 名字包含雨 * 年龄大于20小于40 * 邮箱不能为空 * <p> * WHERE name LIKE '%雨%' AND age BETWEEN 20 AND 40 AND email IS NOT NULL */ @Test public void selectByWrapperTwo() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.like("name", "雨").between("age", 20, 40).isNotNull("email"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 名字为王性 * 或者年龄大于等于25 * 按照年龄降序排序,年龄相同按照id升序排序 * <p> * WHERE name LIKE '王%' OR age >= 25 ORDER BY age DESC , id ASC */ @Test public void selectByWrapperThree() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.likeRight("name", "王").or() .ge("age", 25).orderByDesc("age").orderByAsc("id"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 查询创建时间为2019年2月14 * 并且上级领导姓王 * <p> * WHERE date_format(create_time,'%Y-%m-%d') = '2019-02-14' AND manager_id IN (select id from user where name like '王%') */ @Test public void selectByWrapperFour() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.apply("date_format(create_time,'%Y-%m-%d') = {0}", "2019-02-14") .inSql("manager_id", "select id from user where name like '王%'"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 查询王姓 * 并且年龄小于40或者邮箱不为空 * <p> * WHERE name LIKE '王%' AND ( age < 40 OR email IS NOT NULL ) */ @Test public void selectByWrapperFive() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.likeRight("name", "王").and(qw -> qw.lt("age", 40).or().isNotNull("email")); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 查询王姓 * 并且年龄大于20 、年龄小于40、邮箱不能为空 * <p> * WHERE name LIKE ? OR ( age BETWEEN ? AND ? AND email IS NOT NULL ) */ @Test public void selectByWrapperSix() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.likeRight("name", "王").or( qw -> qw.between("age", 20, 40).isNotNull("email") ); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * (年龄小于40或者邮箱不为空) 并且名字姓王 * WHERE ( age < 40 OR email IS NOT NULL ) AND name LIKE '王%' */ @Test public void selectByWrapperSeven() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.nested(qw -> qw.lt("age", 40).or().isNotNull("email")) .likeRight("name", "王"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 查询年龄为30、31、32 * WHERE age IN (?,?,?) */ @Test public void selectByWrapperEight() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.in("age", Arrays.asList(30, 31, 32)); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } /** * 查询一条数据 * limit 1 */ @Test public void selectByWrapperNine() { QueryWrapper<User> wrapper = Wrappers.query(); wrapper.in("age", Arrays.asList(30, 31, 32)).last("limit 1"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } 三、具体使用操作 注意:以下条件构造器的方法入参中的 column 均表示数据库字段 1、ge、gt、le、lt、isNull、isNotNull @Test public void testDelete() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper .isNull("name") .ge("age", 12) .isNotNull("email"); int result = userMapper.delete(queryWrapper); System.out.println("delete return count = " + result); } SQL:UPDATE user SET deleted=1 WHERE deleted=0 AND name IS NULL AND age >= ? AND email IS NOT NULL 2、eq、ne 注意:seletOne返回的是一条实体记录,当出现多条时会报错 @Test public void testSelectOne() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", "Tom"); User user = userMapper.selectOne(queryWrapper); System.out.println(user); } 3、between、notBetween 包含大小边界 @Test public void testSelectCount() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.between("age", 20, 30); Integer count = userMapper.selectCount(queryWrapper); System.out.println(count); } SELECT COUNT(1) FROM user WHERE deleted=0 AND age BETWEEN ? AND ? 4、allEq @Test public void testSelectList() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); Map<String, Object> map = new HashMap<>(); map.put("id", 2); map.put("name", "Jack"); map.put("age", 20);9 queryWrapper.allEq(map); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); } SELECT id,name,age,email,create_time,update_time,deleted,version FROM user WHERE deleted=0 AND name = ? AND id = ? AND age = ? 5、like、notLike、likeLeft、likeRight selectMaps返回Map集合列表 @Test public void testSelectMaps() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper .notLike("name", "e") .likeRight("email", "t"); List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);//返回值是Map列表 maps.forEach(System.out::println); } SELECT id,name,age,email,create_time,update_time,deleted,version FROM user WHERE deleted=0 AND name NOT LIKE ? AND email LIKE ? 6、in、notIn、inSql、notinSql、exists、notExists in、notIn: notIn("age",{1,2,3})--->age not in (1,2,3) notIn("age", 1, 2, 3)--->age not in (1,2,3) inSql、notinSql:可以实现子查询 例: inSql("age", "1,2,3,4,5,6")--->age in (1,2,3,4,5,6) 例: inSql("id", "select id from table where id < 3")--->id in (select id from table where id < 3) @Test public void testSelectObjs() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); //queryWrapper.in("id", 1, 2, 3); queryWrapper.inSql("id", "select id from user where id < 3"); List<Object> objects = userMapper.selectObjs(queryWrapper);//返回值是Object列表 objects.forEach(System.out::println); } SELECT id,name,age,email,create_time,update_time,deleted,version FROM user WHERE deleted=0 AND id IN (select id from user where id < 3) 7、or、and 注意:这里使用的是 UpdateWrapper 不调用or则默认为使用 and 连 @Test public void testUpdate1() { //修改值 User user = new User(); user.setAge(99); user.setName("Andy"); //修改条件 UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<>(); userUpdateWrapper .like("name", "h") .or() .between("age", 20, 30); int result = userMapper.update(user, userUpdateWrapper); System.out.println(result); } UPDATE user SET name=?, age=?, update_time=? WHERE deleted=0 AND name LIKE ? OR age BETWEEN ? AND ? 8、嵌套or、嵌套and 这里使用了lambda表达式,or中的表达式最后翻译成sql时会被加上圆括号 @Test public void testUpdate2() { //修改值 User user = new User(); user.setAge(99); user.setName("Andy"); //修改条件 UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<>(); userUpdateWrapper .like("name", "h") .or(i -> i.eq("name", "李白").ne("age", 20)); int result = userMapper.update(user, userUpdateWrapper); System.out.println(result); } UPDATE user SET name=?, age=?, update_time=? WHERE deleted=0 AND name LIKE ? OR ( name = ? AND age <> ? ) 9、orderBy、orderByDesc、orderByAsc @Test public void testSelectListOrderBy() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.orderByDesc("id"); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); } SELECT id,name,age,email,create_time,update_time,deleted,version FROM user WHERE deleted=0 ORDER BY id DESC 10、last 直接拼接到 sql 的最后 注意:只能调用一次,多次调用以最后一次为准 有sql注入的风险,请谨慎使用 @Test public void testSelectListLast() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.last("limit 1"); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); } SELECT id,name,age,email,create_time,update_time,deleted,version FROM user WHERE deleted=0 limit 1 11、指定要查询的列 @Test public void testSelectListColumn() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.select("id", "name", "age"); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); } SELECT id,name,age FROM user WHERE deleted=0 12、set、setSql 最终的sql会合并 user.setAge(),以及 userUpdateWrapper.set() 和 setSql() 中 的字段 @Test public void testUpdateSet() { //修改值 User user = new User(); user.setAge(99); //修改条件 UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<>(); userUpdateWrapper .like("name", "h") .set("name", "老李头")//除了可以查询还可以使用set设置修改的字段 .setSql(" email = '123@qq.com'");//可以有子查询 int result = userMapper.update(user, userUpdateWrapper); } UPDATE user SET age=?, update_time=?, name=?, email = '123@qq.com' WHERE deleted=0 AND name LIKE ? 四、项目中实际应用代码实例 (此部分更新于2022年7月20日) 实例1--包含 eq相等的比较方法 实例2--包含 ge le ge等比较方法,及分页查询方法 实例3--多表查询,手写sql示例,五表联查 先了解一下内外连接: 什么是连接表? 多表查询原理:将多个表通过笛卡尔积形成一个虚表,再根据查询条件筛选符合条件的数据。 在关系数据库中,数据分布在多个逻辑表中。 要获得完整有意义的数据集,需要使用连接来查询这些表 中的数据。 SQL Server支持多种 连接包括 INNER JOIN:内连接,关键字在表中存在至少一个匹配时返回行。 left join : 左连接,返回左表中所有的记录以及右表中连接字段相等的记录。 right join : 右连接,返回右表中所有的记录以及左表中连接字段相等的记录。 inner join : 内连接,又叫等值连接,只返回两个表中连接字段相等的行。 full join : 外连接,返回两个表中的行:left join + right join。 cross join : 结果是笛卡尔积,就是第一个表的行数乘以第二个表的行数。 GROUP BY:全外连接, 子句必须放在 WHERE 子句中的条件之后,必须放在 ORDER BY 子句之前 每种连接类型指定SQL Server如何使用一个表中的数据来选择另一个表中的行 SQL内连接(INNER JOIN) 返回两张表中符合连接条件的数据行 内连接是从结果表中删除与被连接表中未匹配行的所有行,所以内连接可能会丢失信息 SQL外连接(OUTER JOIN) 外连接(OUTER JOIN)分 为左连接、右连接和全连接 左连接:返回左表中的所以行,如果左表中行在右表中没有匹配行,则结果中右表中的列返回空值NULL 语法:SELECT * FROM 表1 LEFT OUTER JOIN 表2 ON 条件 eg:我们左连接Student表、Score表查询学生的成绩,SQL 语句如下: SELECT * FROM Student LEFT OUTER JOIN Score ON Student.id = Score.studentID 右 连 接:返回右表中的所以行,如果右表中行在左表中没有匹配行,则结果中左表中的列返回空值NULL 语法:SELECT * FROM 表1 RIGHT OUTER JOIN 表2 ON 条件 eg:我们右连接Student表、Score表查询学生的成绩,SQL 语句如下: SELECT * FROM Student RIGHT OUTER JOIN Score ON Student.id = Score.studentID 全连接:返回左表和右表中的所有行,当某行在另一表中没有匹配行,则另一表中的补NULL 这个是mybatis-plus插件中mapper的一个表查询写法,由多个表内连接或外连接组成的数据。 用到的表结构如下: 分别为问题库表、数据字典、数据字典类型、部门表、用户表 数据示例如下:obdis_problem表里去查type字段内容并返回对应的汉字语义 查询每个字段都进行一次表连接,拿到对应的值,得出一个表结果值,即A\B\C\D等等结果,最后拼起来返回前端展示即可。搞懂了这个,所有的多表查询就基本迎刃而解。 <!-- 问题库列表返回与查询--> <select id="searchMoreProblem" resultType="com.hollysys.obdis.vo.problem.SearchObdisProblem"> select p.id , p.level, A.dict_value as level_text, p.dept_id_mgr, B.org_name as deptIdMgr_text, p.dept_id_work, C.org_name as deptIdWork_text, p.station, D.dict_value as station_text, p.source, E.dict_value as source_text, p.categroy, F.dict_value as categroy_text, p.type, G.dict_value as type_text, p.dev_code, H.dict_value as devCode_text, p.check_time, p.check_user, I.username as checkUser_text, p.deadline, p.deadline_real, p.duty_user, J.username as dutyUser_text, p.correct_user, K.username as correctUser_text, p.state, p.problem, p.requirement, p.solution from obdis_problem p left join t_dict A on p.level = A.id left join t_dept B on p.dept_id_mgr = B.org_id left join t_dept C on p.dept_id_work = C.org_id left join t_dict D on p.station = D.id left join t_dict E on p.source = E.id left join t_dict F on p.categroy = F.id left join t_dict G on p.type = G.id left join t_dict H on p.dev_code = H.id left join t_user I on p.check_user = I.user_id left join t_user J on p.duty_user = J.user_id left join t_user K on p.correct_user = K.user_id where 1=1 <if test="searchObdisProblem.levelText != null and searchObdisProblem.levelText != ''"> and A.dict_value like concat('%', #{searchObdisProblem.levelText}, '%') </if> <if test="searchObdisProblem.deptIdMgrText != null and searchObdisProblem.deptIdMgrText != ''"> and B.org_name like concat('%', #{searchObdisProblem.deptIdMgrText}, '%') </if> <if test="searchObdisProblem.deptIdWorkText != null and searchObdisProblem.deptIdWorkText != '' "> and C.org_name like concat('%', #{searchObdisProblem.deptIdWorkText}, '%') </if> <if test="searchObdisProblem.stationText != null and searchObdisProblem.stationText != ''"> and D.dict_value = #{searchObdisProblem.stationText} </if> <if test="searchObdisProblem.sourceText != null and searchObdisProblem.sourceText != ''"> and E.dict_value = #{searchObdisProblem.sourceText} </if> <if test="searchObdisProblem.categroyText != null and searchObdisProblem.categroyText != ''"> and F.dict_value = #{searchObdisProblem.categroyText} </if> <if test="searchObdisProblem.typeText != null and searchObdisProblem.typeText != ''"> and G.dict_value = #{searchObdisProblem.typeText} </if> <if test="searchObdisProblem.devCodeText != null and searchObdisProblem.devCodeText != ''"> and H.dict_value = #{searchObdisProblem.devCodeText} </if> <if test="searchObdisProblem.checkTimeStart != null "> and p.check_time >= #{searchObdisProblem.checkTimeStart} </if> <if test="searchObdisProblem.checkTimeEnd != null "> and p.check_time <= #{searchObdisProblem.checkTimeEnd} order by p.check_time </if> <if test="searchObdisProblem.checkUserText != null and searchObdisProblem.checkUserText != ''"> and I.username = #{searchObdisProblem.checkUserText} </if> <if test="searchObdisProblem.deadline != null "> and p.deadline = #{searchObdisProblem.deadline} </if> <if test="searchObdisProblem.deadlineReal != null "> and p.deadline_real = #{searchObdisProblem.deadlineReal} </if> <if test="searchObdisProblem.dutyUserText != null and searchObdisProblem.dutyUserText != ''"> and J.username = #{searchObdisProblem.dutyUserText} </if> <if test="searchObdisProblem.correctUserText != null and searchObdisProblem.correctUserText != ''"> and K.username = #{searchObdisProblem.correctUserText} </if> <if test="searchObdisProblem.state != null and searchObdisProblem.state != ''"> and state = #{searchObdisProblem.state} </if> <if test="searchObdisProblem.problem != null and searchObdisProblem.problem != ''"> and p.problem like concat('%', #{searchObdisProblem.problem}, '%') </if> <if test="searchObdisProblem.requirement != null and searchObdisProblem.requirement != ''"> and p.requirement like concat('%', #{searchObdisProblem.requirement}, '%') </if> <if test="searchObdisProblem.solution != null and searchObdisProblem.solution != ''"> and p.solution like concat('%', #{searchObdisProblem.solution}, '%') </if> </select> ———————————————— 原文链接:https://blog.csdn.net/qq_39715000/article/details/120090033
-
el-cascader(联机选择器)动态加载+编辑默认值回显 最近又在工作中遇到了一个问题,就是在我们使用el-cascader加载默认值的时候,如果我们无法拿到全部的options数据,cascader的输入框和联级选择框都会遇到回显问题(只能显示第一层的数据),这个时候我们要怎么做呢,首先我们来看一下我们想要的效果 效果展示 先来看一下效果(由于我不太会用截屏动图工具 所以分成2张): 输入框中的回显数据 联级选择框中的已选数据 解决思路 其实cascader归根结底也就是那么几个属性的事,我们首先来看一下文档,这里列出了一些我们要用到的: 参数 说明 value / v-model 选中项绑定值 options 可选项数据源,键名可通过 Props 属性配置 lazy 是否动态加载子节点,需与 lazyLoad 方法结合使用 lazyload 加载动态数据的方法,仅在 lazy 为 true 时有效 那么首先我们来分析一下问题,问题的原因是因为我们的接口无法提供给我们完全的树形结构options数据,导致即使我们将获取到的已选数据传给v-model也加载不出来,所以我们要做的就是以下几步: 1.获取预选值 需要注意的是,我们这里获取的预选值最好是我们通过el-cascader提交时的数组数据,即每一个数据都是带有选择路径的数组,例如图中的PMO,获取的数据最好是[‘58集团’,‘技术功能平台群’,‘研发管理部’,‘PMO’],如果做不到这样的形式,那我们无论如何也要从其他接口获取到之前的路径项(不然我们就只能从最基础部分遍历获取全部的树形结构了),而后我们需要将取得的集合合并为一个路径群数组,如果和我是一样的多选联机选择框,最好在获取预选值的时候就是用promise const queue = res.result.map(item => { return new Promise(resolve => { that.$axios.get('获取预选值接口').then(data => { resolve(data.单一路径数组) }) }) }) Promise.all(queue).then(result => { result.forEach(i => { that.路径群数组.push(i) }) }) 2.根据预选值制作直线结构数据的数组对象 在我们拿到了想要的数据之后,我们需要将预选值规整为一个数组,并进行去重操作 路径数组 = Array.from(new Set(that.路径群数组.flat())) 这样我们就得到了一个包含路径中所有项的数组,注意:如果是多选型的cascader,那么在这个数组里就会有同级若干项数据,我们不用在意,遍历数组并调用获取下层数据的接口获得下层数据res,并拼接成数组对象,这里要活用Promise,最后我们要的数据形式是(注意这里的pid是指每个数组上一层的父级id) result = [{id: value1, Name: label1, children:res1 , pid: pid1}, {id: value2, Name: label2, children:res2 , pid: pid2}, {id: value3, Name: label3, children:res3 , pid: pid3}] 3.将直线结构的数组对象转换为树形结构 之后再将我们得到的数组对象转化成树形结构,网上有很多方法,这里我随便贴一个: var data = [] this.toTree(result, data, 0) toTree (list, data, fatherId) { list.forEach(item => { if (item.pid === fatherId) { var child = { orgName: item.orgName, id: item.id, children: [] } this.toTree(list, child.children, item.id) data.push(child) } }) }, 这样一来我们就制作了一个包含预选项及其各个父级的树形结构 4.将树形结构赋值给options 将树形结构赋值给options,这样他就可以在最开始的文本框中加载出预选项,并且不影响其他选项动态加载的处理 Options = data 5.总结 最后要声明一下,完成这样效果的方法不止这一种,这是在我走了很多弯路之后做出来的,属于笨办法之一吧; 网上还有大佬说虚拟一个el-cascader的输入框,将预选值放进去,点击的时候再进行动态加载,这也是一种不错的方法; 总之我个人感觉表达的不是很清晰,不过真的尽力了,大家如果有建议或者问题请给我留言,谢谢观看 ———————————————— 原文链接:https://blog.csdn.net/yzy13521758223/article/details/108055393
-
微信分享网页不显示缩略图片的原因为规范自定义分享链接功能在网页上的使用,自2017年4月25日起,JSSDK“分享到朋友圈”及“发送给朋友”接口,自定义的分享链接,其域名或路径必须与当前页面对应的公众号JS安全域名一致,否则将调用失败。也就是说如果你想你的网站在被分享时显示缩略图,那么你要有一个公众号,并且在公众号内设置JS安全域名,也就是添加你网站的网址,这是必须的,如果这一步不能实现,那么你不用往下看了。一、最快的解决方法如果你不是专业的程序员,虽然微信官方给出了代码,以及我下面也会再次给出代码,你可能仍然无法实现。因为在微信官方代码与你的网站之间,还需要二次开发对接代码,把微信代码与你的网站连接起来,这个功能才能最终实现。同时因为每个网站的源程序不一样,所以这个对接代码也不能通用,需要根据你的网站程序单独开发。请在微信内打开链接分享测试。二、微信官方办法步骤一:绑定域名先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。步骤二:设置开发者密码和IP白名单登录微信公众平台,进入开发――基本配置――开发者密码(AppSecret)和IP白名单步骤三:引入JS文件在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.2.0.js备注:支持使用AMD/CMD 标准模块加载方法加载步骤四:通过config接口注入权限验证配置所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用)。wx.config({debug: true,// 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。appId: ‘‘,// 必填,公众号的唯一标识timestamp:,// 必填,生成签名的时间戳nonceStr: ‘‘,// 必填,生成签名的随机串signature: ‘‘,// 必填,签名,见附录1jsApiList: [] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2});微信开发文档说明:https://mp.weixin.qq.com/advanced/wiki?t=t=resource/res_main&id=mp1421141115微信JS 接口签名校验工具https://mp.weixin.qq.com/debug/cgi-bin/sandBox?t=jsapisign
-
前言在平时的开发过程中,我们总是先写好一个组件,然后在需要的页面中用 import 引入即可,但如果是下面这种类型的组件呢上面这种类型的浮层提示有一个很大的特点,就是使用频率特别高,几乎每个页面都会用到它,于是乎我们就要在每个页面中去引入该组件,并且在每个页面都得通过一个变量来控制它的显隐,这显然不是我们想要的。。。那我们想要的是什么样呢?用过一些 UI 框架的同学们应该知道有这样一种用法:js复制代码this.$toast({ duration: 3000, content: '这是一条消息提示' }); 没错,就是这么简单的一句话就万事大吉了(就是用 js 调用组件而已啦)。那这种效果究竟是怎么实现的呢?今天就让我们来(手把手 )一探究竟吧!前置知识不知道小伙伴们有没有用过 Vue.extend() 这个东东,反正我是很少碰过,印象不深,所以这里我们先来短暂了解一下 Vue.extend() 主要是用来干嘛的。先来个官方说明(不多的,坚持下): 没怎么看懂?没关系,不重要,你只要记住(加少许理解)以下用法即可:js复制代码// 导入以往的普通组件 import Main from './main.vue'; // 用 Vue.extend 创建组件的模板(构造函数) let mainConstructor = Vue.extend(Main); // 实例化组件 let instance = new mainConstructor(); // 挂载到相应的元素上 instance.$mount('#app'); 不知道你看懂没有,上面的 Vue.extend(Main) 就是一个基于 main.vue 的组件模板(构造函数),instance 是实例化的组件,$mount() 是手动挂载的意思。其中 Vue.extend() 和 $mount() 就是我们通过 js 调用、渲染并挂载组件的精髓所在,相当于早前的 createElement 和 appendChild,有异曲同工之效。这个点需要我们好好熟悉一下,所以你可以先停下来屡屡思路。补充一下:$mount() 里面如果没有参数,说明组件只是渲染了但还没有挂载到页面上,如果有正确的(元素)参数则直接挂载到元素下面。写一个 toast 组件js 调用归调用,最原始的组件还是要有的,只是我们不通过 import 来引入到页面中而已。ok,我们就以最开始的那个 toast 图片来简单写一下这个 vue 组件(message 和 alert 也是一样的)。这里就直接上代码啦,毕竟它的结构简单到爆了,也不是本章节的重点:html复制代码<!-- main.vue --> <template> <div class="toast"> <p>服务器错误,请稍后重试</p> </div> </template> <script> export default { name: "Toast", mounted() { setTimeout(() => { // 3s 后通过父级移除子元素的方式来移除该组件实例和 DOM 节点 this.$destroy(true); this.$el.parentNode.removeChild(this.$el); }, 3000); } }; </script> <style lang="scss" scoped> .toast { display: flex; align-items: center; justify-content: center; position: fixed; top: 0; bottom: 0; left: 0; right: 0; color: #fff; z-index: 9999; background: transparent; > p { padding: 12px 22px; font-size: 18px; border-radius: 4px; background: rgba(17, 17, 17, 0.7); } } </style> 上面的内容想必大家应该都能看懂,所以这里就直接讲下面的重点了。写一个 main.js我们在 main.vue 的同级目录下新建一个 main.js 文件。我们先瞟一眼文件内容(也不多,已经是个最简版了):js复制代码// main.js import Vue from "vue"; // 引入 Vue 是因为要用到 Vue.extend() 这个方法 import Main from "./main.vue"; // 引入刚才的 toast 组件 let ToastConstructor = Vue.extend(Main); // 这个在前面的前置知识内容里面有讲到 let instance; const Toast = function() { instance = new ToastConstructor().$mount(); // 渲染组件 document.body.appendChild(instance.$el); // 挂载到 body 下 }; export default Toast; 上面的代码暴露了一个 Toast 函数。为什么要暴露一个函数呢?原因很简单:你想想,我们最终是不是要根据 this.$toast() 来调用一个组件,说白了,通过 js 调用,本质就是调用一个 函数。也就是说 this.$toast() 就是执行了上面代码中导出的 export default Toast,也就是执行了 Toast 函数(const Toast = function() {}),所以当我们调用 this.$toast() 的时候其实就是执行了 Toast() 函数。而 Toast() 函数只做了一件事情:就是通过手动挂载的方式把组件挂载到 body 下面。补充一下:一般来说我们常见的是 $mount("#app"),也就是把组件挂载到 #app 下面,<router-view /> 也包含在 #app 中,但是我们这种 toast 提示是放在 body 下面的,也就是说它不受 #app 和 <router-view /> 的管控,所以当我们切换页面(路由)的时候,这个 toast 组件是不会跟着立马消失的,这点要注意哦。这里顺便给个组件的目录结构,如下图所示: 开始调用调用方式很简单,首先我们在入口文件 main.js(和上面不是同一个) 里加上两行代码,这样我们就能在需要的地方直接用 js 调用它了,如下图所示: 然后在页面中测试一下,就像下面这样子: 运行一下代码: 嗯,挺好,小有成就的 feel 。支持可传参数别急,我们好像还漏了点什么。。。对了,现在还不支持传参呢,直接调用 this.$toast() 就只能显示————服务器错误,请稍后重试(这下全都是后端的锅了)。但我们可是个有追求的前端,不能局限于此,所以现在让我们来尝试增加下两个可配置参数,这里拿 duration 和 content 举个栗子。首先我们要修改 main.vue 组件里面的内容(其实没啥大变化),就像下面这样:html复制代码<!-- main.vue 可配置版 --> <template> <div class="toast"> <p>{{ content }}</p> </div> </template> <script> // 主要就改了 data export default { name: "Toast", data() { return { content: "", duration: 3000 }; }, mounted() { setTimeout(() => { this.$destroy(true); this.$el.parentNode.removeChild(this.$el); }, this.duration); } }; </script> 上面的代码应该算是浅显易懂了,接下来我们看下 main.js 里面改了啥:js复制代码// main.js 可配置版 import Vue from "vue"; import Main from "./main.vue"; let ToastConstructor = Vue.extend(Main); let instance; const Toast = function(options = {}) { // 就改了这里,加了个 options 参数 instance = new ToastConstructor({ data: options // 这里的 data 会传到 main.vue 组件中的 data 中,当然也可以写在 props 里 }); document.body.appendChild(instance.$mount().$el); }; export default Toast; 其实 main.js 也没多大变化,就是在函数里面加了个参数。要注意的是 new ToastConstructor({ data: options }) 中的 data 就是 main.vue 组件中的 data,不是随随便便取的字段名,传入的 options 会和组件中的 data 合并(Vue 的功劳)。em。。。是的,就这么简单,现在让我们继续来调用一下它:xml复制代码<script> export default { methods: { showToast() { this.$toast({ content: "哈哈哈哈,消失的贼快", duration: 500 }); } } }; </script> 运行一下就可以看到: 当然,这还没完,我们继续添加个小功能点。。。支持 this.$toast.error()这里我们打算支持 this.$toast.error() 和 this.$toast.success() 这两种方式,所以我们第一步还是要先去修改一下 main.vue 文件的内容(主要就是根据 type 值来修改组件的样式),就像下面这样:html复制代码<!--main.vue--> <template> <div class="toast" :class="type ? `toast--${type}` : ''"> <p>{{ content }}</p> </div> </template> <script> export default { ... data() { return { type: "", content: "", duration: 3000 }; }, ... }; </script> <style lang="scss" scoped> .toast { ... &--error p { background: rgba(255, 0, 0, 0.5); } &--success p { background: rgba(0, 255, 0, 0.5); } } </style> 其次,this.$toast.error() 其实就等价于 Toast.error(),所以我们现在的目的就是要给 Toast 函数扩充方法,也比较简单,就先看代码再解释吧:js复制代码// main.js const Toast = function(options = {}) { ... }; // 以下就是在 Toast 函数中拓展 ["success", "error"] 这两个方法 ["success", "error"].forEach(type => { Toast[type] = options => { options.type = type; return Toast(options); }; }); export default Toast; 我们可以看到 Toast.error() 和 Toast.success() 最终还是调用 Toast(options) 这个函数,只不过在调用之前需要多做一步处理,就是将 ["success", "error"] 作为一个 type 参数给合并进 options 里面再传递,仅此而已。那就试试效果吧:html复制代码<script> export default { methods: { showToast() { this.$toast({ content: "这是正常的" }); }, showErrorToast() { this.$toast.error({ content: "竟然失败了" }); }, showSuccessToast() { this.$toast.success({ content: "居然成功了" }); } } }; </script> 结语至此,一个通过 js 调用的简单 toast 组件就搞定啦,短短的几行代码还是挺考验 js 功底的。当然这只是个超简易版的 demo,显然不够完善和健壮,所以我们可以在此基础上扩充一下,比如当 duration <= 0 的时候,我们让这个 toast 一直显示,然后扩展一个 close 方法来关闭等等之类的。不过还是那句老话,实践才是检验真理的唯一标准。纸上得来终觉浅,绝知此事要躬行。step by step, day day up !作者:尤水就下链接:https://juejin.cn/post/6844903825711562766
-
在 Spring Boot 中,将 long 类型传输到前端时,会发现该类型的值可能会出现精度丢失的问题。 这是因为在 JavaScript 中,数字类型默认会被转换为双精度浮点数,而双精度浮点数的精度有限,只能精确表示 2 的 53 次方以内(即 Number.MAX_SAFE_INTEGER,约为 9 x 10^15)的整数。对于超过该范围的长整数,JavaScript 会发生精度丢失,导致值变得不准确。 解决方案一:将 long 转换为字符串 1:在后端将 long 类型的值转换为字符串类型,可以使用 String.valueOf() 方法或者 Long.toString() 方法,如下所示: long num = 123456789012345L; String str = String.valueOf(num); // 或者 String str = Long.toString(num); 2:在前端通过 AJAX 请求获取该字符串类型的值,并将其解析为数字类型。由于 JavaScript 中的数值类型默认使用 IEEE 754 标准的双精度浮点数表示,因此需要使用 JavaScript 的 BigInt() 方法将其转换为大整数类型。 let str = "123456789012345"; let num = BigInt(str); 解决方案二:使用第三方库进行高精度运算 1:在后端将 long 类型的值转换为 BigDecimal 类型(Java 中的高精度类型),并通过 JSON 序列化后传递到前端。这里以 Spring Boot 中使用 FastJSON 序列化为例,如下所示: BigDecimal num = new BigDecimal("123456789012345"); String jsonStr = JSON.toJSONString(num); 2:在前端使用第三方库 big.js 或 bignumber.js 进行高精度运算。这里以 big.js 为例,首先需要引入 big.min.js 文件,在代码中使用 Big() 类构造高精度对象,并进行相应的运算。 <script src="big.min.js"></script> let num = new Big("123456789012345"); let result = num.plus(1); 此外还可以使用注解来解决 long 类型的精度丢失问题 Spring Boot 中提供了 @JsonFormat 注解,可以对实体类中的属性进行序列化和反序列化格式化。对于 long 类型的属性,可以设置其格式为字符串类型,并在前端进行相应的处理,以保持其精度不丢失。 具体实现方式: 1:在实体类中添加 @JsonFormat 注解,设置其 shape 属性为 JsonFormat.Shape.STRING,如下所示: public class Example { @JsonFormat(shape = JsonFormat.Shape.STRING) private Long num; } 2:在前端获取该值时,直接使用字符串类型进行处理,如下所示: let numStr = data.num; Spring Boot 中可以通过配置文件来解决 long 类型的精度丢失问题。 在 Spring Boot 的配置文件 application.properties 中添加如下配置: spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false # 将 long 类型序列化为字符串类型 spring.jackson.serialization.WRITE_NUMBERS_AS_STRINGS=true 其中,WRITE_DATES_AS_TIMESTAMPS 表示是否将日期类型序列化为时间戳类型,默认为 true,这里设置为 false 如果需要将日期类型序列化为时间戳类型,则不需要设置此属性。而 WRITE_NUMBERS_AS_STRINGS 则表示是否将数字类型序列化为字符串类型,默认为 false,这里设置为 true 即可将 long 类型序列化为字符串类型。 ———————————————— 版权声明:本文为CSDN博主「源末coco」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_58724261/article/details/130974162
-
1 上传对象 1.1 PutObject 调用PutObject接口上传文件(Object) public ObjectWriteResponse putObject(PutObjectArgs args) 注意事项: 添加的Object大小不能超过5 GB。 默认情况下,如果已存在同名Object且对该Object有访问权限,则新添加的Object将覆盖原有的Object,并返回200 OK。 OSS没有文件夹的概念,所有资源都是以文件来存储,但您可以通过创建一个以正斜线(/)结尾,大小为0的Object来创建模拟文件夹。 示例1,InputStream上传: public static void main(String[] args) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { putObject(new File("C:\\Users\\1\\Desktop\\minio.jpg"), "mall4cloud"); } /** * 上传文件 * * @param file 需要上传的文件 * @param bucket 桶名称 */ public static void putObject(File file ,String bucket) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { InputStream inputStream = Files.newInputStream(file.toPath()); minioClient.putObject( PutObjectArgs.builder().bucket(bucket).object(file.getName()).stream( inputStream, inputStream.available(), -1) .build()); inputStream.close(); } 示例2,InputStream使用SSE-C加密上传(后续会介绍): minioClient.putObject( PutObjectArgs.builder().bucket("mall4cloud").object(file.getName()).stream( bais, bais.available(), -1) .sse(ssec) .build()); bais.close(); 示例3,InputStream上传文件,添加自定义元数据及消息头: /** * 上传文件(InputStream上传文件,添加自定义元数据及消息头) * * @param file 需要上传的文件 * @param bucket 桶名称 */ public static void putObject(File file ,String bucket, Map<String, String> headers) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { InputStream inputStream = Files.newInputStream(file.toPath()); Map<String, String> userMetadata = new HashMap<>(); minioClient.putObject( PutObjectArgs.builder().bucket(bucket).object(file.getName()).stream( inputStream, inputStream.available(), -1) .headers(headers) .userMetadata(userMetadata) .build()); inputStream.close(); } public static void main(String[] args) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { Map<String, String> headers = new HashMap<>(); headers.put("Content-Type", "application/octet-stream"); headers.put("X-Amz-Storage-Class", "REDUCED_REDUNDANCY"); putObject(new File("C:\\Users\\1\\Desktop\\minio.jpg"), "mall4cloud", headers); } 1.2 uploadObject 将文件中的内容作为存储桶中的对象上传。 public void uploadObject(UploadObjectArgs args) 示例: minioClient.uploadObject( UploadObjectArgs.builder() .bucket("mall4cloud") .object("minio.jpg") .filename("C:\\Users\\1\\Desktop\\minio.jpg") .build()); 2 获取对象 2.1 getObject GetObject接口用于获取某个文件(Object)。此操作需要对此Object具有读权限。 获取对象的数据。InputStream使用后返回必须关闭以释放网络资源。 public InputStream getObject(GetObjectArgs args) 示例: /** * 获取文件 * * @param bucket 桶名称 * @param filename 文件名 * @param targetPath 存储的路径 */ public static void getObject(String bucket, String filename, String targetPath) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { InputStream stream = minioClient.getObject( GetObjectArgs.builder().bucket(bucket).object(filename).build()); // 读流 File targetFile = new File(targetPath); FileUtils.copyInputStreamToFile(stream, targetFile); stream.close(); } public static void main(String[] args) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { getObject("mall4cloud", "minio.jpg", "C:\\Users\\1\\Desktop\\minio2.jpg"); } 2.2 downloadObject 将对象的数据下载到文件。 public void downloadObject(DownloadObjectArgs args) 示例: /** * 下载文件 * * @param bucket 桶名称 * @param filename 文件名 * @param targetPath 存储的路径 */ public static void downloadObject(String bucket, String filename, String targetPath) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { minioClient.downloadObject( DownloadObjectArgs.builder() .bucket(bucket) .object(filename) .filename(targetPath) .build()); } public static void main(String[] args) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { downloadObject("mall4cloud", "minio.jpg", "C:\\Users\\1\\Desktop\\minio2.jpg"); } 2.3 getPresignedObjectUrl 获取一个指定了 HTTP 方法、到期时间和自定义请求参数的对象URL地址,也就是返回带签名的URL,这个地址可以提供给没有登录的第三方共享访问或者上传对象。 public String getPresignedObjectUrl(GetPresignedObjectUrlArgs args) 1 示例: /** * 获取文件url * * @param bucket 桶名称 * @param filename 文件名 */ public static String getPresignedObjectUrl(String bucket, String filename) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { return minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucket) .object(filename) .expiry(60 * 60 * 24) .build()); } public static void main(String[] args) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { System.out.println(getPresignedObjectUrl("mall4cloud", "minio.jpg")); } 2.4 selectObjectContent 通过 SQL 表达式选择对象的内容。 public SelectResponseStream selectObjectContent(SelectObjectContentArgs args) 示例: 上传一个文件,文件内容为: 示例: /** * 通过 SQL 表达式选择对象的内容 * * @param data 上传的数据 * @param bucket 桶名称 * @param filename 文件名 */ public static String selectObjectContent(byte[] data, String bucket, String filename) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { // 1. 上传一个文件 ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data); minioClient.putObject( PutObjectArgs.builder().bucket(bucket).object(filename).stream( byteArrayInputStream, data.length, -1) .build()); // 调用SQL表达式获取对象 String sqlExpression = "select * from S3Object"; InputSerialization is = new InputSerialization(null, false, null, null, FileHeaderInfo.USE, null, null, null); OutputSerialization os = new OutputSerialization(null, null, null, QuoteFields.ASNEEDED, null); SelectResponseStream stream = minioClient.selectObjectContent( SelectObjectContentArgs.builder() .bucket(bucket) .object(filename) .sqlExpression(sqlExpression) .inputSerialization(is) .outputSerialization(os) .requestProgress(true) .build()); byte[] buf = new byte[512]; int bytesRead = stream.read(buf, 0, buf.length); return new String(buf, 0, bytesRead, StandardCharsets.UTF_8); } public static void main(String[] args) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { List<String> stringList = FileUtil.readLines("C:\\Users\\1\\Desktop\\test.txt", StandardCharsets.UTF_8); System.out.println(selectObjectContent(String.join("\n", stringList).getBytes(), "mall4cloud", "test.txt")); } 2.5 getPresignedPostFormData 使用此方法,获取对象的上传策略(包含签名、文件信息、路径等),然后使用这些信息采用POST 方法的表单数据上传数据。也就是可以生成一个临时上传的信息对象,第三方可以使用这些信息,就可以上传文件。 一般可用于,前端请求一个上传策略,后端返回给前端,前端使用Post请求+访问策略去上传文件,这可以用于JS+SDK的混合方式集成minio public Map<String,String> getPresignedPostFormData(PostPolicy policy) 示例,首先我们创建一个Post 策略,然后,第三方就可以使用这些策略,直接使用POST上传对象,代码如下: /** * 设置并获取Post策略 * * @param bucket 桶名 */ public static Map<String, String> setPostPolicy(String bucket) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { // 为存储桶创建一个上传策略,过期时间为7天 PostPolicy policy = new PostPolicy(bucket, ZonedDateTime.now().plusDays(7)); // 设置一个参数key,值为上传对象的名称 policy.addEqualsCondition("key", bucket); // 添加Content-Type以"image/"开头,表示只能上传照片 policy.addStartsWithCondition("Content-Type", "image/"); // 设置上传文件的大小 64kiB to 10MiB. policy.addContentLengthRangeCondition(64 * 1024, 10 * 1024 * 1024); return minioClient.getPresignedPostFormData(policy); } /** * 使用Post上传 * * @param formData Post策略 * @param url minio服务器地址 * @param bucket 桶名 * @param path 本地文件地址 */ public static boolean uploadByHttp(Map<String, String> formData, String url, String bucket, String path) throws IOException { // 创建MultipartBody对象 MultipartBody.Builder multipartBuilder = new MultipartBody.Builder(); multipartBuilder.setType(MultipartBody.FORM); for (Map.Entry<String, String> entry : formData.entrySet()) { multipartBuilder.addFormDataPart(entry.getKey(), entry.getValue()); } multipartBuilder.addFormDataPart("key", bucket); multipartBuilder.addFormDataPart("Content-Type", "image/png"); multipartBuilder.addFormDataPart( "file", "my-objectname", RequestBody.create(new File(path), null)); // 模拟第三方,使用OkHttp调用Post上传对象 Request request = new Request.Builder() .url(url + "/" + bucket) .post(multipartBuilder.build()) .build(); OkHttpClient httpClient = new OkHttpClient().newBuilder().build(); Response response = httpClient.newCall(request).execute(); return response.isSuccessful(); } public static void main(String[] args) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { String bucket = "mall4cloud"; //第一步:设置并获取Post策略 Map<String, String> formData = setPostPolicy(bucket); //第二步:使用Post请求上传对象 uploadByHttp(formData, url, bucket, "C:\\Users\\1\\Desktop\\minio.jpg"); } 3 复制对象 3.1 copyObject 通过服务器端从另一个对象复制数据来创建一个对象 public ObjectWriteResponse copyObject(CopyObjectArgs args) 示例: /** * 对象拷贝 * * @param sourceBucket 源对象桶 * @param sourceFilename 源文件名 * @param targetBucket 目标对象桶 * @param targetFilename 目标文件名 */ public static void copyObject(String sourceBucket, String sourceFilename, String targetBucket, String targetFilename) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { minioClient.copyObject( CopyObjectArgs.builder() .bucket(targetBucket) .object(targetFilename) .source(CopySource.builder() .bucket(sourceBucket) .object(sourceFilename) .build()) .build()); } public static void main(String[] args) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { copyObject("mall4cloud", "minio.jpg", "mall4cloud", "minioCopy.jpg"); } 4 删除对象 4.1 removeObject 移除一个对象 public void removeObject(RemoveObjectArgs args) 示例: /** * 删除单个对象 * * @param bucket 桶名称 * @param filename 文件名 */ public static void removeObject(String bucket, String filename) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { minioClient.removeObject( RemoveObjectArgs.builder().bucket(bucket).object(filename).build()); } /** * 删除指定版本号的对象 * * @param bucket 桶名称 * @param filename 文件名 * @param versionId 版本号 */ public static void removeObject(String bucket, String filename, String versionId) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { // minioClient.removeObject( RemoveObjectArgs.builder() .bucket(bucket) .object(filename) .versionId(versionId) .build()); } public static void main(String[] args) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { removeObject("mall4cloud", "minioCopy.jpg"); } 4.2 emoveObjects 懒惰地删除多个对象。它需要迭代返回的 Iterable 以执行删除。 public Iterable<Result<DeleteError>> removeObjects(RemoveObjectsArgs args) 示例: /** * 删除多个文件 * * @param bucket 桶名称 * @param filenames 文件名列表 * @return 返回每个文件的执行情况 */ public static Iterable<Result<DeleteError>> removeObjects(String bucket, List<String> filenames){ if (ObjectUtils.isEmpty(filenames)) { return new LinkedList<>(); } // 7. 删除多个文件 List<DeleteObject> objects = new LinkedList<>(); filenames.forEach(filename-> objects.add(new DeleteObject(filename))); return minioClient.removeObjects( RemoveObjectsArgs.builder().bucket(bucket).objects(objects).build()); } public static void main(String[] args) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { List<String> filenames = new ArrayList<>(); filenames.add("minioCopy.jpg"); Iterable<Result<DeleteError>> resultList = removeObjects("mall4cloud", filenames); for (Result<DeleteError> result : resultList) { DeleteError error = result.get(); System.out.println( "Error in deleting object " + error.objectName() + "; " + error.message()); } } 5 对象信息查询及设置 5.1 桶的对象信息列表 listObjects列出桶的对象信息 public Iterable<Result<Item>> listObjects(ListObjectsArgs args) 示例1,查询存储桶下文件信息: Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket("my-bucketname").build()); for (Result<Item> result : results) { Item item = result.get(); System.out.println(item.lastModified() + "\t" + item.size() + "\t" + item.objectName()); } 示例2,递归查询存储桶下文件信息: Iterable<Result<Item>> results = minioClient.listObjects( ListObjectsArgs.builder().bucket("my-bucketname").recursive(true).build()); for (Result<Item> result : results) { Item item = result.get(); System.out.println(item.lastModified() + "\t" + item.size() + "\t" + item.objectName()); } 示例3, 条件查询,指定前缀、后缀、最大数量: // 条件查询,指定前缀、后缀、最大数量 Iterable<Result<Item>> results = minioClient.listObjects( ListObjectsArgs.builder() .bucket("my-bucketname") .startAfter("ExampleGuide.pdf") .prefix("E") .maxKeys(100) .build()); for (Result<Item> result : results) { Item item = result.get(); System.out.println(item.lastModified() + "\t" + item.size() + "\t" + item.objectName()); 5.2 保留配置 获取对象的保留配置 public Retention getObjectRetention(GetObjectRetentionArgs args) 示例: // 获取对象保留配置 Retention retention = minioClient.getObjectRetention( GetObjectRetentionArgs.builder() .bucket("my-bucketname-in-eu-with-object-lock") .object("k3s-arm64") .build()); System.out.println("Mode: " + retention.mode()); System.out.println("Retainuntil Date: " + retention.retainUntilDate()); 添加对象的保留配置,存储桶需要设置为对象锁定模式,并且没有开启版本控制,否则会报错收蠕虫保护。 public void setObjectLockRetention(SetObjectRetentionArgs) 1 // 对象保留配置,保留至当前日期后3天。 ZonedDateTime retentionUntil = ZonedDateTime.now(Time.UTC).plusDays(3).withNano(0); Retention retention1 = new Retention(RetentionMode.COMPLIANCE, retentionUntil); minioClient.setObjectRetention( SetObjectRetentionArgs.builder() .bucket("my-bucketname-in-eu-with-object-lock") .object("k3s-arm64") .config(retention1) .bypassGovernanceMode(true) .build()); 5.3 标签 为对象设置标签 public void setObjectTags(SetObjectTagsArgs args) 示例: Map<String, String> map = new HashMap<>(); map.put("Project", "Project One"); map.put("User", "jsmith"); minioClient.setObjectTags( SetObjectTagsArgs.builder() .bucket("my-bucketname") .object("my-objectname") .tags(map) .build()); 获取对象的标签。 public Tags getObjectTags(GetObjectTagsArgs args) 示例: Tags tags = minioClient.getObjectTags( GetObjectTagsArgs.builder().bucket("my-bucketname").object("my-objectname").build()); System.out.println("Object tags: " + tags.get()); 删除对象的标签。 private void deleteObjectTags(DeleteObjectTagsArgs args) 示例: minioClient.deleteObjectTags( DeleteObjectTagsArgs.builder().bucket("my-bucketname").object("my-objectname").build()); System.out.println("Object tags deleted successfully"); 5.4 合法保留对象 启用对对象的合法保留 public void enableObjectLegalHold(EnableObjectLegalHoldArgs args) 案例: minioClient.enableObjectLegalHold( EnableObjectLegalHoldArgs.builder() .bucket("my-bucketname") .object("my-objectname") .versionId("object-versionId") .build()); System.out.println("Legal hold enabled on object successfully "); 禁用对对象的合法保留 public void disableObjectLegalHold(DisableObjectLegalHoldArgs args) 示例: minioClient.disableObjectLegalHold( DisableObjectLegalHoldArgs.builder() .bucket("my-bucketname") .object("my-objectname") .build()); System.out.println("Legal hold disabled on object successfully "); 5.5 组合对象 通过使用服务器端副本组合来自不同源对象的数据来创建对象,比如可以将文件分片上传,然后将他们合并为一个文件。 public ObjectWriteResponse composeObject(ComposeObjectArgs args) 示例: List<ComposeSource> sources = new ArrayList<ComposeSource>(); sources.add( ComposeSource.builder() .bucket("my-bucketname-one") .object("my-objectname-one") .build()); sources.add( ComposeSource.builder() .bucket("my-bucketname-two") .object("my-objectname-two") .build()); minioClient.composeObject( ComposeObjectArgs.builder() .bucket("my-destination-bucket") .object("my-destination-object") .sources(sources) .build()); System.out.println("Object Composed successfully"); 5.6 元数据 获取对象的对象信息和元数据 public ObjectStat statObject(StatObjectArgs args) 示例: StatObjectResponse stat = minioClient.statObject( StatObjectArgs.builder() .bucket("my-bucketname") .object("start.sh") .build()); System.out.println(stat.toString()); 6 工具类参考 import io.minio.*; import io.minio.errors.*; import io.minio.http.Method; import io.minio.messages.*; import okhttp3.*; import org.apache.commons.io.FileUtils; import org.springframework.util.ObjectUtils; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.time.ZonedDateTime; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * minio对象操作工具类 * * @author wuKeFan * @date 2023-09-19 09:45:43 */ public class MinioObjectUtil { /** * minio地址(自己填写) */ private static final String url = "url"; /** * minio用户名(自己填写) */ private static final String accessKey = "accessKey"; /** * minio密码(自己填写) */ private static final String secretKey = "secretKey"; private static final MinioClient minioClient; static { minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); } /** * 上传文件(InputStream上传) * * @param file 需要上传的文件 * @param bucket 桶名称 */ public static void putObject(File file ,String bucket) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { InputStream inputStream = Files.newInputStream(file.toPath()); minioClient.putObject( PutObjectArgs.builder().bucket(bucket).object(file.getName()).stream( inputStream, inputStream.available(), -1) .build()); inputStream.close(); } /** * 上传文件(InputStream使用SSE-C加密上传) * * @param file 需要上传的文件 * @param bucket 桶名称 */ public static void putObject(File file ,String bucket, ServerSideEncryption ssec) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { InputStream inputStream = Files.newInputStream(file.toPath()); minioClient.putObject( PutObjectArgs.builder().bucket("mall4cloud").object(file.getName()).stream( inputStream, inputStream.available(), -1) .sse(ssec) .build()); inputStream.close(); } /** * 上传文件(InputStream上传文件,添加自定义元数据及消息头) * * @param file 需要上传的文件 * @param bucket 桶名称 */ public static void putObject(File file ,String bucket, Map<String, String> headers) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { InputStream inputStream = Files.newInputStream(file.toPath()); Map<String, String> userMetadata = new HashMap<>(); minioClient.putObject( PutObjectArgs.builder().bucket(bucket).object(file.getName()).stream( inputStream, inputStream.available(), -1) .headers(headers) .userMetadata(userMetadata) .build()); inputStream.close(); } /** * 获取文件 * * @param bucket 桶名称 * @param filename 文件名 * @param targetPath 存储的路径 */ public static void getObject(String bucket, String filename, String targetPath) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { InputStream stream = minioClient.getObject( GetObjectArgs.builder().bucket(bucket).object(filename).build()); // 读流 File targetFile = new File(targetPath); FileUtils.copyInputStreamToFile(stream, targetFile); stream.close(); } /** * 下载文件 * * @param bucket 桶名称 * @param filename 文件名 * @param targetPath 存储的路径 */ public static void downloadObject(String bucket, String filename, String targetPath) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { minioClient.downloadObject( DownloadObjectArgs.builder() .bucket(bucket) .object(filename) .filename(targetPath) .build()); } /** * 获取文件url * * @param bucket 桶名称 * @param filename 文件名 */ public static String getPresignedObjectUrl(String bucket, String filename) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { return minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucket) .object(filename) .expiry(60 * 60 * 24) .build()); } /** * 通过 SQL 表达式选择对象的内容 * * @param data 上传的数据 * @param bucket 桶名称 * @param filename 文件名 */ public static String selectObjectContent(byte[] data, String bucket, String filename) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { // 1. 上传一个文件 ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data); minioClient.putObject( PutObjectArgs.builder().bucket(bucket).object(filename).stream( byteArrayInputStream, data.length, -1) .build()); // 调用SQL表达式获取对象 String sqlExpression = "select * from S3Object"; InputSerialization is = new InputSerialization(null, false, null, null, FileHeaderInfo.USE, null, null, null); OutputSerialization os = new OutputSerialization(null, null, null, QuoteFields.ASNEEDED, null); SelectResponseStream stream = minioClient.selectObjectContent( SelectObjectContentArgs.builder() .bucket(bucket) .object(filename) .sqlExpression(sqlExpression) .inputSerialization(is) .outputSerialization(os) .requestProgress(true) .build()); byte[] buf = new byte[512]; int bytesRead = stream.read(buf, 0, buf.length); return new String(buf, 0, bytesRead, StandardCharsets.UTF_8); } /** * 设置并获取Post策略 * * @param bucket 桶名 */ public static Map<String, String> setPostPolicy(String bucket) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { // 为存储桶创建一个上传策略,过期时间为7天 PostPolicy policy = new PostPolicy(bucket, ZonedDateTime.now().plusDays(7)); // 设置一个参数key,值为上传对象的名称 policy.addEqualsCondition("key", bucket); // 添加Content-Type以"image/"开头,表示只能上传照片 policy.addStartsWithCondition("Content-Type", "image/"); // 设置上传文件的大小 64kiB to 10MiB. policy.addContentLengthRangeCondition(64 * 1024, 10 * 1024 * 1024); return minioClient.getPresignedPostFormData(policy); } /** * 使用Post上传 * * @param formData Post策略 * @param url minio服务器地址 * @param bucket 桶名 * @param path 本地文件地址 */ public static boolean uploadByHttp(Map<String, String> formData, String url, String bucket, String path) throws IOException { // 创建MultipartBody对象 MultipartBody.Builder multipartBuilder = new MultipartBody.Builder(); multipartBuilder.setType(MultipartBody.FORM); for (Map.Entry<String, String> entry : formData.entrySet()) { multipartBuilder.addFormDataPart(entry.getKey(), entry.getValue()); } multipartBuilder.addFormDataPart("key", bucket); multipartBuilder.addFormDataPart("Content-Type", "image/png"); multipartBuilder.addFormDataPart( "file", "my-objectname", RequestBody.create(new File(path), null)); // 模拟第三方,使用OkHttp调用Post上传对象 Request request = new Request.Builder() .url(url + "/" + bucket) .post(multipartBuilder.build()) .build(); OkHttpClient httpClient = new OkHttpClient().newBuilder().build(); Response response = httpClient.newCall(request).execute(); return response.isSuccessful(); } /** * 对象拷贝 * * @param sourceBucket 源对象桶 * @param sourceFilename 源文件名 * @param targetBucket 目标对象桶 * @param targetFilename 目标文件名 */ public static void copyObject(String sourceBucket, String sourceFilename, String targetBucket, String targetFilename) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { minioClient.copyObject( CopyObjectArgs.builder() .bucket(targetBucket) .object(targetFilename) .source(CopySource.builder() .bucket(sourceBucket) .object(sourceFilename) .build()) .build()); } /** * 删除单个对象 * * @param bucket 桶名称 * @param filename 文件名 */ public static void removeObject(String bucket, String filename) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { minioClient.removeObject( RemoveObjectArgs.builder().bucket(bucket).object(filename).build()); } /** * 删除指定版本号的对象 * * @param bucket 桶名称 * @param filename 文件名 * @param versionId 版本号 */ public static void removeObject(String bucket, String filename, String versionId) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { // minioClient.removeObject( RemoveObjectArgs.builder() .bucket(bucket) .object(filename) .versionId(versionId) .build()); } /** * 删除多个文件 * * @param bucket 桶名称 * @param filenames 文件名列表 * @return 返回每个文件的执行情况 */ public static Iterable<Result<DeleteError>> removeObjects(String bucket, List<String> filenames){ if (ObjectUtils.isEmpty(filenames)) { return new LinkedList<>(); } // 7. 删除多个文件 List<DeleteObject> objects = new LinkedList<>(); filenames.forEach(filename-> objects.add(new DeleteObject(filename))); return minioClient.removeObjects( RemoveObjectsArgs.builder().bucket(bucket).objects(objects).build()); } public static void main(String[] args) { } } ———————————————— 版权声明:本文为CSDN博主「吴名氏.」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_37284798/article/details/133014533
-
1 前言 1.1 官方文档和SDK 官方文档:https://min.io/docs/minio/kubernetes/upstream/index.html?ref=docs-redirect SDK:https://github.com/minio/minio-java Minio 提供了多种语言的SDK,比如java、go、python等。JAVA开发平台可以选择JS和java SDK,也就是前端和后端都可以直接集成minio。 1.2 技术方案 每个OSS的用户都会用到上传服务。Web端常见的上传方法是用户在浏览器或App端上传文件到应用服务器,应用服务器再把文件上传到OSS。具体流程如下图所示。 和数据直传到OSS相比,以上方法有三个缺点: 上传慢:用户数据需先上传到应用服务器,之后再上传到OSS。网络传输时间比直传到OSS多一倍。如果用户数据不通过应用服务器中转,而是直传到OSS,速度将大大提升。而且OSS采用BGP带宽,能保证各地各运营商之间的传输速度。 扩展性差:如果后续用户多了,应用服务器会成为瓶颈。 费用高:需要准备多台应用服务器。由于OSS上传流量是免费的,如果数据直传到OSS,不通过应用服务器,那么将能省下几台应用服务器。 目前通过Web前端技术上传文件到OSS,有三种技术方案: 利用OSS js SDK将文件上传到OSS,也就是前端直连OSS,但是容易暴露认证信息,安全性不太高。 使用表单上传方式,将文件上传到OSS。利用OSS提供的接口临时接口,使用表单上传方式将文件上传到OSS。然后请求后端,告知上传完成,进行后续处理。 先上传到应用服务器,再请求OSS上传,这种安全性较高,可以对数据和认证进行管控,但是性能最差。 2 集成 JAVA SDK 因为一般的非互联网项目,对性能要求不高,所以采用JAVA SDK集成MInio,然后提供接口给Web端调用就行了。 2.1 环境搭建 首先搭建一个Maven基础工程,引入相关依赖,这里引入的是最新的8.3.1版本。还引入了okhttp的最新包,不然某些API会提示版本太低。 <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.3.1</version> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.9.2</version> </dependency> 2.2 初始化客户端 可以看到现在minio都是采用Builder构建者模式来构造对象,和之前有很大的区别,所以需要注意。 //url为地址,accessKey和secretKey为用户名和密码 MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); 2.3 存储桶基础操作 2.3.1 存储桶是否存在 检查存储桶是否存在。 public boolean bucketExists(BucketExistsArgs args) 示例代码: /** * 判断桶是否存在 */ public static boolean bucketExists(String url, String accessKey, String secretKey, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); } 2.3.2 创建存储桶 创建一个启用给定区域和对象锁定功能的存储桶。 public void makeBucket(MakeBucketArgs args) 示例代码: /** * 添加存储桶 */ public static void makeBucket(String url, String accessKey, String secretKey, String bucketName, String region, boolean objectLock) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).region(region).objectLock(objectLock).build()); } 创建后,就可以在控制台看到这些存储桶了,最后那个被锁定的存储桶,上传文件及删除后,发现还是会显示存在这些对象,实际磁盘上的文件并没有删除 2.3.3 查询存储桶信息列表 列出所有桶的桶信息。 public List<Bucket> listBuckets() 示例代码: /** * 查询存储桶信息列表 */ public static List<Bucket> listBuckets(String url, String accessKey, String secretKey) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); return minioClient.listBuckets(); } public static void main(String[] args) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { List<Bucket> buckets = listBuckets("url", "accessKey", "secretKey"); for (Bucket bucket : buckets) { System.out.println(bucket.creationDate() + ", " + bucket.name()); } } 打印信息如下,返回的创建时间是美国时间,需要注意。 2.3.4 删除存储桶 删除一个空桶。 public void removeBucket(RemoveBucketArgs args) 示例代码: /** * 删除存储桶 */ public static void removeBucket(String url, String accessKey, String secretKey, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build()); } 注意:要确保存储桶存在,否则会报错,删除时最好调用bucketExists()方法判断是否存在 2.4 设置存储桶操作 2.4.1 加密配置 设置桶的加密配置,以允许在该桶中上传对象时,采用对应加密配置对数据进行加密。当前支持配置的服务端加密方式为KMS托管密钥的服务端加密(SSE-KMS),及AES256加密。 设置桶的加密配置: public void setBucketEncryption(SetBucketEncryptionArgs args) 获取桶的加密配置: public SseConfiguration getBucketEncryption(GetBucketEncryptionArgs args) 2.4.2 生命周期 生命周期管理可适用于以下典型场景: 周期性上传的日志文件,可能只需要保留一个星期或一个月。到期后要删除它们。 某些文档在一段时间内经常访问,但是超过一定时间后便可能不再访问了。这些文档需要在一定时间后转化为低频访问存储,归档存储或者删除。 存储桶生命周期配置: public void setBucketLifecycle(SetBucketLifecycleArgs args) 获取桶的生命周期配置: public LifecycleConfiguration getBucketLifecycle(GetBucketLifecycleArgs args) 示例代码: // 5. 生命周期 List<LifecycleRule> rules = new LinkedList<>(); // 配置生命周期规则 rules.add( new LifecycleRule( Status.ENABLED, // 开启状态 null, new Expiration((ZonedDateTime) null, 365, null), // 保存365天 new RuleFilter("logs/"), // 目录配置 "rule2", null, null, null)); LifecycleConfiguration lifecycleConfiguration = new LifecycleConfiguration(rules); // 添加生命周期配置 minioClient.setBucketLifecycle( SetBucketLifecycleArgs.builder().bucket("my-bucketname").config(lifecycleConfiguration).build()); // 获取配置 LifecycleConfiguration lifecycleConfiguration1111 = minioClient.getBucketLifecycle( GetBucketLifecycleArgs.builder().bucket("my-bucketname").build()); List<LifecycleRule> rules1 = lifecycleConfiguration1111.rules(); for (int i = 0; i < rules1.size(); i++) { System.out.println("Lifecycle status is " + rules1.get(i).status()); System.out.println("Lifecycle prefix is " + rules1.get(i).filter().prefix()); System.out.println("Lifecycle expiration days is " + rules1.get(i).expiration().days()); } 打印结果如下: 2.4.3 通知配置 可以使用存储桶事件通知来监控存储桶中对象上发生的事件。 MinIO 服务器支持的各种事件类型有: 存储桶配置通知: public void setBucketPolicy(SetBucketPolicyArgs args) 获取桶的通知配置: public NotificationConfiguration getBucketNotification(GetBucketNotificationArgs args) 代码示例: // 6. 通知配置 // Add a new SQS configuration. NotificationConfiguration notificationConfiguration = new NotificationConfiguration(); List<QueueConfiguration> queueConfigurationList = notificationConfiguration.queueConfigurationList(); QueueConfiguration queueConfiguration = new QueueConfiguration(); queueConfiguration.setQueue("arn:minio:sqs::1:webhook"); List<EventType> eventList = new LinkedList<>(); eventList.add(EventType.OBJECT_CREATED_PUT); eventList.add(EventType.OBJECT_CREATED_COPY); queueConfiguration.setEvents(eventList); queueConfiguration.setPrefixRule("images"); queueConfiguration.setSuffixRule("pg"); queueConfigurationList.add(queueConfiguration); notificationConfiguration.setQueueConfigurationList(queueConfigurationList); // Set updated notification configuration. minioClient.setBucketNotification( SetBucketNotificationArgs.builder().bucket("my-bucketname").config(notificationConfiguration).build()); System.out.println("Bucket notification is set successfully"); NotificationConfiguration minioClientBucketNotification = minioClient.getBucketNotification( GetBucketNotificationArgs.builder().bucket("my-bucketname").build()); System.out.println(minioClientBucketNotification); 2.4.4 策略配置 添加存储桶策略配置。 public void setBucketPolicy(SetBucketPolicyArgs args) 获取桶的桶策略配置。 public String getBucketPolicy(GetBucketPolicyArgs args) 2.4.5 复制配置 存储桶复制旨在将存储桶中的选定对象复制到目标存储桶,内容较多,后续补上 添加存储桶的复制配置 public void setBucketReplication(SetBucketReplicationArgs args) 获取桶的桶复制配置: public ReplicationConfiguration getBucketReplication(GetBucketReplicationArgs args) 2.4.6 存储桶标签 当为桶添加标签时,该桶上所有请求产生的计费话单里都会带上这些标签,从而可以针对话单报表做分类筛选,进行更详细的成本分析。例如:某个应用程序在运行过程会往桶里上传数据,我们可以用应用名称作为标签,设置到被使用的桶上。在分析话单时,就可以通过应用名称的标签来分析此应用的成本。 setBucketTags可以为存储桶设置标签。 public void setBucketTags(SetBucketTagsArgs args) getBucketTags获取桶的标签。 public Tags getBucketTags(GetBucketTagsArgs args) 示例代码: // 1. 存储桶标签 Map<String, String> map = new HashMap<>(); map.put("Project", "Project One"); map.put("User", "jsmith"); // 设置标签 minioClient.setBucketTags(SetBucketTagsArgs.builder().bucket("my-bucketname").tags(map).build()); // 查询标签 Tags bucketTags = minioClient.getBucketTags(GetBucketTagsArgs.builder().bucket("my-bucketname").build()); System.out.println(bucketTags.get().toString()); 返回结果: 2.4.7 多版本设置 若开启了多版本控制,上传对象时,OBS自动为每个对象创建唯一的版本号。上传同名的对象将以不同的版本号同时保存在OBS中。 若未开启多版本控制,向同一个文件夹中上传同名的对象时,新上传的对象将覆盖原有的对象。 某些功能(例如版本控制、对象锁定和存储桶复制)需要使用擦除编码分布式部署 MinIO。开启了版本控制后,允许在同一密钥下保留同一对象的多个版本。 设置存储桶的版本控制配置。 public void setBucketVersioning(SetBucketVersioningArgs args) 获取存储桶的版本控制配置。 public VersioningConfiguration getBucketVersioning(GetBucketVersioningArgs args) 代码示例: // 2. 版本配置 // 'my-bucketname'启用版本控制 minioClient.setBucketVersioning( SetBucketVersioningArgs.builder() .bucket("my-bucketname") .config(new VersioningConfiguration(VersioningConfiguration.Status.ENABLED, null)) .build()); System.out.println("Bucket versioning is enabled successfully"); // 'my-bucketname'暂停版本控制 minioClient.setBucketVersioning( SetBucketVersioningArgs.builder() .bucket("my-bucketname") .config(new VersioningConfiguration(VersioningConfiguration.Status.SUSPENDED, null)) .build()); System.out.println("Bucket versioning is suspended successfully"); 2.4.8 对象锁定配置 对象锁定设置后,删除对象后,会仍然存在磁盘中。 在存储桶中设置对象锁定配置。 public void setObjectLockConfiguration(SetObjectLockConfigurationArgs args) 获取存储桶中的对象锁配置。 public ObjectLockConfiguration getObjectLockConfiguration(GetObjectLockConfigurationArgs args) 需要先设置存储桶为对象锁定模式,示例代码: // 3. 将保留模式设置为Compliance,且持续时间为100天 // 设置锁定对象的保留模式及时限 ObjectLockConfiguration config = new ObjectLockConfiguration(RetentionMode.COMPLIANCE, new RetentionDurationDays(100)); minioClient.setObjectLockConfiguration( SetObjectLockConfigurationArgs.builder() .bucket("my-bucketname-in-eu-with-object-lock") .config(config) .build()); System.out.println("object-lock configuration is set successfully"); // 获取锁定配置 ObjectLockConfiguration objectLockConfiguration = minioClient.getObjectLockConfiguration( GetObjectLockConfigurationArgs.builder() .bucket("my-lock-enabled-bucketname") .build()); System.out.println("Object-lock configuration of bucket"); System.out.println("Mode: " + objectLockConfiguration.mode()); System.out.println("Duration: " + objectLockConfiguration.duration()); 2.5 删除配置 minio提供了一些列的delete方法用于删除配置,比较简单,就不举例说明了。 2.5.1 删除桶的加密配置 public void deleteBucketEncryption(DeleteBucketEncryptionArgs args) 2.5.2 删除存储桶的生命周期配置 public void deleteBucketLifecycle(DeleteBucketLifecycleArgs args) 2.5.3 删除桶的标签 public void deleteBucketTags(DeleteBucketTagsArgs args) 2.5.4 删除桶的桶策略配置 public void deleteBucketPolicy(DeleteBucketPolicyArgs args) 2.5.5 删除存储桶的存储桶复制配置 public void deleteBucketReplication(DeleteBucketReplicationArgs args) 2.5.6 删除桶的通知配置 public void deleteBucketNotification(DeleteBucketNotificationArgs args) 3 相关工具类 import io.minio.*; import io.minio.errors.*; import io.minio.messages.Bucket; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.List; /** * minio工具类 * * @author wuKeFan * @date 2023-09-08 14:08:10 */ public class MinioUtil { /** * 判断桶是否存在 */ public static boolean bucketExists(String url, String accessKey, String secretKey, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); } /** * 添加存储桶 */ public static void makeBucket(String url, String accessKey, String secretKey, String bucketName, String region, boolean objectLock) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).region(region).objectLock(objectLock).build()); } /** * 指定地区添加存储桶 */ public static void makeBucket(String url, String accessKey, String secretKey, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } /** * 指定地区添加存储桶并锁定对象 */ public static void makeBucket(String url, String accessKey, String secretKey, String bucketName, String region) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).region(region).build()); } /** * 删除存储桶 */ public static void removeBucket(String url, String accessKey, String secretKey, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build()); } /** * 设置桶公有 */ public static void setBucketPublicPolicy(String url, String accessKey, String secretKey, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); String sb = "{\"Version\":\"2012-10-17\"," + "\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":" + "{\"AWS\":[\"*\"]},\"Action\":[\"s3:ListBucket\",\"s3:ListBucketMultipartUploads\"," + "\"s3:GetBucketLocation\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\",\"s3:ListMultipartUploadParts\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}"; minioClient.setBucketPolicy( SetBucketPolicyArgs.builder() .bucket(bucketName) .config(sb) .build()); } /** * 设置桶私有 */ public static void setBucketPrivatePolicy(String url, String accessKey, String secretKey, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); minioClient.setBucketPolicy( SetBucketPolicyArgs.builder().bucket(bucketName) .config( "{\"Version\":\"2012-10-17\",\"Statement\":[]}" ) .build()); } /** * 查询存储桶信息列表 */ public static List<Bucket> listBuckets(String url, String accessKey, String secretKey) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { MinioClient minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey).build(); return minioClient.listBuckets(); } } ———————————————— 版权声明:本文为CSDN博主「吴名氏.」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_37284798/article/details/132964198
-
1 前言 大家平时的工作中,可能也在很多地方用到了加密、解密,比如: 用户的密码不能明文存储,要存储加密后的密文 用户的银行卡号、身份证号之类的敏感数据,需要加密传输 还有一些重要接口,比如支付,客户端要对请求生成一个签名,服务端要对签名进行验证 …… 那么上面提到的这些能力,我们都可以利用哪些加密算法来实现呢?咱们接着往下看。 2 常见加密算法 算法整体上可以分为不可逆加密,以及可逆加密,可逆加密又可以分为对称加密和非对称加密。 2.1 不可逆算法 不可逆加密的算法的加密是不可逆的,密文无法被还原成原文。 散列算法,就是一种不可逆算法。散列算法中,明文通过散列算法生成散列值,散列值是长度固定的数据,和明文长度无关。 散列算法的具体实现有很多种,常见的包括MD5、SHA1、SHA-224、SHA-256等等。 散列算法常用于数字签名、消息认证、密码存储等场景。 散列算法是不需要密钥的,当然也有一些不可逆算法,需要密钥,例如HMAC算法。 2.1.1 MD5 MD5,全称为“Message-Digest Algorithm 5”,翻译过来叫“信息摘要算法”。它可以将任意长度的数据通过散列算法,生成一个固定长度的散列值。MD5算法的输出长度为128位,通常用32个16进制数表示。 我们来看下MD5算法的Java代码实现: public class MD5 { private static final String MD5_ALGORITHM = "MD5"; public static String encrypt(String data) throws Exception { // 获取MD5算法实例 MessageDigest messageDigest = MessageDigest.getInstance(MD5_ALGORITHM); // 计算散列值 byte[] digest = messageDigest.digest(data.getBytes()); Formatter formatter = new Formatter(); // 补齐前导0,并格式化 for (byte b : digest) { formatter.format("%02x", b); } return formatter.toString(); } public static void main(String[] args) throws Exception { String data = "Hello World"; String encryptedData = encrypt(data); System.out.println("加密后的数据:" + encryptedData); }}MD5有一些优点,比如计算速度快、输出长度固定、应用广泛等等。 但是作为一个加密算法,它有一个天大的缺点,那就是不安全。 MD5算法已经被攻破,而且MD5算法的输出长度有限,攻击者可以通过暴力破解或彩虹表攻击等方式,找到与原始数据相同的散列值,从而破解数据。 虽然可以通过加盐,也就是对在原文里再加上一些不固定的字符串来缓解,但是完全可以用更安全的SHA系列算法替代。 2.1.2 SHA-256 SHA(Secure Hash Algorithm)系列算法是一组密码散列函数,用于将任意长度的数据映射为固定长度的散列值。SHA系列算法由美国国家安全局(NSA)于1993年设计,目前共有SHA-1、SHA-2、SHA-3三种版本。 其中SHA-1系列存在缺陷,已经不再被推荐使用。 SHA-2算法包括SHA-224、SHA-256、SHA-384和SHA-512四种散列函数,分别将任意长度的数据映射为224位、256位、384位和512位的散列值。 我们来看一下最常用的SHA-256的Java代码实现: public class SHA256 { private static final String SHA_256_ALGORITHM = "SHA-256"; public static String encrypt(String data) throws Exception { //获取SHA-256算法实例 MessageDigest messageDigest = MessageDigest.getInstance(SHA_256_ALGORITHM); //计算散列值 byte[] digest = messageDigest.digest(data.getBytes()); StringBuilder stringBuilder = new StringBuilder(); //将byte数组转换为15进制字符串 for (byte b : digest) { stringBuilder.append(Integer.toHexString((b & 0xFF) | 0x100), 1, 3); } return stringBuilder.toString(); } public static void main(String[] args) throws Exception { String data = "Hello World"; String encryptedData = encrypt(data); System.out.println("加密后的数据:" + encryptedData); }}SHA-2算法之所以比MD5强,主要有两个原因: 散列值长度更长:例如SHA-256算法的散列值长度为256位,而MD5算法的散列值长度为128位,这就提高了攻击者暴力破解或者彩虹表攻击的难度。 更强的碰撞抗性:SHA算法采用了更复杂的运算过程和更多的轮次,使得攻击者更难以通过预计算或巧合找到碰撞。 当然,SHA-2也不是绝对安全的,散列算法都有被暴力破解或者彩虹表攻击的风险,所以,在实际的应用中,加盐还是必不可少的。 2.2 对称加密算法 对称加密算法,使用同一个密钥进行加密和解密。 加密和解密过程使用的是相同的密钥,因此密钥的安全性至关重要。如果密钥泄露,攻击者可以轻易地破解加密数据。 常见的对称加密算法包括DES、3DES、AES等。其中,AES算法是目前使用最广泛的对称加密算法之一,具有比较高的安全性和加密效率。 2.2.1 DES DES(Data Encryption Standard)算法是一种对称加密算法,由IBM公司于1975年研发,是最早的一种广泛应用的对称加密算法之一。 DES算法使用56位密钥对数据进行加密,加密过程中使用了置换、替换、异或等运算,具有较高的安全性。 我们来看下DES算法的Java代码实现: public class DES { private static final String DES_ALGORITHM = "DES"; /** * DES加密 * * @param data 待加密的数据 * @param key 密钥,长度必须为8位 * @return 加密后的数据,使用Base64编码 */ public static String encrypt(String data, String key) throws Exception { // 根据密钥生成密钥规范 KeySpec keySpec = new DESKeySpec(key.getBytes()); // 根据密钥规范生成密钥工厂 SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(DES_ALGORITHM); // 根据密钥工厂和密钥规范生成密钥 SecretKey secretKey = secretKeyFactory.generateSecret(keySpec); // 根据加密算法获取加密器 Cipher cipher = Cipher.getInstance(DES_ALGORITHM); // 初始化加密器,设置加密模式和密钥 cipher.init(Cipher.ENCRYPT_MODE, secretKey); // 加密数据 byte[] encryptedData = cipher.doFinal(data.getBytes()); // 对加密后的数据进行Base64编码 return Base64.getEncoder().encodeToString(encryptedData); } /** * DES解密 * * @param encryptedData 加密后的数据,使用Base64编码 * @param key 密钥,长度必须为8位 * @return 解密后的数据 */ public static String decrypt(String encryptedData, String key) throws Exception { // 根据密钥生成密钥规范 KeySpec keySpec = new DESKeySpec(key.getBytes()); // 根据密钥规范生成密钥工厂 SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(DES_ALGORITHM); // 根据密钥工厂和密钥规范生成密钥 SecretKey secretKey = secretKeyFactory.generateSecret(keySpec); // 对加密后的数据进行Base64解码 byte[] decodedData = Base64.getDecoder().decode(encryptedData); // 根据加密算法获取解密器 Cipher cipher = Cipher.getInstance(DES_ALGORITHM); // 初始化解密器,设置解密模式和密钥 cipher.init(Cipher.DECRYPT_MODE, secretKey); // 解密数据 byte[] decryptedData = cipher.doFinal(decodedData); // 将解密后的数据转换为字符串 return new String(decryptedData); } public static void main(String[] args) throws Exception { String data = "Hello World"; String key = "12345678"; String encryptedData = encrypt(data, key); System.out.println("加密后的数据:" + encryptedData); String decryptedData = decrypt(encryptedData, key); System.out.println("解密后的数据:" + decryptedData); }}DES的算法速度较快,但是在安全性上面并不是最优选择,因为DES算法的密钥长度比较短,被暴力破解和差分攻击的风险比较高,一般推荐用一些更安全的对称加密算法,比如3DES、AES。 2.2.2 AES AES(Advanced Encryption Standard)即高级加密标准,是一种对称加密算法,被广泛应用于数据加密和保护领域。AES算法使用的密钥长度为128位、192位或256位,比DES算法的密钥长度更长,安全性更高。 我们来看下AES算法的Java代码实现: public class AES { private static final String AES_ALGORITHM = "AES"; // AES加密模式为CBC,填充方式为PKCS5Padding private static final String AES_TRANSFORMATION = "AES/CBC/PKCS5Padding"; // AES密钥为16位 private static final String AES_KEY = "1234567890123456"; // AES初始化向量为16位 private static final String AES_IV = "abcdefghijklmnop"; /** * AES加密 * * @param data 待加密的数据 * @return 加密后的数据,使用Base64编码 */ public static String encrypt(String data) throws Exception { // 将AES密钥转换为SecretKeySpec对象 SecretKeySpec secretKeySpec = new SecretKeySpec(AES_KEY.getBytes(), AES_ALGORITHM); // 将AES初始化向量转换为IvParameterSpec对象 IvParameterSpec ivParameterSpec = new IvParameterSpec(AES_IV.getBytes()); // 根据加密算法获取加密器 Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION); // 初始化加密器,设置加密模式、密钥和初始化向量 cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); // 加密数据 byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); // 对加密后的数据使用Base64编码 return Base64.getEncoder().encodeToString(encryptedData); } /** * AES解密 * * @param encryptedData 加密后的数据,使用Base64编码 * @return 解密后的数据 */ public static String decrypt(String encryptedData) throws Exception { // 将AES密钥转换为SecretKeySpec对象 SecretKeySpec secretKeySpec = new SecretKeySpec(AES_KEY.getBytes(), AES_ALGORITHM); // 将AES初始化向量转换为IvParameterSpec对象 IvParameterSpec ivParameterSpec = new IvParameterSpec(AES_IV.getBytes()); // 根据加密算法获取解密器 Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION); // 初始化解密器,设置解密模式、密钥和初始化向量 cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); // 对加密后的数据使用Base64解码 byte[] decodedData = Base64.getDecoder().decode(encryptedData); // 解密数据 byte[] decryptedData = cipher.doFinal(decodedData); // 返回解密后的数据 return new String(decryptedData, StandardCharsets.UTF_8); } public static void main(String[] args) throws Exception { String data = "Hello World"; String encryptedData = encrypt(data); System.out.println("加密后的数据:" + encryptedData); String decryptedData = decrypt(encryptedData); System.out.println("解密后的数据:" + decryptedData); }}AES算法采用的密钥长度更长,密钥空间更大,安全性更高,能够有效地抵抗暴力破解攻击。 当然,因为密钥长度较长,需要的存储也更多。 对于对称加密算法而言,最大的痛点就在于密钥管理困难,相比而言,非对称加密就没有这个担忧。 2.3 非对称加密算法 非对称加密算法需要两个密钥,这两个密钥互不相同,但是相互匹配,一个称为公钥,另一个称为私钥。 使用其中的一个加密,则使用另一个进行解密。例如使用公钥加密,则需要使用私钥解密。 2.3.1 RSA RSA算法是是目前应用最广泛的非对称加密算法,由Ron Rivest、Adi Shamir和Leonard Adleman三人在1978年发明,名字来源三人的姓氏首字母。 我们看下RSA算法的Java实现: public class RSA { private static final String RSA_ALGORITHM = "RSA"; /** * 生成RSA密钥对 * * @return RSA密钥对 */ public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM); keyPairGenerator.initialize(2048); // 密钥大小为2048位 return keyPairGenerator.generateKeyPair(); } /** * 使用公钥加密数据 * * @param data 待加密的数据 * @param publicKey 公钥 * @return 加密后的数据 */ public static String encrypt(String data, PublicKey publicKey) throws Exception { Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encryptedData); } /** * 使用私钥解密数据 * * @param encryptedData 加密后的数据 * @param privateKey 私钥 * @return 解密后的数据 */ public static String decrypt(String encryptedData, PrivateKey privateKey) throws Exception { byte[] decodedData = Base64.getDecoder().decode(encryptedData); Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] decryptedData = cipher.doFinal(decodedData); return new String(decryptedData, StandardCharsets.UTF_8); } public static void main(String[] args) throws Exception { KeyPair keyPair = generateKeyPair(); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); String data = "Hello World"; String encryptedData = encrypt(data, publicKey); System.out.println("加密后的数据:" + encryptedData); String decryptedData = decrypt(encryptedData, privateKey); System.out.println("解密后的数据:" + decryptedData); }}RSA算法的优点是安全性高,公钥可以公开,私钥必须保密,保证了数据的安全性;可用于数字签名、密钥协商等多种应用场景。 缺点是加密、解密速度较慢,密钥长度越长,加密、解密时间越长;密钥长度过短容易被暴力破解,密钥长度过长则会增加计算量和存储空间的开销。 3 总结 这一期就给大家简单盘点了一下最常用的5种加密算法。 ———————————————— 版权声明:本文为CSDN博主「吴名氏.」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_37284798/article/details/131856488
-
H2数据库在单元测试中的作用及使用方法单元测试是软件开发过程中的一种重要测试方法,用于验证代码的各个模块是否能够正常工作。在Java开发中,常用的单元测试框架有JUnit、TestNG等。本文将介绍H2数据库在单元测试中的作用以及如何使用。一、H2数据库简介H2是一个开源的嵌入式数据库,采用纯Java编写,不需要安装和配置,体积小、速度快、支持事务处理等特点。在Java项目中,经常使用H2数据库作为内存数据库进行开发和测试。二、H2数据库在单元测试中的作用模拟真实数据库环境:在进行单元测试时,需要模拟实际生产环境中的数据存储和访问方式。H2数据库可以作为一个轻量级的内存数据库,方便地创建和管理数据表,为测试提供真实的数据环境。隔离测试数据:使用H2数据库可以避免测试数据与生产数据相互干扰,保证测试的准确性和可靠性。同时,H2数据库支持多线程访问,可以在多线程环境下进行单元测试。便于调试和维护:H2数据库提供了丰富的日志和性能分析工具,可以帮助开发人员快速定位和解决单元测试中出现的问题,提高开发效率。三、如何使用H2数据库进行单元测试添加H2依赖在Java项目的pom.xml文件中添加H2数据库的依赖:<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.200</version> </dependency>初始化H2数据库在使用H2数据库之前,需要先创建一个数据源并初始化:import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import javax.sql.DataSource; import org.h2.tools.Server; import org.h2.tools.ServerSetup; public class H2Database { private static final String DB_URL = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"; private static final String USER = "sa"; private static final String PASSWORD = ""; public static DataSource getDataSource() throws SQLException { ServerSetup setup = new ServerSetup().setPort(8082).setHttpEnabled(false).setAllowCreate(true); org.h2.tools.Console console = Server.createWebServer("", 8082, setup); console.start(); System.out.println("Opening database"); Server server = Server.createWebServer("-webAllowOthers").start(); return new DataSource() { @Override public Connection getConnection() throws SQLException { return DriverManager.getConnection(DB_URL, USER, PASSWORD); } }; } }使用H2数据库进行单元测试在单元测试中,可以通过JUnit或TestNG等框架获取到数据源,然后使用H2数据库进行数据的增删改查操作。例如:```java import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; import org.springframework.test.context.junit4.SpringRunner; import org.junit.runner.RunWith; import javax.sql.DataSource; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.; import static org.mockito.Mockito.; @RunWith(SpringRunner.class) @SpringBootTest(classes = {YourApplicationClass}) // 替换为你的应用类名 public class YourTestClass { // 替换为你的测试类名 @Autowired private JdbcTemplate jdbcTemplate; @Autowired private DataSource dataSource; // 这里注入上面创建的数据源对象即可获取到H2数据库实例 private List users = new ArrayList<>(); // 假设有一个User实体类和对应的repository接口用于操作数据库,这里只是示例数据结构而已。在实际项目中可能会有所不同。注意:此处仅为示例代码,具体实现需要根据实际情况进行调整。)
-
Java中使用H2数据库的详解在Java开发中,我们经常需要使用数据库来存储和管理数据。H2是一个轻量级的嵌入式数据库,非常适合用于开发和测试环境。本文将介绍如何在Java项目中使用H2数据库。添加H2依赖首先,我们需要在项目的pom.xml文件中添加H2数据库的依赖:<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.200</version> </dependency>配置H2数据库在项目的src/main/resources目录下创建一个名为application.properties的文件,然后添加以下内容来配置H2数据库:# 数据库文件路径 spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= # 控制台输出SQL日志 logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE这里我们配置了一个内存型H2数据库,数据库文件路径为testdb,用户名为sa,密码为空。同时,我们还开启了SQL日志输出,以便于调试。创建实体类和Repository接下来,我们创建一个实体类User,并为其添加JPA注解:import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; private String email; // 省略getter和setter方法 }然后,我们创建一个UserRepository接口,继承自JpaRepository:import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface UserRepository extends JpaRepository<User, Long> { }使用H2数据库进行操作现在我们已经配置好了H2数据库和相关代码,可以开始进行数据的增删改查操作了。例如,我们可以在服务类中注入UserRepository,然后使用其提供的方法进行数据操作:import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; @Service public class UserService { @Autowired private UserRepository userRepository; public List<User> findAll() { return userRepository.findAll(); } public Optional<User> findById(Long id) { return userRepository.findById(id); } public User save(User user) { return userRepository.save(user); } public void deleteById(Long id) { userRepository.deleteById(id); } }
-
一、先说一下get和post GET和POST是HTTP请求的两种基本方法,要说它们的区别,接触过WEB开发的人都能说出一二。 最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数。 1、看一下人畜无害的w3schools怎么说: GET在浏览器回退时是无害的,而POST会再次提交请求; GET产生的URL地址可以被Bookmark,而POST不可以; GET请求会被浏览器主动cache,而POST不会,除非手动设置; GET请求只能进行url编码,而POST支持多种编码方式; GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留; GET请求在URL中传送的参数是有长度限制的,而POST么有; 对参数的数据类型,GET只接受ASCII字符,而POST没有限制; GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息; GET参数通过URL传递,POST放在Request body中。 2、问一下文心你言哥,轻轻松松给你一个标准答案: 3、卧槽,懂了,好像又没懂 主要是这么官方的话术,没进入你的心里。 二、让我们扒下GET和POST的外衣,坦诚相见吧! GET和POST是什么?HTTP协议中的两种发送请求的方法。 HTTP是什么?HTTP是基于TCP/IP的关于数据如何在万维网中如何通信的协议。 HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。 在我大万维网世界中,TCP就像汽车,我们用TCP来运输数据,它很可靠,从来不会发生丢件少件的现象。但是如果路上跑的全是看起来一模一样的汽车,那这个世界看起来是一团混乱,送急件的汽车可能被前面满载货物的汽车拦堵在路上,整个交通系统一定会瘫痪。 为了避免这种情况发生,交通规则HTTP诞生了。 HTTP给汽车运输设定了好几个服务类别,有GET, POST, PUT, DELETE等等,HTTP规定,当执行GET请求的时候,要给汽车贴上GET的标签(设置method为GET),而且要求把传送的数据放在车顶上(url中)以方便记录。如果是POST请求,就要在车上贴上POST的标签,并把货物放在车厢里。 当然,你也可以在GET的时候往车厢内偷偷藏点货物,但是这是很不光彩;也可以在POST的时候在车顶上也放一些数据,让人觉得傻乎乎的。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。 但是,我们只看到HTTP对GET和POST参数的传送渠道(url还是requrest body)提出了要求。“标准答案”里关于参数大小的限制又是从哪来的呢? 在我大万维网世界中,还有另一个重要的角色:运输公司。不同的浏览器(发起http请求)和服务器(接受http请求)就是不同的运输公司。 虽然理论上,你可以在车顶上无限的堆货物(url中无限加参数)。但是运输公司可不傻,装货和卸货也是有很大成本的,他们会限制单次运输量来控制风险,数据量太大对浏览器和服务器都是很大负担。 业界不成文的规定是,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。超过的部分,恕不处理。如果你用GET服务,在request body偷偷藏了数据,不同服务器的处理方式也是不同的,有些服务器会帮你卸货,读出数据,有些服务器直接忽略,所以,虽然GET可以带request body,也不能保证一定能被接收到哦。 好了,现在你知道,GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。 你以为本文就这么结束了? 三、我们的大BOSS还等着出场呢 这位BOSS有多神秘?当你试图在网上找“GET和POST的区别”的时候,那些你会看到的搜索结果里,从没有提到他。他究竟是什么呢。。。 GET和POST还有一个重大区别,简单的说: GET产生一个TCP数据包;POST产生两个TCP数据包。 长的说: 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据); 而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。 也就是说,GET只需要汽车跑一趟就把货送到了,而POST得跑两趟,第一趟,先去和服务器打个招呼“嗨,我等下要送一批货来,你们打开门迎接我”,然后再回头把货送过去。 因为POST需要两步,时间上消耗的要多一点,看起来GET比POST更有效。因此Yahoo团队有推荐用GET替换POST来优化网站性能。但这是一个坑!跳入需谨慎。为什么? GET与POST都有自己的语义,不能随便混用; 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点; 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。 现在,当面试官再问你“GET与POST的区别”的时候,你的内心是不是这样的? 你以为本文就这么结束了? 四、java 模拟post请求 言归正传,书接上回,本文的标题是《java 模拟post请求》,你这跑题了啊,吒哥。 1、弯了?那就给它掰回来。 实际的开发中,有很多这样的需求,“调用第三方接口”。 说人话,第三方接口,就是别人的接口,通过POST调用一下,返回点东西给你。 Java提供了多种方式来模拟POST请求,包括使用HttpURLConnection、HttpClient和OkHttp等库。 不扯了,直接上代码。毕竟吒哥这是以干货著称的,虽然一直在想办法转型,写一些有趣的文章,洞穿心灵的文章,脱离低级趣味的文章,但,技术本身就是乏味的~ 2、HttpURLConnection HttpURLConnection是Java自带的用于发送HTTP请求的类。我们可以使用它来模拟POST请求。 (1)发起POST请求: @Controller @RequestMapping("/client") public class TestController { private static final String urlStream = "http://127.0.0.1:8080/MyProject/nezha/getUserStream"; @RequestMapping(value = "/getUserStream", method = RequestMethod.POST) @ResponseBody public User getUserStream(@RequestBody User user) { System.out.println("请求参数:"+user); String userRet = HttpUtil.sendPostByURLConnection(urlStream, user); User ret = JSON.toJavaObject(JSONObject.parseObject(userRet), User.class); System.out.println("URLConnection方式发送POST请求:"+ret); return ret; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 (2)模拟服务端 @Controller @RequestMapping("/server") public class PostServerController { @RequestMapping(value = "/getUserStream", method = RequestMethod.POST) @ResponseBody public User getUserStream(HttpServletRequest request) throws IOException { String user = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); User user1 = JSON.toJavaObject(JSONObject.parseObject(user), User.class); user1.setInfo("getUserStream,我 OK 啦"); System.out.println(user1); return user1; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 (3)通过postman调用 3、HttpClient Apache HttpClient是一个流行的Java库,用于发送HTTP请求。我们可以使用该库来模拟POST请求。 (1)发起POST请求: @Controller @RequestMapping("/client") public class TestController { private static final String url = "http://127.0.0.1:8080/MyProject/nezha/getUser"; @RequestMapping(value = "/getUser", method = RequestMethod.POST) @ResponseBody public User getUser(@RequestBody User user) { System.out.println("请求参数:"+user); String json = JSON.toJSONString(user); String userRet = HttpUtil.sendHttpPost(url, json); User ret = JSON.toJavaObject(JSONObject.parseObject(userRet), User.class); System.out.println("HttpPost方式发送POST请求:"+ret); return ret; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 (2)模拟服务端 @Controller @RequestMapping("/server") public class PostServerController { @RequestMapping(value = "/getUser", method = RequestMethod.POST) @ResponseBody public User getUser(@RequestBody User user) { user.setInfo("getUser,我 OK 啦"); System.out.println(user); return user; } } 1 2 3 4 5 6 7 8 9 10 11 12 (3)通过postman调用 4、CRUD从业人员最爱的“工具类” import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.URL; import java.net.URLConnection; public class HttpUtil { /** * URLConnection方式 * 发送POST请求 */ public static <T> String sendPostByURLConnection(String url, T entity) { PrintWriter out = null; BufferedReader in = null; String result = ""; try { URL realUrl = new URL(url); // 打开和URL之间的连接 URLConnection conn = realUrl.openConnection(); // 设置通用的请求属性 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); // 设置请求类型 //conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setRequestProperty("Content-Type", "application/json"); // 发送POST请求必须设置如下两行 conn.setDoOutput(true); conn.setDoInput(true); // 获取URLConnection对象对应的输出流 out = new PrintWriter(conn.getOutputStream()); // 发送请求参数 out.print(entity); // flush输出流的缓冲 out.flush(); // 定义BufferedReader输入流来读取URL的响应 in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { System.out.println(e); return null; } //使用finally块来关闭输出流、输入流 finally { try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (IOException ex) { } } return result; } /** * HttpPost方式 * 发送POST请求 */ public static String sendHttpPost(String url, String data) { String response = null; try { CloseableHttpClient httpclient = null; CloseableHttpResponse httpresponse = null; try { httpclient = HttpClients.createDefault(); HttpPost httppost = new HttpPost(url); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000) //连接超时时间 .setSocketTimeout(5000) //读取数据超时时间 .build(); httppost.setConfig(requestConfig); StringEntity stringentity = new StringEntity(data, ContentType.create("application/json", "UTF-8")); httppost.setEntity(stringentity); httpresponse = httpclient.execute(httppost); response = EntityUtils.toString(httpresponse.getEntity()); } finally { if (httpclient != null) { httpclient.close(); } if (httpresponse != null) { httpresponse.close(); } } } catch (Exception e) { return null; } return response; } } ———————————————— 原文链接:https://blog.csdn.net/guorui_java/article/details/132122466
-
在一个在线商城系统中,面临一个重要的问题:如何在订单支付过程中保证数据的一致性,并且如何优化支付操作的性能。 1、订单支付需求 在用户下单后,需要执行订单支付操作,确保支付和订单状态的一致性。 2、数据一致性要求 支付成功后,必须将订单状态更新为已支付,以保持数据的一致性。 3、高并发支付 在高并发的情况下,需要确保订单支付的性能和数据一致性。 为了解决以上问题,我们可以使用Redis提供的事务和管道机制。 一、Redis事务 1、什么是Redis事务 在Redis中,事务是一组命令的集合,可以在一个单独的流程中执行,以保证这些命令的原子性、一致性、隔离性和持久性。 (1)事务概述 Redis事务由以下四个关键命令进行管理: 命令 描述 MULTI 开启事务,标记事务块的开始。 EXEC 执行事务中的所有命令。 DISCARD 取消事务,放弃所有已经入队的命令。 WATCH 监视一个或多个键,用于乐观锁。 (2)Redis的事务特性 Redis事务具有以下关键特性: 事务特性 描述 原子性 事务中的所有命令要么全部执行,要么全部不执行。这确保了在事务执行期间,不会发生部分命令执行成功而部分命令执行失败的情况。 一致性 事务中的命令会按照被添加的顺序执行,不会被其他客户端的命令打断。这保证了事务中的操作按照期望的顺序执行,不会受到并发操作的影响。 隔离性 在事务执行期间,事务会被隔离,不会受到其他事务的影响。即使有其他并发事务在执行,事务中的操作也不会被其他事务看到,直到事务被执行提交。 持久性 事务执行结束后对数据库的修改将被持久化到磁盘上。这确保了事务中的操作不会因为系统故障而丢失,从而保证了数据的持久性。 以上是Redis事务的基本概念和特性,它们保证了在Redis中执行的事务是可靠的、具备一致性的操作集合。 上图形表示了Redis事务的关键特性之间的相互关系。这些特性相互支持,共同确保了Redis事务的可靠性和一致性。 原子性保证了事务中的操作要么全部成功,要么全部失败。 一致性保证了事务中的操作按照特定的顺序执行,不会受到其他操作的干扰。 隔离性确保了事务在执行期间与其他事务相互隔离,互不干扰。 持久性确保了事务执行后的修改会被持久保存,不会因系统故障而丢失。这些特性一起构成了Redis事务的可靠性和稳定性的基础。 2、使用Redis事务 (1)开始和提交事务 在Redis中,使用事务需要遵循以下步骤: 使用MULTI命令开启事务。 执行需要在事务中执行的命令。 使用EXEC命令提交事务,执行事务中的所有命令。 下面是一个使用Java代码示例的详细步骤: // 创建与Redis服务器的连接 Jedis jedis = new Jedis("localhost", 6379); // 开启事务 Transaction transaction = jedis.multi(); // 执行事务中的命令 transaction.set("key1", "value1"); transaction.set("key2", "value2"); // 提交事务并获取执行结果 List<Object> results = transaction.exec(); 1 2 3 4 5 6 7 8 9 10 11 12 在上面的示例中,transaction.set("key1", "value1") 和 transaction.set("key2", "value2") 这两个命令会被添加到事务队列中,当transaction.exec()被调用时,事务中的所有命令会被一起执行。如果在MULTI和EXEC之间有错误发生,事务会被取消,命令不会执行。 (2)事务命令 在事务中,您可以使用常规的Redis命令,例如SET、GET、HSET、ZADD等等。这些命令会被添加到事务队列中,直到执行EXEC命令。 (3)事务示例 以下是使用Java代码示例来演示在事务中执行常见的Redis命令: import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; public class RedisTransactionCommandsExample { public static void main(String[] args) { Jedis jedis = new Jedis("localhost", 6379); // 开启事务 Transaction transaction = jedis.multi(); // 执行事务中的命令 transaction.set("name", "Alice"); transaction.hset("user:1", "name", "Bob"); transaction.zadd("scores", 100, "Alice"); transaction.zadd("scores", 200, "Bob"); // 提交事务并获取执行结果 List<Object> results = transaction.exec(); // 打印执行结果 for (Object result : results) { System.out.println("Result: " + result); } // 关闭连接 jedis.close(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 在上述示例中,使用了SET、HSET和ZADD命令,这些命令被添加到了事务队列中。当执行transaction.exec()时,事务中的所有命令会被一起执行。这里的示例是简单的演示,您可以根据需要添加更多的命令来构建更复杂的事务。 二、Redis管道 1、什么是Redis管道 Redis管道(Pipeline)是一种优化Redis操作的技术,它允许在单次通信中发送多个命令到Redis服务器,从而显著减少了通信开销,提高了性能。 管道可以将多个命令一次性发送给服务器,而不需要等待每个命令的响应,这使得Redis能够更高效地处理批量操作和大规模数据的读写。 下图展示了Redis管道的工作原理: 在上图中,客户端(Client)向Redis服务器(Server)发送多个命令,每个命令用Command 1、Command 2等表示。这些命令被一次性发送到服务器,而不需要等待每个命令的响应。服务器在执行所有命令后,一次性将结果响应给客户端。同时说明了Redis管道的工作方式:通过将多个命令打包成一次通信,减少了每个命令的通信开销,提高了系统的性能。 使用Redis管道时,客户端通过创建一个管道对象,将多个命令添加到管道中,然后一次性执行管道中的命令。最后,客户端可以收集所有命令的执行结果。 (1)管道概述 在Redis中,管道是通过以下命令进行管理: 命令 描述 PIPELINE 开启管道模式,用于一次性发送多个命令。 MULTI 开启事务模式,用于在管道中执行一系列命令。 EXEC 提交管道中的事务,执行并返回结果。 使用管道,您可以将多个命令一次性发送给服务器,然后通过一次通信获得所有命令的执行结果,从而减少了每个命令的通信开销,提高了系统的性能。 (2)Redis的管道特性 使用Redis管道可以获得以下优势: 减少通信开销: 在普通的命令传输中,每个命令都需要来回的网络通信,而管道可以将多个命令打包一次性发送给服务器,从而大大减少了通信开销。这对于网络延迟较高的场景尤为重要,有效提高了性能。 提高吞吐量: 管道允许在一次通信中执行多个命令,从而在单位时间内处理更多的命令。这对于需要处理大量命令的场景,如批量数据处理、并发请求处理等,能够有效提高Redis的吞吐量和响应能力。 2、使用Redis管道 (1)管道命令 以下是一个实际案例,展示如何使用Redis管道来执行多个命令并提高性能: import redis.clients.jedis.Jedis; import redis.clients.jedis.Pipeline; import java.util.List; public class RedisPipelineExample { public static void main(String[] args) { Jedis jedis = new Jedis("localhost", 6379); // 创建管道 Pipeline pipeline = jedis.pipelined(); // 向管道中添加命令 for (int i = 0; i < 10000; i++) { pipeline.set("key" + i, "value" + i); } // 执行管道中的命令 List<Object> results = pipeline.syncAndReturnAll(); // 关闭连接 jedis.close(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 在上述案例中,使用了一个循环来向管道中添加10000个SET命令。通过使用管道,可以在一次通信中将所有命令发送到服务器,而不是逐个发送,从而减少了通信开销,提高了性能。 (2)管道优化性能 使用Redis管道可以提高性能,特别是在需要批量处理多个命令的情况下。管道的原理是一次性将多个命令发送给服务器,然后一次性获取结果,这减少了通信的往返次数,从而显著提高了吞吐量。 然而,需要注意以下几点: 管道不支持事务,不能保证多个命令的原子性执行。 使用管道时,命令的执行顺序可能与添加顺序不一致,这需要根据业务需求进行考虑。 管道并非在所有场景下都能带来性能提升,需要根据实际情况进行评估。 通过合理使用管道,可以最大限度地发挥Redis在高性能数据处理中的优势。 三、事务 vs 管道:何时使用何种 1、事务的适用场景 事务在某些场景下可以保证原子性和一致性的操作,特别适用于强一致性要求的业务操作,例如支付操作。 (1)强一致性操作 事务是一种适用于需要强一致性操作的机制。当多个命令需要在一个操作序列中原子性地执行时,事务可以确保这些命令要么全部执行,要么全部不执行,以保持数据的一致性。 在以下示例中,模拟一个银行转账操作,其中需要同时扣减一个账户的余额并增加另一个账户的余额: Jedis jedis = new Jedis("localhost", 6379); // 开启事务 Transaction transaction = jedis.multi(); // 扣减账户1余额 transaction.decrBy("account1", 100); // 增加账户2余额 transaction.incrBy("account2", 100); // 提交事务并获取执行结果 List<Object> results = transaction.exec(); // 关闭连接 jedis.close(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 (2)原子性要求高 当业务要求多个操作要么全部成功,要么全部失败时,事务是更好的选择。事务确保了事务中的一系列命令以原子操作方式执行,从而维护了数据的一致性。 2、管道的适用场景 管道适用于需要批量操作和吞吐量要求较高的场景。通过一次性发送多个命令到服务器,可以减少通信开销,提高性能。 (1)批量操作 使用管道可以有效地执行批量操作。例如,当您需要向数据库中添加大量数据时,使用管道可以减少每个命令的通信成本,从而大大提高操作的效率。 以下示例演示了如何使用管道进行批量设置操作: import redis.clients.jedis.Jedis; import redis.clients.jedis.Pipeline; import java.util.List; public class RedisPipelineBatchExample { public static void main(String[] args) { Jedis jedis = new Jedis("localhost", 6379); Pipeline pipeline = jedis.pipelined(); // 向管道中添加一批设置操作 for (int i = 0; i < 1000; i++) { pipeline.set("key" + i, "value" + i); } // 执行管道中的命令 List<Object> results = pipeline.syncAndReturnAll(); // 关闭连接 jedis.close(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 (2)吞吐量要求高 在需要高吞吐量的场景下,管道可以显著提升性能。当多个命令需要在短时间内执行时,使用管道可以将这些命令打包发送,减少了通信的往返次数。 使用管道来进行大规模数据处理时,尤其可以在高负载的情况下提高系统的处理能力。 四、案例研究:保证订单支付的数据一致性与性能优化 1、场景描述 在一个在线商城系统中,面临一个重要的问题:如何在订单支付过程中保证数据的一致性,并且如何优化支付操作的性能。 (1)订单支付需求 在用户下单后,需要执行订单支付操作,确保支付和订单状态的一致性。 (2)数据一致性要求 支付成功后,必须将订单状态更新为已支付,以保持数据的一致性。 (3)高并发支付 在高并发的情况下,需要确保订单支付的性能和数据一致性。 2、使用Redis事务解决数据一致性问题 (1)事务实现订单支付 Jedis jedis = new Jedis("localhost", 6379); Transaction transaction = jedis.multi(); // 扣除用户余额 transaction.decrBy("user:balance:1", orderAmount); // 更新订单状态为已支付 transaction.hset("order:1", "status", "paid"); List<Object> results = transaction.exec(); 1 2 3 4 5 6 7 8 9 10 以上示例中,使用了Redis事务来确保在一个操作序列中,用户余额的扣除和订单状态的更新同时发生。如果事务中的任何一步操作失败,整个事务都会被回滚,保证了数据的一致性。 (2)事务的一致性保证 使用事务可以保证用户余额和订单状态的一致性,要么同时成功,要么同时失败。这样,可以确保支付和订单状态的正确性,避免了潜在的数据不一致问题。 3、使用Redis管道优化支付性能 (1)管道批量支付 Jedis jedis = new Jedis("localhost", 6379); Pipeline pipeline = jedis.pipelined(); for (Order order : orders) { pipeline.decrBy("user:balance:" + order.getUserId(), order.getAmount()); pipeline.hset("order:" + order.getId(), "status", "paid"); } List<Object> results = pipeline.syncAndReturnAll(); 1 2 3 4 5 6 7 8 9 在这个示例中,使用了Redis管道来批量处理多个订单的支付。通过将多个命令一次性发送给服务器,可以减少通信开销,从而显著提高支付操作的性能。 (2)管道的性能提升 通过使用管道,可以将多个支付操作打包在一次通信中进行,减少了通信往返次数,从而提高了支付的性能。 尤其在高并发支付的场景下,管道可以显著减少服务器负载,提高系统的响应能力。 五、事务和管道的限制与注意事项 1、事务的限制 事务在使用过程中需要注意以下限制,其中包括WATCH命令和乐观锁的使用。 (1)WATCH命令 在事务中使用WATCH命令可以监视一个或多个键,如果被监视的键在事务执行过程中被其他客户端修改,事务会被中断。这是为了保证事务的一致性和避免竞态条件。 正面例子: Jedis jedis = new Jedis("localhost", 6379); Transaction transaction = jedis.multi(); // 监视键"balance" transaction.watch("balance"); // ... 在此期间可能有其他客户端修改了"balance"键的值 ... // 执行事务 List<Object> results = transaction.exec(); 1 2 3 4 5 6 7 8 9 10 反面例子: Jedis jedis = new Jedis("localhost", 6379); Transaction transaction = jedis.multi(); // 监视键"balance" transaction.watch("balance"); // ... 在此期间其他客户端修改了"balance"键的值 ... // 尝试执行事务,但由于"balance"键被修改,事务会被中断 List<Object> results = transaction.exec(); 1 2 3 4 5 6 7 8 9 10 (2)乐观锁 在处理并发更新时,可以使用乐观锁的方式。通过使用版本号或时间戳等机制,在执行命令前先检查数据是否被其他客户端修改过,从而避免并发冲突。 正面例子: Jedis jedis = new Jedis("localhost", 6379); // 获取当前版本号 long currentVersion = Long.parseLong(jedis.get("version")); // 更新数据前检查版本号 if (currentVersion == Long.parseLong(jedis.get("version"))) { Transaction transaction = jedis.multi(); transaction.set("data", "new value"); transaction.incr("version"); List<Object> results = transaction.exec(); } else { // 数据已被其他客户端修改,需要处理冲突 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 2、管道的注意事项 使用管道时需要注意以下事项,包括管道的串行性和慎重使用。 (1)不支持事务 管道不支持事务,因此无法通过管道实现事务的原子性和一致性。如果需要事务支持,应该使用Redis的事务机制。 (2)慎用管道 管道虽然可以提高性能,但并不是在所有场景下都能带来性能提升。在某些情况下,由于管道的串行性,某些命令可能会阻塞其他命令的执行,反而降低了性能。 正面例子: Jedis jedis = new Jedis("localhost", 6379); Pipeline pipeline = jedis.pipelined(); for (int i = 0; i < 1000; i++) { pipeline.set("key" + i, "value" + i); } // 执行管道中的命令并获取结果 List<Object> results = pipeline.syncAndReturnAll(); 1 2 3 4 5 6 7 8 9 反面例子: Jedis jedis = new Jedis("localhost", 6379); Pipeline pipeline = jedis.pipelined(); for (int i = 0; i < 1000; i++) { // 注意:此处执行了耗时的命令,可能阻塞其他命令的执行 pipeline.get("key" + i); } // 执行管道中的命令并获取结果 List<Object> results = pipeline.syncAndReturnAll(); 1 2 3 4 5 6 7 8 9 10 六、总结 本篇博客深入探讨了Redis中的事务和管道机制,以及它们在保证数据一致性和优化性能方面的应用。 通过详细的讲解和代码示例,我们了解了事务和管道的基本概念、特性、使用方法以及适用场景。以下是本篇博客的主要内容总结: 在Redis事务部分,我们了解了事务的概念和特性。事务可以确保一系列命令的原子性、一致性、隔离性和持久性。 通过MULTI、EXEC、DISCARD和WATCH命令,我们可以管理事务的开始、提交、回滚以及监视键变化。事务适用于需要保证原子性和一致性的操作,特别是在强一致性要求的场景下。 在Redis管道部分,我们深入了解了管道的概念和优势。管道允许一次性发送多个命令到服务器,减少通信开销,提高性能。 通过PIPELINE、MULTI和EXEC命令,我们可以创建管道、添加命令,并执行管道中的命令。管道适用于批量操作和吞吐量要求较高的场景,可以显著提高Redis的性能。 在事务 vs 管道:何时使用何种部分,我们对比了事务和管道的适用场景。 事务适用于保证强一致性操作和原子性要求高的场景; 管道适用于批量操作和高吞吐量的场景。 通过示例,我们说明了如何根据业务需求选择合适的机制来满足一致性和性能的需求。 在案例研究:保证订单支付的数据一致性与性能优化部分,我们应用之前的知识解决了一个实际问题。我们展示了如何使用事务保证订单支付的数据一致性,同时如何使用管道优化支付操作的性能。这个案例充分体现了事务和管道在实际业务中的应用。 在事务和管道的限制与注意事项部分,我们指出了事务和管道的一些限制和注意事项。事务受到WATCH命令和乐观锁的限制,而管道不支持事务,并且需要在使用时慎重考虑性能影响。 通过本篇博客,我们详细探讨了Redis中的事务和管道机制,了解了它们如何在实际应用中保证数据一致性和优化性能。无论是强调一致性还是追求性能,都可以根据业务需求选择合适的机制来达到最佳效果。 ———————————————— 版权声明:本文为CSDN博主「哪 吒」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/guorui_java/article/details/132747287
-
使用IntelliJ IDEA内置的远程调试功能 IntelliJ IDEA是一款强大的Java集成开发环境(IDE),它提供了许多实用的特性来帮助开发者提高编程效率。其中,远程调试功能使得开发者可以在不同的计算机上调试程序,这对于团队合作和远程维护项目非常有用。本文将介绍如何使用IntelliJ IDEA内置的远程调试功能。 1. 配置远程服务器 在使用远程调试之前,首先需要配置一个远程服务器。这可以通过以下步骤完成: 1. 打开IntelliJ IDEA,点击 "File" > "New" > "Project",创建一个新的项目。 2. 在 "New Project" 窗口中,选择 "Spring Initializr",然后点击 "Next"。 3. 填写项目的基本信息,如Group、Artifact、Name等,然后点击 "Next"。 4. 在 "Dependencies" 页面中,添加需要的依赖,例如 "Web" -> "Spring Web",然后点击 "Next"。 5. 确认项目设置,点击 "Finish",IntelliJ IDEA将自动创建一个新项目。 接下来,我们需要配置远程服务器的连接信息。点击 "Run" > "Edit Configurations",在弹出的窗口中点击左上角的 "+" 按钮,选择 "Remote"。 2. 配置远程调试参数 在 "Remote" 配置页面中,我们需要填写远程服务器的相关信息: 1. Name:给这个远程调试配置起一个名字,例如 "Remote Debug"。 2. Host:远程服务器的IP地址或域名。 3. Port:远程服务器的端口号,默认为8080。 4. Deployment:选择一个部署方式,例如 "Artifact",然后点击 "..." 按钮,选择刚刚创建的项目生成的jar文件。 5. Keystore:如果需要使用密钥库进行认证,点击 "..." 按钮,选择密钥库文件。如果不需要认证,可以留空。 6. Save configuration:点击 "Apply" 保存配置。 3. 开始远程调试 配置好远程调试后,我们可以像平时一样运行程序。在代码中添加断点,然后点击工具栏上的绿色虫子图标(或按下Shift + F9)启动远程调试。程序将在远程服务器上运行,并在达到断点时暂停。 4. 查看变量和日志 在远程调试过程中,我们可以查看变量值、调用堆栈以及输出日志等信息。这些信息将显示在右侧的 "Variables"、"StackTrace" 和 "Logs" 面板中。此外,我们还可以使用内置的搜索功能快速定位相关信息。 5. 结束远程调试 当程序运行完成后,我们可以点击工具栏上的红色方块图标(或按下Shift + F9)结束远程调试。同时,远程服务器上的程序也会被终止。 总结 通过以上步骤,我们可以轻松地使用IntelliJ IDEA内置的远程调试功能。这不仅可以帮助我们更高效地完成调试任务,还可以促进团队之间的协作和沟通。希望本文能对你有所帮助!
-
提高单元测试覆盖率的策略 单元测试是软件开发过程中的一个重要环节,它帮助我们确保代码的正确性、稳定性和可维护性。然而,在实际开发过程中,由于时间、人力和技术限制,单元测试的覆盖率可能并不高。那么,如何提高单元测试的覆盖率呢?本文将介绍一些有效的策略。 1. 理解代码结构 首先,要提高单元测试覆盖率,需要对代码的结构有深入的理解。这包括了解代码的功能、模块划分、类之间的关系等。只有充分理解了代码结构,才能编写出有针对性的单元测试用例。 2. 使用单元测试框架 使用单元测试框架可以大大提高测试的效率和覆盖率。一个好的单元测试框架应该具备以下特点: - 支持自动生成测试用例:框架应该能够根据代码的结构和功能,自动生成合适的测试用例。 - 支持参数化测试:通过参数化测试,可以方便地为不同的输入数据编写测试用例,从而提高测试覆盖率。 - 支持并发测试:并发测试可以帮助我们发现潜在的竞态条件问题。 - 提供详细的测试报告:测试框架应该能够清晰地展示测试结果,包括通过率、失败原因等信息。 3. 设计测试用例 设计测试用例时,应遵循以下原则: - 尽可能覆盖所有代码路径:确保每个分支、每个条件分支都有对应的测试用例。 - 测试正常情况和异常情况:对于函数和方法,要分别测试其正常输入和异常输入的情况。同时,也要考虑到边界条件和特殊值的测试。 - 使用mock对象:对于依赖于外部资源的对象(如数据库、网络连接等),可以使用mock对象进行隔离,从而简化测试用例的设计和维护。 4. 遵循TDD(测试驱动开发)原则 TDD是一种先编写测试用例,然后编写实现代码的开发模式。这种模式可以帮助我们从一开始就关注代码的质量,确保每个功能都经过充分的测试。在TDD过程中,我们需要: - 为每个函数或方法编写一个或多个独立的测试用例。 - 从这些测试用例出发,逐步实现代码的功能。 - 在实现代码后,运行这些测试用例,确保功能正确无误。 - 如果测试失败,分析原因并修复代码;如果测试通过,则继续添加更多的测试用例。 5. 定期评估和调整测试策略 随着项目的发展,代码结构和需求可能会发生变化。因此,我们需要定期评估现有的测试覆盖率,并根据需要调整测试策略。这可能包括添加新的测试用例、优化现有测试用例、重构代码以便于测试等。 总之,提高单元测试覆盖率是一个持续的过程,需要我们不断地学习、实践和改进。通过遵循上述策略,我们可以有效地提高单元测试的覆盖率,从而提高软件的质量和可维护性。
-
您好,我在使用OBS javasdk的文件上传API,使用中,我发现,对于大文件,使用官方提供的并发上传文件示例,比直接调 putObject方法速度还慢,具体代码如下:1.并发分段上传代码官方示例https://support.huaweicloud.com/sdk-java-devg-obs/obs_21_0607.html2.调用 putObject 上传文件public SaveObjectResult saveObject(String bucket, String objectKey, MultipartFile file) throws Exception { // 校验桶名称是否正确 bucket = checkBucketName(bucket); InputStream in = file.getInputStream(); SaveObjectResult saveObjectResult = new SaveObjectResult(); saveObjectResult.setOk(Boolean.FALSE); saveObjectResult.setStorageMediaType(OSSTypeEnum.OBS.name()); PutObjectResult putObjectResult = getClient().putObject(bucket, objectKey, in); if (putObjectResult != null && putObjectResult.getStatusCode() == HttpStatus.HTTP_OK) { saveObjectResult.setOk(Boolean.TRUE); saveObjectResult.setAccessUrl(putObjectResult.getObjectUrl()); } return saveObjectResult; }此处直接使用ObsCLient 的 PutObjectResult putObject(String bucketName, String objectKey, InputStream input) 方法。测试结果发现,使用并发分段上传,并没有节省时间,反而比直接使用putObject还慢,为什么会这样?怎么优化,才能让上传更快?
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签