• [技术干货] Java下载文件的几种方式【转】
    1. 以流的方式下载public HttpServletResponse download(String path, HttpServletResponse response) { try { // path是指欲下载的文件的路径。 File file = new File(path); // 取得文件名。 String filename = file.getName(); // 取得文件的后缀名。 String ext = filename.substring(filename.lastIndexOf(".") + 1).toUpperCase(); // 以流的形式下载文件。 InputStream fis = new BufferedInputStream(new FileInputStream(path)); byte[] buffer = new byte[fis.available()]; fis.read(buffer); fis.close(); // 清空response response.reset(); // 设置response的Header response.addHeader("Content-Disposition", "attachment;filename=" + new String(filename.getBytes())); response.addHeader("Content-Length", "" + file.length()); OutputStream toClient = new BufferedOutputStream(response.getOutputStream()); response.setContentType("application/octet-stream"); toClient.write(buffer); toClient.flush(); toClient.close(); } catch (IOException ex) { ex.printStackTrace(); } return response;}2. 下载本地文件public void downloadLocal(HttpServletResponse response) throws FileNotFoundException { // 下载本地文件 String fileName = "Operator.doc".toString(); // 文件的默认保存名 // 读到流中 InputStream inStream = new FileInputStream("c:/Operator.doc");// 文件的存放路径 // 设置输出的格式 response.reset(); response.setContentType("bin"); response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); // 循环取出流中的数据 byte[] b = new byte[100]; int len; try { while ((len = inStream.read(b)) > 0) response.getOutputStream().write(b, 0, len); inStream.close(); } catch (IOException e) { e.printStackTrace(); } }3. 下载网络文件public void downloadNet(HttpServletResponse response) throws MalformedURLException { // 下载网络文件 int bytesum = 0; int byteread = 0; URL url = new URL("windine.blogdriver.com/logo.gif"); try { URLConnection conn = url.openConnection(); InputStream inStream = conn.getInputStream(); FileOutputStream fs = new FileOutputStream("c:/abc.gif"); byte[] buffer = new byte[1204]; int length; while ((byteread = inStream.read(buffer)) != -1) { bytesum += byteread; System.out.println(bytesum); fs.write(buffer, 0, byteread); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }public void downLoadFile(HttpServletResponse response, String httpUrl) { ServletOutputStream out = null; try { //与服务器建立连接 URL url = new URL(httpUrl); URLConnection conn = url.openConnection(); InputStream inputStream = conn.getInputStream(); try { //1.设置文件ContentType类型,这样设置,会自动判断下载文件类型 response.setContentType("multipart/form-data"); } catch (Exception e){ e.printStackTrace(); } out = response.getOutputStream(); // 读取文件流 int len = 0; byte[] buffer = new byte[1024 * 10]; while ((len = inputStream.read(buffer)) != -1) { out.write(buffer, 0, len); } out.flush(); out.close(); inputStream.close(); } catch (Exception e){ e.printStackTrace(); } }4. 在线打开的方式public void downLoad(String filePath, HttpServletResponse response, boolean isOnLine) throws Exception { File f = new File(filePath); if (!f.exists()) { response.sendError(404, "File not found!"); return; } BufferedInputStream br = new BufferedInputStream(new FileInputStream(f)); byte[] buf = new byte[1024]; int len = 0; response.reset(); // 非常重要 if (isOnLine) { // 在线打开方式 URL u = new URL("file:///" + filePath); response.setContentType(u.openConnection().getContentType()); response.setHeader("Content-Disposition", "inline; filename=" + f.getName()); // 文件名应该编码成UTF-8 } else { // 纯下载方式 response.setContentType("application/x-msdownload"); response.setHeader("Content-Disposition", "attachment; filename=" + f.getName()); } OutputStream out = response.getOutputStream(); while ((len = br.read(buf)) > 0) out.write(buf, 0, len); br.close(); out.close(); }5. 将文件转换成base64 /** * 将文件转为base64 */ public static String getBase64FromFile(File file) throws IOException { FileInputStream in = null; ByteArrayOutputStream out = null; try { in = new FileInputStream(file); out = new ByteArrayOutputStream(); int read = 0; byte[] buffer = new byte[1024]; while ((read = in.read(buffer, 0, 1024)) != -1) { out.write(buffer, 0, read); } return Base64.getEncoder().encodeToString(out.toByteArray()); } catch (IOException e) { throw e; } finally { if (in != null) { in.close(); } if (out != null){ out.close(); } } }6. 将MultipartFile转换为File /** * 将MultipartFile转换为File */ public static File MultipartFileToFile(MultipartFile multiFile) throws IOException { String fileName = multiFile.getOriginalFilename(); String prefix = fileName.substring(fileName.lastIndexOf(".")); InputStream in = null; OutputStream out = null; try { File file = File.createTempFile(fileName, prefix); out = new FileOutputStream(file); in = multiFile.getInputStream(); int read = 0; byte[] buffer = new byte[1024]; while ((read = in.read(buffer, 0, 1024)) != -1) { out.write(buffer, 0, read); } return file; } catch (Exception e) { throw e; }finally { if (in != null){ in.close(); } if (out != null){ out.close(); } } }转载自https://blog.javaex.cn/article/detail/542270361309343744
  • [技术干货] Java集合的遍历
    Java中对集合的遍历有多种形式迭代器Iterator、游标Cursor。System.out.println("iterator..."); Iterator iterator = redisTemplate.opsForZSet().scan(key, ScanOptions.scanOptions().build()); while (iterator.hasNext()){ ZSetOperations.TypedTuple<String> tuple = (ZSetOperations.TypedTuple<String>) iterator.next(); System.out.println(tuple.getValue()); }Cursor cursor = redisTemplate.opsForZSet().scan(key, ScanOptions.scanOptions().build()); while (cursor.hasNext()){ ZSetOperations.TypedTuple<String> tuple = (ZSetOperations.TypedTuple<String>) cursor.next(); System.out.println(tuple.getValue()); }
  • [技术干货] IDEA--如何解决Java:程序包xxxx不存在
    前言 有时候我们在导入程序之后,系统会给出错误提示:Java:程序包xxxx不存在,现在我这里有一招,就是使用IDEA自动导入Java程序包,这也是IDEA的一大优点。 内容 当出现如题所示的错误时候,不用着急,这是因为配置Java的程序包这块出现了错误,同时可能你还没有设置让IDEA自动加载Jar包,才会报出这种错误的。解决方式如下: File—>Setting—>Build,Execution,Deployment—>Maven—>Importing 然后如下图配置: 如上图配置好之后,点击apply,然后点击OK,这个时候刷新一下IDEA就可以看到,Jar包不足然后报出Java:程序包xxxx不存在的错误就会自动消失,并且在左边栏External Libraries中会出现很多jar包,如下图: 总结 OK,问题解决,就是这么简单,这既是IDEA的魅力所在,方便jar包的下载,也方便大家的编程。  end ———————————————— 原文链接:https://blog.csdn.net/Luojun13Class/article/details/81483151 
  • [技术干货] SpringBoot打包部署时,找不到项目中引用的jar包,报 java.lang.NoClassDefFoundError
    项目中引入的本地的jar包,在自己本地运行也是没有任何问题,但是在打包发布的时候就会报错找不到自己引入的文件 这是本地lib下的jar包   在打包运行后报错NoClassDefFoundError,找不到文件: 本地启动的时候自己本地项目知道根据你导入jar的路径去加载jar包,所以会没问题。但是打包之后,这个jar包是不会被打包进去的,导致报错。所以,我们想要正常运行,就需要把自己的jar包打包到项目中; 1.首先在pom中添加本地jar包的依赖 其中groupId    artifactId    和version自己随便写。scope作用于定义为system,systemPath定义为jar包在项目中的路径(${basedir}就是项目的根目录):  2.光做这些还是不够的,这样只能保证我们在本地能够正常运行,打包还是不会被打包进去,所以说我们需要在引入的springboot的maven插件中告诉maven,将我们的刚刚引入的作用域为system的本地jar也打包进来 ———————————————— 原文链接:https://blog.csdn.net/Handsome__c/article/details/121776225 
  • [技术干货] add as library是什么?有什么用?如何打开?
    在很多地方看见add as library,但就是不知道这是干啥的。上网搜,发现并没有直接介绍这个的,经过一段时间的信息搜集,总结出以下几个知识点,希望给一些和我一样的小小白经验学习,希望每个小白都能成为自己心中向往的大佬。 一. add as library是什么? add as library是IntelliJ IDEA里面的一个功能,当然eclipse里面也有,多用于java开发和Android开发。 二. add as library有什么用?         多用于将将外部jar(lib / * .jar)添加到IntelliJ IDEA项目。        通俗点说,就是你做项目的时候需要一些外部jar包,比如我们用java连接数据库时用到了阿里巴巴公司的数据库连接池Druid,我们就需要导入Druid相关的jar包和配置文件,这些jar包和配置文件就属于外部jar包和配置文件。一般我们都是在项目路径下单独再建一个libs目录来存放这些东西。但直接复制粘贴到这个lib文件夹里面是不够的,我们还要把这些文件和jar包  add as library  来保证各种依赖关系。 三. add as library如何调出? 点击你要add as library的文件或jar包,右键选择add as library即可。 当然还有其他当如jar包的方法,还比较常用的是点File->Project Structure->Moudules->Dependenceies-> 右侧的 + 即可添加jar包 ———————————————— 原文链接:https://blog.csdn.net/doubleguy/article/details/104947149 
  • [技术干货] IDEA将Java项目单个类打JAR包方法(采用自带插件,不用Maven工具)
    一、什么是JAR包 JAR文件的全称是Java Archive File,即Java档案文件。JAR文件是一种压缩文件,与常见的ZIP压缩文件兼容,被称为JAR包。JAR文件与zip文件的主要区别是在JAR文件中默认包含了一个名为META-INF/MANIFEST.MF的清单文件,这个清单文件是在生成JAR文件时系统自动创建的 二、打JAR包步骤 1.先创建一个要打包成JAR包的类 2.File -> Project Structrue -> Artifacts -> + -> JAR -> from modules with dependencies… 3.在Main Class中选择要打包的类 project -> 选择打包的类 -> 然后OK project -> 点击OK后,会弹出错误提示框,关闭该提示框,并关闭类选择框 点击OK 4.Apply -> OK 5.Build -> Build Artifacts 6.点击Build后就会再之前选择的jar存放目录中生成jar包 三、jar包使用 方式一:java -jar 命令 CMD 中打开刚刚创建jar包的目录,运行命令 java -jar myProject.jar 显示运行结果 方式二:导入jar包 (1)File -> Project Structrue -> Modules ->Dependencies -> + -> jARs or directories… (2)在jar包的存放目录中找到jar包,然后点击OK (3)选择jar包 -> Apply -> OK (4)然后就可以在External Libraries中看到导入的jar包 (5)将之前的MyJar类删除掉,然后新建一个测试类Test,测试一下jar包是否可以,这里可以正常使用 ———————————————— 原文链接:https://blog.csdn.net/ZZQHELLO2018/article/details/124431906 
  • [技术干货] element-ui el-select/el-cascader设置默认值,默认值不显示
    最近接手了一个后台管理项目,项目使用了Element-ui组件, 使用了Select 下拉菜单展示并选择内容,使用了cascader做级联城市选择器 看了官方文档我们知道可以使用v-model绑定数据,设置初始值/默认值  <el-form-item label="班级" prop="classId">    <el-select v-model="form.classId" placeholder="请选择班级">      <el-option v-for="(value) of classList" :key="value.classId" :label="value.className" :value="value.classId"></el-option>    </el-select> </el-form-item> <el-form-item label="机构地址" prop="district">    <el-cascader        style="width: 216px;"         v-model="form.district"        :options="dataDistrict"        @change="handleChange"      >     </el-cascader> </el-form-item> 问题来了,明明已经设置了v-model但是没有显示,百度发现问题在于数据类型,form.classId的值为 String ,而 el-select列表的 value 为 Number,el-cascader的value是Array,form.district却是JSON字符串,并不全等,导致无法找到对应的项,把数据类型进行转换后就能正常显示啦 ———————————————— 原文链接:https://blog.csdn.net/weixin_43144209/article/details/120891475 
  • [技术干货] vue中el-select无法显示默认选中值的name,直接显示值value
    一、背景 在写项目的过程中,突然发现el-select这个组件,绑定的值没办法显示name,而是直接显示value。在编辑项目的弹框中,要显示项目绑定了哪个数据库(项目外连数据库表主键ID),结果发现直接把关联数据库的ID显示了出来,这与我的设想不一致,因此来找找是什么问题 二、解决 有问题的,有文档的当然第一时间去看文档,一看数据类型是string。 value-key 作为 value 唯一标识的键名,绑定值为对象类型时必填 数据类型string 再去检查项目传值,发现el-option绑定的list中key的类型是string,但v-model绑定的值是int,所以就导致了无法显示正确的名字,而显示主键ID  知道问题解决起来就很简单了,直接parseInt()转一下数据类型 <el-form-item label="归属数据库:" prop="databaseId">     <el-select v-model="projectForm.databaseId" placeholder="归属项目" style="width: 400px">         <el-option v-for="item in databaseList" :key="item.id" :value="parseInt(item.id)" :label="item.name" />     </el-select> </el-form-item> 但是可以的话,还是尽量数据格式一致,如果都是int的话,也是能可以正确显示的,那就无须做额外的处理了 ———————————————— 原文链接:https://blog.csdn.net/weixin_40295575/article/details/124250581 
  • [技术干货] weui 弹出 $.toast(“我是文本“,“text“); 总是带有图标
    在前段代码中引入 weui.css ,weuix.css 和js jquery-weui.min.js就可以使用weui的一些样式了按照官方的文档中                 $.toast("我是文本","text");但是我在实际中使用弹出的结果却是这个样子: 为什么 原因肯定是weui的样式和其他的样式冲突了 我们看看 $.toast 这一串代码: t.toast=function(t,a,r)    { "function"==typeof a&&(r=a);      var o,s="weui-icon-success-no-circle",       c=i.duration;      "cancel"==a?(o="weui-toast_cancel",s="weui-icon-cancel"):"forbidden"==a?(o="weui-toast--forbidden",s="weui-icon-warn"):"text"==a?o="weui-toast--text":"number"==typeof a&&(c=a), e('<i class="'+s+' weui-icon_toast"></i><p class="weui-toast_content">'+(t||"已经完成")+"</p>",o),setTimeout(function(){n(r)},c)} 大概能看出来 cancel ,forbidden,text 这些都是一些选项,当选择而不同的时候,出现不同的样式 text 应该是没有图标,为什么有图标了 我在页面上设置100,来查看这个样式 可以看到的是图标来源于这一部分 我们选中 i 这个标签,看右侧使用的样式: 果然是其他的样式干扰的 如果我们引入的其他的css作用不大,我们可以找到直接删除了 或者将 important 删除了 最终结果: 因为这次的样式调试用了很长的时间,特此记录 希望对你有所帮助 ———————————————— 原文链接:https://blog.csdn.net/datouniao1/article/details/111311058 
  • [技术干货] html微信分享没有图片不显示,微信分享网页链接缩略图不显示解决方法
    微信分享网页不显示缩略图片的原因为规范自定义分享链接功能在网页上的使用,自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原文链接:https://blog.csdn.net/weixin_31499719/article/details/117834995
  • [技术干货] mybatis-plus中wrapper的用法
    用到了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 &gt;= #{searchObdisProblem.checkTimeStart}       </if>         <if test="searchObdisProblem.checkTimeEnd != null ">             and p.check_time &lt;= #{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> -------------------------------------------------------------------------------------以下无正文-------------------  参考文档  1、(五)springboot + mybatis plus强大的条件构造器queryWrapper、updateWrapper_青蛙与大鹅的博客-CSDN博客_querywrapper.eq  2、https://blog.csdn.net/kepengs/article/details/112345870  3、Wrapper使用_weixin_39615889的博客-CSDN博客_wrapper使用  4、MyBatis-Plus Wrapper条件构造器查询大全_IT贱男的博客-CSDN博客_wrapper.like   5、https://blog.csdn.net/qq_48209375/article/details/114446611  6、https://blog.csdn.net/TBDBTUO/article/details/123550498 ———————————————— 原文链接:https://blog.csdn.net/qq_39715000/article/details/120090033 
  • [技术干货] MySQL函数find_in_set介绍
    场景介绍 人有时会身兼数职,需要查找出其中担任某一职务的都有哪些人,如下面position字段,不同的职务用数字表示,多个职务以逗号隔开。 先要查找出担任1职务的人员,通过以下两种方式来查询。 方式一 采用模糊查询,匹配出1职务的记录,如下SQL: select * from user where position like '%1%' 查询结果如下,仔细观察你会发现position为10的也被查出来了,但这个不符合业务要求。 方式二 采用MySQL的原生函数find_in_set(str,array)来查询,SQL如下: select * from user where find_in_set(1,position) 查询结果如下,符合要求。  函数介绍 FIND_IN_SET(str,strlist),注意其中strlist只识别英文逗号。 https://dev.mysql.com/doc/refman/5.7/en/string-functions.html#function_find-in-set ———————————————— 原文链接:https://blog.csdn.net/loongshawn/article/details/78611636 
  • [技术干货] 使用Vant如何完成各种Toast提示框
    这篇文章主要介绍了使用Vant如何完成各种Toast提示框,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教1)使用前的需要安装Vant奥。  2)在main.js里面引入Toast  import { Toast } from 'vant'; Vue.use(Toast); (3)在页面使用:(根据步骤代码可以运行奥  Toast.vue文件)(上面截图的,在下面代码都有栗子奥)。  <template>   <!-- Toast提示 -->   <div id="toast">     <van-button plain type="primary" @click="toToast">普通文字提示</van-button>     <van-button plain type="primary" @click="toLoading">加载转圈提示</van-button>     <van-button plain type="primary" @click="toSuccessTip">成功提示</van-button>     <van-button plain type="primary" @click="toFailTip">失败提示</van-button>     <van-button plain type="primary" @click="toCustomIcon">自定义图标提示</van-button>     <van-button plain type="primary" @click="toCustomImage">自定义图片提示</van-button>   </div> </template>  <script>   export default {     data() {       return {         msg: ''       }     },     // 引入 Toast 组件后,会自动在 Vue 的 prototype 上挂载 $toast 方法,便于在组件内调用。     methods: {       // 普通文字提示       toToast() {         this.$toast({           message:'我是需要提示的文字',           position:'top'         });       },          // 加载转圈提示       toLoading() {         this.$toast.loading({           mask: true,           message: '加载中...'         });       },          // 成功提示       toSuccessTip() {         this.$toast.success({           message:'成功的提示文案',         })       },          // 失败提示       toFailTip() {         this.$toast.fail({           message:'失败的提示文案'         })       },          //   自定义图标       toCustomIcon() {         this.$toast({           icon: 'star-o', // 找到自己需要的图标           message: '我是提示文字'         })       },          //自定义图片提示       toCustomImage() {         this.$toast({           icon:'https://www.baidu.com/favicon.ico',           message:'我是提示文字'         })       }      },     mounted() {     }   } </script> <style> </style> (4)Toast的相关API和Options 点击去查看  更新补充 position 里面的高度不局限与 top bottom等,也可设置数值,例如:   this.$toast({     message:'我是需要提示的文字',     position:'200px'   // 弹框的位置可以自己设置 }); Vant Toast用法 题外话就不多讲了,这是围绕vue.js写的,爱上vue.js 1.首先引入 import { Toast } from 'vant' 写个小列子 绑定一个click事件 2.写事件 在methods写方法 showToast() {                 this.$toast({                     message: "今日签到+3",                   })       },  一个简单的toast提示成就好了 原文链接:https://www.jb51.net/article/249445.htm
  • [技术干货] html和jsp的区别--静态页面和动态页面的区别
    一、静态web页面,一般指html:1、在静态Web程序中,客户端使用Web浏览器(IE、FireFox等)经过网络(Network)连接到服务器上,使用HTTP协议发起一个请求(Request),告诉服务器我现在需要得到哪个页面,所有的请求交给Web服务器,之后WEB服务器根据用户的需要,从文件系统(存放了所有静态页面的磁盘)取出内容。之后通过Web服务器返回给客户端,客户端接收到内容之后经过浏览器渲染解析,得到显示的效果。2、为了让静态web页面显示更加好看,使用javascript/VBScript/ajax(AJAX即“Asynchronous Javascript And XML”(异步JavaScript和XML),是指一种创建交互式网页应用的网页开发技术。)但是这些特效都是在客户端上借助于浏览器展现给用户的,所以在服务器上本身并没有任何的变化。3、静态web无法连接数据库;4、静态web资源开发技术:HTML;5、由于现在的web页面中,大量使用JS,导致浏览器打开页面,就会占用大量的内存,服务端的压力是减轻了,但压力转移到了客户端。二、动态web页面,一般指jsp:动态WEB中,程序依然使用客户端和服务端,客户端依然使用浏览器(IE、FireFox等),通过网络(Network)连接到服务器上,使用HTTP协议发起请求(Request),现在的所有请求都先经过一个WEB Server来处理。如果客户端请求的是静态资源(*.htm或者是*.htm),则将请求直接转交给WEB服务器,之后WEB服务器从文件系统中取出内容,发送回客户端浏览器进行解析执行。如果客户端请求的是动态资源(*.jsp、*.asp/*.aspx、*.php),则先将请求转交给WEB Container(WEB容器),在WEB Container中连接数据库,从数据库中取出数据等一系列操作后动态拼凑页面的展示内容,拼凑页面的展示内容后,把所有的展示内容交给WEB服务器,之后通过WEB服务器将内容发送回客户端浏览器进行解析执行。再进一步深入分析动态web的访问过程:浏览器访问web时,看似是直接访问的jsp页面,其实是,最先到达的地方是服务器,服务器创建好req和resp对象后再给jsp页面使用。在jsp中完成设置字符集和取得表单参数后再调用servlet,完成业务处理。然后返回到jsp,jsp就会生成相应的html页面。该页面会返回到服务器,再由服务器,通过response对象返回给客户端。为什么需要web服务器?(web server)1)不管什么web资源,想被远程计算机访问,都必须有一个与之对应的网络通信程序,当用户来访问时,这个网络通信程序读取web资源数据,并把数据发送给来访者。2)WEB服务器就是这样一个程序,它用于完成底层网络通迅,处理http协议。使用这些服务器,We应用的开发者只需要关注web资源怎么编写,而不需要关心资源如何发送到客户端手中,从而极大的减轻了开发者的开发工作量。常用动态web资源开发技术:JSP/Servlet、ASP、PHP等。三、关于两者区别的简单直接的描述1、静态页面就是设计者把页面上所有东西都设定好、做死了,然后放上去,不管是谁在任何时候看到的页面内容都是一样的,一成不变(除非手动修改页面内容)。静态html页面文件,可以直接用本地的浏览器打开。比如:file:///Users/Phil/Documents/DevOps/HBuilderProjects/testJSP/index.html。2、动态页面的内容一般都是依靠服务器端的程序来生成的,不同人、不同时候访问页面,显示的内容都可能不同。网页设计者在写好服务器端的页面程序后,不需要手工控制,页面内容会按照页面程序的安排自动更改变换。3、html是w3c规范的一种网页书写格式,是一种统一协议语言,静态网页。我们上网看的网页都是大部分都是基于html语言的。jsp是一种基于动态语言,jsp可以实现html的所有任务。4、HTML(Hypertext Markup Language)文本标记语言,它是静态页面,和JavaScript一样解释性语言,为什么说是解释性语言呢?因为,只要你有一个浏览器那么它就可以正常显示出来,而不需要指定的编译工具,只需在TXT文档中写上HTML标记就可以正常显示。JSP(Java Server Page)是Java服务端的页面,所以它是动态的,它是需要经过JDK编译后把内容发给客户端去显示,我们都知道,Java文件编译后会产生一个class文件,最终执行的就是这个class文件。5、JSP的前身是servlet。6、html和jsp的表头不一样,这个是JSP的头“ <%@ page language="java" import="java.util.*" pageEncoding="gbk"%>”在表头中有编码格式和倒入包等。也是很好区分的,在jsp中用<%%>就可以写Java代码了,而html没有<%%>。7、,不认识jsp或者asp什么什么的,但是有时候界面需要逻辑控制,所以我们就用相应的技术来实现,这样比较方便。而jsp在后台通过服务器解析为相应的html,然后在供浏览器识别显示。例如<% if(flag == a){<label>a<label>}else {<label>b<label>}%>服务器在读取到这段代码后,根据相应的业务逻辑,编译成相应的servlet,再由servlet输出到页面(输出的就是html)。各自的优缺点:一.ajax的优点:    1.开发过程中前端与后端脱离,交互通过JSON传输来实现    2.跨平台能力更强,依托于浏览器的支持    3.使后台数据接口能够得到复用二.ajax的缺点:    1.开发难度大,考虑浏览器的兼容性    2.页面请求过多    3.属于后加载,无法被爬虫爬到    4.接口代码需要新增很多    5.无法直接显示java实体类对象,需要转换为json格式三.jsp的优点:    1.可被爬虫爬到    2.减少请求次数    3.不用考虑浏览器的兼容性四.jsp的缺点:    1.增大了服务器的压力    2.前端与后端未脱离,拖慢开发进度    3.过于依赖java运行环境    4.复用较低。原文链接:https://www.pianshen.com/article/83231978420/
  • [技术干货] BCryptPasswordEncoder的使用及原理
    在 Spring Security 中有一个加密的类 BCryptPasswordEncoder ,它的使用非常的简单而且也比较有趣。让我们来看看它的使用。BCryptPasswordEncoder 的使用首先创建一个 SpringBoot 的项目,在创建项目的时候添加 Spring Security 的依赖。然后我们添加一个测试类,写如下的代码:final private String password = "123456";@Testpublic void TestCrypt(){BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();String encode1 = bCryptPasswordEncoder.encode(password);System.out.println("encode1:" + encode1);String encode2 = bCryptPasswordEncoder.encode(password);System.out.println("encode2:" + encode2);}上面的代码中,首先实例化了一个 BCryptPasswordEncoder 类,然后使用该类的 encode 方法对同一个明文字符串进行了加密,并输出。运行上面的代码,查看输出。encode1:$2a$10$SqbQb0pD3KYrH7ZVTWdRZOhPAelQqa..lUnysXoWag6RvMkyC5SE6encode2:$2a$10$0sjBLlwrrch2EjgYls197e9dGRCMbQ7KUIt/ODPTSU0W.mEPaGkfG从上面的输出可以看出,同一个明文加密两次,却输出了不同的结果。是不是很神奇?但是这样有一个问题,如果使用 BCryptPasswordEncoder 去加密登录密码的话,还能进行验证么?当然是可以验证的。验证的话,使用的是 BCryptPasswordEncoder 的 matches 方法,代码如下。final private String password = "123456";@Testpublic void TestCrypt(){BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();String encode1 = bCryptPasswordEncoder.encode(password);System.out.println("encode1:" + encode1);boolean matches1 = bCryptPasswordEncoder.matches(password, encode1);System.out.println("matches1:" + matches1);String encode2 = bCryptPasswordEncoder.encode(password);System.out.println("encode2:" + encode2);boolean matches2 = bCryptPasswordEncoder.matches(password, encode2);System.out.println("matches2:" + matches2);}使用 matches 方法可以对加密前和加密后是否匹配进行验证。输出如下:encode1:$2a$10$qxU.rFLeTmZg47FyqJlZwu.QNX9RpEvqBUJiwUvUE0p4ENR.EndfSmatches1:trueencode2:$2a$10$NyGEOsQ1Hxv2gvYRmaEENueORlVDtSqoB/fHN76KkvQDeg7fbTy22matches2:true可以看到两次加密后的字符串虽然不同,但是通过 matches 方法都可以匹配出它们是 “123456” 这个明文加密的结果。同样很神奇,这是为什么呢?encode 和 matches 方法的原理我们通过源码来看看它们的原理吧。首先我们将依赖的源码下载到本地,方便我们进行调试。开始我使用 IDEA 进行调试时是没有源码的,后来下载了源码,发现没有源码时调试的是 Class 文件,IDEA 提供了 Class 文件与源码等价的反编译代码。而源码逻辑性更好一些。首先在 encode 代码处下断点,然后我们单步步入,去查看 encode 的实现,代码如下:@Overridepublic String encode(CharSequence rawPassword) {if (rawPassword == null) {throw new IllegalArgumentException("rawPassword cannot be null");}String salt = getSalt();return BCrypt.hashpw(rawPassword.toString(), salt);}直接运行到 return 语句处,查看一下 salt 的值,该值如下:$2a$10$H73jYFFLWWf.VV/mwonqru然后继续单步步入到 BCrypt.hashpw 方法内,该方法代码如下:public static String hashpw(String password, String salt) {byte passwordb[];passwordb = password.getBytes(StandardCharsets.UTF_8);return hashpw(passwordb, salt);}该方法的重点同样是 hashpw 方法,没有做什么处理,继续进入 hashpw 方法中。代码如下:public static String hashpw(byte passwordb[], String salt) {BCrypt B;String real_salt;byte saltb[], hashed[];char minor = (char) 0;int rounds, off;StringBuilder rs = new StringBuilder();if (salt == null) {throw new IllegalArgumentException("salt cannot be null");}int saltLength = salt.length();if (saltLength < 28) {throw new IllegalArgumentException("Invalid salt");}if (salt.charAt(0) != '$' || salt.charAt(1) != '2') {throw new IllegalArgumentException("Invalid salt version");}if (salt.charAt(2) == '$') {off = 3;}else {minor = salt.charAt(2);if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$') {throw new IllegalArgumentException("Invalid salt revision");}off = 4;}// Extract number of roundsif (salt.charAt(off + 2) > '$') {throw new IllegalArgumentException("Missing salt rounds");}if (off == 4 && saltLength < 29) {throw new IllegalArgumentException("Invalid salt");}rounds = Integer.parseInt(salt.substring(off, off + 2));real_salt = salt.substring(off + 3, off + 25);saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);if (minor >= 'a') {passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);}B = new BCrypt();hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0);rs.append("$2");if (minor >= 'a') {rs.append(minor);}rs.append("$");if (rounds < 10) {rs.append("0");}rs.append(rounds);rs.append("$");encode_base64(saltb, saltb.length, rs);encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);return rs.toString();}代码很长,但是真正关键的代码在上面第 43 、44 和 51 行的位置处,43 行处获取真正的 salt ,44 行是使用 base64 进行解码,然后 51 行用 密码、salt 进行处理。在来看看返回值是 rs,在第 63 行和 64 行,对 salt 进行 base64 编码后放入了 rs 中,然后对 hashed 进行 base64 编码后也放入了 rs 中,最后 rs.toString() 返回。虽然上面代码很长,其实真正关键的就只有上面我提到的几句,其余的部分不用看。我们接着看 matches 的源码,同样单步进入,代码如下:@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {if (rawPassword == null) {throw new IllegalArgumentException("rawPassword cannot be null");}if (encodedPassword == null || encodedPassword.length() == 0) {this.logger.warn("Empty encoded password");return false;}if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {this.logger.warn("Encoded password does not look like BCrypt");return false;}return BCrypt.checkpw(rawPassword.toString(), encodedPassword);}单步到第 14 行的 return 处,这里调用 BCrypt.checkpw 的方法,rawPassword.toString() 是我们的密码,即 ”123456“, 后面的 encodePassword 是我们加密后的密码,单步步入进去,代码如下:public static boolean checkpw(String plaintext, String hashed) {return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));}这里只有一行代码,但是代码中同样调用了前面的 hashpw 这个方法,传入的参数是 plaintext 和 hashed,plaintext 是我们的密码,即 “123456”, hashed 是加密后的密码。到这里基本就明白了。hashed 在进入 hashpw 函数后,会通过前面说到第 43 行代码取出真正的 salt,然后对通过 salt 和 我们的密码进行加密,这样流程就串联起来了。总结当时看到使用 BCryptPasswordEncoder 时,同样的密码可以生成不同的 密文 而且还可以通过 matches 方法进行匹配验证,觉得很神奇。后来经过调试发现,密文中本身包含了很多信息,包括 salt 和 使用 salt 加密后的 hash。因为每次的 salt 不同,因此每次的 hash 也不同。这样就可以使得相同的 明文 生成不同的 密文,而密文中包含 salt 和 hash,因此验证过程和生成过程也是相同的。————————————————原文链接:https://blog.csdn.net/easysec/article/details/121719949
总条数:2301 到第 页
上滑加载中