1 Star 1 Fork 0

深夜无法入眠的程序猿 / reggie

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

瑞吉外卖项目——不会知识点总结

1、spring核心包里面的Md5密码工具类--DigestUtils

import org.springframework.util.DigestUtils;

String md5Password = DigestUtils.md5DigestAsHex(user.getPassword().getBytes());

关于Spring中MD5加密工具类的使用 MD5加密的三种形式

  1. 普通的md5进行加密

  2. md5+salt(盐)的形式加密

  3. 密码加密

实际开发中如何使用MD5

同一个字符串产生的md5是一样的,但是在项目的使用中为了保证密码等隐私的安全性,我们通常使用md5+盐或者是密码加密的形式

具体使用 1.通过spring中工具类产生的无盐md5

import org.springframework.util.DigestUtils;
 String pws = DigestUtils.md5DigestAsHex("abd".getBytes());
 System.out.println(pws); 
 //4911e516e5aa21d327512e0c8b197616

2.通过spring中工具类产生的加盐md5 通过加盐的形式可以使每次产生的密码不一样

 // 手动加盐 salt
String s = RandomStringUtils.randomAlphanumeric(10);
String pw = "123"+s;
String s1 = DigestUtils.md5DigestAsHex(pw.getBytes());
System.out.println(s1); 
 // 1.caf48c8d204092e86a290f3f4ad8fcf6  	  2.8e14ee50f31636231fe59002da0ec648

3.通过spring中工具类自己对密码进行加密

import com.heima.utils.common.BCrypt;
String gensalt = BCrypt.gensalt();//这个是盐  29个字符,随机生成
System.out.println(gensalt); //1.$2a$10$Pi.o0Qs2v3tcCsHkD9Kgb. 2.$2a$10$Q3Og2gyFbdfWvStJTfCdtu
//密码是123456
String password = BCrypt.hashpw("123456", gensalt);  //根据盐对密码进行加密
System.out.println(password);//加密后的字符串前29位就是盐 //$2a$10$Pi.o0Qs2v3tcCsHkD9Kgb.AYaNt/yg6Y8KsAsz79s1KrUwhh8w/0u 2.$2a$10$Q3Og2gyFbdfWvStJTfCdtuOc572y/TlXFvzWiVcYRJJfnpQ5W23L.

4.校验密码的正确性

/**
   * plaintext: 自己输入的密码
   * hashed: 通过密码加密产生的密文
   * 通过为true,不通过为false
*/
boolean checkpw = BCrypt.checkpw("123456", "$2a$10$abRa2gRztbu9dPKBY/nBXOTUP7i0gmzz6qLmiuIYb2LHUU2CsofXa");
System.out.println(checkpw); //true

2、路径匹配器,支持通配符——AntPathMatcher

import org.springframework.util.AntPathMatcher;
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

Ant匹配模式是当下最火的对URL进行匹配的模式。很多框架都采用AntURL进行匹配,如Spring框架中的org.springframework.util.AntPathMatcher

Ant基础通配符简介

通配符 介绍 示例说明
? 匹配1个字符 模板:/jd/a?c
匹配示例:/jd/abc
不匹配示例1:/jd/ac
不匹配示例2:/jd/axyzc
* 匹配>=0个字符 模板:/jd/a*c
匹配示例1:/jd/ac
匹配示例2:/jd/abc
匹配示例3:/jd/axyzc
不匹配示例1:/jd/a
不匹配示例2:/jd/xc
不匹配示例3:/jk/ac
** 匹配>=0个目录 示例1:/** /abc.html匹配所有以abc.html结尾的路径
示例2:/abc/** 匹配所有以/abc/打头的路径
示例3:abc** 匹配所有以abc打头的路径
示例4:/xyz/**/qwer匹配所有以/xyz/打头并以/qwer结尾的路径
。。。 。。。 。。。

AntPathMatcher常用方法介绍及示例

常用的构造方法 示例

// 无参构造,默认以 "/" 作为路径分隔符
AntPathMatcher antPathMatcher = new AntPathMatcher();
// 也可以指定路径分隔符
antPathMatcher = new AntPathMatcher("-");
extractUriTemplateVariables(String pattern, String path):根据pattern的规则,从path中抽取对应的变量值

当pattern与path不匹配时,会报错

示例一

AntPathMatcher antPathMatcher = new AntPathMatcher();
Map<String, String> map = antPathMatcher.extractUriTemplateVariables("/root/{a}/{b}/{c}/{d}.html", "/root/aa/bb/cc/xyz.html");
map.forEach((k, v) -> System.out.println(k + "=" + v));

输出

a=aa
b=bb
c=cc
d=xyz

示例二

AntPathMatcher antPathMatcher = new AntPathMatcher();
Map<String, String> map = antPathMatcher.extractUriTemplateVariables("/root/{a}/{b}/{c}", "/root/aa/bb/cc/xyz.html");
map.forEach((k, v) -> System.out.println(k + "=" + v));

输出

Exception in thread "main" java.lang.IllegalStateException: Pattern "/root/{a}/{b}/{c}" is not a match for "/root/aa/bb/cc/xyz.html"
	at org.springframework.util.AntPathMatcher.extractUriTemplateVariables(AntPathMatcher.java:517)
	at com.example.springbootdemo.MainApplication.main(MainApplication.java:17)
isPattern(String str):判断str是否可以作为一个pattern匹配器

注:当str是一个不带任何通配符(如:?、*、**、{}等)的字符串,即:str是一个准确的字符串时。此方法返回false,该方法认为:既然pattern是一个不带通配符的明确字符串,那么你直接和要匹配的path进行相等比较即可

示例一

 AntPathMatcher antPathMatcher = new AntPathMatcher();
 boolean isPattern = antPathMatcher.isPattern("/aa/bb/cc/xyz.html");
 System.out.println(isPattern);

输出

false

示例二

 AntPathMatcher antPathMatcher = new AntPathMatcher();
 boolean isPattern = antPathMatcher.isPattern("/aa/*/cc/xyz.html");
 System.out.println(isPattern);

输出

true

示例三

 AntPathMatcher antPathMatcher = new AntPathMatcher();
 boolean isPattern = antPathMatcher.isPattern("/aa/**/xyz.html");
 System.out.println(isPattern);

输出

true

示例四

AntPathMatcher antPathMatcher = new AntPathMatcher();
 boolean isPattern = antPathMatcher.isPattern("/aa/?/xyz.html");
 System.out.println(isPattern);

输出

true

示例五

 AntPathMatcher antPathMatcher = new AntPathMatcher();
 boolean isPattern = antPathMatcher.isPattern("/aa/{qwer}/xyz.html");
 System.out.println(isPattern);

输出

true

match(String pattern, String path):path是否完全匹配pattern

示例一(示例统配符?)

 AntPathMatcher antPathMatcher = new AntPathMatcher();
 // 输出:true
 System.out.println(antPathMatcher.match("/jd/a?c", "/jd/abc"));
 // 输出:false
 System.out.println(antPathMatcher.match("/jd/a?c", "/jd/ac"));
 // 输出:false
 System.out.println(antPathMatcher.match("/jd/a?c", "/jd/axyzc"));

示例二(示例统配符*)

 AntPathMatcher antPathMatcher = new AntPathMatcher();
 // 输出:true
 System.out.println(antPathMatcher.match("/jd/a*c", "/jd/ac"));
 // 输出:true
 System.out.println(antPathMatcher.match("/jd/a*c", "/jd/abc"));
 // 输出:true
 System.out.println(antPathMatcher.match("/jd/a*c", "/jd/axyzc"));
 // 输出:false
 System.out.println(antPathMatcher.match("/jd/a*c", "/jd/a"));
 // 输出:false
 System.out.println(antPathMatcher.match("/jd/a*c", "/jd/xc"));
 // 输出:false
 System.out.println(antPathMatcher.match("/jd/a*c", "/jk/ac"));

示例三(示例统配符**)

 AntPathMatcher antPathMatcher = new AntPathMatcher();
 //true
 //false
 //false
 System.out.println(antPathMatcher.match("**", ""));
 System.out.println(antPathMatcher.match("**", "/"));
 System.out.println(antPathMatcher.match("**", ""));

 //false
 //true
 //true
 System.out.println(antPathMatcher.match("/**", ""));
 System.out.println(antPathMatcher.match("/**", "/"));
 System.out.println(antPathMatcher.match("/**", ""));

 //true
 //false
 //false
 System.out.println(antPathMatcher.match("**/", ""));
 System.out.println(antPathMatcher.match("**/", "/"));
 System.out.println(antPathMatcher.match("**/", ""));

 //false
 //true
 //true
 System.out.println(antPathMatcher.match("/**/", ""));
 System.out.println(antPathMatcher.match("/**/", "/"));
 System.out.println(antPathMatcher.match("/**/", ""));

 //true
 //true
 //true
 //true
 //false
 //false
 System.out.println(antPathMatcher.match("/**/abc.html", "/abc.html"));
 System.out.println(antPathMatcher.match("/**/abc.html", "//abc.html"));
 System.out.println(antPathMatcher.match("/**/abc.html", "/1/2/3/4/5/abc.html"));
 System.out.println(antPathMatcher.match("/**/abc.html", "/a/b/c//jd/ac/abc.html"));
 System.out.println(antPathMatcher.match("/**/abc.html", "/a/abc"));
 System.out.println(antPathMatcher.match("/**/abc.html", "/a/bc.html"));

示例四(示例占位符{})

 AntPathMatcher antPathMatcher = new AntPathMatcher();
 //true
 //false
 //false
 System.out.println(antPathMatcher.match("a/{b}/c", "a/b0/c"));
 System.out.println(antPathMatcher.match("a/{b}/c", "a/b1/b2/b3/c"));
 System.out.println(antPathMatcher.match("a/{b}", "a/b1/b2/b3/c"));
matchStart(String pattern, String path):pattern的前面一部分是否就能匹配上整个path

注:若path与pattern完全匹配,那么也算:pattern的前面一部分匹配上了整个path

注:可理解为,path是pattern的一个子集

示例:

 AntPathMatcher antPathMatcher = new AntPathMatcher();
 //false
 //true
 //true
 System.out.println(antPathMatcher.matchStart("a/{b}", "a/b0/c"));
 System.out.println(antPathMatcher.matchStart("a/{b}/c", "a/b0/c"));
 System.out.println(antPathMatcher.matchStart("a/{b}/c/d", "a/b0/c"));

 //false
 //true
 //true
 System.out.println(antPathMatcher.matchStart("a/?", "a/b/c"));
 System.out.println(antPathMatcher.matchStart("a/?/c", "a/b/c"));
 System.out.println(antPathMatcher.matchStart("a/?/c/d", "a/b/c"));

 //false
 //true
 //true
 System.out.println(antPathMatcher.matchStart("a/*", "a/b0/c"));
 System.out.println(antPathMatcher.matchStart("a/*/c", "a/b0/c"));
 System.out.println(antPathMatcher.matchStart("a/*/c/d", "a/b0/c"));

 //true
 //true
 //true
 System.out.println(antPathMatcher.matchStart("a/**", "a/b0/c"));
 System.out.println(antPathMatcher.matchStart("a/**/c", "a/b0/c"));
 System.out.println(antPathMatcher.matchStart("a/**/c/d", "a/b0/c"));

3、MyBatis-Plus踩坑

建表之后,想使用MybatisPlus中的**@TableId(type = IdType.AUTO)** 使数据库中的id字段递增有序,而不是使用雪花算法。

问题:

先把application.yml中的mybatis-plus中的全局设置id的进行改变为:

mybatis-plus:
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    #例如:user_name--> userName
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      #递增
      id-type: auto
       #雪花算法
#      id-type: ASSIGN_ID

然后把数据库中的id设置为自动递增。

现在问题出现了,现在id自动递增的范围是一个雪花类型的变量,并不是从1,2,3,,,这样递增。

原因:

设置数据库id自动递增之前,使用了雪花算法生成id的策略,使数据库中存入了雪花算法的id。

解决方法:

先把表删除,重新建表,然后按照之前的步骤一步一步的设置递增即可完成1,2,3,4,5,6,,,这样形式的递增方式。

4、全局异常处理方法

@ControllerAdvice(annotations = {RestController.class,Controller.class}) @ExceptionHandler(SQLIntegrityConstraintViolationException.class)

例如:

//定义全局异常处理方法
@ControllerAdvice(annotations = {RestController.class,Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在" ;
            return R.error(msg);
        }
        return R.error("未知错误");
    }
}

@ControllerAdvice 的介绍及三种用法

首先,ControllerAdvice本质上是一个Component,因此也会被当成组建扫描。

然后,我们来看一下此类的注释:

这个类是为那些声明了(@ExceptionHandler、@InitBinder 或 @ModelAttribute注解修饰的)方法的类而提供的专业化的@Component , 以供多个 Controller类所共享。

说白了,就是aop思想的一种实现,你告诉我需要拦截规则,我帮你把他们拦下来,具体你想做更细致的拦截筛选和拦截之后的处理,你自己通过@ExceptionHandler、@InitBinder 或 @ModelAttribute这三个注解以及被其注解的方法来自定义。

初定义拦截规则:

ControllerAdvice 提供了多种指定Advice规则的定义方式,默认什么都不写,则是Advice所有Controller,当然你也可以通过下列的方式指定规则

