• [技术干货] JAVA编程中常用到的线程和注意点总结
    一般的创建线程的方式有两种:继承 Thread 类(extends Thread)或者实现Runnable 接口(implements Runnable)1)  继承 Thread 类    实现步骤:  继承 Thread 类, 覆盖run()方法, 提供并发运程的过程  创建这个类的实例  使用 start() 方法启动线程2)  实现 Runnable 接口  实现步骤:  实现 Runnable 接口, 实现run()方法, 提供并发运程的过程  创建这个类的实例, 用这个实例作为Thread 构造器参数,创建Thread 类  使用 start() 方法启动线程      线程都会出现最基本的五种状态:1)  New      新建状态    当程序使用new关键字创建了一个线程后,该线程就处于新建状态,此时线程还未启动,  当线程对象调用start()方法时,线程启动,进入Runnable 状态2)  Runnable    可运行(就绪)状态  当线程处于Runnable 状态时,表示线程准备就绪,等待获取CPU3)  Running    运行(正在运行)状态  假如该线程获取了CPU,则进入Running 状态,开始执行线程体,即run()方法中的内容  注意:   (1)如果系统叧有1个CPU,那么在仸意时间点则叧有1条线程处于Running 状态;   (2)如果是双核系统,那么同一时间点会有2条线程处于Running 状态但是,当线程数大于处理器数时,依然会是多条线程在同一个CPU 上轮换执行   (3)当一条线程开始运行时,如果它不是一瞬间完成,那么它不可能一直处于Running 状态, 线程在执行过程中会被中断,目的是让其它线程获得执行的机会,像这样线程调度的策略取决于底层平台。对于抢占式策略的平台而言,系统系统会给每个可执行的线程一小段时间来处理仸务,当该时间段(时间片)用完,系统会剥夺该线程所占资源(CPU), 让其他线程获得运行机会。  (4) 调用yield()方法,可以使线程由Running 状态进入Runnable 状态4)  Block      阻塞(挂起)状态  当如下情冴下,线程会进入阻塞状态:  线程调用了sleep()方法主动放弃所占CPU 资源  线程调用了一个阻塞式IO 方法(比如控制台输入方法),在该方法返回前,该线程被阻塞  当正在执行的线程被阻塞时,其它线程就获得执行机会了。需要注意的是,当阻塞结束时,该线程将进入Runnable 状态,而非直接进入Running 状态5)  Dead      死亡状态    当线程的run()方法执行结束,线程进入Dead 状态  需要注意的是,不要试图对一个已经死亡的线程调用start()方法,线程死亡后将不能再次作为线程执行,系统会抛出IllegalThreadStateException 异常注:1)  new运算创建线程后,线程进入New状态(初始状态)2)  调用  start()方法后,线程从New状态进入Runnable 状态(就绪状态)          start()方法是在main()方法(Running 状态)中调用的3)  线程结束后,进入Dead 状态(死亡状态),被对象垃圾回收4)  线程进入Dead 状态后,叧能被垃圾回收,不能再开始5)  如果线程在运行过程中,自己调用了yield()方法,则主动由 Running 状态进入Runnable 状态
  • [技术干货] Spring MVC请求处理流程和九大组件-转载
    需求:前端浏览器请求url: http://localhost:8080/demo/handle01,前端⻚⾯显示后台服务器的时间 开发过程 配置DispatcherServlet前端控制器 开发处理具体业务逻辑的Handler(@Controller、@RequestMapping) xml配置⽂件配置controller扫描,配置springmvc三⼤件 将xml⽂件路径告诉springmvc(DispatcherServlet)  流程说明 第⼀步:⽤户发送请求⾄前端控制器DispatcherServlet 第⼆步:DispatcherServlet收到请求调⽤HandlerMapping处理器映射器 第三步:处理器映射器根据请求Url找到具体的Handler(后端控制器),⽣成处理器对象及处理器拦截器(如果 有则⽣成)⼀并返回DispatcherServlet 第四步:DispatcherServlet调⽤HandlerAdapter处理器适配器去调⽤Handler 第五步:处理器适配器执⾏Handler 第六步:Handler执⾏完成给处理器适配器返回ModelAndView 第七步:处理器适配器向前端控制器返回 ModelAndView,ModelAndView 是SpringMVC 框架的⼀个底层对象,包括 Model 和 View 第⼋步:前端控制器请求视图解析器去进⾏视图解析,根据逻辑视图名来解析真正的视图。 第九步:视图解析器向前端控制器返回View 第⼗步:前端控制器进⾏视图渲染,就是将模型数据(在 ModelAndView 对象中)填充到 request 域 第⼗⼀步:前端控制器向⽤户响应结果。  Spring MVC 九⼤组件 HandlerMapping(处理器映射器) HandlerMapping 是⽤来查找 Handler 的,也就是处理器,具体的表现形式可以是类,也可以是⽅法。⽐如,标注了@RequestMapping的每个⽅法都可以看成是⼀个Handler。Handler负责具体实际的请求处理,在请求到达后,HandlerMapping 的作⽤便是找到请求相应的处理器Handler 和 Interceptor。  HandlerAdapter(处理器适配器) HandlerAdapter 是⼀个适配器。因为 Spring MVC 中 Handler 可以是任意形式的,只要能处理请求即可。但是把请求交给 Servlet 的时候,由于 Servlet 的⽅法结构都是doService(HttpServletRequest req,HttpServletResponse resp)形式的,要让固定的 Servlet 处理⽅法调⽤ Handler 来进⾏处理,便是HandlerAdapter 的职责。  HandlerExceptionResolver(Handler异常解析器) HandlerExceptionResolver ⽤于处理 Handler 产⽣的异常情况。它的作⽤是根据异常设置ModelAndView,之后交给渲染⽅法进⾏渲染,渲染⽅法会将 ModelAndView 渲染成⻚⾯。  ViewResolver(视图解析器) ViewResolver即视图解析器,⽤于将String类型的视图名和Locale解析为View类型的视图,只有⼀个resolveViewName()⽅法。从⽅法的定义可以看出,Controller层返回的String类型视图名viewName 最终会在这⾥被解析成为View。View是⽤来渲染⻚⾯的,也就是说,它会将程序返回的参数和数据填⼊模板中,⽣成html⽂件。ViewResolver 在这个过程主要完成两件事情: ViewResolver 找到渲染所⽤的模板(第⼀件⼤事)和所⽤的技术(第⼆件⼤事,其实也就是找到视图的类型,如JSP)并填⼊参数。默认情况下,Spring MVC会⾃动为我们配置⼀个InternalResourceViewResolver,是针对 JSP 类型视图的。  RequestToViewNameTranslator(ViewName翻译器) RequestToViewNameTranslator 组件的作⽤是从请求中获取 ViewName。因为 ViewResolver 根据ViewName 查找 View,但有的 Handler 处理完成之后,没有设置 View,也没有设置 ViewName,便要通过这个组件从请求中查找 ViewName。  LocaleResolver(区域设置解析器) ViewResolver 组件的 resolveViewName ⽅法需要两个参数,⼀个是视图名,⼀个是 Locale。LocaleResolver ⽤于从请求中解析出 Locale,⽐如中国 Locale 是 zh-CN,⽤来表示⼀个区域。这个组件也是 i18n 的基础。  ThemeResolver(主题解析器) ThemeResolver 组件是⽤来解析主题的。主题是样式、图⽚及它们所形成的显示效果的集合。Spring MVC 中⼀套主题对应⼀个 properties⽂件,⾥⾯存放着与当前主题相关的所有资源,如图⽚、CSS样式等。创建主题⾮常简单,只需准备好资源,然后新建⼀个“主题名.properties”并将资源设置进去,放在classpath下,之后便可以在⻚⾯中使⽤了。SpringMVC中与主题相关的类有ThemeResolver、ThemeSource和Theme。ThemeResolver负责从请求中解析出主题名,ThemeSource根据主题名找到具体的主题,其抽象也就是Theme,可以通过Theme来获取主题和具体的资源。  MultipartResolver(上传处理器) MultipartResolver ⽤于上传请求,通过将普通的请求包装成 MultipartHttpServletRequest 来实现。MultipartHttpServletRequest 可以通过 getFile() ⽅法 直接获得⽂件。如果上传多个⽂件,还可以调⽤ getFileMap()⽅法得到Map<FileName,File>这样的结构,MultipartResolver 的作⽤就是封装普通的请求,使其拥有⽂件上传的功能。  FlashMapManager(传输参数管理器) FlashMap ⽤于重定向时的参数传递,⽐如在处理⽤户订单时候,为了避免重复提交,可以处理完post请求之后重定向到⼀个get请求,这个get请求可以⽤来显示订单详情之类的信息。这样做虽然可以规避⽤户重新提交订单的问题,但是在这个⻚⾯上要显示订单的信息,这些数据从哪⾥来获得呢?因为重定向时么有传递参数这⼀功能的,如果不想把参数写进URL(不推荐),那么就可以通过FlashMap来传递。只需要在重定向之前将要传递的数据写⼊请求(可以通过ServletRequestAttributes.getRequest()⽅法获得)的属性OUTPUT_FLASH_MAP_ATTRIBUTE中,这样在重定向之后的Handler中Spring就会⾃动将其设置到Model中,在显示订单信息的⻚⾯上就可以直接从Model中获取数据。FlashMapManager 就是⽤来管理 FalshMap 的。 ———————————————— 版权声明:本文为CSDN博主「共饮一杯无」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_35427589/article/details/132873827 
  • [技术干货] SpringMVC 域对象共享数据-转载
     在Spring MVC中,可以使用域对象来共享数据。域对象是一个Map类型的对象,可以在请求处理方法之间共享数据。 数据共享可以实现数据在不同组件、请求或模块之间的传递和共享,以方便数据的处理和展示,提高应用程序的性能和灵活性。 SpringMVC主要提供了一下几种方式来达到数据共享: 1、使用ServletAPI向request域对象共享数据 在处理请求的方法中将数据存储到了HttpServletRequest对象的request域中,使用setAttribute方法。"myData"是数据的键,"data"是数据的值。  示例代码: // 获取要共享的数据 // 将数据存储在request域对象中 // 转发请求至其他Servlet或JSP页面  protected void doGet(HttpServletRequest request, HttpServletResponse response) {         String data = "Hello, World!";          request.setAttribute("myData", c);          RequestDispatcher dispatcher =     request.getRequestDispatcher("/path/to/your/servlet-or-jsp");     dispatcher.forward(request, response); } 2、使用ModelAndView向request域对象共享数据 ModelAndView对象是一个包含数据模型和视图信息的容器,在控制器方法中可以将需要共享的数据存储到ModelAndView对象中,然后将该对象返回给Spring MVC框架。  创建一个ModelAndView对象,并使用addObject方法将数据存储到该对象的模型中。"key"是数据的键,“value"是数据的值。然后,使用setViewName方法设置视图信息,指定要渲染的视图名为"example-view”。  然后,Spring MVC框架会将ModelAndView对象中的数据传递给视图,并在视图渲染时将数据存储到request域对象中。在视图中可以使用相应的表达式语言(如JSTL或Thymeleaf的EL表达式)来获取并展示存储在request域中的数据。  @RequestMapping("/testModelAndView") public ModelAndView testModelAndView(){     /**      * ModelAndView有Model和View的功能      * Model主要用于向请求域共享数据      * View主要用于设置视图,实现页面跳转      */     ModelAndView mav = new ModelAndView();     //向请求域共享数据     mav.addObject("testScope", "hello,ModelAndView");     //设置视图,实现页面跳转     mav.setViewName("success");     return mav; } 建议尽量在实际开发中使用更现代和推荐的方式,例如使用@ModelAttribute注解或在方法参数中使用Model对象,以更加简洁和便捷地向request域对象共享数据。  3、使用Model向request域对象共享数据 Model是一个接口,它可以在控制器方法的参数中声明,并使用它的方法将数据添加到其中,然后这些数据将自动传递到request域中。  @RequestMapping("/testModel") public String testModel(Model model){     model.addAttribute("testScope", "hello,Model");     return "success"; } 4、使用map向request域对象共享数据 在控制器方法的参数中声明一个Map参数,Spring MVC会自动将一个Map对象注入到该参数中。您可以向这个Map对象添加键值对,然后它们将自动传递到request域中。  @RequestMapping("/testMap") public String testMap(Map<String, Object> map){     map.put("testScope", "hello,Map");     return "success"; } 当控制器方法返回视图名称时,Spring MVC框架会自动将Map对象中的数据传递到request域中,并在渲染视图时可以使用这些数据。例如,如果您使用JSP作为视图技术,您可以在JSP文件中使用EL表达式${key}来获取存储在request域中的数据  5、使用ModelMap向request域对象共享数据 ModelMap是一个具体实现了Model接口的类,它提供了更多的便捷方法来添加和访问数据。  @RequestMapping("/testModelMap") public String testModelMap(ModelMap modelMap){     modelMap.addAttribute("testScope", "hello,ModelMap");     return "success"; } 当控制器方法返回视图名称时,Spring MVC框架会自动将ModelMap对象中的数据传递到request域中,并在渲染视图时可以使用这些数据。例如,如果您使用JSP作为视图技术,您可以在JSP文件中使用EL表达式${key}来获取存储在request域中的数据。  6、Model、ModelMap、Map的关系 Model、ModelMap、Map类型的参数其实本质上都是 BindingAwareModelMap 类型的  public interface Model{} public class ModelMap extends LinkedHashMap<String, Object> {} public class ExtendedModelMap extends ModelMap implements Model {} public class BindingAwareModelMap extends ExtendedModelMap {} 7、向session域共享数据 在控制器类上使用@SessionAttributes注解并指定了"myData"作为需要存储在Session域中的模型属性。在example方法中,我们使用model.addAttribute方法将数据添加到"myData"模型属性中。  @RequestMapping("/testSession") public String testSession(HttpSession session){     session.setAttribute("myData", "hello,session");     return "success"; } 8、向application域共享数据 控制器方法的参数中声明了一个ServletContext对象,并将其命名为"servletContext"。然后,我们使用setAttribute方法将数据添加到ServletContext对象中,"key"是数据的键,"value"是数据的值。  数据将被存储在Application域中,可以在整个应用程序中访问和共享。  @RequestMapping("/testApplication") public String testApplication(HttpSession session){     ServletContext application = session.getServletContext();     application.setAttribute("testApplicationScope", "hello,application");     return "success"; } 9、总结 本文主要讲解了以下几种域对象可以使用: request域对象:通过使用HttpServletRequest对象的setAttribute方法向request域中存储数据,使用getAttribute方法从request域中获取数据。 session域对象:通过使用HttpSession对象的setAttribute方法向session域中存储数据,使用getAttribute方法从session域中获取数据。 application域对象:通过使用ServletContext对象的setAttribute方法向application域中存储数据,使用getAttribute方法从application域中获取数据。 这些域对象都可以在控制器中使用,可以在不同的请求处理方法之间共享数据。例如,一个请求处理方法可以将数据存储到request域中,然后另一个请求处理方法可以从request域中获取这些数据。 ———————————————— 版权声明:本文为CSDN博主「我有一颗五叶草」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/m0_60915009/article/details/132835848 
  • [技术干货] JAVA中的泛型-转载
    如下是定义了一个类,这个类中可以存放一组 int 类型的数据。 class Number{     int[] arr;       public Number(int a) {         this.arr = new int[10];         this.arr[0] = a;     }       public void setArr(int n, int data) {         this.arr[n] = data;     }       public int getArr(int n) {         return arr[n];     } } 可是虽然这样写没有问题可是它的应用范围实在是太小了,它只能存储 int 类型的数据。如果我们现在想让这个类中可以存放任何类型的数据应该怎么做呢? 在 JAVA 中利用泛型就可以实现这点。 泛型 泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化。  泛型的语法: class 泛型类名称<类型形参列表> { }  class ClassName<T1, T2, ..., Tn> { } 类名后的 <T> 代表占位符,表示当前类是一个泛型类。 类型形参一般使用一个大写字母表示,常用的名称有: E 表示 Element K 表示 Key V 表示 Value N 表示 Number T 表示 Type S, U, V 等等 - 第二、第三、第四个类型 泛型类   我们将开始的代码进行改进:  class Number<T>{     Object[] arr;//这里可以暂时忽略Object,下文会说       public Number(T a) {         this.arr = new Object[10];         this.arr[0] = a;     }       public void setArr(int n, T data) {         this.arr[n] = data;     }       public T getArr(int n) {         return (T)arr[n];     } }  在 JAVA 中由于数组比较特殊,不能new泛型类型的数组,数组在new的时候必须要指定其类型所以如果定义成这样就会报错  。 为了不让它报错就需要进行强制类型转换因为 Object类 是所有类的父类所以也可以这样定义: 虽然这样写编译器没有报错可是还是报了警告,也就是说我们这样写会存在安全隐患。 这里推荐将数组定义成这样: 调用的时候需要注意: 如果是简单类型(如:int , char……)必须要传对应的包装类。 基本数据类型和对应的包装类  基本数据类型    包装类 byte    Byte short    Short int    Integer long    Long float    Float double    Double char    Character boolean    Boolean 这个表看起来多其实除了 Integer 和 Character, 其余基本类型的包装类都是首字母大写。 如图,此时我们的类泛用性就非常高了,我们可以使用 main 方法测试一下:     public static void main(String[] args) {         Number<Integer> a = new Number<>(3);         Number<Double> b = new Number<>(3.14);         Number<String> c = new Number<>("hello");         System.out.println(a.getArr(0));         System.out.println(b.getArr(0));         System.out.println(c.getArr(0));              } 泛型方法:  泛型除了可以应用于类之外也可以应用于方法。  语法: 方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }      public static <T> void swap(T[] array, int i, int j) {         T t = array[i];         array[i] = array[j];         array[j] = t;     }  泛型方法也和类一样不能使用基本类型,需要使用对应的包装类。 我们可以测试一下:      public static void main(String[] args) {         Integer[] b = {1,2,3,4};         swap(b, 0, 1);         System.out.println(Arrays.toString(b));     } 小结: 泛型类<类型实参> 变量名; // 定义一个泛型类引用 new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象,此时可以省略类型实参  Number<Integer> a = new Number<>(3); Number<Integer> list = new Number<Integer>(3); 泛型是将数据类型参数化,进行传递 使用 <T> 表示当前类是一个泛型类。 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换  ———————————————— 版权声明:本文为CSDN博主「休息一下…」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/2302_76339343/article/details/132867383 
  • [技术干货] MySQL常见面试题(2023年最新)-转载
     前言 java最新面试题(java基础、集合、多线程、jvm、锁、算法、CAS、Redis、数据库、mybatis、spring、springMVC、springBoot、微服务)  1.char和varchar的区别 ①char设置多少长度就是多少长度,varchar可以改变长度,所以char的空间利用率不如varchar的空间利用率高。 ②因为长度固定,所以存取速度要比varchar快。 ③char适用于固定长度的字符串,比如身份证号、手机号等,varchar适用于不固定的字符串。  2.数据库的三大范式 第一范式(1NF): 保证字段不可再分,保证原子性。 第二范式(2NF): 满足1NF前提下,表的每一列都必须和主键有关系。消除部分依赖关系。 第三范式(3NF): 满足2NF前提下,表的每一列比必须和主键有直接关系,不能是间接关系。消除传递依赖  3.你了解sql的执行顺序吗? ⑧select ⑨distinct(去重) ⑥聚合函数  ①from 表1  ③[inner join | left join | right join](连接) 表2  ②on(连接条件) 表1.字段 = 表2.字段  ④where 查询条件  ⑤group by(分组) 字段  ⑦having 分组过滤条件  ⑩order by(排序) 字段  ⑪limit(分页) 0,10  4.索引是什么 是一种高效获取数据的数据结构,相当于目录,更快的找到数据,是一个文件,占用物理空间。  5.索引的优点和缺点 优点: ①提高检索的速度。 ②索引列对数据排序,降低排序成本。 ③mysql 8之后引入了,隐藏索引,当一个索引被隐藏就不会被优化器所使用,就可以看出来索引对数据库的影响,有利于调优。 缺点: ①索引也是一个文件,所以会占用空间。 ②降低更新的速度,因为不光要更新数据,还要更新索引。  6.索引的类型 ①普通索引: 基本索引类型,允许定义索引的字段为空值和重复值。 ②唯一索引: 索引的值必须唯一,允许定义索引的字段为空值。 ③主键索引: 索引的值必须唯一,不可以为空。 ④复合索引: 多个字段加索引,遵守最左匹配规则。 ⑤全局索引: 只有在 MyISAM 引擎上才能使用。  7.索引怎么设计(优化) ①选择唯一性索引:值是唯一的,查询的更快。 ②经常作为查询条件的字段加索引。 ③为经常需要排序、分组和联合操作的字段建立索引:order by、group by、union(联合)、distinct(去重)等。 ④限制索引个数:索引数量多,需要的磁盘空间就越多,更新表时,对索引的重构和更新就很费劲。 ⑤表数据少的不建议使用索引(百万级以内):数据过少,有可能查询的速度,比遍历索引的速度都快。 ⑥删除不常用和不再使用的索引。 ⑦用类型小的类型做索引:比如:int和BIGINT能用int就使用int。因为类型小,查询速度快和索引占用的空间更少。 ⑧使用前缀索引,要是字符串越长,那么索引占的空间越大,并且比较起来就时间就越长。  8.怎么避免索引失效(也属于sql优化的一种) ①某列使用范围查询(>、<、like、between and)时, 右边的所有列索引也会失效。 ②不要对索引字段进行运算。 ③在where子句中不要使用 OR、!=、<>和对值null的判断。 ④避免使用’%'开头的like的模糊查询。 ⑤字符串不加单引号,造成索引失效。  9.索引的数据类型 Hash: 查询时调用Hash函数获得地址,回表查询实际数据。(InnoDB和MylSAM不支持,Memory支持)。 B+树: 每次从根节点出发去查询,然后得到地址,回表查询实际数据。  10.索引为什么使用树结构 因为可以加快查询效率,而且可以保持有序。  11.二叉查找树、B树、B+树 二叉查找树(二叉排序树、二叉搜索树): 一个节点最多两个子节点(左小右大),查询次数和比较次数都是最小的,但是索引是存在磁盘的,当数据量过大的时候,不能直接把整个索引文件加载到内存,需要分多次IO,最坏的情况IO的次数就是树的高度,为了减少IO,需要把树从竖向变成横向。 B树( B- ): 是一种多路查询树,每个节点包含K个子节点,节点都存储索引值和数据,K是B树的阶(树高被称为树的阶)。虽然比较的次数比较多,但是是在内存的比较,可以忽略不计,但是B树IO的次数要比二叉查找树要少,因为B树的高度可以更低。 B+树: B树的升级版,只有叶子节点储存的是索引值指向的数据库的数据。  12.为什么使用B+树不用B树 ①B树只适合随机检索,而B+树同时支持随机检索和顺序检索(因为叶子节点相当于链表,保存索引值都是有序的)。 顺序检索: 按照序列顺序遍历比较找到给定值。 随机检索: 不断从序列中随机抽取数据进行比较,最终找到结果。  ②减少了磁盘IO,提高空间利用率: 因为B+树非叶子节点不会存放数据,只有索引值,所以非叶子节点可以保存更多的索引值,这样B+树就可以更矮,减少IO次数。  ③B+树适合范围查找: 这才是关键,因为数据库大部分都是范围查找,B+树的叶子节点是有序链表,直接遍历就行,而B树的范围查找可能两个节点距离很远,只能通过中序遍历去查找,所以使用B+树更合适。 中序遍历: (根在中,从左往右,一棵树的左子树永远在根前面,根永远在右子树前面)  13.最左匹配原则 最左优先,以最左边为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between and、like)就会停止匹配。 例如:Z表建立联合索引 (a,b,c) //这样索引abc列都起效,因为符合最左匹配原则,where子句几个搜索条件顺序调换不影响查询结果,因为Mysql中有查询优化器,会自动优化查询顺序 select  *  from Z where a = 1 and b = 2 and c = 3   //因为a列是起点,没有a列匹配不上,所以索引失效 select * from table_name where  b = 2 and c = 3   //因为连续不到b,所以只有a列索引生效 select * from table_name where  a = 1 and c = 3  14.Mysql怎么查看是否使用到索引或怎么查看sql执行计划 使用explain  例如:explain select * from 表名 where 条件 结果:会查出key,key就是你使用的索引。还有type这个字段,可以看到索引是全表扫描还是索引扫描等等。 type字段内容性能对比:ALL < index < range ~ index_merge < ref < eq_ref < const < system  15.一条sql查询非常慢,我们怎么去排查和优化? 排查: (1) 开启慢查询。 (2) 查看慢查询日志(定位低效率sql,命令:show processlist)。 (3) 使用explain查看sql的执行计划(看看索引是否失效或者性能低)  优化: sql优化 + 索引 + 数据库结构优化 + 优化器优化  16.MylSAM和InnoDB、Memory的区别 MylSAM: mysql5.5之前的存储引擎,是表锁(悲观锁)级别的,不支持事务和外键。 InnoDB: mysql5.5之后的存储引擎,是行锁(乐观锁)级别的,支持事务和外键。 Memory: 内存数据库引擎,因为在内存操作,所以读写很快,但是Mysql服务重启,会丢失数据,不支持事务和外键。  17.什么是事务 事务和隔离级别详解及实际应用  事务是对数据库中一系列操作进行统一的回滚或者提交的操作,主要用来保证数据的完整性和一致性。  18.事务的四大特性(ACID) 原子性(Atomicity): 要么全部成功要么全部失败。 一致性(Consistency): 事务执行前和事务执行后,原本和数据库一致的数据仍然一致。 隔离性(Isolation): 事务与事务之间互不干扰。 持久性(Durability): 事务一旦被提交了,那么对数据库中的数据的改变就是永久的。  19.脏读、不可重复读、幻读 脏读: 也叫"读未提交",顾名思义,就是某一事务A读取到了事务B未提交的数据。 不可重复读: 在一个事务内,多次读取同一个数据,却返回了不同的结果。实际上,这是因为在该事务间隔读取数据的期间,有其他事务对这段数据进行了修改,并且已经提交,就会发生不可重复读事故。 幻读: 在同一个事务中,第一次读取到结果集和第二次读取到的结果集不同。像幻觉一样所以叫幻读。 从上面可以看出脏读和不可重复读是基于数据值的错误,幻读是基于条数增加或者减少的错误  20.事务的隔离级别? ① read uncommited(读取未提交内容): 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。读取未提交的数据,也被称之为脏读(Dirty Read) ② read committed(读取提交内容): 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。一个事务只能看见已经提交事务所做的改变。可解决脏读 ③ repeatable read(可重读): 这是MySQL的默认事务隔离级别,同一事务的多个实例在并发读取数据时,会看到同样的数据。不过理论上,这会导致另一个棘手的问题:幻读(Phantom Read)。可解决脏读、不可重复读 ④ serializable(可串行化) : 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。可解决脏读、不可重复读、幻读。  21.怎么优化数据库 ①SQL优化 ②加缓存 ③分表 ④读写分离  22.SQL优化 ①不要用select *,要使用具体字段。 ②使用数值代替字符串,比如:0=唱,1=跳,2=rap。 ③避免返回大量数据,采用分页最好。 ④使用索引,提升查询速度,不宜建太多索引,不能建在重复数据比较多的字段上。 ⑤批量插入比单条插入要快,因为事务只需要开启一次,数据量太小体现不了。 ⑥避免子查询,优化为多表连接查询。 ⑦尽量使用union all替代union,因为union会自动去重。  23.常用的聚合函数 ①sum(列名) 求和      ②max(列名) 最大值      ③min(列名) 最小值      ④avg(列名) 平均值      ⑤first(列名) 第一条记录 ⑥last(列名) 最后一条记录 ⑦count(列名) 统计记录数不包含null值 count(*)包含null值。  24.几种关联查询 内连接(inner join): 查询两个表匹配数据。 左连接(left join): 查询左表全部行以及右表匹配的行。 右连接(right join): 查询右表全部行以及左表匹配的行。  25.in和exists的区别 in(): 适合子表(子查询)比主表数据小的情况。 exists(): 适合子表(子查询)比主表数据大的情况。  26.drop、truncate、delete的区别 速度: drop > truncate > delete。 回滚: delete支持,truncate和drop不支持。 删除内容: delete表结构还在,删除部分或全部数据,不释放空间。truncate表结构还在,删除全部数据,释放空间。drop表结构和数据不在,包括索引和权限,释放空间。 ———————————————— 版权声明:本文为CSDN博主「爱穿背带裤的馫」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/twotwo22222/article/details/129115194 
  • [技术干货] 【JAVA】Object类与抽象类-转载
     Object类 Object类是java中默认提供的一个类。java里面除了Object类,其他全部的类都是会默认继承Object类的。所以所有的类的对象都可以用Object类的引用来接收。 举个栗子: class Person{    } class Student{     } public class Test {     public static void function(Object obj) {     System.out.println(obj); }     public static void main(String[] args) {         function(new Person());         function(new Student());     }      } 在java开发的过程中,Objec类是参数的最高统一类型。但是Object类也存在有定义好的一些方法。  这些都是Object类定义好的方法: 这几个方法对于java学习来说是必须掌握,不可缺少的。 获取对象信息toString()方法 如果要打印对象中的内容,直接重写Object类的toSting方法即可: // Object类中的toString()方法实现:     public String toString() {         return getClass().getName() + "@" + Integer.toHexString(hashCode());     } 其中的道理就是:object类是所有类的父类,重写了tostring方法后,在使用print方法时就会调用我们重写的toString方法以达到效果。 对象比较equals方法 在java中,==进行比较时: 要是两个比较对象是基本类型时,比较的是变量值是否相等 要是两个比较对象是引用类型变量时,比较的是引用地址是否相等 如果要比较对象中的内容,就必须重写Object类的equals方法,equals方法默认是按地址来比较的 // Object类中的equals方法     public boolean equals(Object obj) {         return (this == obj); // 使用引用中的地址直接来进行比较     } class Person{     private String name ;     private int age ;     public Person(String name, int age) {         this.age = age ;         this.name = name ;     } } public class Test {     public static void main(String[] args) {         Person p1 = new Person("qeo", 20) ;         Person p2 = new Person("qeo", 20) ;         int a = 10;         int b = 10;         System.out.println(a == b); // 输出true         System.out.println(p1 == p2); // 输出false         System.out.println(p1.equals(p2)); // 输出false     } } 上面比较的是地址,想要比较他们的内容就要重写equals方法:  class Person{ ...     @Override     public boolean equals(Object obj) {         if (obj == null) {             return false ;         }          if(this == obj) {             return true ;         } // 不是Person类对象         if (!(obj instanceof Person)) {             return false ;         } Person person = (Person) obj ; // 向下转型,比较属性值         return this.name.equals(person.name) && this.age==person.age ;     } } 所以要比较对象的内容是不是相同的时候,就一定要重写equals方法   hashcode方法 通过观看toString方法的源码我们发现了里面有一个hashcode方法:      public String toString() {         return getClass().getName() + "@" + Integer.toHexString(hashCode());     } 它的作用就是帮助我们算一个具体的对象位置,它的结果是一个内存地址然后会调用toHexString方法,将这个地址以16进制来打印输出。  hashcode方法源码:   public native int hashCode(); 它是一个native方法,底层是用c/c++代码写的,在java中我们是看不到的。  在我们的认知中,两个名字相同,年龄相同的对象存储在同一个位置,但是不重写hashcode方法的话,结果是不同的:  class Person {     public String name;     public int age;     public Person(String name, int age) {         this.name = name;         this.age = age;     } } public class TestDemo4 {     public static void main(String[] args) {         Person per1 = new Person("go", 20) ;         Person per2 = new Person("go", 20) ;         System.out.println(per1.hashCode());         System.out.println(per2.hashCode());     } } //执行结果         460141958         1163157884 这时我们再重写hashcode方法:  class Person {     public String name;     public int age;     public Person(String name, int age) {         this.name = name;         this.age = age;     } @Override     public int hashCode() {         return Objects.hash(name, age);     } } public class TestDemo4 {     public static void main(String[] args) {         Person per1 = new Person("go", 20) ;         Person per2 = new Person("go", 20) ;         System.out.println(per1.hashCode());         System.out.println(per2.hashCode());     } } //执行结果         460141958         460141958 这时发现他们的哈希值就是一样的了。  结论:  hashcode方法用来确定对象在内存中储存的位置相不相同。  hashcode在散列表中才有用,在散列表中hashcode的作用是获取对象的散列码,来确定对象在散列表中的位置。  内部类 当一个事物的内部,还有一部分需要一个完整的结构进行描述,而这个内部的完整的结构只要为外部的事物提供服务,那这个内部结构使用内部类。  在Java中,可以将一个类定义在另一个类或者方法的内部,前者称为内部类,后者称为外部类。  public class OutClass {     class InnerClass{     } } // OutClass是外部类 // InnerClass是内部类 注意:  定义在class类名{}的外部的,即便在一个文件夹内,都不是内部类  public class A{ } class B{ } // A 和 B是两个独立的类,彼此之前没有关系 内部类和外部类共用同一个Java源文件,但是经过编译后,内部类会形成单独的字节码文件  内部类的分类 根据内部类在类中位置不同,可以分为以下几种:  成员内部类:未被static修饰的实例内部类和被static修饰的静态内部类  局部内部类(不谈修饰符)  匿名内部类  public class OutClass {     // 成员位置定义:未被static修饰 --->实例内部类     public class InnerClass1{     } // 成员位置定义:被static修饰 ---> 静态内部类     static class InnerClass2{     }     public void method(){ // 方法中也可以定义内部类 ---> 局部内部类:几乎不用         class InnerClass5{         }     } } 实例内部类 未被static修饰的内部类:  public class OutClass {     private int a;     static int b;     int c;       public void methodA() {         a = 10;         System.out.println(a);     }       public static void methodB() {         System.out.println(b);     } //实例内部类:未被static修饰       class InnerClass {         int c;           public void methodInner() { // 在实例内部类中可以直接访问外部类中:任意访问限定符修饰的成员             a = 100;             b = 200;             methodA();             methodB(); // 如果外部类和实例内部类中具有相同名称成员时,优先访问的是内部类自己的             c = 300;             System.out.println(c); // 如果要访问外部类同名成员时候,必须:外部类名称.this.同名成员名字             OutClass.this.c = 400;             System.out.println(OutClass.this.c);         }     }       public static void main(String[] args) { // 外部类:对象创建 以及 成员访问         OutClass outClass = new OutClass();         System.out.println(outClass.a);         System.out.println(OutClass.b);         System.out.println(outClass.c);         outClass.methodA();         outClass.methodB();         System.out.println("=============实例内部类的访问============="); // 要访问实例内部类中成员,必须要创建实例内部类的对象 // 而普通内部类定义与外部类成员定义位置相同,因此创建实例内部类对象时必须借助外部类 // 创建实例内部类对象         OutClass.InnerClass innerClass1 = new OutClass().new InnerClass(); // 上述语法比较怪异,也可以先将外部类对象先创建出来,然后再创建实例内部类对象         OutClass.InnerClass innerClass2 = outClass.new InnerClass();         innerClass2.methodInner();     } } 注意:  1. 外部类中的任何成员都可以在实力内部类中直接访问    2. 实例内部类所处的位置与外部类成员位置相同,因此也受public、private等访问限定符的约束  3. 在实例内部类方法中访问同名的成员时,优先访问自己的,如果要访问外部类同名的成员,必须:外部类名称.this.同名成员 来访问  4. 实例内部类对象必须在先有外部类对象前提下才能创建  5. 实例内部类的非静态方法中包含了一个指向外部类对象的引用   6. 外部类中,不能直接访问实例内部类中的成员,如果要访问必须先要创建内部类的对象。   静态内部类 被static修饰的内部类就称为静态内部类  public class OutClass {     private int a;     static int b;     public void methodA(){         a = 10;         System.out.println(a);     }     public static void methodB(){         System.out.println(b);     } // 静态内部类:被static修饰的成员内部类     static class InnerClass{         public void methodInner(){ // 在内部类中只能访问外部类的静态成员 // a = 100; // 编译失败,因为a不是类成员变量             b =200; // methodA(); // 编译失败,因为methodA()不是类成员方法             methodB();         }     }     public static void main(String[] args) { // 静态内部类对象创建 & 成员访问         OutClass.InnerClass innerClass = new OutClass.InnerClass();         innerClass.methodInner();     } } 注意:  在静态内部类中只能访问外部类中的静态成员   创建静态内部类对象时,不需要先创建外部类对象   局部内部类  定义在外部类的方法体或者{}中,这种内部类只能在其定义的位置使用,一般使用的非常少:  public class OutClass {     int a = 10;     public void method(){         int b = 10; // 局部内部类:定义在方法体内部 // 不能被public、static等访问限定符修饰         class InnerClass{             public void methodInnerClass(){                 System.out.println(a);                 System.out.println(b);             }         } // 只能在该方法体内部使用,其他位置都不能用         InnerClass innerClass = new InnerClass();         innerClass.methodInnerClass();     }     public static void main(String[] args) { // OutClass.InnerClass innerClass = null; 编译失败     } } 注意:  1. 局部内部类只能在所定义的方法体内部使用  2. 不能被public、static等修饰符修饰  3. 编译器也有自己独立的字节码文件,命名格式:外部类名字$数字内部类名字.class  4. 几乎不会使用   匿名内部类  这个内部类是没有名字的:  interface IA {     void test(); }     public class Test {     public static void main(String[] args) {         IA a = new IA() {             @Override             public void test() {                 System.out.println("重写的方法");             }         };         a.test();     }  ———————————————— 版权声明:本文为CSDN博主「paper jie」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/paperjie/article/details/132684042 
  • [技术干货] 使用HttpURLConnection发送POST请求并携带请求参数-转载
     1、先创建URL对象,指定请求的URL地址。 URL url = new URL("http://example.com/api"); 2、调用URL对象的openConnection()方法创建HttpURLConnection对象。 HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 3、设置请求方法为POST。 connection.setRequestMethod("POST"); 4、设置请求头,包括Content-Type、Content-Length等。其中Content-Type表示请求体的格式,Content-Length表示请求体的长度。 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); connection.setRequestProperty("Content-Length", String.valueOf(param.getBytes().length)); 5、设置连接超时和读取超时时间。 connection.setConnectTimeout(5000); connection.setReadTimeout(5000); 6、允许向服务器写入写出数据。 connection.setDoOutput(true);   connection.setDoInput(true); 7、获取输出流,向服务器写入数据。 OutputStream outputStream = connection.getOutputStream(); outputStream.write(param.getBytes()); outputStream.flush(); outputStream.close();  这里的param是请求参数,需要将其转换为字节数组后写入输出流。  8、获取响应码,判断请求是否成功。 int statusCode = connection.getResponseCode(); 9、读取响应数据。 InputStream inputStream = statusCode == 200 ? connection.getInputStream() : connection.getErrorStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) {     response.append(line); } reader.close(); inputStream.close();  这里的response是响应数据,需要将其读取为字符串后使用。 完整的示例代码如下所示:  String param = "name=张三&age=18"; URL url = new URL("http://example.com/api"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); connection.setRequestProperty("Content-Length", String.valueOf(param.getBytes().length)); connection.setConnectTimeout(5000); connection.setReadTimeout(5000); connection.setDoOutput(true); OutputStream outputStream = connection.getOutputStream(); outputStream.write(param.getBytes()); outputStream.flush(); outputStream.close(); int statusCode = connection.getResponseCode();   InputStream inputStream = statusCode == 200 ? connection.getInputStream() : connection.getErrorStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) {    response.append(line); } reader.close(); inputStream.close(); connection.disconnect(); System.out.println(response.toString());  需要注意的是,以上示例代码中的请求参数是以字符串形式传递的,如果需要传递复杂的请求参数,可以考虑使用JSON等格式。同时,如果请求的URL需要携带查询参数,可以在URL中添加查询参数。  下面使用HttpURLConnection 发送POST 请求 参数类型是json  下面是使用HttpURLConnection微信小程序发送订阅消息的一个例子  POST请求  json组装成了一个JSONObject  json类似是这样的  {   "touser": "OPENID",   "template_id": "TEMPLATE_ID",   "page": "index",   "data": {       "name01": {           "value": "某某"       },       "amount01": {           "value": "¥100"       },       "thing01": {           "value": "广州至北京"       } ,       "date01": {           "value": "2018-01-01"       }   } }    try {               URL url = new URL(" https://api.weixin.qq.com/cgi-bin/message/subscribe/send?" +                     "access_token=" +                     "自己的小程序token");               HttpURLConnection connection = (HttpURLConnection) url.openConnection();             connection.setRequestMethod("POST");             connection.setRequestProperty("Content-Type", "application/json");               connection.setDoOutput(true);             connection.setDoInput(true);   //构造发送给用户的订阅消息内容             Map messageContent = new HashMap<String, Object>();             messageContent.put("character_string1", new HashMap<String, Object>() {{                 put("value", "a123456789");             }});             messageContent.put("amount2", new HashMap<String, Object>() {{                 put("value", "1元");             }});             messageContent.put("thing3", new HashMap<String, Object>() {{                 put("value", "西安大学长安学区");             }});             messageContent.put("time4", new HashMap<String, Object>() {{                 put("value", "2021年10月20日");             }});             messageContent.put("thing5", new HashMap<String, Object>() {{                 put("value", "这是备注");             }});             JSONObject messageContentJson = new JSONObject(messageContent);               //构造订阅消息             Map subscribeMessage = new HashMap<String, Object>();             subscribeMessage.put("touser", " 。。。");//填写你的接收者openid             subscribeMessage.put("template_id", " 填写你的模板ID");//填写你的模板ID             subscribeMessage.put("data", messageContentJson);             JSONObject subscribeMessageJson = new JSONObject(subscribeMessage); /*             String s = subscribeMessageJson.toJSONString();             System.out.println("JSONString:" + s); */             String s1 = subscribeMessageJson.toString();             System.out.println("String:" + s1);             byte[] bytes = s1.getBytes();               DataOutputStream wr = new DataOutputStream(connection.getOutputStream());             wr.write(bytes);             wr.close();               int statusCode = connection.getResponseCode();               InputStream inputStream = statusCode == 200 ? connection.getInputStream() : connection.getErrorStream();             BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));             StringBuilder response = new StringBuilder();             String line;             while ((line = reader.readLine()) != null) {                 response.append(line);             }             reader.close();             inputStream.close();             connection.disconnect();             System.out.println(response.toString());               connection.disconnect();           } catch (Exception e) {             e.printStackTrace();         } ———————————————— 版权声明:本文为CSDN博主「ImisLi」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/ImisLi/article/details/129946651 
  • [技术干货] 三路排序算法(Java 实例代码)-转载
     三路排序算法 一、概念及其介绍 三路快速排序是双路快速排序的进一步改进版本,三路排序算法把排序的数据分为三部分,分别为小于 v,等于 v,大于 v,v 为标定值,这样三部分的数据中,等于 v 的数据在下次递归中不再需要排序,小于 v 和大于 v 的数据也不会出现某一个特别多的情况),通过此方式三路快速排序算法的性能更优。 二、适用说明 时间和空间复杂度同随机化快速排序。  三路快速排序算法是使用三路划分策略对数组进行划分,对处理大量重复元素的数组非常有效提高快速排序的过程。它添加处理等于划分元素值的逻辑,将所有等于划分元素的值集中在一起。  三、过程图示  图片看转载链接哈~我们分三种情况进行讨论 partiton 过程,i 表示遍历的当前索引位置: (1)当前处理的元素 e=V,元素 e 直接纳入蓝色区间,同时i向后移一位。 (2)当前处理元素 e<v,e 和等于 V 区间的第一个位置数值进行交换,同时索引 lt 和 i 都向后移动一位(3)当前处理元素 e>v,e 和 gt-1 索引位置的数值进行交换,同时 gt 索引向前移动一位。 最后当 i=gt 时,结束遍历,同时需要把 v 和索引 lt 指向的数值进行交换,这样这个排序过程就完成了,然后对 <V 和 >V 的数组部分用同样的方法再进行递归排序。  四、Java 实例代码 源码包下载: Download https://www.runoob.com/wp-content/uploads/2020/09/runoob-algorithm-QuickSort3Ways.zip  QuickSort3Ways.java 文件代码: package runoob;  /**  * 三路快速排序  */ public class QuickSort3Ways {     //核心代码---开始     // 递归使用快速排序,对arr[l...r]的范围进行排序     private static void sort(Comparable[] arr, int l, int r){         if (l >= r) {             return;         }         // 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot         swap( arr, l, (int)(Math.random()*(r-l+1)) + l );         Comparable v = arr[l];         int lt = l;     // arr[l+1...lt] < v         int gt = r + 1; // arr[gt...r] > v         int i = l+1;    // arr[lt+1...i) == v         while( i < gt ){             if( arr[i].compareTo(v) < 0 ){                 swap( arr, i, lt+1);                 i ++;                 lt ++;             }             else if( arr[i].compareTo(v) > 0 ){                 swap( arr, i, gt-1);                 gt --;             }             else{ // arr[i] == v                 i ++;             }         }         swap( arr, l, lt );         sort(arr, l, lt-1);         sort(arr, gt, r);     }     //核心代码---结束      public static void sort(Comparable[] arr){          int n = arr.length;         sort(arr, 0, n-1);     }      private static void swap(Object[] arr, int i, int j) {         Object t = arr[i];         arr[i] = arr[j];         arr[j] = t;     }      // 测试 QuickSort3Ways     public static void main(String[] args) {          // 三路快速排序算法也是一个O(nlogn)复杂度的算法         // 可以在1秒之内轻松处理100万数量级的数据         int N = 1000000;         Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);         sort(arr);         SortTestHelper.printArray(arr);     } } ———————————————— 版权声明:本文为CSDN博主「彼岸的菜鸟」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/2301_78835635/article/details/132144215 
  • [技术干货] 【Java 基础篇】Java 迭代器详解-转载
     导言 在 Java 中,迭代器是一种常用的设计模式,用于遍历集合中的元素。它提供了一种统一的方式来访问集合中的元素,而不必暴露集合的内部实现细节。本文将介绍 Java 迭代器的概念、使用方法和常见技巧,并提供一些示例代码。 一、迭代器的概念 迭代器是一种对象,它允许按顺序访问集合中的元素,而不需要知道集合的底层结构。通过使用迭代器,我们可以遍历集合并访问其中的元素,而无需关心集合的具体实现方式。 Java 提供了 Iterator 接口作为迭代器的基础接口。该接口定义了一组用于访问集合元素的方法,包括 hasNext、next 和 remove 等。  二、使用迭代器 要使用迭代器遍历集合,我们需要进行以下步骤:  获取集合的迭代器对象:通过调用集合的 iterator 方法获取迭代器对象。例如,对于 ArrayList 集合,可以使用 iterator() 方法获取迭代器对象。 List<String> list = new ArrayList<>(); // 添加元素到集合中  Iterator<String> iterator = list.iterator(); 遍历集合元素:通过使用迭代器的 hasNext 和 next 方法来遍历集合中的元素。hasNext 方法用于检查是否还有下一个元素,next 方法用于获取下一个元素的值。 while (iterator.hasNext()) {     String element = iterator.next();     // 处理元素 } 可选操作:迭代器还提供了 remove 方法,用于从集合中删除当前迭代的元素。需要注意的是,该方法只能在调用 next 方法后才能调用,且每次只能调用一次。 iterator.remove(); 三、迭代器的优势 使用迭代器遍历集合具有以下优势:  抽象集合的实现:通过使用迭代器,我们可以在不了解集合内部实现的情况下遍历和访问集合元素。这样,集合的实现细节对外部代码是透明的。  安全性:迭代器提供了一种安全的方式来遍历集合。它通过维护迭代器的状态来保证在遍历过程中不会出现并发修改的问题。  通用性:迭代器是一种通用的设计模式,在 Java 中被广泛应用于各种集合类型。无论是数组、列表、集合还是映射,我们都可以使用迭代器来遍历和访问元素。  四、迭代器的常见技巧 除了基本的使用方法外,还有一些常见的技巧可以帮助我们更好地使用迭代器。  1. 使用增强的 for 循环 Java 提供了增强的 for 循环(foreach 循环),可以简化迭代器的使用。它可以直接遍历集合中的元素,而不需要显式地使用迭代器。  for (String element : list) {     // 处理元素 } 2. 遍历过程中的修改 在使用迭代器遍历集合时,如果需要在遍历过程中修改集合,应使用迭代器的 remove 方法,而不是直接操作集合。直接操作集合可能会导致并发修改异常。  Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) {     String element = iterator.next();     if (shouldRemove(element)) {         iterator.remove(); // 使用迭代器删除元素     } } 3. 避免重复创建迭代器 在迭代器的使用过程中,应避免在每次迭代时都创建新的迭代器对象。如果需要多次遍历集合,可以在第一次遍历时创建迭代器,并在后续的遍历中重复使用该迭代器。  Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) {     String element = iterator.next();     // 处理元素 }  // 再次使用同一个迭代器进行遍历 while (iterator.hasNext()) {     String element = iterator.next();     // 处理元素 } 4. 使用迭代器的限制功能 迭代器提供了一些限制功能,如只读迭代器和单向迭代器。如果在遍历过程中不需要修改集合或只需要向前遍历,可以使用只读或单向迭代器,以提高性能和安全性。  List<String> list = new ArrayList<>(); // 添加元素到集合中  Iterator<String> readOnlyIterator = list.iterator(); Iterator<String> forwardIterator = list.listIterator(); 五、示例代码 下面是一个使用迭代器遍历集合并打印元素的示例代码:  List<String> list = new ArrayList<>(); list.add("Apple"); list.add("Banana"); list.add("Orange");  Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) {     String element = iterator.next();     System.out.println(element); } 在上面的示例中,我们创建了一个 ArrayList 集合,并使用迭代器遍历集合中的元素,然后打印每个元素的值。  总结 迭代器是一种在 Java 中常用的设计模式,用于遍历集合中的元素。通过使用迭代器,我们可以统一访问集合元素,而不需要了解集合的具体实现。本文介绍了迭代器的概念、使用方法和常见技巧,并提供了示例代码。  希望本文对你理解和使用 Java 迭代器提供了帮助。如果你有任何问题或建议,请随时留言。 ———————————————— 版权声明:本文为CSDN博主「繁依Fanyi」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_21484461/article/details/131424468 
  • [技术干货] 【JAVA】多态的概念与实际利用-转载
     前言 在面向对象(OOP)的程序设计语言中,多态与封装、继承合称为OOP的三大特性。在今天,我们就来学习一下JAVA中的多态是什么样子的。、 多态 指一个对象在不同情况下可以表现出不同的行为。Java多态性分为两种:编译时多态性(静态多态性)和运行时多态性(动态多态性)。 编译时多态性:也称为静态多态性,是指在编译期间就能确定方法的调用方式、参数类型及返回值类型等,主要通过方法重载实现。 运行时多态性:也称为动态多态性,是指在运行期间才能确定方法的调用方式,主要通过方法重写实现。Java中实现运行时多态性的关键是继承和方法重写。 具体来说,当一个类的子类重新定义了一个或多个已在父类中定义的方法时,那么子类的对象将可以调用重新定义的方法,而不是调用父类中的方法。这种现象称为方法重写。通过父类的引用变量来引用一个子类的对象时,父类引用变量不能直接调用子类中重新定义的方法,而是要调用子类中的方法,这种多态性称为动态多态性。 多态实现的重要条件 Java多态实现的重要条件包括: 继承:必须有继承关系,子类必须继承父类。 覆盖:子类必须重写父类的方法。 向上转型:可以声明一个父类引用类型的变量,将它指向一个子类对象。通过这种方式调用方法,就可以实现多态。 动态绑定:在运行时而不是编译时进行方法调用。这样就能够根据实际调用的对象类型来决定调用哪个方法,实现多态。 虚函数 在Java中,所有方法都是虚函数,因为它们都是在运行时动态绑定的。当子类继承父类时,它可以覆盖父类的方法,但具体调用哪个方法取决于对象的实际类型而不是变量的声明类型。因此,可以在运行时动态处理对象的多态性。 使用关键字“override”来覆盖父类的方法。子类中的方法必须与父类中被覆盖的方法具有相同的名称、参数列表和返回类型。这样,当调用子类中的方法时,根据对象的类型来确定应该调用哪个方法。 实例 假设我们有一个Animal类和一个Cat类,Cat类是Animal类的子类。 Animal类中定义了一个虚方法makeSound(),它会输出动物发出的声音: public class Animal {     public void makeSound() {         System.out.println("The animal makes a sound");     } } Cat类继承了Animal类并覆盖了makeSound()方法: public class Cat extends Animal {     public void makeSound() {         System.out.println("The cat meows");     } } 现在,我们可以实例化一个Animal对象和一个Cat对象,并调用它们makeSound()方法: Animal animal = new Animal(); Cat cat = new Cat(); animal.makeSound(); // 输出 "The animal makes a sound" cat.makeSound(); // 输出 "The cat meows" 注意,由于Cat类继承了Animal类,因此可以使用Cat对象来代替Animal对象,因为它们都是Animal类的实例。这就是面向对象编程中的多态性。 多态的实现方式 方式一:重写: 这个内容已经在上一篇文章中我有详细的讲过Java 重写(Override)与重载(Overload) 方式二:接口 生活中的接口最具代表性的就是插座,例如一个三接头的插头都能接在三孔插座中,因为这个是每个国家都有各自规定的接口规则,有可能到国外就不行,那是因为国外自己定义的接口类型。 java中的接口类似于生活中的接口,就是一些方法特征的集合,但没有方法的实现。具体可以看 java接口 这一章节的内容。 方式三:抽象类和抽象方法 ———————————————— 版权声明:本文为CSDN博主「许思王」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_73602725/article/details/133038845 
  • [技术干货] Java:不支持发行版本5-转载
     错误 Java:不支持发行版本5 详细错误 同学在github上找到一个微服务项目(基于maven进行构建),进行二开,导入项目运行控制台报错 Java:不支持发行版本5,笔者修改项目结构(F i l e FileFile→ \rightarrow→P r o j e c t S t r u c t u r e Project StructureProjectStructure)以及设置(F i l e FileFile→ \rightarrow→S e t t i n g s SettingsSettings)后,依旧报错 解决方案 对于pom.xml依赖文件加入如下配置 <properties>     <maven.compiler.source>1.8</maven.compiler.source>     <maven.compiler.target>1.8</maven.compiler.target> </properties> 报错原因 报错提示 “Java:不支持发行版本5” 表明项目使用了 Java 5 的发行版本,但当前环境不支持该版本。 解决原因 通过将项目的编译器源码和目标版本设置为 1.8,确保项目在 Java 8 环境下进行编译和运行。这样可以与当前环境的 Java 版本保持一致,避免报错。 笔者在此对pom.xml依赖文件加入如下配置进行详细解释 <properties>     <maven.compiler.source>1.8</maven.compiler.source>     <maven.compiler.target>1.8</maven.compiler.target> </properties> <properties> </properties> 标签用于定义项目的属性。在上述解决方案中,我们向 标签添加了两个属性: <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>:指定项目源代码的 Java 版本。在此处,我们将其设置为 1.8,表示使用 Java 8 的语法和特性编写源代码。 <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>:指定编译生成的字节码的 Java 版本。同样地,我们将其设置为 1.8,以确保编译后的代码与 Java 8 兼容。 通过将这两个属性设置为 1.8,我们告诉 Maven 使用 Java 8 的编译器进行编译,并生成与 Java 8 兼容的字节码文件。这样可以解决报错问题,确保项目在 Java 8 环境下正确编译和运行。 参考文献 解决方案参考错误“ Java:不支持发行版本5”的正确解决方案 解决原因参考chatgpt 原创不易 转载请标明出处 如果对你有所帮助 别忘啦点赞支持哈 ———————————————— 版权声明:本文为CSDN博主「飞滕人生TYF」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/T_Y_F_/article/details/131317847 
  • [技术干货] html简洁漂亮的个人简历,个人主页,个人简介网页版(源码) -转载
     1.设计来源 主旨: 简洁,干净,明亮,舒适的风格; 语言: 可以html+js+css,也可以html+css; 版块: 人信息,基本资料,专业技能,教育经历(支持更多扩展); 适用: 个人简介,个人简历,个人主页,个人博客,个人空间等方向; 1.1 主界面 主界面动态的朦胧天空背景图,可以配置自己的图标,个人名言,名字,职称,邮箱,手机号,工作地址等信息,也可以扩展新的。 1.2 基本资料 基本资料动态的朦胧天空背景图,可以配置个人职业技能,大图标,姓名,年龄,性别,手机号,邮箱,qq号,居住地,户籍,学历,学校,专业等信息,也可以扩展新的。 1.3 专业技能 专业技能动态的科技线路背景图,可以配置个人专业技能,按列表排序出来,个人的掌握的工具,及熟练度等信息,也可以扩展新的。 1.4 教育经历 教育经历动态的科技线路背景图,可以配置个人教育的学校,时间,几学到的东西等信息,也可以扩展新的。 1.5 工作经验 工作经验动态的科技线路背景图,可以配置个人工作过的公司,工作的时间,负责的项目,开发工具等信息,也可以扩展新的。 2.效果和源码 2.1 动态效果 下面咋们一起来看看这个个人简历的动态效果,改变图片,可以配置多种风格,灵活运用,效果酷炫。  html简洁漂亮的个人简历,个人主页,个人简介网页版  2.2 源代码 这里是主界面的代码,其他图片、js、css等代码,见下面的 源码下载 ,里面有所有代码资源和相关说明。  <!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8">     <meta name="viewport" content="width=device-width, initial-scale=1">     <title>徐纯宇 - 个人简历,个人主页,个人介绍,个人简介</title>     <link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">     <link rel="stylesheet" type="text/css" href="css/main.css"> </head> <body oncontextmenu="return false;" onselectstart="return false;" unselectable="on" ondragstart="return false;"> <aside>     <a class="cur_a"><span>徐纯宇</span></a>     <a><span>基本资料</span></a>     <a><span>专业技能</span></a>     <a><span>教育经历</span></a>     <a><span>工作经验</span></a> </aside>     <section class="page_one" id="page1">     <div class="cen_con">         <div class="portrait">             <img onmousemove="this.src='images/user_c.jpg'" onmouseout="this.src='images/user.jpg'"/ src="images/user.jpg">         </div>         <div class="cen_text">             <h2>业精于勤;荒于嬉;行成于思;毁于随。</h2>             <hr>             <h3>徐纯宇</h3>             <h3>全栈工程师</h3>             <h3> 1376174032@qq.com |  13300000000</h3>             <h3>在职北京 ▣ 海淀</h3>         </div>     </div>     <div class="down_arrow">         <a class="scroll"><span></span></a>      </div> </section> <section class="page_two" id="page2">     <div class="con_wrap">         <div class="tit_wrap">             <h1 style="font-weight: bold;">基本资料</h1>             <div class="scissors" style="border-top:1px dashed orange;">             </div>             <h2> &nbsp;&nbsp;&nbsp;&nbsp;毕业于北京理工大学的计算机应用专业。从业十年有余,熟悉C#和Java前后端开发,熟悉oracle和mysql数据库,熟悉Html和js及css前端开发,熟悉uniapp跨平台开发小程序和app。</h2>         </div>                 <div class="myinfo">             <table>                 <tbody>                     <tr>                         <td rowspan="6">                             <img src="images/user.jpg">                         </td>                         <td>姓名 | 徐纯宇</td>                         <td>手机 | 13300000000</td>                     </tr>                     <tr>                         <td>性别 | 男</td>                         <td>邮箱 | 1376174032@qq.com</td>                     </tr>                     <tr>                         <td>出生 | 1992.02.20</td>                         <td>QQ号 | 1376174032</td>                     </tr>                     <tr>                         <td>居住 | 北京市海淀区</td>                         <td>户籍 | 北京市海淀区</td>                     </tr>                     <tr>                         <td>学历 | 本科</td>                         <td>学校 | 北京理工大学</td>                     </tr>                     <tr>                         <td>专业 | 计算机应用</td>                         <td></td>                     </tr>                 </tbody>             </table>         </div>     </div>     <div class="down_arrow">         <a class="scroll"><span></span></a>      </div> </section> <section class="page_three" id="page3">     <div class="con_wrap">         <div class="tit_wrap">             <h1 style="font-weight: bold;">专业技能</h1>             <div class="scissors" style="border-top:1px dashed orange;">             </div>             <h2>长年累月后,精进又向前。唯有勤学练,擅长最优先。</h2>             </div>         <div class="skill_con">             <div class="canvas_wrap">                 <div class="canvas_con">                     <div class="text_con">                         <p class="percent">98%</p>                         <p class="chart_title">C#</p>                     </div>                     <canvas id="html5" width=160 height=160></canvas>                 </div>                 <div class="canvas_con">                     <div class="text_con">                         <p class="percent">93%</p>                         <p class="chart_title">JAVA</p>                     </div>                     <canvas id="css3" width=160 height=160></canvas>                 </div>                 <div class="canvas_con">                     <div class="text_con">                         <p class="percent">96%</p>                         <p class="chart_title">HTML</p>                     </div>                     <canvas id="js" width=160 height=160></canvas>                 </div>                 <div class="canvas_con">                     <div class="text_con">                         <p class="percent">99%</p>                         <p class="chart_title">ORACLE</p>                     </div>                     <canvas id="jq" width=160 height=160></canvas>                 </div>             </div>             <div class="text_wrap">                 <p>1.  熟练使用java,基于ssh能快速搭建系统框架。</p>                 <p>2.  熟练使用java,基于spring boot能快速搭建系统框架。</p>                 <p>3.  熟练使用HTML/CSS技术,精通js/jquery编程,能够熟练使用angularjs,vue,reactjs等前端框架。</p>                 <p>4.  熟悉oracle,mysql,sqlserver等各平台安装使用,熟练使用SQL语句增删改查,触发器,存储过程,索引,序列。</p>                 <p>5.  熟练的使用grunt,gulp等前端工具。</p>                 <p>6.  熟悉c#语言研发,基于asp.new mvc能快速搭建系统框架,新技术asp.net core做后台数据服务能快速搭建,跨平台使用。</p>                 <p>7.  熟悉c#语言研发,基于winform的c/s应用程序能快速搭建框架。</p>             </div>             </div>     </div>     <div class="down_arrow">         <a class="scroll"><span></span></a>      </div> </section> <section class="page_four" id="page4">     <div class="con_wrap">         <div class="tit_wrap">             <h1 style="font-weight: bold;">教育经历</h1>             <div class="scissors" style="border-top:1px dashed orange;">             </div>             <h2>学则智,不学则愚;学则治,不学则乱。自古圣贤,成大业,未有不由学而成者。</h2>         </div>           <div class="work_con">               <div class="programe">                 <div class="work_time">4年<br>北京理工大学</div>                 <div class="work_text">                       <div class="triangle-left"></div>                       <div class="exCon">                         <h4>学习时间:2008/06 -- 2012/06</h4>                         <p>学习技能:</p>                         <p>大学四年时间完成了所有的学业,并参与了很多项目研发工作,配合老师和同学攻关了很多难题。</p>                         <p> 1.掌握了办公五剑客,Word,PPT,excel,ps,viso。</p>                                <p> 2.掌握了C#和java的前后端开发,数据库基本应用。</p>                      </div>                 </div>              </div>             <div class="programe">                 <div class="work_time">1年<br>Java培训班</div>                 <div class="work_text">                       <div class="triangle-left"></div>                       <div class="exCon">                         <h4>学习时间:2011/05 -- 2012/04</h4>                         <p>学习技能:</p>                         <p>参加了一年的JAVA培训班,跟着老师一起做了多个项目。积累了很多经验,有了很多经历。</p>                         <p>1.掌握了springboot框架下的api开发,并开发了自己的一套数据接口。</p>                             <p>2.掌握了ssm框架下的前后端开发,并开发了自己的博客。</p>                           </div>                 </div>             </div>         </div>     </div>     <div class="down_arrow">         <a class="scroll"><span></span></a>      </div> </section> <section class="page_five" id="page5">     <div class="con_wrap">         <div class="tit_wrap">             <h1 style="font-weight: bold;">工作经验</h1>             <div class="scissors" style="border-top:1px dashed orange;">             </div>             <h2>生命,需要我们去努力。年轻时,我们要努力锻炼自己的能力,掌握知识、掌握技能、掌握必要的社会经验。</h2>         </div>           <div class="work_con">               <div class="programe">                 <div class="work_time">22个月<br>汽车之家官网</div>                 <div class="work_text">                       <div class="triangle-left"></div>                       <div class="exCon">                         <h4>开发时间:2019 /12--2021 /10</h4>                         <h5>开发工具:VS2015,VSCode,ORACLE</h5>                         <p>项目描述:</p>                         <p>参与与客户交流需求,参与项目整体开发,主要负责首页面各方面数据综合显示,系统采用预加载,通过wcf对硬件控制,实时更新,一些页面设计,数据库的设计,数据测试。</p>                         <p> 1.数据信息展示块,2.软硬件交互块,3.数据解析块。</p>                            </div>                 </div>              </div>             <div class="programe">                 <div class="work_time">8个月<br>联想后台研发</div>                 <div class="work_text">                       <div class="triangle-left"></div>                       <div class="exCon">                         <h4>开发时间:2021 /10--2022 /06</h4>                         <h5>开发工具:IDEA,VSCode,MYSQL</h5>                         <p>项目描述:</p>                         <p>参与与客户交流需求,参与项目整体开发,主要负责首页面各方面数据综合显示,系统采用预加载,实时更新,一些页面设计,数据库的设计,数据测试。 </p>                         <p>1.设备基础信息 2.部门信息 3.用户信息 4.数据报表 5.调试工具,6.数据监测,7.系统配置等7个大功能块 系统采用角色分配,操作分配等权限,实现各个角色的权限功能。精确划分各个部门的职责。</p>                           </div>                 </div>             </div>         </div>     </div> </section> </body> <script type="text/javascript" src="js/main.js"></script> </html> 
  • [技术干货] 整合Security 到SSM框架中 使用Spring Security下的BCryptPasswordEncoder进行密码加密
     一.Spring Security安全框架实现密码加密方法简述 1.首先:Spring Security提供了强大的加密工具PasswordEncoder,PasswordEncoder接口的代码如下:  package org.springframework.security.crypto.password;  public interface PasswordEncoder {     String encode(CharSequence var1);//是是对密码加密的方法      boolean matches(CharSequence var1, String var2);//是用来验证密码和加密后密码是否一致的如果一致则返回true } 2.其次:Spring Security提供了BCryptPasswordEncoder类,该类实现了Spring的PasswordEncoder接口,使用BCrypt强哈希方法来对密码进行加密,通过BCrypt强哈希方法每一次加密的结果都不一样:可以看看示例加密后的密码,需要说明的是这两种加密后的密码,其明文密码都是:123 3.加密的代码,可以看到每次加密产生的都是随机字符串:  public String encode(CharSequence rawPassword) {         String salt;         if (this.strength > 0) {             if (this.random != null) {                 salt = BCrypt.gensalt(this.strength, this.random);             } else {                 salt = BCrypt.gensalt(this.strength);             }         } else {             salt = BCrypt.gensalt();         }          return BCrypt.hashpw(rawPassword.toString(), salt);     } 二.加密的具体实现步骤 1.在配置文件中配置加密所需要的工具类  <!--配置加密工具类--> <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/> 2.在service的实现层中注入BCryptPasswordEncoder  @Autowired  private BCryptPasswordEncoder passwordEncoder;//通过注解拿到加密对象 3.然后在在service的实现层中的需要加密的操作方法中添加比如保存操作  //保存一个用户,使用加密算法把加密后的密码保存入数据库     @Override     public void save(SysUser sysUser) {         //二.使用安全框架加密方式         //1.Security安全框架加密操作:加密密码并存入sysUser对象中         String encode = passwordEncoder.encode(sysUser.getPassword());//拿到加密后的密码         sysUser.setPassword(encode);//将加密后的密码设置到sysUser对象中         userDao.save(sysUser);     } 4.以上配置结束你需要运行一下,添加一个用户(这个用户的密码是加密的),否则用之前没有加密的密码登录是会失败的 ;然后去除"{noop}"  //2.Security安全框架加密操作:去除+"{noop}"+————————————非常重要             User user = new User(sysUser.getUsername(), sysUser.getPassword(), authorities);             return user; 5.在配置文件中spring_security.xml 里面引用第一步配置的id  <!-- Security安全框架加密操作:引入加密操作--> <security:password-encoder ref="passwordEncoder"/> ———————————————— 原文链接:https://blog.csdn.net/weixin_43330884/article/details/104632267 
  • [技术干货] Spring Security:密码编码器PasswordEncoder介绍与Debug分析
    博主在之前已经介绍了Spring Security的用户UserDetails与用户服务UserDetailsService,本篇博客介绍Spring Security的密码编码器PasswordEncoder,它们是相互联系的,博主会带大家一步步深入理解Spring Security的实现原理,也会带来Spring Security的实战分享。  Spring Security:用户UserDetails源码与Debug分析 Spring Security:用户服务UserDetailsService源码分析 为什么是介绍而不是源码分析?博主虽然在研一上过密码学的课,但毕竟没有细致研究过密码学领域,因此不敢管中窥豹。再者,这些加密算法的实现原理、是否能抵御攻击、明文与密文(明文经过加密得到)的匹配方法以及时间成本等因素都不是学习Spring Security框架的核心内容,因此本篇博客只会简单介绍Spring Security的密码编码器PasswordEncoder及其实现类,以及密码编码器在Spring Security中的使用时机。  PasswordEncoder PasswordEncoder接口有很多实现类,也有被标记了@Deprecated注解的实现类,一般是该类表示的密码编码器(加密算法)不安全,比如可以在能接受的时间内被破解,比如彩虹表攻击。  这里不去分析每个密码编码器的实现原理,因为密码编码器的种类太多了,而且没有必要,密码编码器的主要作用无非就是对密码进行编码(加密),以及原始密码(客户端登录验证时输入的密码)与编码密码(正确原始密码通过密码编码器编码的结果)的正确匹配,因此密码编码器必定需要实现PasswordEncoder接口的两个方法,而其他方法的实现是服务于这两个方法。  package org.springframework.security.crypto.password;  // 首选实现是BCryptPasswordEncoder public interface PasswordEncoder {      /**      * 对原始密码进行编码      */     String encode(CharSequence rawPassword);      /**      * 验证从存储(比如数据库或者内存等)中获取的编码密码是否与需要验证的密码匹配       * 如果密码匹配,则返回 true,否则返回 false      * 存储的编码密码永远不会被解码      * 因此会将需要验证的密码进行编码,然后与编码密码进行匹配      */     boolean matches(CharSequence rawPassword, String encodedPassword);      /**      * 如果为了更好的安全性需要再次对编码的密码进行编码,则返回 true,否则返回 false      * 默认实现始终返回 false      */     default boolean upgradeEncoding(String encodedPassword) {         return false;     } } 很显然密码编码器的主要作用是为了编码与匹配,而有些加密算法需要经过多次迭代加密,因此也需要实现upgradeEncoding方法,比如BCryptPasswordEncoder类的实现(strength属性越大,需要做更多的工作来加密密码,默认值为10):      @Override     public boolean upgradeEncoding(String encodedPassword) {         if (encodedPassword == null || encodedPassword.length() == 0) {             logger.warn("Empty encoded password");             return false;         }          Matcher matcher = BCRYPT_PATTERN.matcher(encodedPassword);         if (!matcher.matches()) {             throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword);         }         else {             int strength = Integer.parseInt(matcher.group(2));             return strength < this.strength;         }     } PasswordEncoderFactories PasswordEncoderFactories类源码:  package org.springframework.security.crypto.factory;  import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;  import java.util.HashMap; import java.util.Map;  /**  * 用于创建PasswordEncoder实例  */ public class PasswordEncoderFactories {      /**      * 使用默认映射创建一个DelegatingPasswordEncoder      * 可能会添加其他映射,并且将更新编码以符合最佳实践      * 但是,由于DelegatingPasswordEncoder的性质,更新不应影响用户      */     @SuppressWarnings("deprecation")     public static PasswordEncoder createDelegatingPasswordEncoder() {         // 默认bcrypt         String encodingId = "bcrypt";         Map<String, PasswordEncoder> encoders = new HashMap<>();         encoders.put(encodingId, new BCryptPasswordEncoder());         encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());         encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());         encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));         encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());         encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());         encoders.put("scrypt", new SCryptPasswordEncoder());         encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));         encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));         encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());         encoders.put("argon2", new Argon2PasswordEncoder());          return new DelegatingPasswordEncoder(encodingId, encoders);     }      private PasswordEncoderFactories() {} } PasswordEncoderFactories类可以看作密码编码器工厂,它将已有的密码编码器存储在HashMap中,通过静态方法createDelegatingPasswordEncoder即可获取,该方法的返回值是一个DelegatingPasswordEncoder实例。  DelegatingPasswordEncoder DelegatingPasswordEncoder类源码:  public class DelegatingPasswordEncoder implements PasswordEncoder {     private static final String PREFIX = "{";     private static final String SUFFIX = "}";     private final String idForEncode;     private final PasswordEncoder passwordEncoderForEncode;     private final Map<String, PasswordEncoder> idToPasswordEncoder;     private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();      /**      * 创建一个新实例      * idForEncode:用于查找应使用哪个PasswordEncoder进行encode      * idToPasswordEncoder:id到PasswordEncoder的映射,用于确定应使用哪个PasswordEncoder进行matches      */     public DelegatingPasswordEncoder(String idForEncode,         Map<String, PasswordEncoder> idToPasswordEncoder) {         if (idForEncode == null) {             throw new IllegalArgumentException("idForEncode cannot be null");         }         if (!idToPasswordEncoder.containsKey(idForEncode)) {             throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);         }         for (String id : idToPasswordEncoder.keySet()) {             if (id == null) {                 continue;             }             // 如果id有'{'或者'}'字符则会出现问题,比如:             // 基于prefixEncodedPassword获取id,是根据'{'和'}'字符对第一次出现的位置来截取             // 以及去除{id}得到encodedPassword,是根据'}'字符第一次出现的位置来截取             if (id.contains(PREFIX)) {                 throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);             }             if (id.contains(SUFFIX)) {                 throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);             }         }         // 初始化         this.idForEncode = idForEncode;         this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);         this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);     }      /**      * 设置defaultPasswordEncoderForMatches,默认为UnmappedIdPasswordEncoder实例      */     public void setDefaultPasswordEncoderForMatches(         PasswordEncoder defaultPasswordEncoderForMatches) {         if (defaultPasswordEncoderForMatches == null) {             throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");         }         this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;     }      // 编码,{id}前缀拼接委托的PasswordEncoder的编码结果     @Override     public String encode(CharSequence rawPassword) {         return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);     }      // 匹配     @Override     public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {         if (rawPassword == null && prefixEncodedPassword == null) {             return true;         }         // 根据prefixEncodedPassword提取id         String id = extractId(prefixEncodedPassword);         // 根据id获取PasswordEncoder          PasswordEncoder delegate = this.idToPasswordEncoder.get(id);         // 是否有对应的PasswordEncoder          if (delegate == null) {             // 没有对应的PasswordEncoder             // 则使用defaultPasswordEncoderForMatches进行匹配             return this.defaultPasswordEncoderForMatches                 .matches(rawPassword, prefixEncodedPassword);         }         // 有对应的PasswordEncoder         // 提取encodedPassword,即去掉{id}前缀         String encodedPassword = extractEncodedPassword(prefixEncodedPassword);         // 返回匹配结果         return delegate.matches(rawPassword, encodedPassword);     }      // 提取id     private String extractId(String prefixEncodedPassword) {         if (prefixEncodedPassword == null) {             return null;         }         // 第一个'{'字符的位置         int start = prefixEncodedPassword.indexOf(PREFIX);         if (start != 0) {             return null;         }         // 从start开始的第一个'}'字符的位置         int end = prefixEncodedPassword.indexOf(SUFFIX, start);         if (end < 0) {             return null;         }         // 截取得到id         return prefixEncodedPassword.substring(start + 1, end);     }      @Override     public boolean upgradeEncoding(String prefixEncodedPassword) {         // 提取id         String id = extractId(prefixEncodedPassword);         // id与idForEncode属性不匹配,则返回true         if (!this.idForEncode.equalsIgnoreCase(id)) {             return true;         }         else {             // 提取encodedPassword              String encodedPassword = extractEncodedPassword(prefixEncodedPassword);             // 根据id获取PasswordEncoder             // 返回该PasswordEncoder的upgradeEncoding方法基于encodedPassword的返回值             return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);         }     }          // 提取encodedPassword     private String extractEncodedPassword(String prefixEncodedPassword) {         // 第一个'}'字符的位置         int start = prefixEncodedPassword.indexOf(SUFFIX);         // 截取得到encodedPassword         return prefixEncodedPassword.substring(start + 1);     }      /**      * 引发异常的默认PasswordEncoder      */     private class UnmappedIdPasswordEncoder implements PasswordEncoder {          @Override         public String encode(CharSequence rawPassword) {             throw new UnsupportedOperationException("encode is not supported");         }          @Override         public boolean matches(CharSequence rawPassword,             String prefixEncodedPassword) {             String id = extractId(prefixEncodedPassword);             throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");         }     } } DelegatingPasswordEncoder是基于前缀标识符并委托给另一个PasswordEncoder的密码编码器,可以使用PasswordEncoderFactories类创建一个DelegatingPasswordEncoder实例,也可以创建自定义的DelegatingPasswordEncoder实例。  密码存储格式为 {id}encodedPassword(prefixEncodedPassword),id是用于查找应该使用哪个PasswordEncoder的标识符,encodedPassword是使用PasswordEncoder对原始密码进行编码的结果,id必须在密码的开头,以{开头,}结尾。 如果找不到id,则id将为空。 例如,以下可能是使用不同id(PasswordEncoder)编码的密码列表:  {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG {noop}password {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 第一个密码的id为bcrypt,encodePassword为$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG,匹配时,将委托给BCryptPasswordEncoder。第二个密码的id为noop,encodePassword为password。匹配时,将委托给NoOpPasswordEncoder。以此类推。  传递给构造函数的idForEncode确定将使用哪个PasswordEncoder来编码原始密码。匹配是基于id和构造函数中提供的idToPasswordEncoder来完成的。matches方法可能使用带有未映射id(包括空id)的密码,调用matches方法将抛出IllegalArgumentException异常, 可以使用setDefaultPasswordEncoderForMatches方法自定义此行为,即设置defaultPasswordEncoderForMatches属性,当根据id获取不到PasswordEncoder时使用。 Debug分析 依赖:  <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">     <modelVersion>4.0.0</modelVersion>      <groupId>com.kaven</groupId>     <artifactId>security</artifactId>     <version>1.0-SNAPSHOT</version>      <parent>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-parent</artifactId>         <version>2.3.1.RELEASE</version>     </parent>      <properties>         <maven.compiler.source>8</maven.compiler.source>         <maven.compiler.target>8</maven.compiler.target>     </properties>      <dependencies>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-web</artifactId>         </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-security</artifactId>         </dependency>         <dependency>             <groupId>org.projectlombok</groupId>             <artifactId>lombok</artifactId>         </dependency>     </dependencies> </project>  接口:  @RestController public class MessageController {      @GetMapping("/message")     public String getMessage() {         return "hello kaven, this is security";     } } 启动类:  @SpringBootApplication public class Application {     public static void main(String[] args) {         SpringApplication.run(Application.class);     } }  Debug启动应用,访问接口,会被重定向到默认登录页。  使用Spring Security自动创建的用户(用户名为user,密码在启动日志中)进行登录验证。 用户验证时Spring Security会使用DelegatingPasswordEncoder类的matches方法进行密码匹配,提取的id为noop(prefixEncodedPassword有{noop}前缀,应用启动时,如果没有用户与用户源的相关配置,Spring Security会创建一个默认用户,即一个UserDetails实例,该实例的密码就是prefixEncodedPassword),因此委托给NoOpPasswordEncoder进行密码匹配。 NoOpPasswordEncoder类的matches方法只是简单的字符串匹配,上图的rawPassword和encodedPassword很显然是匹配的。      public boolean matches(CharSequence rawPassword, String encodedPassword) {         return rawPassword.toString().equals(encodedPassword);     } 验证成功。 配置PasswordEncoder 增加配置:  package com.kaven.security.config;  import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder;  @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter {           // 重写验证处理的配置     @Override     protected void configure(AuthenticationManagerBuilder auth) throws Exception {         auth.userDetailsService(new UserDetailsServiceImpl()).passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());     }      // 自定义的用户服务     public static class UserDetailsServiceImpl implements UserDetailsService {                  // 使用PasswordEncoderFactories工厂创建DelegatingPasswordEncoder实例作为该用户服务的密码编码器         private static final PasswordEncoder PASSWORD_ENCODER = PasswordEncoderFactories.createDelegatingPasswordEncoder();          @Override         public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {             // TODO 查找数据库             // 使用密码编码器对原始密码进行编码             String encodedPassword = PASSWORD_ENCODER.encode("itkaven");             // 默认存在该用户名的用户,并且原始密码都为itkaven,角色都为USER和ADMIN             return User.withUsername(username).password(encodedPassword).roles("USER", "ADMIN").build();         }     } } Debug启动应用,访问接口,然后进行验证登录,由于自定义的用户服务默认任意用户名的用户都存在,并且原始密码都为itkaven,角色都为USER和ADMIN,因此登录时用户名可任意,但密码必须为itkaven才能通过验证。  客户端进行验证登录时,Spring Security通过用户服务加载匹配用户名的UserDetails实例,而博主自定义的用户服务直接默认该UserDetails实例存在,并且设置默认的密码(编码后的密码,使用UserDetailsServiceImpl类中的PASSWORD_ENCODER进行编码)与角色(权限)。密码匹配使用在重写验证处理的配置时指定的密码编码器来完成(passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()))。  id为bcrypt,使用BCryptPasswordEncoder进行密码匹配,因为通过自定义的用户服务加载的UserDetails实例的密码就是DelegatingPasswordEncoder的{bcrypt}前缀与BCryptPasswordEncoder对原始密码(itkaven)的编码的拼接,最后会返回true。  所以,密码编码器在用户验证时用于密码的匹配,以及创建UserDetails实例时对密码进行编码(可选,如果是基于用户服务加载的UserDetails实例创建的新实例,新实例一般不更改该UserDetails实例的密码,因此,通过用户服务加载的UserDetails实例的密码应该是编码后的密码),因此密码的编码与匹配过程需要使用相同的密码编码器,不然一样的原始密码也有可能匹配不成功。  Spring Security的密码编码器PasswordEncoder的介绍与Debug分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。 ———————————————— 原文链接:https://blog.csdn.net/qq_37960603/article/details/122318269 
  • [技术干货] BCryptPasswordEncoder的使用
    在 Spring Security 中有一个加密的类 BCryptPasswordEncoder ,它的使用非常的简单而且也比较有趣。让我们来看看它的使用。 BCryptPasswordEncoder 的使用         首先创建一个 SpringBoot 的项目,在创建项目的时候添加 Spring Security 的依赖。然后我们添加一个测试类,写如下的代码: final private String password = "123456"; @Test public 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..lUnysXoWag6RvMkyC5SE6 encode2:$2a$10$0sjBLlwrrch2EjgYls197e9dGRCMbQ7KUIt/ODPTSU0W.mEPaGkfG         从上面的输出可以看出,同一个明文加密两次,却输出了不同的结果。是不是很神奇?但是这样有一个问题,如果使用 BCryptPasswordEncoder 去加密登录密码的话,还能进行验证么?当然是可以验证的。验证的话,使用的是 BCryptPasswordEncoder 的 matches 方法,代码如下。  final private String password = "123456";   @Test public 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.EndfS matches1:true encode2:$2a$10$NyGEOsQ1Hxv2gvYRmaEENueORlVDtSqoB/fHN76KkvQDeg7fbTy22 matches2:true        可以看到两次加密后的字符串虽然不同,但是通过 matches 方法都可以匹配出它们是 “123456” 这个明文加密的结果。同样很神奇,这是为什么呢?  encode 和 matches 方法的原理        我们通过源码来看看它们的原理吧。         首先我们将依赖的源码下载到本地,方便我们进行调试。开始我使用 IDEA 进行调试时是没有源码的,后来下载了源码,发现没有源码时调试的是 Class 文件,IDEA 提供了 Class 文件与源码等价的反编译代码。而源码逻辑性更好一些。         首先在 encode 代码处下断点,然后我们单步步入,去查看 encode 的实现,代码如下:  @Override public 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 rounds   if (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 的源码,同样单步进入,代码如下:  @Override public 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 
总条数:2294 到第
上滑加载中