比如对于 String[] value() default {} , 写成@ControllerAdvice("org.my.pkg") 或者 @ControllerAdvice(basePackages="org.my.pkg"), 则匹配org.my.pkg包及其子包下的所有Controller,

当然也可以用数组的形式指定,如:@ControllerAdvice(basePackages={"org.my.pkg", "org.my.other.pkg"}),

也可以通过指定注解来匹配,比如我自定了一个 @CustomAnnotation 注解,我想匹配所有被这个注解修饰的 Controller, 可以这么写:@ControllerAdvice(annotations={CustomAnnotation.class})

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {

	@AliasFor("basePackages")
	String[] value() default {};

	@AliasFor("value")
	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

	Class<?>[] assignableTypes() default {};

	Class<? extends Annotation>[] annotations() default {};

}

1、处理全局异常

@ControllerAdvice 配合 @ExceptionHandler 实现全局异常处理

用于在特定的处理器类、方法中处理异常的注解

接收Throwable类作为参数,我们知道Throwable是所有异常的父类,所以说,可以自行指定所有异常

比如在方法上加:@ExceptionHandler(IllegalArgumentException.class),则表明此方法处理

IllegalArgumentException 类型的异常,如果参数为空,将默认为方法参数列表中列出的任何异常(方法抛出什么异常都接得住)。

下面的例子:处理所有IllegalArgumentException异常,域中加入错误信息errorMessage 并返回错误页面error

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IllegalArgumentException.class)
    public ModelAndView handleException(IllegalArgumentException e){
        ModelAndView modelAndView = new ModelAndView("error");
        modelAndView.addObject("errorMessage", "参数不符合规范!");
        return modelAndView;
    }
}

@ControllerAdvice最常见的使用场景就是全局异常处理。比如文件上传大小限制的配置,如果用户上传的文件超过了限制大小,就会抛出异常,此时可以通过@ControllerAdvice结合@ExceptionHandler定义全局异常捕获机制,代码如下:

@ControllerAdvice
public class CustomExceptionHandler {
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public void uploadException(MaxUploadSizeExceededException e, HttpServletResponse resp) throws IOException {
        resp.setContentType("text/html;charset=utf-8");
        System.out.println(1111);
        PrintWriter out = resp.getWriter();
        out.write("上传文件大小超出限制!");
        out.flush();
        out.close();
    }
}

只需在系统中定义CustomExceptionHandler类,然后添加@ControllerAdvice注解即可。

当系统启动时,该类就会被扫描到Spring容器中,然后定义uploadException方法,在该方法上添加了@ExceptionHandler注解,其中定义的MaxUploadSizeExceededException.class 表明该方法用来处理MaxUploadSizeExceededException类型的异常。

如果想让该方法处理所有类型的异常,只需将MaxUploadSizeExceededException改为 Exception即可。

方法的参数可以有异常实例、HttpServletResponse以及HttpServletRequest、Model 等,返回值可以是一段JSON、一个ModelAndView、一个逻辑视图名等。此时,上传一个超大文件会有错误提示给用户。

如果返回参数是一个ModelAndView,假设使用的页面模板为Thymeleaf(注意添加Thymeleaf相关依赖),此时异常处理方法定义如下:

@ControllerAdvice
public class CustomExceptionHandler {
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ModelAndView uploadException(MaxUploadSizeExceededException e) throws IOException {
        ModelAndView mv = new ModelAndView();
        mv.addObject("msg", "上传文件大小超出限制! ");
        mv.setViewName("error");
        return mv;
    }
}

然后在resources/templates目录下创建error.html文件,内容如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title></head>
<body>
<div th:text="${msg}"></div>
</body>
</html>

2、预设全局数据

@ControllerAdvice 配合 @ModelAttribute 预设全局数据

/**
 * Annotation that binds a method parameter or method return value
 * to a named model attribute, exposed to a web view. Supported
 * for controller classes with {@link RequestMapping @RequestMapping}
 * methods.
 * 此注解用于绑定一个方法参数或者返回值到一个被命名的model属性中,暴露给web视图。支持在
 * 在Controller类中注有@RequestMapping的方法使用(这里有点拗口,不过结合下面的使用介绍
 * 你就会明白的)
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelAttribute {

	@AliasFor("name")
	String value() default "";

	@AliasFor("value")
	String name() default "";

	boolean binding() default true;

}

实际上这个注解的作用就是,允许你往 Model 中注入全局属性(可以供所有Controller中注有@Request Mapping的方法使用),valuename 用于指定 属性的 keybinding 表示是否绑定,默认为 true

具体使用方法如下:

  • 全局参数绑定

    • 方式一

      @ControllerAdvice
      public class MyGlobalHandler {
          @ModelAttribute
          public void presetParam(Model model){
              model.addAttribute("globalAttr","this is a global attribute");
          }
      }

      这种方式比较灵活,需要什么自己加就行了,加多少属性自己控制

    • 方式二

      @ControllerAdvice
      public class MyGlobalHandler {
      
          @ModelAttribute()
          public Map<String, String> presetParam(){
              Map<String, String> map = new HashMap<String, String>();
              map.put("key1", "value1");
              map.put("key2", "value2");
              map.put("key3", "value3");
              return map;
          }
      
      }

      这种方式对于加单个属性比较方便。默认会把返回值(如上面的map)作为属性的value,而对于key有两种指定方式:

      ​ 当 @ModelAttribute() 不传任何参数的时候,默认会把返回值的字符串值作为key,如上例的 key 则是 ”map"(值得注意的是,不支持字符串的返回值作为key)。 ​ 当 @ModelAttribute("myMap") 传参数的时候,则以参数值作为key,这里 key 则是 ”myMap“。

  • 全局参数使用

    @RestController
    public class AdviceController {
    
        @GetMapping("methodOne")
        public String methodOne(Model model){ 
            Map<String, Object> modelMap = model.asMap();
            return (String)modelMap.get("globalAttr");
        }
    
      
        @GetMapping("methodTwo")
        public String methodTwo(@ModelAttribute("globalAttr") String globalAttr){
            return globalAttr;
        }
    
    
        @GetMapping("methodThree")
        public String methodThree(ModelMap modelMap) {
            return (String) modelMap.get("globalAttr");
        }
        
    }
    

    这三种方式大同小异,其实都是都是从Model 中存储属性的 Map里取数据。

3、请求参数预处理

@ControllerAdvice 配合 @InitBinder 实现对请求参数的预处理

/**
 * Annotation that identifies methods which initialize the
 * {@link org.springframework.web.bind.WebDataBinder} which
 * will be used for populating command and form object arguments
 * of annotated handler methods.
 * 粗略翻译:此注解用于标记那些 (初始化[用于组装命令和表单对象参数的]WebDataBinder)的方法。
 * 原谅我的英语水平,翻译起来太拗口了,从句太多就用‘()、[]’分割一下便于阅读
 *
 * Init-binder methods must not have a return value; they are usually
 * declared as {@code void}.
 * 粗略翻译:初始化绑定的方法禁止有返回值,他们通常声明为 'void'
 *
 * <p>Typical arguments are {@link org.springframework.web.bind.WebDataBinder}
 * in combination with {@link org.springframework.web.context.request.WebRequest}
 * or {@link java.util.Locale}, allowing to register context-specific editors.
 * 粗略翻译:典型的参数是`WebDataBinder`,结合`WebRequest`或`Locale`使用,允许注册特定于上下文的编辑 
 * 器。
 * 
 * 总结如下:
 *  1. @InitBinder 标识的方法的参数通常是 WebDataBinder。
 *  2. @InitBinder 标识的方法,可以对 WebDataBinder 进行初始化。WebDataBinder 是 DataBinder 的一
 * 		           个子类,用于完成由表单字段到 JavaBean 属性的绑定。
 *  3. @InitBinder 标识的方法不能有返回值,必须声明为void。
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InitBinder {
	/**
	 * The names of command/form attributes and/or request parameters
	 * that this init-binder method is supposed to apply to.
	 * <p>Default is to apply to all command/form attributes and all request parameters
	 * processed by the annotated handler class. Specifying model attribute names or
	 * request parameter names here restricts the init-binder method to those specific
	 * attributes/parameters, with different init-binder methods typically applying to
	 * different groups of attributes or parameters.
	 * 粗略翻译:此init-binder方法应该应用于的命令/表单属性和/或请求参数的名称。默认是应用于所有命	   		* 令/表单属性和所有由带注释的处理类处理的请求参数。这里指定模型属性名或请求参数名将init-binder		 * 方法限制为那些特定的属性/参数,不同的init-binder方法通常应用于不同的属性或参数组。
	 * 我至己都理解不太理解这说的是啥呀,我们还是看例子吧
	 */
	String[] value() default {};
}

我们来看看具体用途,其实这些用途在 Controller里也可以定义,但是作用范围就只限当前Controller,因此下面的例子我们将结合 ControllerAdvice 作全局处理。

  • 参数处理
@ControllerAdvice
public class MyGlobalHandler {

    @InitBinder
    public void processParam(WebDataBinder dataBinder){

        /*
         * 创建一个字符串微调编辑器
         * 参数{boolean emptyAsNull}: 是否把空字符串("")视为 null
         */
        StringTrimmerEditor trimmerEditor = new StringTrimmerEditor(true);

        /*
         * 注册自定义编辑器
         * 接受两个参数{Class<?> requiredType, PropertyEditor propertyEditor}
         * requiredType:所需处理的类型
         * propertyEditor:属性编辑器,StringTrimmerEditor就是 propertyEditor的一个子类
         */
        dataBinder.registerCustomEditor(String.class, trimmerEditor);
        
        //同上,这里就不再一步一步讲解了
        binder.registerCustomEditor(Date.class,
                new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
    }
}

这样之后呢,就可以实现全局的实现对 ControllerRequestMapping标识的方法中的所有 StringDate类型的参数都会被作相应的处理。

Controller层:

@RestController
public class BinderTestController {

    @GetMapping("processParam")
    public Map<String, Object> test(String str, Date date) throws Exception {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("str", str);
        map.put("data", date);
        return  map;
    }
}

测试结果:

我们可以看出,strdate 这两个参数在进入 Controller 的test的方法之前已经被处理了,str 被去掉了两边的空格(%20 在Http url 中是空格的意思),String类型的 1997-1-10被转换成了Date类型。

  • 参数绑定
class Person {

    private String name;
    private Integer age;
    // omitted getters and setters.
}

class Book {

    private String name;
    private Double price;
    // omitted getters and setters.
}

@RestController
public class BinderTestController {

    @PostMapping("bindParam")
    public void test(Person person, Book book) throws Exception {
        System.out.println(person);
        System.out.println(book);
    }
}

我们会发现 Person类和 Book 类都有 name属性,那么这个时候就会出先问题,它可没有那么只能区分哪个name是哪个类的。因此 @InitBinder就派上用场了:

@ControllerAdvice
public class MyGlobalHandler {

	/*
     * @InitBinder("person") 对应找到@RequstMapping标识的方法参数中
     * 找参数名为person的参数。
     * 在进行参数绑定的时候,以‘p.’开头的都绑定到名为person的参数中。
     */
    @InitBinder("person")
    public void BindPerson(WebDataBinder dataBinder){
        dataBinder.setFieldDefaultPrefix("p.");
    }

    @InitBinder("book")
    public void BindBook(WebDataBinder dataBinder){
        dataBinder.setFieldDefaultPrefix("b.");
    }
}

因此,传入的同名信息就能对应绑定到相应的实体类中:

p.name -> Person.name b.name -> Book.name

还有一点注意的是如果 @InitBinder("value") 中的 value 值和 Controller 中 @RequestMapping() 标识的方法的参数名不匹配,则就会产生绑定失败的后果,如:

@InitBinder(“p”)、@InitBinder(“b”)

public void test(Person person, Book book)

上述情况就会出现绑定失败,有两种解决办法

第一中:统一名称,要么全叫p,要么全叫person,只要相同就行。

第二种:方法参数加 @ModelAttribute,有点类似@RequestParam

@InitBinder(“p”)、@InitBinder(“b”)

public void test(@ModelAttribute(“p”) Person person, @ModelAttribute(“b”) Book book)

5、private static final long serialVersionUID = 2611556444074013268L;

很显然这行代码的意思是将SerialVersionUID的值定义为一个常量,那这是干什么的呢?

解决这个问题,首先要了解包含SerialVersionUID的Serializable接口是什么?

**Serializable:**一个对象序列化的接口,一个类只有实现了Serializable接口,它的对象才能被序列化。Serializable是java.io包中定义的、用于实现Java类的序列化操作而提供的一个语义级别的接口。Serializable序列化接口没有任何方法或者字段,只是用于标识可序列化的语义。

实现了Serializable接口的类可以被ObjectOutputStream转换为字节流,同时也可以通过ObjectInputStream再将其解析为对象。例如,我们可以将序列化对象写入文件后,再次从文件中读取它并反序列化成对象,也就是说,可以使用表示对象及其数据的类型信息和字节在内存中重新创建对象。

下一个问题,什么是序列化?

序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。这两个过程结合起来,可以轻松地存储和传输数据。任何类型只要实现了Serializable接口,就可以被保存到文件中,或者作为数据流通过网络发送到别的地方。也可以用管道来传输到系统的其他程序中。

最后一步,为什么要定义这个serialVersionUID呢?

serialVersionUID叫做流标识符,即类的版本定义,作用是在序列化时保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性。jvm在反序列化的时候先去对比这个版本名字,如果数据流中的serialVersionUID和类中的serialVersionUID相同,才会进行反序列化,而不同的话就会抛出异常。一般来说,如果你对一个实现了serializable接口的类进行修改之后,需要修改这个版本信息。serialVersionUID可以显示声明也可以隐式声明。隐式声明是通过包名,类名等多个因素计算出来的,而显示声明就是通过赋值自己设置。

如果你不写private static final long serialVersionUID = 1L,在对这个类进行修改时,若你忘记修改serialVersionUID,版本上就会出现不兼容的问题,于是就会出现反序列化报错的情况

如果你显示定义了private static final long serialVersionUID = 1L,在对这个类进行修改时,若你忘记修改serialVersionUID,这个类也能被进行反序列化,它就会自动向上兼容版本,不会报错。

变量serialVersionUID称为序列化版本号,这个变量多用于实现了Serializable的类中,试用场景是类的序列化。 当我们没有定义这个变量的时候,虚拟机会根据类的属性算出一个独一无二的该变量值,在序列化的时候对该变量赋值,并随类一同序列化。

反序列化的时候,虚拟机同样会先读取该变量值,然后再当前读取的类中寻找同样的变量值,如果找到,那么反序列话成功,找不到即会报异常。

使用虚拟机默认计算的serialVersionUID就会有一个明显的劣势,那就是类一旦序列化后,我们就不能修改该类了,因为计算的serialVersionUID会改变,导致后期反序列话失败。

为了避免以上情况,我们手动定义一个静态常量来人为定义serialVersionUID,因为一旦手动定义了,虚拟机就不会再进行计算了,这也就是你上面写的那就话。

表单序列化serialize()

serialize()可以把表单中所有表单项的内容都获取到,并以name=value&name=value 的形式进行拼接。

6、分页查询——自己使用Vue进行编写

<template>
	<div class="pagination">
		<!-- 上 -->
		<button :disabled="pageNo == 1" @click="$emit('getPageNo', pageNo - 1)">
			上一页
		</button>
		<button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo', 1)" :class="{ active: pageNo == 1 }">
			1
		</button>
		<button v-if="startNumAndEndNum.start > 2">···</button>
		<!-- 中间部分 -->
		<button v-for="(page, index) in startNumAndEndNum.end" :key="index" v-if="page >= startNumAndEndNum.start"
			@click="$emit('getPageNo', page)" :class="{ active: pageNo == page }">
			{{ page }}
		</button>

		<!-- 下 -->
		<button v-if="startNumAndEndNum.end < totalPage - 1">···</button>
		<button v-if="startNumAndEndNum.end < totalPage" @click="$emit('getPageNo', totalPage)"
			:class="{active:pageNo==totalPage}">
			{{ totalPage }}
		</button>
		<button :disabled="pageNo == totalPage" @click="$emit('getPageNo', pageNo + 1)">
			下一页
		</button>

		<button style="margin-left: 30px">{{ total }}</button>
	</div>
</template>

<script>
	export default {
		name: "Pagination",
		props: ["pageNo", "pageSize", "total", "continues"],
		computed: {
			//总共多少页
			totalPage() {
				//向上取整
				return Math.ceil(this.total / this.pageSize);
			},
			//计算出连续的页码的起始数字与结束数字[连续页码的数字:至少是5]
			startNumAndEndNum() {
				// console.log(this)
				const {
					continues,
					pageNo,
					totalPage
				} = this;
				//先定义两个变量存储起始数字与结束数字
				let start = 0,
					end = 0;
				//连续页码数字5【就是至少五页】,如果出现不正常的现象【就是不够五页】
				//不正常现象【总页数没有连续页码多】
				if (continues > totalPage) {
					start = 1;
					end = totalPage;
				} else {
					//正常现象【连续页码5,但是你的总页数一定是大于5的】
					//起始数字
					start = pageNo - parseInt(continues / 2);
					//结束数字
					end = pageNo + parseInt(continues / 2);
					//把出现不正常的现象【start数字出现0|负数】纠正
					if (start < 1) {
						start = 1;
						end = continues;
					}
					//把出现不正常的现象[end数字大于总页码]纠正
					if (end > totalPage) {
						end = totalPage;
						start = totalPage - continues + 1;
					}
				}
				return {
					start,
					end
				};
			},
		},
	};
</script>

<style lang="less" scoped>
	.pagination {
		text-align: center;

		button {
			margin: 0 5px;
			background-color: #f4f4f5;
			color: #606266;
			outline: none;
			border-radius: 2px;
			padding: 0 4px;
			vertical-align: top;
			display: inline-block;
			font-size: 13px;
			min-width: 35.5px;
			height: 28px;
			line-height: 28px;
			cursor: pointer;
			box-sizing: border-box;
			text-align: center;
			border: 0;

			&[disabled] {
				color: #c0c4cc;
				cursor: not-allowed;
			}

			&.active {
				cursor: not-allowed;
				background-color: #409eff;
				color: #fff;
			}
		}
	}

	.active {
		background: skyblue;
	}
</style>

————————————————————————————————————————-——————————————
//别的组件调用分页组件
<Pagination :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="total"
						:continues="5" @getPageNo="getPageNo" />

7、Element-ui使用@keyup.enter.native的原因

@keyup.enter加.native有什么作用? W3C 标准中有如下规定: 即:当一个 form 元素中只有一个输入框时,在该输入框中按下回车应提交该表单。如果希望阻止这一默认行为,可以在 标签上添加 @submit.native.prevent。

使用Element-ui,因为Element-ui是封装组件,这个时候使用按键修饰符需要加上.native覆盖原有封装的keyup事件即可,可以理解为该修饰符的作用就是把一个vue组件转化为一个普通的HTML标签

<el-input v-model="account" placeholder="请输入账号" @keyup.enter.native="search()"></el-input>

8、Vue中的插槽

在本次项目中,看到这个语句后,想到了插槽,特此复习一下插槽。

在使用ElementUI中的el-table标签时,里面用到了<template slot-scope="scope">{{ scope.row.size }} 字节</template>这句话:

<template slot-scope="scope">
    {{ String(scope.row.status) === '0' ? '已禁用' : '正常' }}
</template>

Vue中的插槽:

  • 默认插槽

使用方法

//子组件:          
<div>
     <p>这里是子组件</p>
     <slot></slot>
</div>

//父组件:
<div>
    <!--引用子组件-->
    <myslot>
        <p>这里是父组件</p>//当想在父组件中渲染这一行数据时,必须要在引用的子组件中定义一个slot标签
    </myslot>
</div>
 
//最后显示内容:
这里是子组件
这里是父组件 

父组件想在引入的组件中显示

这里是父组件

,就必须先在子组件中用slot定义一个插槽,用来接收父组件传递的代码,再才能正常的在页面中渲染。
  • 具名插槽

顾名思义,具名插槽是在slot标签上有一个属性:name,可以将指定的代码放入指定的插槽中

//子组件 : (假设名为:ebutton)
<template>
  <div class= 'button'>
      <button>  </button>
      <slot name= 'one'> 这就是默认值1</slot>
      <slot name='two'> 这就是默认值2 </slot>
      <slot name='three'> 这就是默认值3 </slot>
  </div>
</template>


父组件通过v-slot : name 的方式添加内容:
//父组件:(引用子组件 ebutton)
<template>
  <div class= 'app'>
     <ebutton> 
        <template v-slot:one> 这是插入到one插槽的内容 </template>
        <template v-slot:two> 这是插入到two插槽的内容 </template>
        <template v-slot:three> 这是插入到three插槽的内容 </template>
     </ebutton>
  </div>
  • 作用域插槽
  • 首先在子组件的slot上动态绑定一个值( :key='value')
  • 然后在父组件通过v-slot : name = ‘values ’的方式将这个值赋值给 values
  • 最后通过{{ values.key }}的方式获取数据
<template>
  <div>
    <h2>效果一: 显示TODO列表时, 已完成的TODO为绿色</h2>
    <List :todos="todos">
        <!-- 书写template -->
        <template slot-scope="todo">
            <h5 :style="{color:todo.todo.isComplete?'green':'black'}">{{todo.todo.text}}</h5>
        </template>
    </List>
    
    <hr>
    <h2>效果二: 显示TODO列表时, 带序号, TODO的颜色为蓝绿搭配</h2>
    <List1 :data="todos">
         <template slot-scope="{row,index}">
               <h1 :style="{color:row.isComplete?'green':'hotpink'}">索引值{{index}}---------{{row.text}}</h1>
         </template>
    </List1>
  </div>
</template>

<script type="text/ecmascript-6">
  //子组件
  import List from './List'
  import List1 from './List1'
  export default {
    name: 'ScopeSlotTest',
    data () {
      return {
        todos: [
          {id: 1, text: 'AAA', isComplete: false},
          {id: 2, text: 'BBB', isComplete: true},
          {id: 3, text: 'CCC', isComplete: false},
          {id: 4, text: 'DDD', isComplete: false},
        ]
      }
    },

    components: {
      List,
      List1 
    }
  }
</script>
<template>
  <ul>
    <li v-for="(todo,index) in todos" :key="index">
       <!-- 坑:熊孩子挖到坑,父亲填坑 -->
       <!-- 数据来源于父亲:但是子组件决定不了结构与外网-->
       <slot :todo="todo"></slot>
    </li>
  </ul>
</template>

<script>
export default {
  name: 'List',
  props: {
    todos: Array
  }
}
</script>
<template>
  <ul>
    <li v-for="(todo,index) in data" :key="index">
       <!-- 坑:熊孩子挖到坑,父亲填坑 -->
       <!-- 数据来源于父亲:但是子组件决定不了结构与外网-->
       <slot :row="todo" :index="index "></slot>
    </li>
  </ul>
</template>

<script>
export default {
  name: 'List1',
  props: {
    data: Array
  }
}
</script>

9、JS 只能处理到16位数字,丢失精度

前端js处理超过16位的数字会出现精度丢失。

在js中number类型有个最大值(安全值),为9007199254740992,是2的53次方。如果超过这个值,那么js会出现不精确的问题。

前端代码:

// 把前端传递id,传递成String类型的,这样就不会造成精度的丢失。

//状态修改
statusHandle(row) {
     this.id = row.id
     this.status = row.status
     this.$confirm('确认调整该账号的状态?', '提示', {
          'confirmButtonText': '确定',
          'cancelButtonText': '取消',
          'type': 'warning'
      }).then(() => {
           enableOrDisableEmployee({'id': this.id, 'status': !this.status ? 1 : 0}).then(res => {
                 console.log('enableOrDisableEmployee', res)
                 if (String(res.code) === '1') {
                        this.$message.success('账号状态更改成功!')
                        this.handleQuery()
                  }
            }).catch(err => {
                  this.$message.error('请求出错了:' + err)
            })
       })
 },       

可以在服务端给页面响应JSON数据时进行处理,将long型数据统一转为String字符型

具体实现步骤:

1)提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换。

2)在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换。

代码详解:

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 		将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 		从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)    // 主要解决精度问题
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}





------------------------------------
在WebMvcConfig中调用该方法

    
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 扩展mvc框架的消息转换器
     * 默认的转换器一共有8个
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // log.info("扩展消息转换器...");
        //创建消息转换器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将Java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合中
        converters.add(0,messageConverter);
    }

}

详解:

依赖:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.11.1</version>
</dependency>
  1. jackson-annotations

  2. jackson-core

  3. 使用ObjectMapper读写

让我们从基本的读写操作开始。

ObjectMapper的简单readValue API是一个很好的切入点。我们可以使用它将 JSON 内容解析或反序列化为 Java 对象。

此外,在写入方面,我们可以使用 writeValue API 将任何 Java 对象序列化为 JSON 输出。

在本文中,我们将使用以下带有两个字段的Car类作为对象进行序列化或反序列化:

public class Car {

    private String color;
    private String type;

    // standard getters setters
}

3.1. Java 对象到 JSON

让我们看第一个使用ObjectMapper类的writeValue方法将 Java 对象序列化为 JSON 的示例:

ObjectMapper objectMapper = new ObjectMapper();
Car car = new Car("yellow", "renault");
objectMapper.writeValue(new File("target/car.json"), car);

文件中上述内容的输出将是:

{"color":"yellow","type":"renault"}

方法writeValueAsString和writeValueAsBytes的ObjectMapper类生成从Java对象的JSON,并返回所生成的JSON作为一个字符串或字节数组:

String carAsString = objectMapper.writeValueAsString(car);

3.2. JSON 到 Java 对象

String json = "{ \"color\" : \"Black\", \"type\" : \"BMW\" }";
Car car = objectMapper.readValue(json, Car.class);	

所述readValue()函数还接受其他形式的输入,诸如包含JSON字符串文件:

Car car = objectMapper.readValue(
new File("src/test/resources/json_car.json"), Car.class);

Car car = 
  objectMapper.readValue(
new URL("file:src/test/resources/json_car.json"), Car.class);

3.3. JSON 到 Jackson JsonNode

或者,可以将 JSON 解析为JsonNode对象并用于从特定节点检索数据:

String json = "{ \"color\" : \"Black\", \"type\" : \"FIAT\" }";
JsonNode jsonNode = objectMapper.readTree(json);
String color = jsonNode.get("color").asText();

3.4. 从 JSON 数组字符串创建 Java 列表

我们可以使用TypeReference将数组形式的 JSON 解析为 Java 对象列表:

String jsonCarArray = 
  "[{ \"color\" : \"Black\", \"type\" : \"BMW\" }, { \"color\" : \"Red\", \"type\" : \"FIAT\" }]";
List<Car> listCar = objectMapper.readValue(
jsonCarArray, new TypeReference<List<Car>>(){});

3.5. 从 JSON 字符串创建 Java Map

String json = "{ \"color\" : \"Black\", \"type\" : \"BMW\" }";
Map<String, Object> map 
  = objectMapper.readValue(json, new TypeReference<Map<String,Object>>(){});

4. 高级功能

Jackson 库的最大优势之一是高度可定制的序列化和反序列化过程。

在本节中,我们将介绍一些高级功能,其中输入或输出 JSON 响应可能与生成或使用响应的对象不同。

4.1. 配置序列化或反序列化功能

在将 JSON 对象转换为 Java 类时,如果 JSON 字符串有一些新字段,默认过程将导致异常

String jsonString 
  = "{ \"color\" : \"Black\", \"type\" : \"Fiat\", \"year\" : \"1970\" }";

上例中的 JSON 字符串在默认解析为类 Car的 Java 对象的过程中将导致UnrecognizedPropertyException异常。

通过configure方法,我们可以扩展默认流程来忽略新字段

objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Car car = objectMapper.readValue(jsonString, Car.class);

JsonNode jsonNodeRoot = objectMapper.readTree(jsonString);
JsonNode jsonNodeYear = jsonNodeRoot.get("year");
String year = jsonNodeYear.asText();

另一个选项 FAIL_ON_NULL_FOR_PRIMITIVES,它定义是否允许原始值的值:

objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);

类似地,**FAIL_ON_NUMBERS_FOR_ENUM 控制是否允许将枚举值序列化/反序列化为数字:

objectMapper.configure(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, false);

4.2. 创建自定义序列化程序或反序列化程序

ObjectMapper类的另一个基本特性是能够注册自定义序列化器反序列化器

自定义序列化器和反序列化器在输入或输出 JSON 响应的结构与必须对其进行序列化或反序列化的 Java 类不同的情况下非常有用。

以下是自定义 JSON 序列化程序的示例

public class CustomCarSerializer extends StdSerializer<Car> {
    
    public CustomCarSerializer() {
        this(null);
    }

    public CustomCarSerializer(Class<Car> t) {
        super(t);
    }

    @Override
    public void serialize(
      Car car, JsonGenerator jsonGenerator, SerializerProvider serializer) {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeStringField("car_brand", car.getType());
        jsonGenerator.writeEndObject();
    }
}

这个自定义序列化器可以这样调用:

ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("CustomCarSerializer", new Version(1, 0, 0, null, null, null));
module.addSerializer(Car.class, new CustomCarSerializer());
mapper.registerModule(module);

Car car = new Car("yellow", "renault");
String carJson = mapper.writeValueAsString(car);

这是自定义 JSON 反序列化器的示例:

public class CustomCarDeserializer extends StdDeserializer<Car> {
    
    public CustomCarDeserializer() {
        this(null);
    }

    public CustomCarDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public Car deserialize(JsonParser parser, DeserializationContext deserializer) {
        Car car = new Car();
        ObjectCodec codec = parser.getCodec();
        JsonNode node = codec.readTree(parser);
        
        // try catch block
        JsonNode colorNode = node.get("color");
        String color = colorNode.asText();
        car.setColor(color);
        return car;
    }
}

可以通过以下方式调用此自定义反序列化器:

String json = "{ \"color\" : \"Black\", \"type\" : \"BMW\" }";
ObjectMapper mapper = new ObjectMapper();
SimpleModule module =
  new SimpleModule("CustomCarDeserializer", new Version(1, 0, 0, null, null, null));
module.addDeserializer(Car.class, new CustomCarDeserializer());
mapper.registerModule(module);

Car car = mapper.readValue(json, Car.class);

4.3. 处理日期格式

java.util.Date的默认序列化会产生一个数字,即纪元时间戳(自 1970 年 1 月 1 日起的毫秒数,UTC)。但这不是人类可读的,需要进一步转换才能以人类可读的格式显示。

public class Request 
{
    private Car car;
    private Date datePurchased;

    // standard getters setters
}

要控制日期的字符串格式并将其设置为例如yyyy-MM-dd HH:mm az,请考虑以下代码段:

ObjectMapper objectMapper = new ObjectMapper();
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm a z");
objectMapper.setDateFormat(df);
String carAsString = objectMapper.writeValueAsString(request);
// output: {"car":{"color":"yellow","type":"renault"},"datePurchased":"2016-07-03 11:43 AM CEST"}

Spring全局JSON格式化时间(全局序列化日期时间)

方式1,在Spring配置文件中指定JSON序列化时间的格式

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

方式2,使用@JsonFormat

相信这种方式大家都知道,就是在传输的Bean对象属性上加上 @JsonFormat 注解

@Data
public class TestBO {
@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date date;

private LocalDateTime localDateTime;

@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyyMMdd")
private LocalDate localDate;

@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "HHmmss")
private LocalTime localTime;
}

方式3,配置全局格式化的类

直接上代码,可自己定义任何的类型,当然也可以使用网上说的那个@JsonComponent注解代替@Configuration

@Configuration
public class JacksonDateformatConfig {
    /**
     * Date格式化字符串
     */
    private static final String DATE_FORMAT = "yyyy-MM-dd";
    /**
     * DateTime格式化字符串
     */
    private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    /**
     * Time格式化字符串
     */
    private static final String TIME_FORMAT = "HH:mm:ss";
/**
 * 自定义Bean
 *
 * @return
 */
@Bean
@Primary
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
    return builder -> builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATETIME_FORMAT)))
        .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT)))
        .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_FORMAT)))
        .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATETIME_FORMAT)))
        .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT)))
        .deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(TIME_FORMAT)))
        .serializerByType(Date.class, new DateSerializer(true, new SimpleDateFormat(DATETIME_FORMAT))).deserializerByType(Date.class, new JsonDeserializer<String>() {
            @Override
            public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
                return p.getValueAsString();
            }
        }).deserializerByType(BigDecimal.class, new NumberDeserializers.BigDecimalDeserializer());
}

@JsonFormat 注解的优先级比较高,会以 @JsonFormat 注解的时间格式为主。

10、公共字段自动填充

Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。

实现步骤:

1、在实体类的属性上加入@TableField注解,指定自动填充的策略

2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口

具体详解看如下代码:

/**
 * @Description: 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * @Description: 插入操作,自动填充
     */

    @Override
    public void insertFill(MetaObject metaObject) {

        //log.info("公共字段填充insert"+metaObject.toString());
        //设置值
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", BaseContext.getCurrentId());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());

    }

    /**
     * @Description: 更新操作,自动填充
     */

    @Override
    public void updateFill(MetaObject metaObject) {

        //log.info("公共字段填充update"+metaObject.toString());

        metaObject.setValue("updateTime", LocalDateTime.now());

        metaObject.setValue("updateUser", BaseContext.getCurrentId());

    }
}


——————————————————————————————————————————————
BaseContext.java

/**
 * @Description: 基于TreadLocal封装工具类,用户保存和获取当前登录用户id
 */

public class BaseContext {
    public static ThreadLocal<Long> threadLocal = new ThreadLocal();

    /**
     * @Description: 设置值
     */
    
    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    /**
     * @Description: 得到值 
     */
    
    public static Long getCurrentId() {
        return threadLocal.get();
    }
}

说明:

MetaObjectHandler接口是mybatisPlus为我们提供的的一个扩展接口,我们可以利用这个接口在我们插入或者更新数据的时候,为一些字段指定默认值。实现这个需求的方法不止一种,在sql层面也可以做到,在建表的时候也可以指定默认值。

在实体类上加入**@tableField**注解

public class Role{
    @TableId(type = IdType.AUTO)
    private Integer id;

    private String roleName;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;
}

创建配置类实现MetaObjectHandler接口

@Log4j2
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("插入时自动填充...");
        this.strictInsertFill(metaObject,"createTime", LocalDateTime.class,LocalDateTime.now(ZoneId.of("Asia/Shanghai")));
    }
@Override
public void updateFill(MetaObject metaObject) {
    log.info("更新时自动填充...");
    this.strictInsertFill(metaObject,"updateTime", LocalDateTime.class,LocalDateTime.now(ZoneId.of("Asia/Shanghai")));
}
}

11、文件的上传、下载

文件上传介绍

文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。

文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。

文件上传时,对页面的form表单有如下要求:

method="post" 采用post方式提交数据

enctype="multipart/form-data" 采用multipart格式上传文件

type="file" 使用input的file控件上传

举例:

<form method="post" action="/common/upload" enctype="multipart/form-data">
  <input name="myFile" type="file" />
  <input type="submit" value="提交" /> 
</form>

文件上传和下载的代码:

/**
 * @Description: 文件上传和下载
 */

@RestController
@RequestMapping("/common")
public class CommonController {

    @Value("${reggie.path}")
    private String bathPath;
    /**
     * @Description: 文件上传
     */

    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        // file 是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
        // 原始文件名,abc.jpg
        String originnalFilename = file.getOriginalFilename();
        String suffix = originnalFilename.substring(originnalFilename.indexOf("."));
        // 使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
        String fileName = UUID.randomUUID().toString() + suffix;

        // 创建一个目录对象
        File dir = new File(bathPath);
        // 判断当前目录是否存在
        if (!dir.exists()) {
            // 目录不存在,需要创建
            dir.mkdirs();
        }

        try {
            // 将临时文件转存到指定位置
            file.transferTo(new File(bathPath + fileName ));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return R.success(fileName);
    }

    /**
     * @Description: 文件下载
     */

    @GetMapping("/download")
    public void download(String name, HttpServletResponse response){
        // 输入流,通过输入流读取文件内容
        try {
            FileInputStream fileInputStream = new FileInputStream(new File(bathPath + name ));

            // 输出流,通过输出流将文件写回浏览器,再浏览器展示图片
            ServletOutputStream outputStream = response.getOutputStream();
            response.setContentType("image/jpeg");

            int len = 0;
            byte[] bytes = new byte[1024];
            while ((len = fileInputStream.read(bytes)) != -1){
                outputStream.write(bytes,0,len);
                outputStream.flush();
            }
            // 关闭资源
            outputStream.close();
            fileInputStream.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

文件下载介绍

文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。

通过浏览器进行文件下载,通常有两种表现形式:

以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录

直接在浏览器中打开

通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。

12、stream

 flavors.stream().map((item)->{
      item.setDishId(dishId);
      return item;
 }).collect(Collectors.toList());

Stream流的使用

流操作是Java8提供一个重要新特性,它允许开发人员以声明性方式处理集合,其核心类库主要改进了对集合类的 API和新增Stream操作。Stream类中每一个方法都对应集合上的一种操作。将真正的函数式编程引入到Java中,能 让代码更加简洁,极大地简化了集合的处理操作,提高了开发的效率和生产力。

同时stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。在Stream中的操作每一次都会产生新的流,内部不会像普通集合操作一样立刻获取值,而是惰性 取值,只有等到用户真正需要结果的时候才会执行。并且对于现在调用的方法,本身都是一种高层次构件,与线程模型无关。因此在并行使用中,开发者们无需再去操 心线程和锁了。Stream内部都已经做好了

如果刚接触流操作的话可能会感觉不太舒服其实理解流操作的话可以对比数据库操作把流的操作理解为对数据库中 数据的查询操作 
    集合 = 数据表
    元素 = 表中的每条数据 
    属性 = 每条数据的列
    流API = sql查询 

流操作详解

Stream流接口中定义了许多对于集合的操作方法,总的来说可以分为两大类:中间操作和终端操作。

  • 中间操作:会返回一个流,通过这种方式可以将多个中间操作连接起来,形成一个调用链,从而转换为另外 一个流。除非调用链后存在一个终端操作,否则中间操作对流不会进行任何结果处理。
  • 终端操作:会返回一个具体的结果,如boolean、list、integer等。

一 :Stream流的介绍

stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果; stream不会改变数据源,通常情况下会产生一个新的集合; stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。

对stream操作分为终端操作和中间操作,那么这两者分别代表什么呢?

终端操作:会消费流,这种操作会产生一个结果的,如果一个流被消费过了,那它就不能被重用的。 中间操作:中间操作会产生另一个流。因此中间操作可以用来创建执行一系列动作的管道。一个特别需要注意的点是:中间操作不是立即发生的。相反,当在中间操作创建的新流上执行完终端操作后,中间操作指定的操作才会发生。所以中间操作是延迟发生的,中间操作的延迟行为主要是让流API能够更加高效地执行。

stream不可复用,对一个已经进行过终端操作的流再次调用,会抛出异常。

二: 创建流

public class Demo1 {
    public static void main ( String[] args ) {
        //创建stream流,通过Arrays.stream
        int[] arr = {1, 2, 3};
        IntStream stream = Arrays.stream(arr);
        Person[] personStr = {new Person(18, "xiaoliu"), new Person(18, "xiaojing")};
        Stream<Person> personStream = Arrays.stream(personStr);
        // 通过stream.of
        Stream<Integer> integerStream = Stream.of(1, 2, 3, 4);
        // 通过集合创建流
        List<String> stringList = Arrays.asList("123", "456", "789");
        // 创建普通流
        Stream<String> stringStream = stringList.stream();
        // 创建并行流
        Stream<String> parallelStream = stringList.parallelStream();
}
}
public class Demo2 {
    public static void main ( String[] args ) {
        // 流的筛选
        List<Integer> integerList = Arrays.asList(1, 2, 3,3, 4, 5, 6);
        // 筛选出集合中数字大于4的元素
        List<Integer> collect = integerList.stream().filter(x -> x > 4).collect(Collectors.toList());
        System.out.println(collect); //[5, 6]
        // 集合中的去重
        List<Integer> collect1 = integerList.stream().distinct().collect(Collectors.toList());
        System.out.println(collect1);//[1, 2, 3, 4, 5, 6]
        // 获取流中的第一个元素
        Optional<Integer> first = integerList.stream().filter(x -> x > 4).findFirst();
        Optional<Integer> any = integerList.stream().filter(x -> x > 4).findAny();
        Optional<Integer> any1 = integerList.parallelStream().filter(x -> x > 4).findAny();
        System.out.println(first); //Optional[5]
        System.out.println(any); //Optional[5]
        System.out.println(any1); // 预期结果不稳定
}
}

三:Stream流中获取最值 max、min和count

public class Demo3 {
    public static void main(String[] args) {
        List<String> stringList = Arrays.asList("huainvhai", "xiaotiancai", "bennvhai");
        // 获取集合中最长的字符串
        Optional<String> maxString = stringList.stream().max(Comparator.comparing(String::length));
        // 获取集合中最短字符串
        Optional<String> minString = stringList.stream().min(Comparator.comparing(String::length));
        System.out.println(maxString);
        System.out.println(minString);
       
    // 获取集合中的最大值
    List<Integer> integerList = Arrays.asList(1, 2, 3);
    Optional<Integer> maxInteger = integerList.stream().max((i, j) -> {
        return i - j;
    });
    // 获取集合中的最小值
    Optional<Integer> minInteger = integerList.stream().max((i, j) -> {
        return j - i;
    });
    System.out.println(maxInteger);
    System.out.println(minInteger);


    // 集合泛型是个对象的最值
    ArrayList<Person> personList = new ArrayList<>();
    personList.add(new Person("xiao",12));
    personList.add(new Person("xiao",20));
    personList.add(new Person("xiao",18));
    Optional<Person> max = personList.stream().max(Comparator.comparing(Person::getAge));
    // 获取集合中的元素数量
    long count = personList.stream().filter(p -> p.getAge() > 12).count();
    System.out.println(max);
    System.out.println(count);
}
}

四: 缩减

缩减:就是把一个流缩减成一个值,比如说对一个集合中求和,求乘积等

Stream流定义了三个reduce

public interface Stream<T> extends BaseStream<T, Stream<T>> {
 // 方法1
 T reduce(T identity, BinaryOperator<T> accumulator);
 // 方法2
 Optional<T> reduce(BinaryOperator<T> accumulator);
 // 方法3
 <U> U reduce(U identity,
          BiFunction<U, ? super T, U> accumulator,
          BinaryOperator<U> combiner);
}

前两种缩减方式:

第一种:接收一个BinaryOperator accumulator function(二元累加计算函数)和identity(标示值)为参数,返回值是一个T类型(代表流中的元素类型)的对象。accumulator代表操作两个值并得到结果的函数。identity按照accumulator函数的规则参与计算,假如函数是求和运算,那么函数的求和结果加上identity就是最终结果,假如函数是求乘积运算,那么函数结果乘以identity就是最终结果。

第二种:不同之处是没有identity,返回值是Optional(JDK8新类,可以存放null)。

public class Demo4 {
    public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(1, 2, 3, 4);
        // 写法一 集合中的元素求和 (就是集合中的元素求和再加上1)
        Integer integer = integers.stream().reduce(1, Integer::sum);
        // 写法二
        integers.stream().reduce(1,(x,y)->x+y);
        System.out.println(integer); // 11
        
    // 第二种缩减方式 集合中的元素求和
    Optional<Integer> reduce = integers.stream().reduce(Integer::sum);
    // 写法二
    Optional<Integer> reduce1 = integers.stream().reduce((x, y) -> x + y);
    System.out.println(reduce); //Optional[10]
    System.out.println(reduce1); //Optional[10]

    // 集合中使用reduce求最值问题
    Optional<Integer> reduce2 = integers.stream().reduce(Integer::max);
    System.out.println(reduce2);

    /**
     * 对象集合求和 求最值问题
     */
    ArrayList<Person> personList = new ArrayList<>();
    personList.add(new Person("xiao",12));
    personList.add(new Person("xiao",20));
    personList.add(new Person("xiao",18));
    // 求集合中对象的年龄的总和
    Optional<Integer> reduce3 = personList.stream().map(p -> p.getAge()).reduce(Integer::sum);
    System.out.println(reduce3);  //Optional[50]

    // 求集合中年龄最大的对象
    Optional<Person> reduce4 = personList.stream().reduce((p1, p2) -> p1.getAge() > p2.getAge() ? p1 : p2);
    Optional<Person> max = personList.stream().max(Comparator.comparingInt(Person::getAge));
    System.out.println(reduce4);
    System.out.println(max);
}
}

五:collect

collect操作可以接受各种方法作为参数,将流中的元素汇集:

public class Demo5 {
    public static void main(String[] args) {
        ArrayList<Person> personList = new ArrayList<>();
        personList.add(new Person("xiao",12));
        personList.add(new Person("xiao",20));
        personList.add(new Person("xiao",18));
        // 获取平均年龄 averaging
        Double collect = personList.stream().collect(Collectors.averagingInt(Person::getAge));
        System.out.println(collect); //16.666666666666668

    // summarizing
    DoubleSummaryStatistics collect1 = personList.stream().collect(Collectors.summarizingDouble(Person::getAge));
    System.out.println(collect1); // DoubleSummaryStatistics{count=3, sum=50.000000, min=12.000000, average=16.666667, max=20.000000}

    //joining
    String collect2 = personList.stream().map(p -> p.getName()).collect(Collectors.joining(","));
    System.out.println(collect2);  //xiao,xiao,xiao

    // reduce
    Integer collect3 = personList.stream().collect(Collectors.reducing(0, Person::getAge, (x, y) -> x + y));
    Optional<Integer> reduce = personList.stream().map(Person::getAge).reduce(Integer::sum);
    System.out.println(collect3); //50
    System.out.println(reduce);   //Optional[50]

    // groupingBy
    // 以名字进行分组
    Map<String, List<Person>> collect4 = personList.stream().collect(Collectors.groupingBy(Person::getName));
    System.out.println(collect4); //{xiao=[Person(name=xiao, age=12), Person(name=xiao, age=20), Person(name=xiao, age=18)]}
    // 先以名字分组,再以年龄分组
    Map<String, Map<Integer, List<Person>>> collect5 = personList.stream().collect(Collectors.groupingBy(Person::getName, Collectors.groupingBy(Person::getAge)));
    System.out.println(collect5); //{xiao={18=[Person(name=xiao, age=18)], 20=[Person(name=xiao, age=20)], 12=[Person(name=xiao, age=12)]}}

    // toList、toSet、toMap
    Set<Person> collect6 = personList.stream().collect(Collectors.toSet());
    System.out.println(collect6);//[Person(name=xiao, age=18), Person(name=xiao, age=20), Person(name=xiao, age=12)]

}
}

六:映射

Stream流中,map可以将一个流的元素按照一定的映射规则映射到另一个流中。

public class Demo6 {
    public static void main(String[] args) {
        String[] strArr = { "abcd", "bcdd", "defde", "ftr" };
        Arrays.stream(strArr).map(x->x.toUpperCase()).forEach(System.out::print); //ABCDBCDDDEFDEFTR
        List<String> collect = Arrays.stream(strArr).map(x -> x.toUpperCase()).collect(Collectors.toList());
        System.out.println(collect); // [ABCD, BCDD, DEFDE, FTR]
}
}

七:排序 Sorted方法是对流进行排序,并得到一个新的stream流,是一种中间操作。Sorted方法可以使用自然排序或特定比较器。

public class Demo7 {
    public static void main(String[] args) {
        String[] strArr = { "ab", "bcdd", "defde", "ftr" };
        // 自然排序
        List<String> collect = Arrays.stream(strArr).sorted().collect(Collectors.toList());
        System.out.println(collect);  // [ab, bcdd, defde, ftr]
    // 自定义排序
    // 按照字符串的长度 长度 从小到大
    List<String> collect1 = Arrays.stream(strArr).sorted(Comparator.comparing(String::length)).collect(Collectors.toList());
    System.out.println(collect1); //[ab, ftr, bcdd, defde]
    // 按照字符串的长度逆序排序
    List<String> collect2 = Arrays.stream(strArr).sorted(Comparator.comparing(String::length).reversed()).collect(Collectors.toList());
    System.out.println(collect2); //[defde, bcdd, ftr, ab]

    // 首字母倒序
    List<String> collect3 = Arrays.stream(strArr).sorted(Comparator.reverseOrder()).collect(Collectors.toList());
    System.out.println(collect3); //[ftr, defde, bcdd, ab]
    // 首字母自然排序
    List<String> collect4 = Arrays.stream(strArr).sorted(Comparator.naturalOrder()).collect(Collectors.toList());
    System.out.println(collect4); //[ab, bcdd, defde, ftr]
}
}

八 :提取流和组合流

public class Demo8 {
    public static void main(String[] args) {
        String[] arr1 = {"a","b","c","d"};
        String[] arr2 = {"d","e","f","g"};
        String[] arr3 = {"i","j","k","l"};
        
    Stream<String> stream1 = Arrays.stream(arr1);
    Stream<String> stream2 = Arrays.stream(arr2);
    Stream<String> stream3 = Arrays.stream(arr3);
    // 可以把两个stream合并成一个stream(合并的stream类型必须相同),只能两两合并

//        List<String> collect = Stream.concat(stream1, stream2).collect(Collectors.toList());
//        System.out.println(collect); //[a, b, c, d, d, e, f, g]
        // 合并去重
        List<String> collect1 = Stream.concat(stream1, stream2).distinct().collect(Collectors.toList());
        System.out.println(collect1); //[a, b, c, d, e, f, g]

    // limit,限制从流中获得前n个数据

//        List<String> collect = collect1.stream().limit(3).collect(Collectors.toList());
//        System.out.println(collect); //[a, b, c]

    // skip,跳过前n个数据
    List<String> collect = collect1.stream().skip(1).limit(3).collect(Collectors.toList());
    System.out.println(collect);//[b, c, d]
}
}

13、@EnableTransactionManagement

一、概述

在分析Spring事务原理之前,我们有必要先回顾下数据库事务相关的知识。如事务的概念、事务的属性、事务隔离级别、事务传播行为等。

首先介绍一些什么是事务?

事务由单独单元的一个或者多个sql语句组成,在这个单元中,每个mysql语句是相互依赖的。而整个单独单元作为一个不可分割的整体,如果单元中某条sql语句一旦执行失败或者产生错误,整个单元将会回滚,也就是所有受到影响的数据将会返回到事务开始以前的状态;如果单元中的所有sql语句均执行成功,则事务被顺利执行。一般来说,我们说的事务单指“数据库事务”,接下来我们会以MySQL数据库、Spring声明式事务为主要研究对象。

二、事务的ACID属性

提到事务,不可避免需要涉及到事务的ACID属性:

原子性(atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行;

一致性(consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束;

隔离性(isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行;

持久性(durability): 事务一旦提交,对数据库的修改应该永久保存在数据库中;

三、事务的隔离级别

MySQL的InnoDB引擎提供四种隔离级别:

①读未提交(READ UNCOMMITTED) 事务中的修改,即使没有提交,对其他事务也是可见的,也就是说事务可以读取到未提交的数据,这也被称为脏读。

②读已提交(READ COMMITTED) 一个事务从开始到提交之前,所做的任何修改对其他事务都是不可见的,这个级别有时候也叫不可重复读,因为两次执行同样的查询,可能会得到不一样的结果。

③可重复读(REPEATABLE READ) 该隔离级别保证了在同一个事务中多次读取同样的记录的结果是一致的,但是无法解决另外一个幻读的问题,所谓的幻读就是指当某个事务在读取某个范围内的记录是,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时就会产生幻行。

④可串行化(SERIALIZABLE) SERIALIZABLE是最高的隔离级别,通过强制事务的串行执行,避免了前面说的幻读问题,简单来说,SERIALIZABLE会在读取的每一行数据上加上锁。

四、事务的传播行为

Spring针对方法嵌套调用时事务的创建行为定义了七种事务传播机制,分别是:

传播行为
PROPAGATION_REQUIRED 这是默认的传播属性,如果外部调用方有事务,将会加入到事务,没有的话新建一个。
PROPAGATION_SUPPORTS 表示当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行(如果当前存在事务,则加入到该事务;如果当前没有事务,则以非事务的方式继续运行。)
PROPAGATION_MANDATORY 表示当前方法必须在一个事务中运行,如果没有事务,将抛出异常
PROPAGATION_NESTED 表示如果当前方法正有一个事务在运行中,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同PROPAGATION_REQUIRED的一样
PROPAGATION_NEVER 表示当前方法务不应该在一个事务中运行,如果存在一个事务,则抛出异常
PROPAGATION_REQUIRES_NEW 表示当前方法必须运行在它自己的事务中。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。
PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起

五、Spring声明式事务环境搭建

这里是在Spring源码里面新建了一个子模块搭建事务功能测试环境,所以使用的是Gradle进行构建。当然也可以使用maven搭建。

(一)、build.gradle:添加Druid数据源、mysql驱动等依赖

description = "spring test demo"

dependencies {
    compile(project(":spring-beans"))
    compile(project(":spring-core"))
    compile(project(":spring-context"))
    compile(project(":spring-tx"))
    compile(project(":spring-jdbc"))
    implementation 'com.alibaba:druid:1.1.10'
    compile ("mysql:mysql-connector-java:5.1.24")
}

repositories {
    mavenLocal()
    maven { url "https://maven.aliyun.com/nexus/content/groups/public" }
    maven { url "https://repo.springsource.org/plugins-release" }
    mavenCentral()
}

(二)、定义Service接口以及实现类,实现类对应方法添加@Transactional开启声明式事务功能

public interface UserService {

    void insert();

}

@Service
public class UserServiceImpl implements UserService {

	@Autowired
	private UserDao userDao;
	 
	@Override
	// 声明式事务
	@Transactional
	public void insert() {
		userDao.insert();
		System.out.println("插入完成");

//		int value = 1 / 0;
	}

}

(三)、定义持久层接口

@Repository
public class UserDao {

	@Autowired
	private JdbcTemplate jdbcTemplate;
	 
	public void insert() {
		String sql = "INSERT INTO user(name, age) VALUES(?,?)";
		jdbcTemplate.update(sql, "lisi", 20);
	}

}

(四)、定义事务配置类

使用@EnableTransactionManagement开启Spring注解版事务功能,并往容器中注册DataSource、JdbcTemplate、PlatformTransactionManager这三个bean对象。

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

// 开启Spring注解版事务功能
@EnableTransactionManagement
@Configuration
@ComponentScan("com.wsh.transaction")
public class TxConfig {

	/**
	 * 数据源配置
	 */
	@Bean
	public DataSource dataSource() {
		DruidDataSource dataSource = new DruidDataSource();
		dataSource.setUsername("root");
		dataSource.setPassword("0905");
		dataSource.setUrl("jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai");
		dataSource.setDriverClassName("com.mysql.jdbc.Driver");
		return dataSource;
	}
	 
	@Bean
	public JdbcTemplate jdbcTemplate(DataSource dataSource) {
		return new JdbcTemplate(dataSource);
	}
	 
	/**
	 * 事务管理器
	 */
	@Bean
	public PlatformTransactionManager platformTransactionManager() {
		return new DataSourceTransactionManager(dataSource());
	}

}

(五)、客户端类

public class Client {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TxConfig.class);
		UserService userService = (UserService) annotationConfigApplicationContext.getBean("userServiceImpl");
		System.out.println(userService);
		userService.insert();
	}
}

六、@EnableTransactionManagement分析

在前面的例子中,我们使用@EnableTransactionManagement开启了Spring注解版本的事务功能,所以我们从@EnableTransactionManagement注解开始分析Spring事务的整体流程。

先来看看@EnableTransactionManagement注解的源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 开启Spring事务支持
// 通过@EnableTransactionManagement引入TransactionManagementConfigurationSelector组件,进而导入两个bean:
// a、AutoProxyRegistrar
// b、ProxyTransactionManagementConfiguration
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

	/**
	 * 使用JDK动态代理还是Cglib动态代理,默认是false,但前提是AdviceMode必须是PROXY才适用
	 */
	boolean proxyTargetClass() default false;
	 
	/**
	 * 通知的模式,默认是PROXY,还有一种是ASPECTJ
	 */
	AdviceMode mode() default AdviceMode.PROXY;
	 
	/**
	 * 指定通知器的执行顺序
	 */
	int order() default Ordered.LOWEST_PRECEDENCE;

}

通过@EnableTransactionManagement的源码,我们看到,使用@Import注解@Import(TransactionManagementConfigurationSelector.class)注入了TransactionManagementConfigurationSelector类。

TransactionManagementConfigurationSelector间接实现了ImportSelector接口,先来看看ImportSelector有什么作用?

public interface ImportSelector {

	String[] selectImports(AnnotationMetadata importingClassMetadata);

}

ImportSelector接口只定义了一个方法selectImports(),用于指定需要注册为bean的Class名称。当在@Configuration标注的Class上使用@Import引入了一个ImportSelector实现类后,会把实现类中返回的Class名称都定义为bean。

我们再回来看TransactionManagementConfigurationSelector类的selectImports()方法返回了什么Class名称。

protected String[] selectImports(AdviceMode adviceMode) {
    switch (adviceMode) {
        case PROXY:
            return new String[] {
                AutoProxyRegistrar.class.getName(),
                // 导入了BeanFactoryTransactionAttributeSourceAdvisor组件,bean名称为"org.springframework.transaction.config.internalTransactionAdvisor"
                ProxyTransactionManagementConfiguration.class.getName()};
        case ASPECTJ:
            return new String[] {determineTransactionAspectClass()};
        default:
            return null;
    }
}

selectImports()方法根据@EnableTransactionManagement注解指定的AdviceMode,分别返回不同的Class名称。

这里我们以默认的AdviceMode.PROXY模式为例,返回了两个bean的名称:

(1). AutoProxyRegistrar (2). ProxyTransactionManagementConfiguration

七、AutoProxyRegistrar类分析

public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

	private final Log logger = LogFactory.getLog(getClass());
	 
	@Override
	// org.springframework.context.annotation.AutoProxyRegistrar.registerBeanDefinitions
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		boolean candidateFound = false;
		Set<String> annTypes = importingClassMetadata.getAnnotationTypes();
		for (String annType : annTypes) {
			// 获取注解的属性
			AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);
			if (candidate == null) {
				continue;
			}
			// 模式
			Object mode = candidate.get("mode");
			Object proxyTargetClass = candidate.get("proxyTargetClass");
			if (mode != null && proxyTargetClass != null && AdviceMode.class == mode.getClass() &&
					Boolean.class == proxyTargetClass.getClass()) {
				candidateFound = true;
				if (mode == AdviceMode.PROXY) {
					// 注册自动代理创建器:InfrastructureAdvisorAutoProxyCreator
					AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
	 
					if ((Boolean) proxyTargetClass) {
						AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
						return;
					}
				}
			}
		}
		if (!candidateFound && logger.isInfoEnabled()) {
			String name = getClass().getSimpleName();
			logger.info(String.format("%s was imported but no annotations were found " +
					"having both 'mode' and 'proxyTargetClass' attributes of type " +
					"AdviceMode and boolean respectively. This means that auto proxy " +
					"creator registration and configuration may not have occurred as " +
					"intended, and components may not be proxied as expected. Check to " +
					"ensure that %s has been @Import'ed on the same class where these " +
					"annotations are declared; otherwise remove the import of %s " +
					"altogether.", name, name, name));
		}
	}

}

// org.springframework.aop.config.AopConfigUtils#registerAutoProxyCreatorIfNecessary
public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
    return registerAutoProxyCreatorIfNecessary(registry, null);
}

public static BeanDefinition registerAutoProxyCreatorIfNecessary(
    BeanDefinitionRegistry registry, @Nullable Object source) {
    // 注册或者升级InfrastructureAdvisorAutoProxyCreator组件
    return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source);
}

从源码可以看到,AutoProxyRegistrar实现了ImportBeanDefinitionRegistrar接口,ImportBeanDefinitionRegistrar接口内部定义了registerBeanDefinitions()方法实现向容器中注册bean的功能。

AutoProxyRegistrar#registerBeanDefinitions()方法中注册了自动代理创建器:InfrastructureAdvisorAutoProxyCreator。接下来看看InfrastructureAdvisorAutoProxyCreator是什么东西?

先看看InfrastructureAdvisorAutoProxyCreator的层级关系:

我们看到,InfrastructureAdvisorAutoProxyCreator类跟我们前面介绍到的AOP类一样,根父类也是AbstractAutoProxyCreator。主要分析下面两点:

(1)、实现了InstantiationAwareBeanPostProcessor接口 该接口有2个方法postProcessBeforeInstantiation()和postProcessAfterInstantiation(),其中实例化之前会执行postProcessBeforeInstantiation()方法:

InfrastructureAdvisorAutoProxyCreator中并没有实现postProcessBeforeInstantiation(),而是在其父类AbstractAutoProxyCreator中实现。

// AbstractAutoProxyCreator#postProcessBeforeInstantiation
// postProcessBeforeInstantiation()方法在bean实例化之前调用
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
    // 组装缓存key
    Object cacheKey = getCacheKey(beanClass, beanName);

if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
    // 如果advisedBeans缓存中已经存在,即当前正在创建的Bean已经被解析过,则直接返回null
    if (this.advisedBeans.containsKey(cacheKey)) {
        return null;
    }
 
    // 注意:AnnotationAwareAspectJAutoProxyCreator重写了isInfrastructureClass()方法.
 
    // 1.isInfrastructureClass():判断当前正在创建的Bean是否是基础的Bean(Advice、PointCut、Advisor、AopInfrastructureBean)
    // 2.shouldSkip():判断是否需要跳过(在这个方法内部,Spring Aop解析直接解析出我们的切面信息(并且把我们的切面信息进行缓存))
    // 满足两个条件其中之一,都将跳过,直接返回null
    if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return null;
    }
}
 
// Create proxy here if we have a custom TargetSource.
// Suppresses unnecessary default instantiation of the target bean:
// The TargetSource will handle target instances in a custom fashion.
 
// 获取用户自定义TargetSource(目标源)
TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
if (targetSource != null) {
    if (StringUtils.hasLength(beanName)) {
        this.targetSourcedBeans.add(beanName);
    }
    // 获取目标对象的拦截器链
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
    // 创建AOP代理
    Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
    this.proxyTypes.put(cacheKey, proxy.getClass());
    return proxy;
}
 
return null;
}

postProcessBeforeInstantiation()方法在bean实例化之前调用,主要是通过isInfrastructureClass(beanClass)方法判断当前正在创建的Bean是否是基础的Bean(Advice、PointCut、Advisor、AopInfrastructureBean);以及通过

shouldSkip(beanClass, beanName)方法判断是否需要跳过,在shouldSkip()方法内部,Spring Aop解析直接解析出我们的切面信息,并且把我们的切面信息进行缓存,后面我们创建代理对象时直接从缓存中获取使用。

(2)、实现了BeanPostProcessor接口

该接口有2个方法postProcessBeforeInitialization()和postProcessAfterInitialization(),其中组件初始化之后会执行postProcessAfterInitialization()(该方法创建Aop和事务的代理对象)方法:

// 所有Spring管理的bean在初始化后都会去调用所有BeanPostProcessor的postProcessAfterInitialization()方法
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    // 如果bean非空的话,判断是否需要进行代理,需要代理的话则会进行包装
    if (bean != null) {
        // 获取缓存key:
        // a.如果beanName非空的话,则还会判断是否是FactoryBean,是FactoryBean的话使用"&+beanName"作为缓存key,否则直接使用beanName;
        // b.如果beanName为空,直接使用beanClass作为缓存key;
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            // 如果有必要的话,则执行具体的包装
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    // 如果bean为空,则直接返回
    return bean;
}

所有Spring管理的bean在初始化后都会去调用所有BeanPostProcessor的postProcessAfterInitialization()方法,postProcessAfterInitialization()方法内部最主要的是完成了代理对象的创建工作,返回给容器中。

八、ProxyTransactionManagementConfiguration类分析

前面我们介绍了AutoProxyRegistrar组件,主要是向容器中注册了自动代理创建器---InfrastructureAdvisorAutoProxyCreator,它间接实现了InstantiationAwareBeanPostProcessor接口,在postProcessBeforeInstantiation()方法中完成了切面信息的解析工作,并进行缓存,后面创建代理对象时直接从缓存中获取使用。同时它还实现了BeanPostProcessor接口,在postProcessAfterInitialization()方法内部完成了代理对象的创建工作。

接下来,我们看TransactionManagementConfigurationSelector导入的另外一个组件---ProxyTransactionManagementConfiguration发挥了什么作用。

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {

	/**
	 * 注册BeanFactoryTransactionAttributeSourceAdvisor增强器
	 */
	@Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor() {
		// 注册BeanFactoryTransactionAttributeSourceAdvisor增强器,beanName = "org.springframework.transaction.config.internalTransactionAdvisor"
		BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
		advisor.setTransactionAttributeSource(transactionAttributeSource());
		advisor.setAdvice(transactionInterceptor());
		if (this.enableTx != null) {
			advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
		}
		return advisor;
	}
	 
	/**
	 * 注册TransactionAttributeSource,类型是AnnotationTransactionAttributeSource
	 */
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public TransactionAttributeSource transactionAttributeSource() {
		return new AnnotationTransactionAttributeSource();
	}
	 
	/**
	 * 注册TransactionInterceptor,实现了MethodInterceptor接口,主要用于拦截事务方法的执行
	 */
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public TransactionInterceptor transactionInterceptor() {
		TransactionInterceptor interceptor = new TransactionInterceptor();
		interceptor.setTransactionAttributeSource(transactionAttributeSource());
		if (this.txManager != null) {
			interceptor.setTransactionManager(this.txManager);
		}
		return interceptor;
	}

}

从源码中我们看到,ProxyTransactionManagementConfiguration是一个配置类,通过@Bean + @Configuration注解往容器中注册了三个Bean:

(1). BeanFactoryTransactionAttributeSourceAdvisor增强器 (2). TransactionAttributeSource,类型是AnnotationTransactionAttributeSource (3). TransactionInterceptor,实现了MethodInterceptor接口,主要用于拦截事务方法的执行

九、总结

最后,通过一张图总结一下@EnableTransactionManagement注解的作用:

AutoProxyRegistrar组件 主要是向容器中注册了自动代理创建器---InfrastructureAdvisorAutoProxyCreator,它间接实现了InstantiationAwareBeanPostProcessor接口,在postProcessBeforeInstantiation()方法中完成了切面信息的解析工作,并进行缓存,后面创建代理对象时直接从缓存中获取使用。同时它还实现了BeanPostProcessor接口,在postProcessAfterInitialization()方法内部完成了代理对象的创建工作。

ProxyTransactionManagementConfiguration组件

通过@Bean + @Configuration注解往容器中注册了三个Bean:

(1). BeanFactoryTransactionAttributeSourceAdvisor增强器; (2). TransactionAttributeSource,类型是AnnotationTransactionAttributeSource; (3). TransactionInterceptor,实现了MethodInterceptor接口,主要用于拦截事务方法的执行;

14、BeanUtils.copyProperties();源码分析

使用Spring的BeanUtils进行对象拷贝很容易。

首先引入响应的jar包:

org.springframework spring-beans 5.0.7.RELEASE

使用时代码就一行:

org.springframework.beans.BeanUtils.copyProperties(orig, dest);

对应形参:Object source, Object target

参数表示要把orig里面的属性值拷贝到dest对象中。

注意,对象拷贝的是属性值的引用,如果是基础数据类型还好,如果是一个对象类型,拷贝完成后,orig里面的对象类型属性值发生变化,dest里面相应的属性值会发生变化。会有一定的风险。

查看spring源码,方法实现代码如下:

private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
      @Nullable String... ignoreProperties) throws BeansException {

   Assert.notNull(source, "Source must not be null");
   Assert.notNull(target, "Target must not be null");

   Class<?> actualEditable = target.getClass();
   if (editable != null) {
      if (!editable.isInstance(target)) {
         throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
               "] not assignable to Editable class [" + editable.getName() + "]");
      }
      actualEditable = editable;
   }
//获取target对象的PropertyDescriptor属性数组targetPds
   PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
   List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
//遍历target对象属性
   for (PropertyDescriptor targetPd : targetPds) {
//获取其set方法
      Method writeMethod = targetPd.getWriteMethod();
      if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
//获取source对象与target对象targetPd属性同名的PropertyDescriptor对象sourcePd
         PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
         if (sourcePd != null) {
//获取source对应属性的get方法
            Method readMethod = sourcePd.getReadMethod();
            if (readMethod != null &&
                  ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
               try {
                  if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                     readMethod.setAccessible(true);
                  }
//通过反射获取source对象属性的值
                  Object value = readMethod.invoke(source);
                  if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                     writeMethod.setAccessible(true);
                  }
//通过反射给target对象属性赋值
                  writeMethod.invoke(target, value);
               }
               catch (Throwable ex) {
                  throw new FatalBeanException(
                        "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
               }
            }
         }
      }
   }
}

执行过程比较简单,底层对象取值、赋值还是用的反射。

在看获取对象方法列表之前,先看一下属性描述类是干嘛的。java.beans.PropertyDescriptor,是jdk提供的一个bean相关的类,它封装了一个Javabean中属性相关的信息,比如属性名称name、get方法名readMethodName、set方法名writeMethodName、get方法readMethodRef、write方法writeMethodRef等信息。

而方法的类型为java.beans.MethodRef,里面用到了java.lang.ref.SoftReference和java.lang.ref.WeakReference。

其中java.lang.ref.SoftReference的API描述为:软引用对象,在响应内存需要时,由垃圾回收器决定是否清除此对象。软引用对象最常用于实现内存敏感的缓存。 这个对象是放在jvm内存中的,后面会讲到。可能会被jvm清除。

java.lang.ref.WeakReference的API描述:弱引用对象,它们并不禁止其指示对象变得可终结,并被终结,然后被回收。弱引用最常用于实现规范化的映射。

继续看获取对象属性描述列表的方法:

org.springframework.beans.BeanUtils#getPropertyDescriptors(Class<?> clazz)

org.springframework.beans.BeanUtils#getPropertyDescriptor(Class<?> clazz, String propertyName)

第一个获取的是类属性列表,第二个是根据属性名称获取其具体描述。

public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws BeansException {
   CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz);
   return cr.getPropertyDescriptors();
}
static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
   CachedIntrospectionResults results = strongClassCache.get(beanClass);
   if (results != null) {
      return results;
   }
   results = softClassCache.get(beanClass);
   if (results != null) {
      return results;
   }
//根据class创建CachedIntrospectionResults对象
   results = new CachedIntrospectionResults(beanClass);
   ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse;

   if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) ||
         isClassLoaderAccepted(beanClass.getClassLoader())) {
      classCacheToUse = strongClassCache;
   }
   else {
      if (logger.isDebugEnabled()) {
         logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe");
      }
      classCacheToUse = softClassCache;
   }

   CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results);
   return (existing != null ? existing : results);
}
private CachedIntrospectionResults(Class<?> beanClass) throws BeansException {
   try {
      if (logger.isTraceEnabled()) {
         logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
      }
//获取BeanInfo对象
      this.beanInfo = getBeanInfo(beanClass);

      if (logger.isTraceEnabled()) {
         logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]");
      }
      this.propertyDescriptorCache = new LinkedHashMap<>();

//将BeanInfo中的PropertyDescriptor数组放入map
      // This call is slow so we do it once.
      PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
      for (PropertyDescriptor pd : pds) {
         if (Class.class == beanClass &&
               ("classLoader".equals(pd.getName()) ||  "protectionDomain".equals(pd.getName()))) {
            // Ignore Class.getClassLoader() and getProtectionDomain() methods - nobody needs to bind to those
            continue;
         }
         if (logger.isTraceEnabled()) {
            logger.trace("Found bean property '" + pd.getName() + "'" +
                  (pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") +
                  (pd.getPropertyEditorClass() != null ?
                        "; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));
         }
         pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
         this.propertyDescriptorCache.put(pd.getName(), pd);
      }

      // Explicitly check implemented interfaces for setter/getter methods as well,
      // in particular for Java 8 default methods...
      Class<?> clazz = beanClass;
      while (clazz != null && clazz != Object.class) {
         Class<?>[] ifcs = clazz.getInterfaces();
         for (Class<?> ifc : ifcs) {
            if (!ClassUtils.isJavaLanguageInterface(ifc)) {
               for (PropertyDescriptor pd : getBeanInfo(ifc).getPropertyDescriptors()) {
                  if (!this.propertyDescriptorCache.containsKey(pd.getName())) {
                     pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
                     this.propertyDescriptorCache.put(pd.getName(), pd);
                  }
               }
            }
         }
         clazz = clazz.getSuperclass();
      }
     
      this.typeDescriptorCache = new ConcurrentReferenceHashMap<>();

   }
   catch (IntrospectionException ex) {
      throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex);
   }
}
//获取BeanInfo对象,主要是Introspector.getBeanInfo
private static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
   for (BeanInfoFactory beanInfoFactory : beanInfoFactories) {
      BeanInfo beanInfo = beanInfoFactory.getBeanInfo(beanClass);
      if (beanInfo != null) {
         return beanInfo;
      }
   }
   return (shouldIntrospectorIgnoreBeaninfoClasses ?
         Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) :
         Introspector.getBeanInfo(beanClass));
}

​ 从以上的代码可以看出,Spring底层是通过BeanInfo.getBeanInfo(Class<?> beanClass)获取到BeanInfo对象,然后将其属性放入到jvm内存,每当程序再次请求时,会优先从内存中读取。

15、@PathVariable 和@PathParam 注解

一般来讲的话是以两种方式为主,分别为Post和Get,这两种方式都是向一个url传参,而Get方式体现到了地址栏里,Post方式则将内容放在了 body 里面。

@PathParam 和 @PathVariable 注解是用于从 request 中接收请求的,两个都可以接收参数,关键点不同的是@PathParam 是从 request 里面拿取值,而 @PathVariable 是从一个url模板里面来填充(绑定 URL 占位符到功能处理方法的参数上,主要实现RESTFULL风格的请求),也就是从地址栏中取值(以键值对形式)。

@PathVariable 它是以“/”方式来获取参数值。 也是RSET风格的springmvc取值。

21

如上图:此种获取参数值的方式,需要在value中指定一个key,并且在方法参数中一定要有这个key,不然报500错误。

当你只设置一个属性的话,可以把value属性去掉。如下图:

22

注意{UserNuber}对应的@PathVariable(“UserNuber”)的英文字母必须一致,否则将会获取不到任何值。

@PathVariable 属性中的name与value功能是相同的。

required属性为true的话, 地址中如果没有这个参数会报错,为false时则会忽略与@Autowired中的required功能是相同的。 比如:@PathVariable(value = “name”, required = false)这样它就不会报错,会直接忽略没有参数的报错。

23

它的访问地址:http://localhost:8080/HNZGDXSYS/ImgbyNumber/YG0001 这时候,我们拿到的参数Number为:YG0001,效果图如下:

24

@PathParam

它是以键值对方式来获取参数值的。 这个注解相对简单,就是从地址栏取参数值,采用的是传统的拼接参数方法。

如:http://localhost:8080/HNZGDXSYS/ImgbyNumber?name=李四&name1=张三

25

它的不同点的是@PathVariab在没有对应属性时会是一个null值,不会报错,而@PathParam则会报404异常。 效果如下图:

26

@PathVariable与@PathParam两者之前的优缺点:

@PathVariable

直接获取url模板里的值是很方便的,不用自己去获取 request 里的固定参数,如果只是 ID 这种单个或者多个数字字母,使用 @PathVariable 会好很多。

1.虽然说直接获取 URI 模板里的值是很方便的,但是我们需要拿到一些带有后缀名的参数值的话,它会把参数值的后缀名给忽略掉,我们要是拿到后缀名就还需要写正则来获取到。 2.如果你斜杠后面的参数为空值,那么它会报404错误。

@PathParam

1.如果你需要获取的参数值存在一些符号之类的。 比如:string;str;ing等等 这时,我们可以优先使用@PathParam,我们都知道它是获取request中的值的,这时我们只需要直接获取你需要拿到的那个参数值对应的那个键后,我们就直接拿到值了。 2.就算你参数后面的值为空,那么它也能拿到空值,不会报异常。

个人总结:

如果我们是传单个参数的时候,我们可以直接放在url地址里这样就比较方便,也就是@PathParam ,而不使用@PathVariable。

如果我们使用@PathVariable的话,还需要从 request 请求里提取指定参数和这,这样我们就多了一步操作。

当我们传多个参数和不同类型的参数,我觉得就使用@PathParam。

但是如果为了安全起见的话,使用@PathVariable会比较安全一点,而@PathParam的话,就是从地址栏取参数值,采用的是传统的拼接参数方法,这样的话我们在请求的同时会在地址栏上看到你的参数值等等。

因此我们要根据不同的需求来决定我们使用这两者。

16、短信服务验证SMS——第一次尝试

阿里云短信服务

阿里云短信服务(Short Message Service)是广大企业客户快速触达手机用户所优选使用的通信能力。调用API或用群发助手,即可发送验证码、通知类和营销类短信;国内验证短信秒级触达,到达率最高可达99%;国际/港澳台短信覆盖200多个国家和地区,安全稳定,广受出海企业选用。

阿里云官网:https://www.aliyun.com/

设置短信签名

注册成功后,点击登录按钮进行登录。登录后进入短信服务管理页面,选择国内消息菜单:

27

短信签名是短信发送者的署名,表示发送方的身份。

切换到【模板管理】标签页:

28

短信模板包含短信发送内容、场景、变量信息。

光标移动到用户头像上,在弹出的窗口中点击【AccessKey 管理】:

29

代码开发-导入maven坐标

<dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-core</artifactId>
        <version>4.5.16</version>
</dependency>
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
    <version>2.1.0</version>
</dependency>

相关代码:

/**
 * 短信发送工具类
 */
public class SMSUtils {
	/**
	 * 发送短信
	 * @param signName 签名
	 * @param templateCode 模板
	 * @param phoneNumbers 手机号
	 * @param param 参数
	 */
	public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
		DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "<accessKeyId>", "<accessKeySecret>");

		IAcsClient client = new DefaultAcsClient(profile);

		SendSmsRequest request = new SendSmsRequest();
		request.setSysRegionId("cn-hangzhou");
		request.setPhoneNumbers(phoneNumbers);
		request.setSignName(signName);
		request.setTemplateCode(templateCode);
		request.setTemplateParam("{\"code\":\""+param+"\"}");
		try {
			SendSmsResponse response = client.getAcsResponse(request);
			System.out.println("短信发送成功");
		}catch (ClientException e) {
			e.printStackTrace();
		}
	}
}

17、IdWorker——用于生成唯一的订单号

import com.baomidou.mybatisplus.core.toolkit.IdWorker;

这个idwoker所生成的id是自增长的。 所用到的id是Long类型的,于uuid的String类型所不同;

话不多说,上工具类:

public class IdWorker {
  /**

   * 这个就是代表了机器id
     */
       private long workerId;
       // 这个就是代表了机房id
       private long datacenterId;
       // 这个就是代表了一毫秒内生成的多个id的最新序号
       private long sequence;
       // 注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
       private long twepoch = 1581647829999L;


  // 机器占的位数
  private long workerIdBits = 5L;
  // 机房占的位数
  private long datacenterIdBits = 5L;
  // 表示的序号,就是某个机房某台机器上这一毫秒内同时生成的id的序号位数
  private long sequenceBits = 12L;

  // 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
  private long maxWorkerId = -1L ^ (-1L << workerIdBits);

  // 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
  private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

  // 这个是二进制运算,就是12 bit随机数字随机数 只能在这个数字只能4095以内
  private long sequenceMask = -1L ^ (-1L << sequenceBits);

  // 机器id移位
  private long workerIdShift = sequenceBits;
  // 机房id移位=
  private long datacenterIdShift = sequenceBits + workerIdBits;
  // 时间位移
  private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

  /**

   * 最近一次生成id的时间戳,单位是毫秒
     */
       private long lastTimestamp = -1L;

  /**

   * @param workerId     机器id

   * @param datacenterId 业务id

   * @param sequence     一毫秒内生成的多个id的最新序号
     */
       public IdWorker(long workerId, long datacenterId, long sequence) {

     // sanity check for workerId
     // 要求就是你传递进来的机器id不能超过32,不能小于0
     if (workerId > maxWorkerId || workerId < 0) {

         throw new IllegalArgumentException(
                 String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));

     }
     // 要求就是你传递进来的机房id不能超过32,不能小于0
     if (datacenterId > maxDatacenterId || datacenterId < 0) {

         throw new IllegalArgumentException(
                 String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));

     }

     this.workerId = workerId;
     this.datacenterId = datacenterId;
     this.sequence = sequence;
       }

  public long getWorkerId() {
      return workerId;
  }

  public long getDatacenterId() {
      return datacenterId;
  }

  public long getTimestamp() {
      return System.currentTimeMillis();
  }

  // 这个是核心方法,通过调用nextId()方法,让当前这台机器上的snowflake算法程序生成一个全局唯一的id
  public synchronized long nextId() {

      // 这儿就是获取当前时间戳,单位是毫秒
      long timestamp = timeGen();
    
      if (timestamp < lastTimestamp) {
          System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
          throw new RuntimeException(String.format(
                  "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
      }
    
      // 下面是说假设在同一个毫秒内,又发送了一个请求生成一个id
      // 这个时候就得把seqence序号给递增1,最多就是4096
      if (lastTimestamp == timestamp) {
    
          // 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,
          // 这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
    
          System.out.println("sequence-1:" + sequence);
    
          sequence = (sequence + 1) & sequenceMask;
    
          if (sequence == 0) {
              timestamp = tilNextMillis(lastTimestamp);
          }
          System.out.println("sequence:" + sequence);
    
      } else {
          sequence = 0;
      }
    
      // 这儿记录一下最近一次生成id的时间戳,单位是毫秒
      lastTimestamp = timestamp;
    
      // 这儿就是最核心的二进制位运算操作,生成一个64bit的id
      // 先将当前时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后12 bit
      // 最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型
      return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)
              | (workerId << workerIdShift) | sequence;

  }

  private long tilNextMillis(long lastTimestamp) {

      long timestamp = timeGen();
    
      while (timestamp <= lastTimestamp) {
          timestamp = timeGen();
      }
      return timestamp;

  }

  private long timeGen() {
      return System.currentTimeMillis();
  }

调用:

public static void main(String[] args) {
  IdWorker idWorker = new IdWorker(1,1,1);
      System.out.println("idWorker:"+idWorker.nextId());
}

18、AtomicInteger amout = new AtomicInteger(0);

 // 原子操作,保证线程安全
 AtomicInteger amount = new AtomicInteger(0);

 List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) -> {
     OrderDetail orderDetail = new OrderDetail();
     orderDetail.setOrderId(orderId);
     orderDetail.setNumber(item.getNumber());
     orderDetail.setDishFlavor(item.getDishFlavor());
     orderDetail.setSetmealId(item.getSetmealId());
     orderDetail.setName(item.getName());
     orderDetail.setImage(item.getImage());
     orderDetail.setAmount(item.getAmount());
     amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
     return orderDetail;
 }).collect(Collectors.toList());

一、什么是AtomicInteger

AtomicInteger类是系统底层保护的int类型,通过提供执行方法的控制进行值的原子操作。AtomicInteger它不能当作Integer来使用

从JAVA 1.5开始,AtomicInteger 属于java.util.concurrent.atomic 包下的一个类。

二、创建AtomicInteger 设置值获取值

AtomicInteger通过调用构造函数可以直接创建。在AtomicInteger提供了两种方法来获取和设置它的实例的值

//初始值是 0
AtomicInteger atomicInteger = new AtomicInteger(); 
 
//初始值是 100
AtomicInteger atomicInteger = new AtomicInteger(100);
 
int currentValue = atomicInteger.get();         //100
 
atomicInteger.set(1234);                        //当前值1234

三、什么情况下使用AtomicInteger

在现实生活中,我们需要AtomicInteger两种情况:

1、作为多个线程同时使用的原子计数器。 2、在比较和交换操作中实现非阻塞算法。

1、AtomicInteger作为原子计数器

要将它用作计数器,AtomicIntegerclass提供了一些以原子方式执行加法和减法操作的方法。

addAndGet()- 以原子方式将给定值添加到当前值,并在添加后返回新值。
getAndAdd() - 以原子方式将给定值添加到当前值并返回旧值。
incrementAndGet()- 以原子方式将当前值递增1并在递增后返回新值。它相当于i ++操作。
getAndIncrement() - 以原子方式递增当前值并返回旧值。它相当于++ i操作。
decrementAndGet()- 原子地将当前值减1并在减量后返回新值。它等同于i-操作。
getAndDecrement() - 以原子方式递减当前值并返回旧值。它相当于-i操作。
public class Main{
    public static void main(String[] args)
    {
        AtomicInteger atomicInteger = new AtomicInteger(100);
         
        System.out.println(atomicInteger.addAndGet(2));         //102
        System.out.println(atomicInteger);                      //102
         
        System.out.println(atomicInteger.getAndAdd(2));         //102
        System.out.println(atomicInteger);                      //104
         
        System.out.println(atomicInteger.incrementAndGet());    //105  
        System.out.println(atomicInteger);                      //105  
                 
        System.out.println(atomicInteger.getAndIncrement());    //105
        System.out.println(atomicInteger);                      //106
         
        System.out.println(atomicInteger.decrementAndGet());    //105
        System.out.println(atomicInteger);                      //105
         
        System.out.println(atomicInteger.getAndDecrement());    //105
        System.out.println(atomicInteger);                      //104
    }
}

2、比较和交换操作

1、比较和交换操作将内存位置的内容与给定值进行比较,并且只有它们相同时,才将该内存位置的内容修改为给定的新值。这是作为单个原子操作完成的。

2、原子性保证了新值是根据最新信息计算出来的; 如果在此期间该值已被另一个线程更新,则写入将失败。 为了支持比较和交换操作,此类提供了一种方法,如果将该值原子地设置为给定的更新值current value == the expected value。

boolean compareAndSet(int expect, int update)

我们可以compareAndSet()在Java并发集合类中看到amy实时使用方法ConcurrentHashMap。

import java.util.concurrent.atomic.AtomicInteger;
 
public class Main{
    public static void main(String[] args){
        //1、默认初始值
        AtomicInteger atomicInteger = new AtomicInteger(100);
        //2、默认初始值和给定值,都是100,所以会更改成功
        boolean isSuccess = atomicInteger.compareAndSet(100,110);   //current value 100
        //3、返回true
        System.out.println(isSuccess);      //true
        //4、默认初始值是11,给定值是100,所以会更改失败
        isSuccess = atomicInteger.compareAndSet(100,120);       //current value 110
        //5、返回false
        System.out.println(isSuccess);      //false
         
    }
}

程序输出

true
false

四、总结

如上所述,主要用途AtomicInteger是当我们处于多线程上下文时,我们需要在不使用关键字的情况下对值执行原子操作。intsynchronized

AtomicInteger与使用同步执行相同操作相比,使用它同样更快,更易读。

19、Swagger

使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,以及在线接口调试页面等等。

官网:https://swagger.io/

knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。

<dependency>

     <groupId>com.github.xiaoymin</groupId>

     <artifactId>knife4j-spring-boot-starter</artifactId>

     <version>3.0.2</version>

</dependency>

操作步骤:

1、导入knife4j的maven坐标

<dependency>
       <groupId>com.github.xiaoymin</groupId>
       <artifactId>knife4j-spring-boot-starter</artifactId>
       <version>3.0.2</version>
</dependency>

2、导入knife4j相关配置类

@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 设置静态资源映射
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始进行静态资源映射...");
        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }

   
    @Bean
    public Docket createRestApi() {
        // 文档类型
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.tjut.reggie.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("瑞吉外卖")
                .version("1.0")
                .description("瑞吉外卖接口文档")
                .build();
    }
}

3、设置静态资源,否则接口文档页面无法访问

(WebMvcConfig类中的addResourceHandlers方法),否则接口文档页面无法访问

registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");

registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");

4、在LoginCheckFilter中设置不需要处理的请求路径

 @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String requestURI = request.getRequestURI();
        //定义不需要处理的请求路径
        // /common/**上传下载图片   /user/sendMsg移动端发送短信   /user/login移动端登录
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",
                "/common/**",
                "/user/sendMsg",
                "/user/login",
                "/doc.html",
                "/webjars/**",
                "/swagger-resources",
                "/v2/api-docs"
        };
        //判断路径是否需要处理
        boolean check = check(urls,requestURI);

        //不需要处理,直接放行
        if(check){
            filterChain.doFilter(request,response);
            return;
        }
        //如果已经登录,直接放行
        if(request.getSession().getAttribute("employee") != null){

            Long empId = (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);

            filterChain.doFilter(request,response);
            return;
        }

        // 移动端,如果已经登录,直接放行
        if(request.getSession().getAttribute("user") != null){

            Long userId = (Long) request.getSession().getAttribute("user");
            BaseContext.setCurrentId(userId);

            filterChain.doFilter(request,response);
            return;
        }

        //如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;

    }

常用注解

注解 说明
@Api 用在请求类上,例如Controller,表示对类的说明
@ApiModel 用在类上,通常是实体类,表示一个返回响应数据的信息
@ApiModelProperty 用在属性上,描述响应类的属性
@ApiOperation 用在请求的方法上,说明方法的用途、作用
@ApiImplicitParams 用在请求的方法上,表示一组参数说明
@ApiImplicitParam 用在@ApiImplicitParams注解中,指定一个请求参数的各个方面

20、Mybatis-plus新增分页

/**
 * @Description: 配置MP的分页插件
 */
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

代码使用:

@GetMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
        //log.info("page={},pageSize={},name={}",page,pageSize,name);
        //构造分页构造器
        Page pageInfo = new Page(page,pageSize);
        //构造条件构造器
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper();

        //添加排序条件,根据sort进行排序
        queryWrapper.orderByAsc(Category::getSort);

        //执行查询
        categoryService.page(pageInfo,queryWrapper);

        return R.success(pageInfo);
    }

21、随机生成验证码工具类

/**
 * 随机生成验证码工具类
 */
public class ValidateCodeUtils {
    /**
     * 随机生成验证码
     * @param length 长度为4位或者6位
     * @return
     */
    public static Integer generateValidateCode(int length){
        Integer code =null;
        if(length == 4){
            code = new Random().nextInt(9999);//生成随机数,最大为9999
            if(code < 1000){
                code = code + 1000;//保证随机数为4位数字
            }
        }else if(length == 6){
            code = new Random().nextInt(999999);//生成随机数,最大为999999
            if(code < 100000){
                code = code + 100000;//保证随机数为6位数字
            }
        }else{
            throw new RuntimeException("只能生成4位或6位数字验证码");
        }
        return code;
    }

    /**
     * 随机生成指定长度字符串验证码
     * @param length 长度
     * @return
     */
    public static String generateValidateCode4String(int length){
        Random rdm = new Random();
        String hash1 = Integer.toHexString(rdm.nextInt());
        String capstr = hash1.substring(0, length);
        return capstr;
    }
}

空文件

简介

这是前台是vue,后台是springboot。 展开 收起
JavaScript 等 4 种语言
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
1
https://gitee.com/yang-leipeng/reggie.git
git@gitee.com:yang-leipeng/reggie.git
yang-leipeng
reggie
reggie
master

搜索帮助