0%

在开发中,我们会经常使用公司或其他平台的各种接口,每个接口的功能各不相同,但是它们的响应结果基本上是一致的,都会包含code(响应码)、msg(错误信息)、data(真实数据)这三部分,有些公司会使用Map来返回这些信息,但是需要编写重复性的代码,不太优雅,而Spring为我们提供了能够简化代码编写的功能,下面就来尝试一下吧

项目准备

  1. 创建一个SpringBoot项目并引入spring-boot-starter-web依赖,分别创建controller和service

    • UserController.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      @RequestMapping(value = "/user")
      @RestController
      public class UserController {

      @Resource
      private UserService userService;

      @GetMapping(value = "/id/{id}")
      public UserDO getById(@PathVariable(value = "id") Long id) {
      UserDO user = userService.getById(id);

      return user;
      }
      }
    • UserService.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @Service
      public class UserService {

      @Resource
      private UserMapper userMapper;

      public UserDO getById(Long id) {
      return userMapper.getById(id);
      }

      }
    • UserMapper.xml省略(可以直接硬编码返回,省略数据库配置)

  2. 测试controller返回结果

统一结果响应

  1. 创建CommonResponseCommonResponseAdvice

    • CommonResponse.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      @Getter
      @Setter
      @ToString
      public class CommonResponse<T> {

      /**
      * 响应是否成功:
      * true: 成功
      * false: 失败
      */
      private Boolean success;

      /**
      * 错误码
      */
      private int errCode;

      /**
      * 错误信息
      */
      private String errMsg;

      /**
      * 响应数据
      */
      private T data;

      public CommonResponse(Boolean success) {
      this.success = success;
      }

      public CommonResponse(Boolean success, T data) {
      this.success = success;
      this.data = data;
      }

      public CommonResponse(Boolean success, int errCode, String errMsg) {
      this.success = success;
      this.errCode = errCode;
      this.errMsg = errMsg;
      }

      /**
      *
      * @desc 响应成功无返回数据
      * @author codecho
      * @date 2021-12-05 15:22:07
      */
      public static <T> CommonResponse<T> success() {
      return new CommonResponse<>(true);
      }

      /**
      *
      * @desc 响应成功并返回数据
      * @author codecho
      * @date 2021-12-05 15:22:28
      * @param data 响应数据
      */
      public static <T> CommonResponse<T> success(T data) {
      return new CommonResponse<>(true, data);
      }

      /**
      *
      * @desc 响应失败,无错误码,有错误信息
      * @author codecho
      * @date 2021-12-05 17:37:39
      * @param errMsg 错误信息
      */
      public static<T> CommonResponse<T> fail(String errMsg) {
      return new CommonResponse<>(false, -1, errMsg);
      }

      /**
      *
      * @desc 响应失败,有错误码和错误信息
      * @author codecho
      * @date 2021-12-05 15:25:27
      * @param errCode 错误码
      * @param errMsg 错误信息
      */
      public static <T> CommonResponse<T> fail(int errCode, String errMsg) {
      return new CommonResponse<>(false, errCode, errMsg);
      }

      }
    • CommonResponseAdvice.java

      supports方法返回true表示响应结果需要进行重写

      beforeBodyWrite方法对响应结果进行封装

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @RestControllerAdvice(basePackages = {"com.codecho.base.controller"})
      public class CommonResponseAdvice implements ResponseBodyAdvice {

      @Override
      public boolean supports(MethodParameter returnType, Class converterType) {
      return true;
      }

      @Override
      public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
      return CommonResponse.success(body);
      }
      }
  2. 重新运行程序,再次测试controller返回结果

统一异常处理

  1. 当程序出现异常时,controller返回的结果并不是我们希望获得的

  2. 创建ResponseStatusEnum、CommonExceptionCommonExceptionHandler

    • ResponseStatusEnum.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      public enum ResponseStatusEnum {

      /**
      * 错误
      */
      ERROR_500(500, "服务器未知错误"),
      ERROR_400(400, "错误请求"),
      USER_NOT_FOUND(233, "用户不存在");

      private int code;

      private String msg;

      ResponseStatusEnum(int code, String msg) {
      this.code = code;
      this.msg = msg;
      }

      public int getCode() {
      return code;
      }

      public String getMsg() {
      return msg;
      }
      }
    • CommonException.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      public class CommonException extends RuntimeException {

      private static final long serialVersionUID = 6124960120588564481L;

      @Getter
      private int code;

      @Getter
      private ResponseStatusEnum statusEnum;

      public CommonException(String message) {
      super(message);
      }

      public CommonException(int code, String message) {
      super(message);
      this.code = code;
      }

      public CommonException(ResponseStatusEnum statusEnum) {
      super(statusEnum.getMsg());
      this.code = statusEnum.getCode();
      this.statusEnum = statusEnum;
      }

      }
    • CommonExceptionHandler.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      @Slf4j
      @RestControllerAdvice(basePackages = {"com.codecho.base.controller"})
      public class CommonExceptionHandler {

      /**
      * @desc 处理基础异常
      * @author codecho
      * @date 2022-11-23 15:59:48
      * @param ex 基础异常
      */
      @ExceptionHandler(value = {CommonException.class})
      public CommonResponse exceptionHandler(CommonException ex) {
      // 输出日志
      log.error("CommonException: {}", ex);

      return CommonResponse.fail(ex.getCode(), ex.getMessage());

      }

      // ...处理自定义或常见的异常

      /**
      * @desc 处理其他异常
      * @author codecho
      * @date 2022-11-23 16:00:05
      * @param ex 其他异常
      */
      @ExceptionHandler(value = {Exception.class})
      public CommonResponse exceptionHandler(Exception ex) {
      // 输出日志
      log.error("Exception: {}", ex);

      return CommonResponse.fail(ex.getMessage());
      }

      }
  3. 修改CommonResponseAdvice

    • CommonResponseAdvice.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      @RestControllerAdvice(basePackages = {"com.codecho.base.controller"})
      public class CommonResponseAdvice implements ResponseBodyAdvice {

      private static final String COMMON_RESPONSE = "CommonResponse";

      @Override
      public boolean supports(MethodParameter returnType, Class converterType) {
      return true;
      }

      @Override
      public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
      // 配置统一异常处理或controller返回类型为CommonResponse,直接返回,不需要再次封装
      if (Objects.nonNull(body) && COMMON_RESPONSE.equals(body.getClass().getSimpleName())) {
      return body;
      }

      return CommonResponse.success(body);
      }
      }
  4. 修改UserController的getById方法

    • UserController.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @GetMapping(value = "/{id}")
      public UserDO getById(@PathVariable(value = "id") Long id) {
      UserDO user = userService.getById(id);
      if (null == user) {
      throw new CommonException(ResponseStatusEnum.USER_NOT_FOUND);
      }

      return user;
      }
  5. 重新运行程序,测试controller返回结果

补充

有时候不是所有的接口都必须返回统一的格式,某些情况下我们想要自定义controller的返回,但是配置统一结果响应后,它是对注解@RestControllerAdvice的属性basePackages的包和其子包生效的。如果想要在某些使其不生效,可以考虑使用自定义注解和重写ResponseBodyAdvice接口的supports方法来实现

  1. 创建自定义注解@MyResponse

    • MyResponse.java

      1
      2
      3
      4
      5
      @Target(ElementType.METHOD)
      @Retention(RetentionPolicy.RUNTIME)
      public @interface MyResponse {

      }
  2. 修改CommonResponseAdvicesupports方法

    • CommonResponseAdvice.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Override
      public boolean supports(MethodParameter returnType, Class converterType) {
      // 如果方法上有@MyResponse注解,返回false,不需要设置统一响应结果
      if (returnType.hasMethodAnnotation(MyResponse.class)) {
      return false;
      }

      return true;
      }
  3. UserController中创建一个带有@MyResponse注解的请求

    • UserController.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @MyResponse
      @GetMapping(value = "/mock")
      public UserDO mockUser() {
      UserDO user = new UserDO();
      user.setUsername("july");
      user.setMobilePhone("18756989090");
      user.setUserState(0);

      return user;
      }
  4. 重新运行程序,测试新的请求

    可以看到,添加自定义注解后,统一结果响应未生效

需求背景

最近给电脑换桌面背景的时候,想起硬盘里还有之前大学时期下载的搜狗壁纸图片,于是从硬盘将壁纸文件夹拷贝到电脑上,打开后发现有两个目录都保存有壁纸,每个都有几百张,但是有些是各自目录独有的,也有互相重复的。为了能够获得最完整的壁纸合集,打算将所有图片进行合并去重,很明显,这么重复枯燥的工作不可能人工去一张一张地比对处理(其实我一开始真的手动处理了几张,发现实在hold不住),作为一名程序猿,当然要使用随身佩带的武器(Programming)来披荆斩棘,虽然我是Java开发者,但是显然这种场景最适合出手的还是Python这把小巧轻灵的匕首。

多说一句,以前的搜狗壁纸真的是个很良心的壁纸软件,有很多精美的壁纸,还是免费的,后来好像因为不盈利而停止运行了,可惜之前没有多下载一些好看的壁纸。现在有时候找壁纸都是去国外的壁纸网站,没有之前直接在客户端下载那么方便了。

事先说明,我是一个Python菜鸟,学完Python基础后,如果长时间不用就要重新看教程的那种。

思路分析

  1. 首先遍历需要去重的图片集,计算图片的md5值(很多软件下载页面都会提供md5值用于检查数据完整性)
  2. 判断字典中是否存在该图片的md5值,如果不存在,将md5值作为key,图片文件作为value存入字典中
  3. 如果字典中存在md5值,表示存在重复的图片,将当前图片标记或直接删除
  4. 如果图片名称不规则,遍历图片集,一一重命名

开始上手

  1. 重复图片预览

  2. 主代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    # -*- coding: utf-8 -*-
    import hashlib
    import os


    # 获取图片md5值
    def get_md5(filename):
    file_txt = open(filename, 'rb').read()
    m = hashlib.md5(file_txt)
    return m.hexdigest()


    def main():
    path = 'D:\搜狗壁纸下载\python_test'
    md5_dict = {} # md5字典
    file_count = 0 # 总图片数
    real_count = 0 # 去重后图片数
    dup_count = 0 # 重复图片数
    for file in os.listdir(path):
    real_path = os.path.join(path, file)
    if os.path.isfile(real_path):
    file_md5 = get_md5(real_path)
    file_count += 1

    if md5_dict.get(file_md5):
    # 当前图片不在md5对应的图片列表中,说明该图片重复
    if not (file in md5_dict.get(file_md5)):
    print('发现相似图片------> ', '当前图片: ', file, '相似图片: ', md5_dict.get(file_md5))
    md5_dict.get(file_md5).append(file)
    dup_count += 1
    # os.remove(real_path) # 删除重复图片
    # print('删除图片: ', file)
    else:
    md5_dict[file_md5] = [file]

    print('========================================================================')
    print('========================================================================')
    print('========================================================================')

    real_count = len(md5_dict)
    print('总图片数: ', file_count, '| 重复图片数: ', dup_count, '| 去重后图片数: ', real_count)
    for key in md5_dict.keys():
    if len(md5_dict.get(key)) > 1:
    print('图片md5: ', key, '相似图片: ', md5_dict.get(key))
    else:
    print('图片: ', md5_dict.get(key), '无相似图片')


    def rename_wallpaper():
    path = 'D:\搜狗壁纸下载\python_test'
    file_name_prefix = '壁纸'
    count = 0
    for file_name in os.listdir(path):
    count += 1
    file_name_suffix = os.path.splitext(file_name)[-1]
    os.renames(os.path.join(path, file_name), os.path.join(path, file_name_prefix + str(count) + file_name_suffix))


    if __name__ == '__main__':
    main()
    # rename_wallpaper()
  3. 查看控制台输出(未实际删除)

  4. 查看删除后目录

这里只是提供了一种去除重复文件的简单思路,实际上有很多方法都可以实现这个功能,不过作为程序猿,这种折腾、自己造轮子的经历也是帮助我们提高自己的一种手段。

  1. 序列化

    • 概念:

      将Java中的对象转换为字节序列

    • 使用场景:

      如在程序停止运行后,需要将之前的对象状态保存到文件中,在程序重新启动后,可以根据该文件重新读取相应的对象信息(持久化存储);

      在网络中传输对象

    • 实现方式:

      实现java.io.Serializable接口

    • 注意点:

      基本类型、数组、枚举等也会进行序列化

      transient修饰的属性和静态属性不参与序列化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      and the values of the non-transient
      and non-static fields of the class and all of its supertypes are
      written.

      if (obj instanceof String) {
      writeString((String) obj, unshared);
      } else if (cl.isArray()) {
      writeArray(obj, desc, unshared);
      } else if (obj instanceof Enum) {
      writeEnum((Enum<?>) obj, desc, unshared);
      } else if (obj instanceof Serializable) {
      writeOrdinaryObject(obj, desc, unshared);
      } else {
      if (extendedDebugInfo) {
      throw new NotSerializableException(
      cl.getName() + "\n" + debugInfoStack.toString());
      } else {
      throw new NotSerializableException(cl.getName());
      }
      }

      private void writeArray(xxx) {
      if (ccl.isPrimitive()) {
      if (ccl == Integer.TYPE) {
      int[] ia = (int[]) array;
      bout.writeInt(ia.length);
      bout.writeInts(ia, 0, ia.length);
      } else if (ccl == Byte.TYPE) {
      byte[] ba = (byte[]) array;
      bout.writeInt(ba.length);
      bout.write(ba, 0, ba.length, true);
      } else if (ccl == Long.TYPE) {
      ......
      }
      ......
      }

      }
    • 代码实现:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      class Student implements Serializable {
      private String name;
      private int number;
      // 被transient标记的属性不参与序列化,创建对象时赋值18,序列化后重新读取该属性的值为0
      private transient int age;
      // 静态属性不参与序列化,序列化后该属性值为null
      private static String gender;

      public void showStudent() {
      System.out.println("name: " + name);
      System.out.println("number: " + number);
      System.out.println("age: " + age);
      }

      .......省略getter、setter方法
      }

      @Test
      public void serializable() {
      // 创建对象
      Student student = new Student();
      student.setName("小花");
      student.setNumber(36);
      student.setAge(18);
      student.setGender("female");
      String greeting = "HelloWorld";

      // 创建FileOutputStream
      // 创建ObjectOutputStream
      try (FileOutputStream fos = new FileOutputStream("D:\\MyWork\\student.ser");
      ObjectOutputStream oos = new ObjectOutputStream(fos)) {
      // 将对象写入到文件
      oos.writeObject(student);
      oos.writeObject(greeting);
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
  2. 反序列化

    • 概念:

      将字节序列转换为Java对象

    • 使用场景

      如在程序停止运行后,需要将之前的对象状态保存到文件中,在程序重新启动后,可以根据该文件重新读取相应的对象信息(持久化存储)

      在网络中传输对象,接收字节序列后重新读取对象信息

    • 注意点:

      必须确保该读取程序的CLASSPATH中包含有序列化类的class文件

    • 代码实现:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      @Test
      public void deserializable() {
      Student student = null;
      String greeting = "";
      // 创建FileInputStream
      // 创建ObjectInputStream
      try (FileInputStream fis = new FileInputStream("D:\\MyWork\\student.ser");
      ObjectInputStream ois = new ObjectInputStream(fis);) {
      // 从文件读取对象
      student = (Student) ois.readObject();
      greeting = (String) ois.readObject();
      } catch (IOException | ClassNotFoundException e) {
      e.printStackTrace();
      }

      // 使用对象
      if (null != student) {
      student.showStudent();
      }
      System.out.println(greeting);
      }

      // 输出结果
      name: 小花
      number: 36
      age: 0
      gender: null
      HelloWorld
  3. serialVersionUID

    • 概念:

      序列化版本号,用来标识当前实现序列化接口的类,如果不显式指定,编译器会给类自动生成一个serialVersionUID

    • 作用:

      先看一个场景,在上述(1)中,Student类定义了name、number、age、gender四个属性,然后我们创建一个student对象并将其序列化保存到student.ser文件中,然后我们在Student类中增加一个属性,如className,再通过反序列化读取student.ser文件中的字节信息,转换成student对象,此时程序会报错

      1
      java.io.InvalidClassException: com.easyfun.base.Student; local class incompatible: stream classdesc serialVersionUID = -6313947761321746093, local class serialVersionUID = 4891437043098475710

      根据异常信息找到相应的代码:java.io.ObjectStreamClass#initNonProxy,当原class的序列化版本号和当前class的序列化版本号不一致会导致反序列化失败

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      void initNonProxy(xxx){
      long suid = Long.valueOf(model.getSerialVersionUID());
      ......
      if (model.serializable == osc.serializable &&
      !cl.isArray() &&
      suid != osc.getSerialVersionUID()) {
      throw new InvalidClassException(osc.name,
      "local class incompatible: " +
      "stream classdesc serialVersionUID = " + suid +
      ", local class serialVersionUID = " +
      osc.getSerialVersionUID());
      }
      ......
      }

      现在我们在Student类中添加上serialVersionUID(可以通过Idea自动生成,方法可自行搜索),然后调用序列化方法后,在Student类增加一个属性className,然后再调用反序列化方法,发现此次序列化不会再报错了

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      private static final long serialVersionUID = -6313947761321746093L;


      // 输出结果
      name: 小花
      number: 36
      age: 0
      gender: null
      className: null
      HelloWorld

      因此serialVersionUID的作用就比较好理解了,我们可以显式去指定某个类的序列化版本号,这样我们在反序列化时,即使该类的结构发生了变化,也能保证反序列化能够正常进行

JDK版本现在已经更新到20了,但是目前大部分企业和开发者仍然使用的是JDK8(你发任你发,我用JAVA8),不过随着时间的推移,切换到新的JDK版本是大势所趋。因此作为开发者,为了保证我们能够持续进步,了解和掌握新版本的功能和特性是非常重要和有价值的。在已有一个稳定版本(如JDK8)的基础上,我们可以选择再安装一个新版本(如JDK17)来供我们学习和测试,由于大部分JAVA应用是部署在Linux上的,所以这次我们通过shell脚本的方式来切换JDK版本。

思路分析

  1. 安装JDK后需要在/etc/profile中配置环境变量,因此我们的目标就是更改配置文件
  2. 想要更改配置文件,首先需要知道要修改的内容,我的配置是export JAVA_HOME=/usr/java/jdk,这里=后面的路径就是我们要修改的内容
  3. 由于每个人的/etc/profile文件各不相同,因此上述配置项可能在x行,也可能在y行,所以我们需要找到该配置项所在的位置
  4. 找到配置项位置后,我们将原JDK路径替换为目标JDK路径
  5. 再使用source命令更新配置信息即可完成JDK版本的切换

开始动手

  1. 创建脚本文件

    1
    touch switch_jdk.sh
  2. 编写脚本文件

    简要说一下思路

    • 定义一个数组来存放JDK版本和路径信息,使用==关联数组==,有点类似hashmap,key为JDK版本号,value为JDK安装路径
    • 执行脚本格式为. switch_jdk.sh version,其中version即为要切换的JDK版本号,在shell脚本中使用$1(脚本第一个参数)可以获取version的值
    • 获取到目标version后,在关联数组中获取其对应的JDK安装路径,如果获取不到,则表示目标version不存在,输出提示信息
    • 根据配置项export JAVA_HOME获取其所在的行位置信息,使用grep查找配置项所在行信息,使用awk提取行号
    • 使用sed命令替换该行配置内容,-i表示直接修改文件内容,79c表示替换第79行所在的内容
    • 使用source命令更新环境变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #!/bin/sh

    # here are jdk versions you installed
    my_versions=([8]="/usr/java/jdk" [17]="/usr/java/jdk17")

    if [ -z ${my_versions[$1]} ]
    then
    echo "jdk$1 does not exist! please check whether you installed"
    else
    # 79:export JAVA_HOME=/usr/java/jdk
    line_number=`grep "export JAVA_HOME" -n profile | awk -F ":" '{print $1}'`

    sed -i "79c export JAVA_HOME=${my_versions[$1]}" /etc/profile

    source /etc/profile

    echo "switch to jdk$1 successfully!"

    java -version
    fi
  3. 执行脚本并测试

    • 先查看当前JDK版本,我这里是JDK8

      1
      java -version
    • 执行脚本,切换到JDK17

      1
      . switch_jdk.sh 17
    • 执行脚本,切换到不存在的版本

      1
      . switch_jdk.sh 10

==注意==

一开始,我使用./switch_jdk.sh来执行的脚本(已添加执行权限),控制台输出的JDK版本信息并没有更新,检查了下脚本,觉得思路没有什么问题,于是新开一个terminal测试,但是使用java -version查看JDK版本时,发现已经是新版本了。

这时候我猜测可能是脚本在当前session没有生效,在开启的新session中生效了,那只有可能是source命令的问题了,其他命令和环境变量没有关系。

查看了下相关信息,发现执行shell脚本的方式不同,其执行效果也是不同的,下面给出相关的信息

  1. ./xxx.sh,用此种方式执行脚本,会开启子shell来执行脚本,继承父shell环境变量,但是不会更改父shell环境变量,即更改只对子shell生效
  2. sh xxx.sh,此种方式效果同上
  3. source xxx.sh,等价于. xxx.sh,在当前shell执行脚本,环境变量更改对当前shell生效

对于脚本中存在source或其他会影响到环境变量的命令,最好使用source.来执行脚本

从刚工作时使用的JDK8到现在的JDK17,Java的版本一直在飞速迭代,每个版本都会引入一些新特性和优化改进,比如模块化系统、var类型推断、switch表达式增强、record类、instanceof增强、Sealed类/接口、新的GC等,下面就让我们来体验一下这些新特性吧。

==对于某些不太好测试的特性,这里就忽略了,如模块化系统、ZGC等==

var类型推断

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static void main(String[] args) {
    var greeting = "Hello, JDK17";
    var num1 = 10;
    long num2 = 1000L;
    var sum = num1 + num2;
    var result = sum % 2 != 0;

    System.out.println("greeting: " + greeting);
    System.out.println("" + num1 + " + " + num2 + " = " + sum);
    System.out.println("result: " + result);
    }
  • 输出

    1
    2
    3
    greeting: HELLO, JDK17
    10 + 1000 = 1010
    result: false
  • class文件

    可以看到,var只是一种语法糖,编译时会自动转成对应的类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static void main(String[] args) {
    String greeting = "Hello, JDK17";
    int num1 = 10;
    long num2 = 1000L;
    long sum = (long)num1 + num2;
    boolean result = sum % 2L != 0L;
    System.out.println("greeting: " + (String)greeting.transform(String::toUpperCase));
    System.out.println("" + num1 + " + " + num2 + " = " + sum);
    System.out.println("result: " + result);
    }

Http请求客户端

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static void main(String[] args) throws IOException, InterruptedException {
    HttpClient httpClient = HttpClient.newHttpClient();
    HttpRequest httpRequest = HttpRequest
    .newBuilder()
    .uri(URI.create("http://localhost:9090/user/get/202000"))
    .build();
    HttpResponse<String> httpResponse = httpClient
    .send(httpRequest, HttpResponse.BodyHandlers.ofString());
    System.out.println("response: " + httpResponse.body());
    }
  • 输出

    1
    response: {"data":{"id":1001,"userNo":"10010","nickname":"codecho","mobilePhone":"18755667788","createTime":"2023-03-08 12:04:20","updateTime":"2023-03-08 12:04:20"},"success":true}

switch表达式增强

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public static void main(String[] args) {
    int month = 4;
    System.out.println("month: " + month + ", season: " + getSeason(4));
    }

    public static String getSeason(int month) {
    String season = switch (month) {
    case 3, 4, 5 -> "Spring";
    case 6, 7, 8 -> "Summer";
    case 9, 10, 11 -> "Autumn";
    case 12, 1, 2 -> {
    System.out.println("month: " + month);
    yield "Winter";
    }
    default -> "illegal month!";
    };

    return season;
    }
  • 输出

    1
    month: 4, season: Spring
  • class文件

    可以看到,switch增强也是语法糖,和普通的switch没什么区别

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    public static void main(String[] args) {
    int month = 4;
    System.out.println("month: " + month + ", season: " + getSeason(4));
    }

    public static String getSeason(int month) {
    String var10000;
    switch (month) {
    case 1:
    case 2:
    case 12:
    System.out.println("month: " + month);
    var10000 = "Winter";
    break;
    case 3:
    case 4:
    case 5:
    var10000 = "Spring";
    break;
    case 6:
    case 7:
    case 8:
    var10000 = "Summer";
    break;
    case 9:
    case 10:
    case 11:
    var10000 = "Autumn";
    break;
    default:
    var10000 = "illegal month!";
    }

    String season = var10000;
    return season;
    }

文本块

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static void main(String[] args) {

    var text = """
    {
    "name": "codecho",
    "age": 28,
    "gender": "male"
    }
    """;
    System.out.println("text block: ");
    System.out.println(text);
    }
  • 输出

    1
    2
    3
    4
    5
    6
    text block: 
    {
    "name": "codecho",
    "age": 28,
    "gender": "male"
    }
  • class文件

    实际还是使用的转换符

    1
    2
    3
    4
    5
    public static void main(String[] args) {
    String text = "{\n \"name\": \"codecho\",\n \"age\": 28,\n \"gender\": \"male\"\n}\n";
    System.out.println("text block: ");
    System.out.println(text);
    }

record类

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    public record Boy(String name, int age) {

    }

    public static void main(String[] args) {
    Boy boy = new Boy("Gary", 12);
    System.out.println("the boy's name is " + boy.name() + ", he is " + boy.age() + " years old");
    }
  • 输出

    1
    the boy's name is Gary, he is 12 years old
  • 反编译class文件

    实际继承了Record类(==注意:当前类是final的,不可继承==),编译器自动添加了各个属性方法,和equals、hashCode、toString方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public abstract class Record {

    protected Record() {}

    @Override
    public abstract boolean equals(Object obj);

    @Override
    public abstract int hashCode();

    @Override
    public abstract String toString();
    }

instanceof增强

  • 增强前

    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) {
    Object o = "instanceof";
    if (o instanceof String) {
    String str = (String) o;
    System.out.println("str: " + str);
    }
    }
  • 增强后

    1
    2
    3
    4
    5
    6
    public static void main(String[] args) {
    Object o = "instanceof";
    if (o instanceof String str) {
    System.out.println("str: " + str);
    }
    }

sealed密封类/接口

  • 代码

    • permit:指定可以继承的类/接口
    • sealed`:密封类/接口
    • no-sealed:非密封类/接口
    • final:不可被继承类/接口
    • 子类继承被sealed标记的超类后,同样要求指定密封性,使用sealedno-sealedfinal其中之一指定
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // MotoVehicle只能由Bus、Car继承
    public sealed class MotoVehicle permits Bus, Car {

    }

    // Bus不能再被继承
    public final class Bus extends MotoVehicle {

    }

    // Car非密封,可以被继承
    public non-sealed class Car extends MotoVehicle {

    }

    // Suv继承Car
    public class Suv extends Car {

    }
  • 作用

    使用密封类/接口可以更好地限制类/接口的继承范围,提高代码的安全性和可维护性。

一、linux系统启动
  1. 修改root用户密码

    1
    sudo passwd root
  2. 停机和关机

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    shutdown -H		停机
    shutdown -P 关机(断电)
    shutdown -r 重启
    shutdown -h 相当于 -P ,除非指定 -H
    shutdown [options] [time] 在指定时间执行操作
    shutdown -h now 马上关机
    shutdown -h +n 在 n 分钟后关机
    shutdown -h 13:14 在今天 13:14 关机

    shutdown -k 只发送 wall message
    shutdown -c 取消 shutdown 命令 执行
    shutdown --no-wall 执行 shutdown 命令前不发送 wall message
二、linux系统目录结构
  1. 输入 ls -/ 命令查看目录结构

    1
    bin  boot  dev  etc  home  init  lib  lib64  media  mnt  opt  proc  root  run  sbin  snap  srv  sys  tmp  usr  var
  2. 目录结构的作用和含义

    • /bin 存放常用的命令,如 cat、chmod、cp 等
    • /boot 存放启动linux需要的核心文件
    • /dev 存放linux的外部设备
    • /etc 存放系统管理需要的配置文件和子目录
    • /home 用户主目录,以用户名命名,如 /codecho
    • /lib 存放系统最基本的动态链接共享库,作用和windows中的DLL文件类似,几乎每个程序都使用到这些共享库
    • /lost+found 一般为空,当系统非法关机后,该目录存放一些“孤儿???”文件
    • /media linux识别设备后将其挂载到此目录
    • /mnt 系统临时挂载点
    • /opt 存放安装的第三方软件
    • /proc 虚拟的文件系统,非真实存在,记录和系统相关的信息
    • /root root用户主目录
    • /sbin 存放root用户使用的命令
    • /srv 存放一些服务启动后需要的数据
    • /sys 也是一个虚拟的文件系统,是对linux系统下所有设备的映射
    • /tmp 存放临时文件
    • /usr 存放用户的程序和文件
    • /usr/bin 存放一般的应用程序
    • /usr/sbin 存放root用户使用的应用程序
    • /usr/src 存放linux内核源码
    • /var 存放经常会变化的文件,比如系统的日志
    • /run 存放系统启动后的信息,重启时清空
三、linux文件基本属性
  1. 使用 ls -l 或 ll 命令显示文件/文件夹的属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    drwxr-xr-x  23 root root      4096 Jan 25 19:47 ./
    drwxr-xr-x 23 root root 4096 Jan 25 19:47 ../
    drwxr-xr-x 2 root root 4096 Jan 25 17:23 bin/
    drwxr-xr-x 3 root root 4096 Dec 12 2018 boot/
    drwxr-xr-x 18 root root 3740 Jan 25 16:29 dev/
    drwxr-xr-x 91 root root 4096 Jul 19 09:49 etc/
    drwxr-xr-x 3 root root 4096 Dec 26 2018 home/
    lrwxrwxrwx 1 root root 33 Dec 12 2018 initrd.img -> boot/initrd.img-4.15.0-42-generic
    lrwxrwxrwx 1 root root 33 Dec 12 2018 initrd.img.old -> boot/initrd.img-4.15.0-20-generic
    drwxr-xr-x 19 root root 4096 Dec 12 2018 lib/
    drwxr-xr-x 2 root root 4096 Jan 25 17:22 lib64/
    drwx------ 2 root root 16384 Dec 12 2018 lost+found/
    drwxr-xr-x 4 root root 4096 Dec 12 2018 media/
    drwxr-xr-x 2 root root 4096 Apr 27 2018 mnt/
    drwxr-xr-x 3 root root 4096 Dec 27 2018 opt/
    dr-xr-xr-x 134 root root 0 Jan 25 16:29 proc/
    drwx------ 11 root root 4096 Jul 16 20:19 root/
    drwxr-xr-x 25 root root 800 Jul 19 09:09 run/
    drwxr-xr-x 2 root root 4096 Dec 27 2018 sbin/
    drwxr-xr-x 2 root root 4096 Apr 27 2018 srv/
    -rw------- 1 root root 993249280 Dec 12 2018 swapfile
    dr-xr-xr-x 13 root root 0 Jan 26 00:29 sys/
    drwxr-xr-x 3 root root 4096 Jan 25 19:47 test/
    drwxrwxrwt 22 root root 4096 Jul 19 10:58 tmp/
    drwxr-xr-x 12 root root 4096 Jan 25 18:36 usr/
    drwxr-xr-x 12 root root 4096 Jun 18 15:40 var/
    • 第一个字符为 d 时,表示是目录
    • 第一个字符为 - 时,表示是文件
    • 第一个字符为 l 时,表示是链接文件
    • 第一个字符为 b 时,表示是装置文件里面的可供储存的接口设备(可随机存取装置)
    • 第一个字符是 c 时,表示是装置文件里面的串行端口设备,例如键盘、鼠标(一次性读取装置)
    • rwx rwx rwx 分别表示 文件所有者(owner) 、所有者同组用户(group)、其他用户(others) 拥有该文件的权限
    • r 表示读权限,- 表示没有读权限
    • w 表示写权限,- 表示没有写权限
    • x 表示执行权限,- 表示没有执行权限
    • 特殊情况: t 表示 sticky bit ,可以理解为 防删除位,一般用在目录上,如 /temp 目录,用户只能删除自己的文件,不能删除其他用户的文件
  2. 更改文件属性的命令

    • chgrp:更改文件所属的用户组 -R 表示递归更改所属的用户组,更改某个目录下的所有文件的所属的用户组
    1
    chgrp [-R] 所属的用户组 文件/目录
    • chown:更改文件所属用户,也可以同时更改文件的所属的用户组

      1
      2
      chown [-R] 所属的用户 文件/目录
      chown [-R] 所属的用户:所属的用户组 文件/目录
    • chmod:更改文件的9个属性 其中 xyz 表示三个权限的数字之和,如下所示

      1
      2
      3
      4
      5
      6
      -----------------使用数字改变文件的权限-----------------
      chmod [-R] xyz 文件/目录
      各权限代表的数字:r=4 w=2 x=1 -=0(没有权限则为0)
      x: [r+w+x] = [4+2+1] --> 7 表示 owner 所拥有的权限
      y: [r+w+x] = [4+2+1] --> 7 表示 group 所拥有的的权限
      z: [r+w+x] = [4+2+1] --> 7 表示 others 所拥有的权限
      1
      2
      3
      4
      5
      6
      -----------------使用符号改变文件的权限-----------------
      将用户分为四种类型:a=all(所有用户) u=user(所属用户) g=group(所属用户组) o=others(其他用户)
      将操作分为 +(添加) -(去除) =(设置)
      chmod u=rw,g=wx,o=x test.txt 将test.txt文件的权限设为rw--wx--x
      chmod a-w test.txt 去除所有用户对test.txt文件的写权限
      chmod a+x test.txt 添加所有用户对test.txt文件的执行权限
四、linux文件与目录的操作
  1. linux根目录:/

  2. 文件/目录的常用操作命令

    • ls:列出目录

      1
      2
      3
      ls -a 列出所有的文件/目录,包括以 . 开头的文件
      ls -d 列出目录本身,不包括其内容
      ls -l 列出所有详细信息,包括权限、日期、所属用户等
    • cd:切换目录

      1
      2
      3
      cd [绝对路径/相对路径] 
      cd / 切换到根目录
      cd .. 返回当前目录的上一级目录
    • pwd:显示当前目录

      1
      pwd	显示当前目录
    • mkdir:创建目录

      1
      2
      mkdir -m xyz 目录名 创建目录时同时设置目录的权限,如 mkdir -m 777 test
      mkdir -p 目录1/目录2/目录3 递归创建多级目录,如 test1/hello/world
    • rmdir:删除空目录

      1
      rmdir -p 目录1/目录2/目录3 连带其上级目录一起删除
    • cp:复制文件/目录

      1
      2
      cp [options] source dest
      cp -r test test1/test2 递归复制
    • rm:删除目录或文件

      1
      2
      3
      4
      rm [-fir] 文件/目录
      rm -f 文件/目录 直接删除文件/目录,没有警告信息
      rm -i 文件/目录,删除前显示警告
      rm -r 文件/目录,递归删除
    • mv:移动目录/文件,或者修改名称

      1
      2
      3
      mv [-fi] source dest
      mv -f source dest 若目标文件存在,直接覆盖
      mv -i source dest 若目标文件存在,询问是否覆盖
  3. linux文件内容查看

    • cat:从第一行开始显示内容

      1
      2
      3
      4
      cat -b test.txt 列出行号,空白行不显示行号
      cat -n test.txt 列出行号,空白行也显示行号
      cat -E test.txt 显示结尾的断行字节 &
      cat -T test.txt 将 tab 制表符显示为 ^|
    • tac:从最后一行开始显示内容,和 cat 相反

    • tail:显示文件最后 n 行内容

      1
      tail [-n number] 文件名 number 为要显示的行数
五、linux用户和用户组管理
  1. 添加用户账号

    • useradd [options] 用户名

      1
      2
      3
      4
      5
      6
      sudo useradd -d /home/pain -m pain
      -d 指定用户主目录
      -m 如果主目录不存在则创建
      -g 指定用户所属用户组
      -G 指定用户所属的附加组
      -s 指定用户登录shell
  2. 设置用户账号密码

    • passwd [options] 账号

      1
      2
      3
      -l 锁定账号
      -e 强制账号密码过期
      -d 强制删除账号密码
  3. 删除用户账号

    • userdel [options] 账号

      1
      2
      userdel -r pain 
      -r 将账号的主目录也删除
  4. 修改用户账号

    • usermod [options] 账号

      1
      2
      3
      4
      usermod -s /bin/hello -d /home/hello -g test pain
      -s 指定新的登录shell
      -d 指定新的用户主目录
      -g 指定新的用户组
  5. 添加用户组

    • groupadd [options] 用户组

      1
      2
      -g GID 指定用户组的id
      -o 表示新用户组的id可以和已有用户组的id相同
  6. 删除用户组

    • groupdel 用户组
  7. 修改用户组

    • groupmod [options] 用户组

      1
      2
      groupmod -g 102 group2
      将group2的用户组id改为102
  8. 切换用户组(前提是该用户属于要切换的用户组)

    • newgrp 要切换的用户组
六、linux磁盘管理
  1. df:查看文件系统的整体磁盘空间使用量

    1
    2
    3
    df [options] 目录/文件
    -a 列出所有的文件系统
    -T 显示文件系统类型
  2. du:查看文件和目录的磁盘使用空间

    1
    2
    3
    du [options] 文件/目录
    -a 列出所有的文件与目录容量
    -h 以易读的方式显示
七、linux vi/vim
  1. vi/vim主要分为三种模式:命令行模式、编辑模式、底部命令行模式

    • 命令行模式:此时用户输入会被识别为命令

      1
      2
      3
      i 切换到编辑模式
      x 删除光标所在位置的字符
      : 切换到底部命令行模式
    • 编辑模式:可以输入内容编辑文件,按下 esc ,退出编辑模式

    • 底部命令行模式:输入命令

      1
      2
      3
      4
      q 退出
      w 保存
      wq 保存退出
      q! 不保存退出
    • 常用按键:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      —————————————底部命令行使用的复制粘贴、搜索替换等操作—————————————
      /key 寻找光标后方的名为 key 的字符串
      ?key 寻找光标前方的名为 key 的字符串
      n 重复上一个寻找的操作,如执行 /key 寻找光标后方的名为 key 的字符串,按下 n 继续向光标后方寻找
      N 反向重复上一个寻找的操作,如上,按下 N 向光标前方寻找
      :n1,n2s/key1/key2/g 在n1行到n2行之间寻找名为key1的字符串,并将其替换为key2
      :1,$s/key1/key2/g 从第一行到最后一行之间寻找key1的字符串,并将其替换为key2
      :$s/word1/word2/g 同上,写法不同
      :1,$s/key1/key2/gc 从第一行到最后一行之间寻找key1的字符串,并将其替换为key2,取代之前会有提示信息,让用户确认是否替换
      :$s/word1/word2/gc 同上,写法不同
      dd 删除光标所在的一整行
      nx 连续向后删除 n 个字符
      ndd 删除光标后方的 n 行
      d$ 删除光标到该行最后一个字符
      d0 删除光标到改行第一个字符
      yy 复制光标所在行的内容
      nyy 复制光标后方的 n 行内容
      y$ 复制光标到该行最后一个字符
      y0 复制光标到该行第一个字符
      p,P p 将复制的内容粘贴到光标的下一行, P 将复制的内容粘贴到光标的上一行
      c 重复删除多个数据,如 10cj 删除光标下方10行
      u 复原前一个动作
      ctrl+r 重做前一个动作

      —————————————底部命令行模式切换到编辑模式的操作—————————————
      i,I i 从光标处输入 I 从光标所在行第一个非空格符处输入
      a,A a 从光标处的下一个字符处输入 A 从光标所在行最后一个字符处输入
      o,O o 从光标处的下一行输入 O 从光标处的上一行输入

      —————————————编辑模式切换到底部命令行模式的操作—————————————
      ESC 从编辑模式退出到底部命令行模式

      —————————————底部命令行模式切换到命令行模式的操作—————————————
      :w 保存内容
      :w! 强制保存
      :q 不保存退出
      :q! 强制不保存退出
      :wq 保存并退出
八、linux yum命令
  1. yum:查找、安装、删除软件包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    yum [options] [command] [package]
    -y 安装过程选择 yes
    -q 不显示安装过程
    command 要执行的命令
    package 软件包

    yum check-update 列出可更新的软件
    yum update 更新所有软件
    yum list 列出可安装的软件
    yum install package 安装指定的软件
    yum update package 更新指定的软件
    yum remove package 删除指定的软件
九、Shell脚本
  1. shell:一种应用程序,它是用户和linux内核之间的桥梁,用户通过shell来间接地使用内核

  2. shell脚本:一种为shell编写的脚本程序

  3. shell的分类:

    • Bourne Shell(/usr/bin/sh或/bin/sh)
    • Bourne Again Shell(/bin/bash)是linux的默认shell
    • C Shell(/usr/bin/csh)
    • K Shell(/usr/bin/ksh)
    • Shell for Root(/sbin/sh)
  4. 查看系统可用的shell

    1
    2
    3
    4
    5
    6
    7
    8
    cat /etc/shells

    /bin/sh
    /bin/bash
    /bin/rbash
    /bin/dash
    /usr/bin/tmux
    /usr/bin/screen
  5. 查看当前系统的默认shell

    1
    2
    3
    echo $SHELL

    /bin/bash
  6. 编写一个简单的shell脚本

    1
    2
    #!/bin/bash
    echo "hello world,this is a shell script"
  7. 执行shell脚本:

    1
    2
    3
    4
    5
    ./helloworld.sh		注意要在shell脚本所在目录
    hello world,this is a shell script

    /bin/sh helloworld.sh
    hello world,this is a shell script
十、linux命令(经常使用的)
  1. 文件管理

    • cat [options] filename

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      -n | --number	从1开始对输出的行数编号
      -b | --number-nonblank 和-n相似,但是对空白行不编号
      -s | --squeeze-blank 连续两行以上空白行,替换为一行空白行
      -E | --show-ends 在每行结束处显示 $
      -T | --show-tabs 将 TAB 符显示为 ^|

      cat test1.txt 显示test1.txt中的内容
      cat test1.txt test2.txt 同时显示test1.txt和test2.txt中的内容
      cat test1.txt test2.txt > test3.txt 将test1.txt和test2.txt中的内容写到test3.txt中
      cat /dev/null > test4.txt 将test4中的内容清空

      /dev/null 是一个特殊的设备文件,它丢弃一切写入其中的数据(报告写入成功),读取它则会立即得到一个 EOF
    • chgrp:参考 三

    • chmod:同上

    • chown:同上

    • find path [options],命令行上第一个 -(),! 之前的部分为path,后面的是expression,如果path为空字符串,使用当前路径,如果expression为空字符串,使用-print为预设expression

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      find . -name ".txt"		在当前目录查找所有以 .txt 结尾的文件
      find . -iname "T*" 查找文件名满足T*的文件,忽略大小写
      find . -cmin +10 查找修改时间超过10分钟的文件
      find . -amin +10 查找超过10分钟内被访问的文件
      find . -newer test3.txt 查找比test3.txt文件修改时间更晚的文件
      find . -anewer test3.txt 查找比test3.txt文件更晚被读取的文件
      find . -type c 查找文件类型是 c 的文件
      c的取值
      d 目录
      l 符号链接
      f 普通文件
      c 字符设备
      b 块设备
      s 套接字
    • mv:参考 三

    • rm:同上

    • tee [-ai][file]

      1
      2
      3
      tee test1.txt -a	读取用户输入的内容,将其输出到test1.txt文件中
      -a | --append 附加到文件后面,而非覆盖
      -i | --ignore-interrupts 忽略中断信号

自定义配置类,注入ObjectMapper

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Configuration
    public class JacksonConfig {

    @Bean
    @Primary
    @ConditionalOnMissingBean(ObjectMapper.class)
    public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
    ObjectMapper objectMapper = builder.createXmlMapper(false).build();
    objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    return objectMapper;
    }

    }
  • 测试

    配置前:

    配置后:

在网上看到有关Java中代码执行顺序的题目,第一次回答发现和答案不一致,于是想要亲自做个实验验证一下,解答心中的疑惑。

代码准备

  1. 创建父类Dog

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public class Dog {

    private String name = getName();

    private static String gender = getGender();

    public String getName() {
    System.out.println("==>父类普通成员变量初始化");
    return "Dog";
    }

    public static String getGender() {
    System.out.println("==>父类静态成员变量初始化");
    return "male";
    }

    static {
    System.out.println("==>父类静态代码块1");
    }

    {
    System.out.println("==>父类普通代码块1");
    }

    public Dog() {
    System.out.println("==>父类构造方法");
    }

    static {
    System.out.println("==>父类静态代码块2");
    }

    {
    System.out.println("==>父类普通代码块2");
    }

    }
  2. 创建子类Husky

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public class Husky extends Dog {

    private int age = getAge();

    private static String color = getColor();

    public int getAge() {
    System.out.println("==>子类普通成员变量初始化");
    return 2;
    }

    public static String getColor() {
    System.out.println("==>子类静态成员变量初始化");
    return "BlackAndWhite";
    }

    static {
    System.out.println("==>子类静态代码块1");
    }

    {
    System.out.println("==>子类普通代码块1");
    }

    public Husky() {
    System.out.println("==>子类构造方法");
    }

    static {
    System.out.println("==>子类静态代码块2");
    }

    {
    System.out.println("==>子类普通代码块2");
    }

    }
  3. 创建测试主类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class TestRunOrder {

    public static void main(String[] args) {

    new Husky();
    System.out.println("");
    new Husky();

    }

    }

测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
==>父类静态成员变量初始化
==>父类静态代码块1
==>父类静态代码块2
==>子类静态成员变量初始化
==>子类静态代码块1
==>子类静态代码块2
==>父类普通成员变量初始化
==>父类普通代码块1
==>父类普通代码块2
==>父类构造方法
==>子类普通成员变量初始化
==>子类普通代码块1
==>子类普通代码块2
==>子类构造方法

==>父类普通成员变量初始化
==>父类普通代码块1
==>父类普通代码块2
==>父类构造方法
==>子类普通成员变量初始化
==>子类普通代码块1
==>子类普通代码块2
==>子类构造方法

结论

  • 父类静态成员变量/代码块>子类静态成员变量/代码块
  • 父类普通成员变量/代码块>子类普通成员变量/代码块
  • 父类构造方法>子类构造方法
  • 静态成员变量/代码块、普通成员变量/代码块执行顺序和代码位置一致

  1. 起别名

    • 优点

      • 便于理解
      • 如果要查询的字段有重名的情况,使用别名可以区分开来
    • 使用方式

      1
      2
      3
      4
      5
      6
      # 方式一
      select count(*) '总预约数' from reservation_record
      # 方式二
      select count(*) as '总预约数' from reservation_record
      # 如果别名是关键字,可以使用 '' 或 "" 包裹别名
      select count(*) as "select" from reservation_record
  2. 去重

    • 使用方式

      1
      select distinct hospital_id from reservation_record 
  3. “+”号的作用:mysql中的 “+” 只用来作为运算符

    • 使用方式

      1
      2
      3
      4
      5
      6
      # 如果两个操作数都是数值型,做加法运算
      select 500+20
      # 如果其中一个操作数为字符型,先尝试将其转换为数值型,如果转换成功,做加法运算;否则将其当作 0 ,再做加法运算
      select "500"+20
      # 如果其中一个操作数为 null ,则结果必为 null
      select 500+null
  4. concat()函数实现拼接

    • 说明

      • 使用concat()函数可以拼接多个内容
    • 使用方式

      1
      select CONCAT('how ','are ','u') as title
  5. 比较运算符

    • 说明

      运算符 作用
      >、<、= 大于、小于、等于
      !=、<> 不等于,<>是mysql的标准写法,建议使用<>
      >=、<= 大于等于、小于等于
      <=> 严格比较两个null值是否相等,均为null时结果为1,只有一个为null时结果为0
      between、not between (不)在两个范围之间
      in、not in (不)在集合中
      is null、is not null (不)为空
      like 模糊匹配
    • 通配符

      • %:替代0个或多个字符

      • _:替代单个字符

      • 转义:

        1
        2
        3
        4
        # 通过 "\"转义符 ,这里 \ 后的 % 不再是通配符
        select * from reservation_record where title like '_\%%'
        # 通过 escape关键字 ,这里 & 之后的 % 不再是通配符
        select * from reservation_record where title like '_&%%' escape '&'
    • 使用方式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      # 查找年龄大于1的宠物信息
      select * from pet_archives where age > 1
      # 查找年龄不等于2的宠物信息
      select * from pet_archives where age <> 2
      # 查找年龄大于等于2的宠物信息
      select * from pet_archives where age >= 2
      # 查找年龄不在1~2范围的宠物信息
      select * from pet_archives where age not between 1 and 2
      # 查找宠物主id在(1,3,5)中的宠物信息
      select * from pet_archives where pet_user_id in (1,3,5)
      # 查找品种不为null的宠物信息
      select * from pet_archives where variety is not null
      # 查找标题第三个字符为“哈”的预约信息
      select * from reservation_record where title like '__哈%'
  6. 逻辑运算符

    • 说明:用于连接条件表达式

      运算符 作用
      &&或and 逻辑与
      ||或or 逻辑或
      !或not 逻辑非
      xor 逻辑异或
    • 使用方式

      1
      2
      3
      4
      5
      6
      7
      8
      # 查找性别为m,年龄大于1的宠物信息
      select * from pet_archives where gender = 'm' and age > 1
      # 查找性别为m或者年龄大于1的宠物信息
      select * from pet_archives where gender = 'm' or age > 1
      # 查找性别不为m的宠物信息
      select * from pet_archives where not gender = 'm'
      # 查找性别为m,年龄小于等于1、性别不为m,年龄大于1的宠物信息
      select * from pet_archives where gender = 'm' xor age > 1
  7. 排序查询

    • 说明

      • select 查询列表 from 表名 [where 查询条件] order by 排序列表 asc|desc
      • 默认升序
    • 使用方式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      # 按照年龄从大到小顺序查询宠物信息
      select * from pet_archives order by age desc
      # 按照评分(按表达式排序)从低到高顺序查询预约信息
      select *,rate*1.0 from reservation_record order by rate*1.0 asc
      # 按照评分(按别名排序)从低到高顺序查询预约信息
      select *,rate*1.0 评分 from reservation_record order by 评分 asc
      # 按照标题长度(按函数排序)从大到小顺序查询预约信息
      select * from reservation_record order by length(title) desc
      # 按照年龄从大到小,id从小到大顺序(按多字段排序)查询宠物信息
      select * from pet_archives order by age desc,id asc
  8. 字符函数

    • length(str)
      • 返回参字符串的字节个数

        1
        select length(title) 字节数 from reservation_record
    • char_length(str)
      • 返回字符串的字符个数

        1
        select char_length(title) 字符数 from reservation_record
    • concat(str1,str2,…)
      • 拼接多个字符串

        1
        select concat(title,reservation_date) from reservation_record
    • lower(str)
      • 将字符串转为小写

        1
        select lower('HELLO')
    • upper(str)
      • 将字符串转为大写

        1
        select upper('hello')
    • substr(str,pos)
      • 截取从pos位置及末尾所有字符

        1
        select SUBSTR('welcometomysql',10)
    • substr(str,pos,len)
      • 截取从pos位置开始,字符长度为len的字符

        1
        select SUBSTR('welcometomysql',8,2)
    • instr(str,substr)
      • 返回substr子串在str中出现的第一个位置,找不到返回0

        1
        select instr('HelloWorldWorld','World')
    • trim(str)
      • 去除字符串str开始和结尾的空格

        1
        select trim('    hellowor ld   ')
    • trim(remstr from str)
      • 去除字符串str开始和结尾的remstr字符

        1
        select TRIM('A' FROM 'Ahelloworld')
    • lpad(str1,len,str2)
      • 在str1字符串开始处填充字符串str2,使其长度达到len,如果len小于原长度,从右边截断

        1
        select lpad('HELLO',8,'z')
    • rpad(str1,len,str2)
      • 在str1字符串结尾处填充字符串str2,….

        1
        select rpad('HELLO',8,'z')
    • replace(str1,s,str2)
      • 使用str2字符串替换str1字符串中的s字符串

        1
        select replace('hello','e','a')
  9. 数字函数

    • round(x)
    • 对x四舍五入,等同于round(x,0)
    • round(x,d)
      • 对x四舍五入,保留d位小数,d为负数时,指定小数点左边d位整数位为0,同时小数位也为0

        1
        2
        select round(3213.1415926,-2)
        # 3200
    • ceil(x)
      • 向上取整,返回>=x的最小整数

        1
        2
        select ceil(1.23)
        # 2
    • floor(x)
      • 向下取整,返回<=x的最大整数

        1
        2
        select floor(1.01)
        # 1
    • truncate(x,d)
    • 截断x,保留d位小数,不会进行四舍五入,d为负数时,指定小数点左边d为整数位为0
      1
      2
      select truncate(1.234,-2)
      # 0
    • mod(x,y)
      • 返回x除以y后的余数

      • 运算规则:x-x/y*y

        1
        select mod(-10,3)
  10. 日期函数

    • now()
  • 返回系统当前日期和时间
      
    1
    2
    select NOW()
    # 2019-08-12 09:35:59
    • current_date、curdate
      • 返回系统当前日期

        1
        2
        select CURRENT_DATE()
        # 2019-08-12
    • current_time、curtime
      • 返回系统当前时间

        1
        2
        select CURRENT_TIME()
        # 09:37:27
    • year(d)
      • 根据d返回年份

        1
        2
        3
        4
        select year(now())
        # 2019
        select year('2019-08-12')
        # 2019
    • month(d)
      • 根据d返回月份

        1
        2
        select month(now())
        # 8
    • day(d)
      • 根据d返回日

        1
        2
        select day(now())
        # 12
    • hour(d)
      • 根据d返回小时

        1
        2
        select hour(now())
        # 9
    • minute(d)
      • 根据d返回分钟

        1
        2
        select minute(now())
        # 41
    • second(d)
      • 根据d返回秒

        1
        2
        select second(now())
        # 50
    • str_to_date(str,format)
      • 将日期格式的字符串转换成指定格式的日期

        1
        2
        select str_to_date('2019-08-12 9:42:00','%Y-%m-%d %H:%i:%s')
        # 2019-08-12 09:42:00
    • date_format(date,format)
      • 将日期转换成字符串

        1
        2
        3
        4
        select date_format(now(),'%Y_%m_%d %H:%i:%s')
        # 2019_08_12 09:49:51
        select date_format('2019/08/12 09:51:00','%Y/%m/%d %H:%i:%s')
        # 2019/08/12 09:51:00
    • mysql日期格式
      格式 描述
      %Y 4位年份
      %y 2位年份
      %M 英文月名(January…December)
      %m 月份(01,02,03…12)
      %c 月份(1,2,3…12)
      %D 带有英文的天
      %d 天(00,01,02,03…31)
      %e 天(0,1,2,3…31)
      %H 小时,二十四小时制(00,01,02…23)
      %h 小时,十二小时制(01,02,03…12)
      %i 分钟(00,01,02…59)
      %s 秒(00,01,02…59)
  1. 其他函数

    • version()

      • 查询数据库版本

        1
        2
        select version()
        # 8.0.14
    • database()

      • 查询当前数据库名称

        1
        2
        select database()
        # pet_hospital
    • user()

      • 查看当前用户

        1
        2
        select user()
        # root@101.231.252.114
  2. 流程控制函数

    • if(expr,v1,v2)

      • 如果表达式 expr 成立,返回结果 v1;否则,返回结果 v2

        1
        2
        select IF(10%2=0,'偶数','奇数')
        # 偶数
    • case结构

      • CASE 表示函数开始,END表示函数结束。如果condition1成立,返回result1;如果condition2成立,返回result2;如果所有condition都不成立,返回result;其中一个condition成立,后面的condition不再判断

      • 语法

        1
        2
        3
        4
        5
        6
        7
        CASE expression
        WHEN condition1 THEN result1
        WHEN condition2 THEN result2
        ...
        WHEN conditionN THEN resultN
        ELSE result
        END
      • 简单case函数

        1
        2
        3
        4
        5
        6
        7
        8
        select 
        case age
        when 1 then '一岁大的狗'
        when 2 then '两岁大的狗'
        when 3 then '三岁大的狗'
        else '老狗'
        end '年龄'
        from pet_archives
      • case搜索函数

        1
        2
        3
        4
        5
        6
        7
        8
        select 
        case
        when age = 1 then '一岁大的狗'
        when age = 2 then '两岁大的狗'
        when age = 3 then '三岁大的狗'
        else '老狗'
        end '年龄'
        from pet_archives
      • 注意:简单case函数只返回第一个符合条件的值,剩下的自动忽略

  3. 分组函数

    • 功能:用作统计使用,又称作聚合函数、统计函数

    • 分类

      • 求和:sum

        1
        select sum(age) from pet_archives
      • 平均值:avg

        1
        select avg(age) from pet_archives
      • 最大值:max

        1
        select max(age) from pet_archives
      • 最小值:min

        1
        select min(age) from pet_archives
      • 统计数量:count

        1
        select count(*) from pet_archives
    • 特点

      • sum()、avg()一般用于处理数值型
      • max()、min()、count()可以处理任意类型
      • 以上函数都可以忽略null值
      • 可以和distinct关键字搭配上述函数进行去重
    • count()函数详解

      • count(*):对行的数目进行统计,包含NULL
      • count(column):对指定列不为NULL的行的数目进行统计
      • 效率
        • MYISAM引擎下,count(*)的效率高
        • INNODB引擎下,count(*)和count(1)的效率差不多,比count(column)要高一些
      • 注意:和分组函数一同查询的字段要求是group by后面的字段
  4. 分组查询

    • group by

      • 语法

        1
        2
        3
        4
        5
        select 分组函数,字段(需要出现在group by后面)
        from table
        [where 筛选条件]
        group by 分组列表
        [order by 字句]
    • 特点

      • 分组查询中的筛选条件分为两类

        数据源 位置 关键字
        分组前筛选 原始表 group by子句前面 where
        分组后筛选 分组后的结果集 group by子句后面 having
      • 分组函数做条件肯定是放在having子句中

      • 能用分组前筛选的,就优先考虑使用分组前筛选

    • 按函数分组

      • 示例

        1
        select count(*),length(name) lth from pet_archives group by lth
    • 按多个字段分组

      • 示例

        1
        select id,name,age lth from pet_archives group by name,id,age
    • 添加排序

      • 示例

        1
        select id,name,age from pet_archives group by id,name,age order by age desc
  5. 连接查询

    • 含义:多表查询,查询的字段来自多个表时,使用连接查询

    • 笛卡尔乘积:A表m行数据,B表n行数据,查询结果X=m*n行数据

      • 原因:没有有效的连接条件
      • 解决:添加有效的连接条件
    • 连接查询分类

      • 按年代分类
        • sql92标准:仅仅支持内连接
        • sql99标准:推荐,支持内连接+外连接(左外和内外)+交叉连接
      • 按功能分类
        • 内连接
          • 等值连接
          • 非等值连接
          • 自连接
        • 外连接
          • 左外连接
          • 右外连接
          • 全外连接
        • 交叉连接
    • sql92标准

      • 等值连接

        • 多表等值连接的结果为多表的交际部分

        • n表连接,至少需要n-1个连接条件

        • 多表的顺序没有要求

        • 一般需要给表起别名,方便书写、避免字段模棱两可

        • 可以和之前介绍的子句配合使用,如排序、分组、筛选

        • 示例

          1
          select a.name 宠物,a.age 宠物年龄,b.nickname 主人 from pet_archives a,pet_owner_user b where a.pet_user_id = b.id and a.age > 1 group by a.name,a.age,b.nickname order by a.age desc
      • 非等值连接

        • 示例

          1
          select salary,grade_level from employee a,job_grade b where salary between b.lowest_salary and highest_salary
      • 自连接

        • 示例

          1
          select a.employee_id 员工号,a.name 员工姓名,b.employee_id 主管员工号,b.name 主管姓名 from employee a,employee b where a.manager_id = b.employee_id
    • sql99标准

      • 语法

        1
        select 查询列表 from table1 [连接类型] join table2 on 连接条件 [where 筛选条件] [group by 分组列表] [having 筛选条件] [order by 排序列表] 
      • 连接类型

        • 内连接:inner
        • 外连接
          • 左外:left [outer]
          • 右外:right [outer]
          • 全外:full [outer]
        • 交叉连接:cross
      • 内连接

        • 语法

          1
          select 查询列表 from table1 inner join table2 on 连接条件
        • 特点

          • 添加排序、分组、筛选
          • inner可以省略
          • 筛选条件放在where后面,连接条件放在on后面,提高阅读性
          • inner join连接和sql92中的等值连接效果一样,都是查询多表的交集
        • 等值连接

          • 示例:三表联查

            1
            select a.name,a.age,b.nickname from pet_archives a inner join pet_owner_user b on a.pet_user_id = b.id inner join visit_record c on a.id = c.pet_id group by a.name,a.age,b.nickname order by a.age desc
        • 非等值连接

          • 示例

            1
            2
            select count(*),grade_level from employee a join job_grade b on a.salary between b.lowest_salary and highest_salary group by grade_level having
            count(*) > 20 order by grade_level desc
        • 自连接

          • 示例

            1
            select a.name 员工姓名,b.name 主管姓名 from employee a join employee b on a.manager_id = b.employee_id
      • 外连接

        • 应用场景:用于查询表1有,表2没有的记录

        • 特点

          • 外连接的查询结果为主表中的所有记录,如果从表中有和它匹配的,显示匹配的值,如果从表中没有和它匹配的,显示NULL,外连接查询结果=内连接查询结果+主表中存在但从表中不存在的记录
          • 左外连接,left join左边的是主表;右外连接,right join右边的的是主表
          • 左外和右外交换两个表的顺序,可以实现同样的效果
          • 全外连接=内连接的结果+表1有但表2没有的记录+表2有但表1没有的记录
        • 左外连接

          1
          select name,nickname from pet_owner_user a left outer join pet_archives b on a.id = b.pet_user_id
        • 全外连接

          • 注意:MySQL不支持全连接,可以通过下面的sql来实现全连接
          1
          select a.name,b.nickname from pet_archives a left join pet_owner_user b on a.pet_user_id = b.id union select a.name,b.nickname from pet_archives a right join pet_owner_user b on a.pet_user_id = b.id
      • 交叉连接

        • 示例

          1
          select a.name,b.nickname from pet_archives a cross join pet_owner_user b
  6. 子查询

    • 含义:出现在其他语句中的select语句,称为子查询或内查询;外部的查询语句,称为主查询或外查询

    • 分类

      • 按子查询出现的位置
        • select后面:仅仅支持标量子查询
        • from后面:支持表子查询
        • where或having后面:标量子查询、列子查询、行子查询
        • exist后面(相关子查询):表子查询
      • 按结果集的行列数不同
        • 标量子查询(结果集只有一行一列)
        • 列子查询(结果集只有一列多行)
        • 行子查询(结果集只有一行多列)
        • 表子查询(结果集一般为多行多列)
    • where或having后面的子查询

      • 分类

        • 标量子查询(单行子查询)

        • 列子查询(多行子查询)

        • 行子查询(一行多列)

      • 特点

        • 子查询放在小括号内
        • 子查询一般放在条件的右侧
        • 标量子查询,一般配合单行操作符使用 > < >= <= = <> ;列子查询,一般配合多行操作符使用 IN ANY/SOME ALL
        • 子查询的执行优先于主查询执行,主查询的条件需要使用子查询的结果
      • 标量子查询

        1
        select min(salary),department_id from employee group by department_id having min(salary) > (select min(salary) from employee where department_id = 50)
      • 多行比较操作符

        操作符 含义
        IN/NOT IN 等于列表中的任意一个
        ANY/SOME 和子查询返回的某个值比较
        ALL 和子查询返回的所有值比较
      • 列子查询

        1
        select id,name,pet_user_id from pet_archives where pet_user_id in (select id from pet_owner_user where age > 20)
    • select后面的子查询

      • 示例

        1
        select a.nickname 姓名,(select count(*) from pet_archives b where a.id = b.pet_user_id) 宠物总数 from pet_owner_user a
    • from后面的子查询

      • 示例

        1
        select temp.*,a.grade_level from (select avg(salary) avg_salary,department_id from employee group by department_id) temp inner join job_grade b on temp.avg_salary between lowest_sal and highest_sal
    • exist后面的子查询

      • 示例

        1
        select a.nickname from pet_owner_user a where exists (select * from pet_archives b where a.id = b.pet_user_id)
  7. 分页查询

    • 应用场景:当要显示的数据,一页显示不完,需要分页提交sql请求

    • 语法

      • offset:要显示条目的起始索引(起始索引从0开始)
      • size:要显示的条目个数
      1
      select 查询列表 from table [join type join 表2 on 连接条件 where 筛选条件 group by 分组字段 having 分组后的筛选 oder by 排序列表] limit offset,size 
    • 特点

      • limit语句放在查询语句的最后
    • 示例

      1
      select id,name,pet_user_id from pet_archives group by id limit 0,5
  8. 联合查询

    • union:联合,合并,将多条查询语句的结果合并成一个结果

    • 语法

      1
      2
      3
      4
      5
      6
      查询语句1
      union
      查询语句2
      union
      查询语句3
      ...
    • 特点

      • 要求多条查询语句的查询列数是一致的
      • 多条查询语句的查询列的类型和顺序最好保持一致
      • union关键字默认去重,如果使用union all,可以包含重复项
    • 应用场景

      • 要查询的结果来自多个表,且多个表没有直接的连接,但查询的信息一致时
    • 示例

      1
      2
      3
      select * from pet_archives where name like '%哈%' 
      union
      select * from pet_archives where age > 1
  9. DML语言

    • 数据操纵语言

      • 插入:insert
      • 删除:delete
      • 修改:update
    • 插入语句

      • 方式1

        1
        insert into table(column1,...) values(value1,...)
      • 示例

        1
        insert into reservation_type(name,total,hospital_id) values('宠物寄养',0,1001)
      • 方式2

        1
        insert into table set column1 = value1,...
      • 示例

        1
        insert into reservation_type set name = '代遛狗',total = 0,hospital_id = 1001
      • 批量插入

        1
        insert into table(column1,...) values(value,...),(value,...),(value,...)
      • 示例

        1
        insert into reservation_type(name,total,hospital_id) values('测试1',0,1001),('测试2',0,1001),('测试3',0,1001)
      • insert方式1和方式2的比较

        • 方式1支持插入多行
        • 方式1支持子查询,方式2不支持
          • insert into table(column1,…) select xxx,…
    • 修改语句

      • 修改单表的记录

        1
        update table set column1 = value1,... where 筛选条件
      • 示例

        1
        update reservation_type set name = '代撸猫' where id = 2010
      • 修改多表的记录

        • sql92语法

          1
          update table1 别名,table2 别名,... set column1 = value1,... where 连接条件 and 筛选条件
        • sql99语法

          1
          update table1 别名 inner|left|right join table2 别名 on 连接条件 set column = value1,... where 筛选条件
      • 示例

        1
        update pet_archives a left join pet_owner_user b on a.pet_user_id = b.id set a.gender = 'f',b.gender = 'f' where b.id = 18
    • 删除语句

      • 方式1:delete

        • 删除单表的记录

          1
          delete from table where 筛选条件
        • 示例

          1
          delete from reservation_type where id = 2016
        • 删除多表的记录

          • sql92语法

            1
            delete table1的别名,table2的别名 from table1 别名,table2 别名 where 连接条件 and 筛选条件
          • sql99语法

            1
            delete table1的别名,table2的别名 from table1 别名 inner|left|right join table2 别名 on 连接条件 where 筛选条件
        • 示例

          1
          delete a,b from pet_archives a left join pet_owner_user b on a.pet_user_id = b.id where b.id = 18
      • 方式2:truncate

        • 语法

          1
          truncate table 表名
      • delete和truncate的比较

        • delete可以添加where筛选条件,truncate不可以添加筛选条件
        • truncate删除效率比delete要高一些
        • 使用delete删除记录后,再插入数据,自增列的值从断点开始;truncate删除记录后,再插入数据,自增列的值从1开始
  • delete删除有返回值;truncate删除没有返回值
    - delete是DML语句,可以使用rollback进行回滚;truncate是DDL语句,需要drop权限,因此会隐式提交,不能rollback
    
  1. DDL语言

    • 数据定义语言

      • 库的管理
        • 创建、修改、删除
      • 表的管理
        • 创建、修改、删除
      • 创建:create
      • 修改:alter
      • 删除:drop
    • 库的管理

      • 库的创建

        • 语法

          1
          create database 库名
      • 库的修改

        • 修改库名

          1
          rename 旧库名 to 新库名
        • 修改库的字符集

          1
          alter database 库名 character set utf8
      • 库的删除

        • 语法

          1
          drop database 库名
    • 表的管理

      • 表的创建

        • 语法

          1
          2
          3
          4
          create table 表名(
          列名 列类型(长度) 约束,# 注释
          ...
          )
        • 示例

          1
          2
          3
          4
          create table testTable(
          testId int(10) primary key,# 测试id
          testName varchar(255)# 测试名称
          )
      • 表的修改

        • 语法

          1
          alter table 表名 add|drop|modify|change column 列名 [列类型 约束]
        • 修改列名

          1
          alter table 表名 change column 旧列名 新列名 类型(长度) 
        • 修改列类型或约束

          1
          alter table 表名 modify column 列名 类型(长度)
        • 添加新列

          1
          alter table 表名 add column 列名 类型(长度)
        • 删除列

          1
          alter table 表名 drop column 列名
        • 修改表名

          1
          alter table 表名 rename to 新表名
      • 表的删除

        • 语法

          1
          drop table if exists 旧表名
      • 表的复制

        • 仅仅复制表的结构

          1
          create table 目标表 like 源表 
        • 复制表的结构和数据

          1
          create table 目标表 select * from 源表
        • 只复制部分数据

          1
          create table 目标表 select 查询列表 from 源表 where 筛选条件
        • 只复制某些字段

          1
          create table 目标表 select 要复制的字段 from 源表 where 0 
  2. 常见数据类型

    • 数值型
      • 整数:默认有符号,如果要设置无符号,使用unsigned关键字
        • tinyint:一个字节
        • smallint:两个字节
        • mediumint:三个字节
        • int/integer:四个字节
        • bigint:八个字节
      • 小数
        • 定点型
          • decimal(m,d):如果m>d,m+2个字节,否则d+2个字节
        • 浮点型
          • fload(m,d):四个字节,单精度
          • double(m,d):八个字节,双精度
    • 字符型
      • 较短的文本
        • char(m):固定长度字符串,m在0~255之间
        • varchar(m):可变长度字符串,m在0~65535之间
      • 较长的文本
        • text:长文本数据
        • blob:二进制长文本数据
    • 日期型
      • date:最小值1000-01-01,最大值9999-12-31
      • datetime:最小值1000-01-01 00:00:00,最大值9999-12-31 23:59:59
      • timestamp:最小值1970-01-01 00:00:00,最大值2038-01-19
      • year:最小值1901,最大值2155
      • time:最小值-838:59:59,最大值838:59:59
  3. 常见约束

    • 含义:为了保证表中数据的准确性和可靠性,对表中的数据进行的一种限制

    • 分类

      • not null:非空,保证该字段的值不能为空
      • default:默认,保证该字段有默认值
      • primary key:主键,保证该字段的值在表中是唯一的,并且不为空
      • unique:唯一,保证该字段的值具有唯一性,可以为空
      • check:检查约束,mysql不支持
      • foreign key:外键,限制两个表的关系,保证该字段的值必须来自主表的关联列的值,在从表中添加外键约束,用于引用主表中某列的值
    • 列级约束和表级约束

      • 列级约束:对一个数据列建立的约束,可在列定义时声明,也可在列定义后声明,not null、default只存在列级约束
      • 表级约束:对多个数据列简历的约束,只能在列定义后声明,primary key、unique、foreign key同时存在列级约束和表级约束
    • 创建表时添加约束

      • 添加列级约束:字段名 数据类型 约束类型

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        create table student(
        id int(11) primary key,# 主键
        st_name varchar(255) not null,# 非空
        gender char(1),
        age int default 0,# 默认
        c_id int foreign key reference class(id)# 外键
        )

        create table class(
        id int(11) primary key,# 主键
        c_name varchar(255) not null# 非空
        )
      • 添加表级约束:constraint 约束名 约束类型(字段名)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        create table student(
        id int,
        st_name varchar(255),
        gender varchar(255),
        age int,
        c_id int,

        constraint pk primary key(id),# 主键
        constraint uk unique(st_name),# 唯一
        constraint fk_c_id foreign key(c_id) reference class(id),# 外键
        )
    • 主键和唯一的比较

      • 主键:保证唯一性;不允许为空;一张表中只能有一个
      • 唯一:保证唯一性;允许为空;一张表中可以有多个
    • 外键特点

      • 需要在从表中设置外键关系
      • 从表的外键列的类型和主表的关联列的类型要求一致或兼容
      • 主表的关联列必须是一个key(一般为主键或唯一)
      • 插入数据时,先插入主表,再插入从表;删除数据时正好相反
    • 修改表时添加约束

      • 添加列级约束

        1
        alter table 表名 modify column 列名 数据类型 约束
      • 添加表级约束

        1
        alter table 表名 add constraint 约束名 约束类型(列名) [主表(关联列)]
      • 添加非空约束

        1
        alter table 表名 modify column 列名 数据类型 not null
      • 添加默认约束

        1
        alter table 表名 modify column 列名 数据类型 default 默认值
      • 添加主键

        1
        2
        3
        4
        # 列级约束
        alter table 表名 modify column 列名 数据类型 primary key
        # 表级约束
        alter table 表名 add primary key(列名)
      • 添加唯一

        1
        2
        3
        4
        # 列级约束
        alter table 表名 modify column 列名 数据类型 unique
        # 表级约束
        alter table 表名 add unique(列名)
      • 添加外键

        1
        alter table 从表名 add constraint 外键名称 foreign key(列名) reference 主表名(列名)
    • 修改表时删除约束

      • 删除非空约束

        1
        alter table 表名 modify column 列名 数据类型 NULL
      • 删除默认

        1
        alter table 表名 modify column 列名 数据类型
      • 删除主键

        1
        alter table 表名 drop primary key
      • 删除唯一

        1
        alter table 表名 drop index 约束名
      • 删除外键

        1
        alter table 表名 drop foreign key 约束名
  4. 标识列

    • 含义:不需要手动设置值,系统提供默认的序列值,又叫自增序列

    • 特点

      • 自增序列必须和主键配合使用吗?
        • 不一定,但是必须是一个key()
      • 一个表最多有几个自增序列?
        • 最多一个
      • 自增序列可以是什么类型?
        • 只能是数值型
      • 自增序列如何设置自增的步长值?
        • set auto_increment_increment = x
    • 创建表时设置自增序列

      1
      2
      3
      create table 表名(
      id int primary key auto_increment
      )
    • 修改表时设置自增序列

      1
      alter table 表名 modify column 列名 数据类型 [约束] auto_increment
    • 修改表时删除自增序列

      1
      alter table 表名 modify column 列名 数据类型
  5. 事务

    • 含义

      • 一条或多条sql语句执行时要么全部执行成功,要么全部失败
    • 特性

      • 原子性(Atomicity):事务中的操作像原子一样不可分割,要么全部成功,要么全部失败
      • 一致性(Consistency):事务的执行结果必须使数据库从一个一致性状态变为另一个一致性状态(系统状态满足数据的完整性约束;系统的状态反映数据库本应描述的现实世界的真实状态)
      • 隔离性(Isolation):并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样
      • 持久性(Durability):事务一旦提交,其对数据库的影响是永久性的。任何事务或系统故障都不会导致数据丢失
    • 事务的创建

      • 隐式事务:事务没有明显的开启和结束的标记,如insert、update、delete语句

      • 显式事务:事务具有明显的开启和结束的标记

        • 前提:必须设置禁用自动提交

        • 步骤

          1
          2
          3
          4
          5
          begin或start transaction
          sql语句,
          ...
          ...
          commit
    • 事务并发问题

      • 脏读:事务A读取了被事务B更新的数据,但事务B未提交,后面如果事务B回滚,事务A之前读取到的数据就是临时且无效的
      • 不可重复读:事务A第一次读取到一行记录row1,事务B提交后,事务A第二次读取到row1,但row1的数据已经发生变化
      • 幻读:事务A第一次读取到一行记录row1,事务B提交修改后,事务A第二次读取到row1和row2…
    • 数据库事务的隔离性:数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题

    • 事务隔离级别

      • READ UNCOMMITED:允许事务读取未被其他事务提交的变更,脏读、不可重复读、幻读都可能存在
      • READ COMMITED:只允许事务读取已经被其他事务提交的变更,可以避免脏读,但不可重复读、幻读可能存在
      • REPEATABLE READ:一个事务在其提交之前,读取的数据都是相同的,即使其他事务作了修改,但幻读可能存在
      • SERIALIZABLE:一个事务持续期间,禁止其他事务进行任何操作,但效率很低
    • Oracle支持的事务隔离级别:READ COMMITED(默认)、SERIALIZABLE

    • MySQL支持的事务隔离级别:READ UNCOMMITED、READ COMMITED、REPEATABLE READ(默认)、SERIALIZABLE

Arthas(阿尔萨斯)是什么

Arthas是Alibaba开源的Java诊断工具

Arthas能做什么

  1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  5. 是否有一个全局视角来查看系统的运行状况?
  6. 有什么办法可以监控到JVM的实时运行状态?
  7. 怎么快速定位应用的热点,生成火焰图?
  8. 怎样直接从JVM内查找某个类的实例?

开发中常见的问题

  1. 前端调用一个后端的接口,该接口逻辑非常复杂,可能会调用其他service接口或其他模块的FeignClient,但是在其中某个接口报错了,报错信息又不是很明显,这种情况下,一般要么分析代码逻辑,要么增加日志输出来排查发生异常的原因

    使用Arthas后,可以通过watch命令观察指定方法的调用情况,可以观察入参返回值异常信息本次调用的方法信息本次调用的类信息等,配合ognl表达式可以实现复杂的操作

  2. 后端改动或者增加一个复杂的接口后,联调时可能遇到一个问题,改一遍代码,然后pull代码,编译,重启服务,然后又遇到问题,再修改代码,再重复同样的操作,这些步骤十分的耗时,如果开发时间比较紧迫,更加让人难以忍受

    使用Arthas的jadmcretransform命令修改java代码,编译字节码文件,加载新的字节码,不需要重新pull代码,编译,重启等一系列耗时的操作,就能直接实现修改后的效果

如何使用

  1. 安装启动

    安装方式很多,有直接启动jar包、使用shell脚本、rpm安装等,这里使用直接启动jar包的方式

    1
    2
    3
    4
    5
    # 下载arthas-boot.jar
    curl -O https://arthas.aliyun.com/arthas-boot.jar

    # 启动
    java -jar arthas-boot.jar
  2. 选择要attach的进程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    [INFO] arthas-boot version: 3.5.4
    [INFO] Process 10294 already using port 3658
    [INFO] Process 10294 already using port 8563
    [INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
    * [1]: 10294 java-study-0.0.1-SNAPSHOT.jar
    [2]: 13008 org.apache.zookeeper.server.quorum.QuorumPeerMain
    [3]: 19681 /usr/nacos/target/nacos-server.jar
    [4]: 4819 org.elasticsearch.bootstrap.Elasticsearch
    [5]: 21381 /usr/my_service/java_study/java-study-0.0.1-SNAPSHOT.jar
    [6]: 22535 kafka.Kafka
    [7]: 21772 /usr/my_service/java_study/java-study-0.0.1-SNAPSHOT.jar
    1
    [INFO] arthas home: /root/.arthas/lib/3.5.4/arthas
    [INFO] The target process already listen port 3658, skip attach.
    [INFO] arthas-client connect 127.0.0.1 3658
    ,---. ,------. ,--------.,--. ,--. ,---. ,---.
    / O \ | .--. ''--. .--'| '--' | / O \ ' .-'
    | .-. || '--'.' | | | .--. || .-. |`. `-.
    | | | || |\ \ | | | | | || | | |.-' |
    `--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'


    wiki https://arthas.aliyun.com/doc
    tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
    version 3.5.4
    main_class
    pid 10294
    time 2021-09-15 10:18:15
  3. 输入help查看命令帮助信息

使用watch命令观察方法的调用情况,如入参,返回值,异常信息等

  1. watch命令参数

    参数名称 参数说明
    class-pattern 类名表达式匹配
    method-pattern 方法名表达式匹配
    express 观察表达式,默认值:{params, target, returnObj}
    condition-express 条件表达式
    [b] 方法调用之前观察
    [e] 方法异常之后观察
    [s] 方法返回之后观察
    [f] 方法结束之后(正常返回和异常返回)观察
    [E] 开启正则表达式匹配,默认为通配符匹配
    [x:] 指定输出结果的属性遍历深度,默认为 1
  2. 观察表达式

    观察表达式的构成主要由ognl表达式组成,使用它可以获取对象的属性,调用对象的方法等

    ognl表达式可以使用的对象有params(入参)、target(当前调用的类信息)、returnObj(返回值)、throwExp(异常信息)等,详细内容参考表达式核心变量

  3. 测试代码

    根据id查询用户信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Override
    public User getUserById(Long id) throws Exception {
    if (StringUtils.isEmpty(id)) {
    throw new Exception("用户id为空!");
    }

    List<User> collect = userList.stream().filter(u -> id.equals(u.getId()))
    .collect(Collectors.toList());
    if (CollectionUtils.isEmpty(collect)) {
    throw new Exception("根据id未找到用户信息");
    }

    return collect.get(0);
    }
  4. 观察方法调用情况

    • 观察方法入参和返回值

      执行watch命令:

      1
      2
      # {params, returnObj} 表示需要观察方法入参和返回值,-n 5 表示执行5次后结束 -x 3 表示遍历对象的深度
      watch com.codecho.test.service.UserService getUserById '{params, returnObj}' -n 5 -x 3

      调用接口:

      1
      curl http://localhost:8088/test/user/1

      查看控制台输出:这里result中包含了入参和返回的对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      [arthas@10294]$ watch com.codecho.test.service.UserService getUserById '{params, returnObj}' -n 5 -x 3
      Press Q or Ctrl+C to abort.
      Affect(class count: 2 , method count: 1) cost in 71 ms, listenerId: 25
      method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExit
      ts=2021-09-17 14:28:29; [cost=1.151249ms] result=@ArrayList[
      @Object[][
      @Long[1],
      ],
      @User[
      id=@Long[1],
      username=@String[1001],
      avatar=@String[https://xxxx.com/1001.png],
      phoneNumber=@String[10010010001],
      age=@Integer[20],
      gender=@String[男],
      ],
      ]
    • 观察方法抛出的异常信息

      执行watch命令:

      1
      2
      # {params, throwExp} 表示需要观察方法入参和异常对象
      watch com.codecho.test.service.UserService getUserById '{params, throwExp}' -n 5 -x 3

      调用接口:

      1
      curl http://localhost:8088/test/user/1001

      查看控制台输出:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      [arthas@10294]$ watch com.codecho.test.service.UserService getUserById '{params, throwExp}' -n 5 -x 3
      Press Q or Ctrl+C to abort.
      Affect(class count: 2 , method count: 1) cost in 70 ms, listenerId: 26
      method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExceptionExit
      ts=2021-09-17 14:29:56; [cost=2.055759ms] result=@ArrayList[
      @Object[][
      @Long[1001],
      ],
      java.lang.Exception: 根据id未找到用户信息
      at com.codecho.test.service.UserServiceImpl.getUserById(UserServiceImpl.java:48)
      at com.codecho.test.controller.TestController.getUserById(TestController.java:28)
      at sun.reflect.GeneratedMethodAccessor25.invoke(Unknown Source)
      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
      at java.lang.reflect.Method.invoke(Method.java:498)
      ...

      可以通过调整观察表达式只展示异常具体信息

      1
      watch com.codecho.test.service.UserService getUserById '{params, throwExp.getMessage}' -n 5 -x 3

      重新调用接口,再次查看控制台输出

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      [arthas@10294]$ watch com.codecho.test.service.UserService getUserById '{params, throwExp.getMessage}' -n 5 -x 3
      Press Q or Ctrl+C to abort.
      Affect(class count: 2 , method count: 1) cost in 71 ms, listenerId: 27
      method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExceptionExit
      ts=2021-09-17 14:31:08; [cost=1.685761ms] result=@ArrayList[
      @Object[][
      @Long[1001],
      ],
      @String[根据id未找到用户信息],
      ]
    • 只观察满足条件的方法调用

      执行watch命令:

      1
      2
      # params[0]%2==0 表示当用户id为偶数时才会记录调用信息
      watch com.codecho.test.service.UserService getUserById '{params, returnObj}' 'params[0]%2==0' -n 5 -x 3

      调用接口:

      1
      2
      3
      4
      curl http://localhost:8088/test/user/1
      curl http://localhost:8088/test/user/2
      curl http://localhost:8088/test/user/3
      curl http://localhost:8088/test/user/4

      查看控制台输出:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      [arthas@10294]$ watch com.codecho.test.service.UserService getUserById '{params, returnObj}' 'params[0]%2==0' -n 5 -x 3
      Press Q or Ctrl+C to abort.
      Affect(class count: 2 , method count: 1) cost in 71 ms, listenerId: 28
      method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExit
      ts=2021-09-17 14:32:06; [cost=0.067629ms] result=@ArrayList[
      @Object[][
      @Long[2],
      ],
      @User[
      id=@Long[2],
      username=@String[1002],
      avatar=@String[https://xxxx.com/1002.png],
      phoneNumber=@String[10010010002],
      age=@Integer[23],
      gender=@String[男],
      ],
      ]
      method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExit
      ts=2021-09-17 14:32:11; [cost=0.081801ms] result=@ArrayList[
      @Object[][
      @Long[4],
      ],
      @User[
      id=@Long[4],
      username=@String[1004],
      avatar=@String[https://xxxx.com/1004.png],
      phoneNumber=@String[10010010004],
      age=@Integer[21],
      gender=@String[女],
      ],
      ]

      执行watch命令:

      1
      2
      # #cost > 5 表示耗时超过5ms才会记录调用信息 -v 表示打印条件表达式的值和结果
      watch com.codecho.test.service.UserService * '{params, returnObj}' '#cost > 5' -v -n 1 -x 3

      调用接口:

      1
      curl http://localhost:8088/test/user/1

      查看控制台输出:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      [arthas@10294]$ watch com.codecho.test.service.UserService * '{params, returnObj}' '#cost > 5' -v -n 1 -x 3
      Press Q or Ctrl+C to abort.
      Affect(class count: 2 , method count: 8) cost in 76 ms, listenerId: 29
      Condition express: #cost > 5 , result: false
      Condition express: #cost > 5 , result: false
      Condition express: #cost > 5 , result: false
      Condition express: #cost > 5 , result: false
      Condition express: #cost > 5 , result: false
      Condition express: #cost > 5 , result: false

使用trace命令查看方法的调用路径和每个节点的耗时

  1. trace命令参数

    参数名称 参数说明
    class-pattern 类名表达式匹配
    method-pattern 方法名表达式匹配
    condition-express 条件表达式
    [E] 开启正则表达式匹配,默认为通配符匹配
    [n:] 命令执行次数
    #cost 方法执行耗时
  2. 观察接口调用路径

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    [arthas@40528]$ trace *ActionInfoServiceImpl interactionAssist -n 2
    Press Q or Ctrl+C to abort.
    Affect(class count: 2 , method count: 2) cost in 857 ms, listenerId: 4
    `---ts=2021-09-17 15:37:53;thread_name=http-nio-8325-exec-1;id=85;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@4258106
    `---[26.143404ms] com.xxx.interaction.base.actioninfo.service.ActionInfoServiceImpl$$EnhancerBySpringCGLIB$$34d24eff:interactionAssist() [throws Exception]
    +---[25.926649ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() [throws Exception]
    | `---[21.546574ms] com.xxx.interaction.base.actioninfo.service.ActionInfoServiceImpl:interactionAssist() [throws Exception]
    | +---[0.131916ms] com.alibaba.fastjson.JSONObject:toJSONString() #6957
    | +---[0.190136ms] org.apache.logging.log4j.Logger:info() #6957
    | +---[0.00505ms] com.xxx.core.util.UserUtil:getUser() #6960
    | +---[0.00535ms] com.xxx.interaction.base.xxx.model.ActionInfo:getInteractionId() #6966
    | +---[1.262189ms] com.xxx.interaction.base.interaction.service.InteractionService:queryByIdCache() #6966
    | +---[0.00564ms] com.xxx.interaction.base.interaction.model.Interaction:getEndTime() #6970
    | +---[0.00526ms] com.xxx.interaction.base.interaction.model.Interaction:getLicenseMode() #6977
    | +---[0.010319ms] com.xxx.interaction.base.xxx.service.ActionInfoServiceImpl:checkUserInfo() #6977
    | +---[0.004508ms] com.xxx.interaction.base.xxx.model.ActionInfo:getInteractionId() #6980
    | +---[0.00497ms] com.xxx.interaction.base.xxx.model.ActionInfo:getOpenId() #6980
    | +---[0.006282ms] com.xxx.interaction.base.xxx.model.ActionInfo:getMobilePhone() #6980
    | +---[0.127087ms] com.xxx.interaction.base.util.CommonUtils:tianYu() #6980
    | +---[0.005831ms] com.xxx.base.user.model.User:getOpenId() #6982
    | +---[0.005991ms] com.xxx.core.util.StringUtils:isBlank() #6983
    | +---[0.009688ms] com.xxx.interaction.base.interaction.model.Interaction:getBoostType() #6988
    | +---[0.008306ms] com.xxx.interaction.base.xxx.model.ActionInfo:getHpOpenid() #6988
    | +---[0.004388ms] com.xxx.core.util.StringUtils:isBlank() #6989
    | +---[0.007474ms] com.xxx.interaction.base.xxx.model.ActionInfo:setHpOpenid() #6992
    | +---[min=0.003366ms,max=0.007244ms,total=0.01061ms,count=2] com.xxx.interaction.base.interaction.model.Interaction:getInteractionFormat() #6997
    | +---[0.005109ms] com.xxx.interaction.base.interaction.model.Interaction:getShareMode() #7026
    | +---[0.004529ms] com.xxx.interaction.base.interaction.model.Interaction:getId() #7028
    | +---[0.005249ms] com.xxx.interaction.base.interaction.model.Interaction:getBoostType() #7035
    | +---[19.019048ms] com.xxx.interaction.base.xxx.service.ActionInfoServiceImpl:newMemBoostValidate() #7050 [throws Exception]
    | `---throw:com.xxx.core.basic.service.BasicServiceException #7585 [您已经是会员,无法助力!]
    `---throw:com.hand.core.basic.service.BasicServiceException #49 [您已经是会员,无法助力!]

使用jadmcretransform命令热更新代码

  1. 测试代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Override
    public void payMoney(User user) throws Exception {
    // 查找用户
    User queryUser = null;
    if (!StringUtils.isEmpty(user.getId())) {
    queryUser = getUserById(user.getId());
    } else {
    queryUser = getUserByUsername(user.getUsername());
    }

    // 测试调用普通方法
    callPublicMethod(queryUser);
    callPrivateMethod(queryUser);

    // 支付收款
    payService.pay(queryUser, new BigDecimal("88.8"));
    }
  2. 使用jad命令反编译要修改的类的字节码,并保存到指定目录

    1
    jad --source-only *UserServiceImpl > /usr/arthas/UserServiceImpl.java
  3. 使用vim编辑反编译后的java代码,将金额从88.8改成99.9

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public void payMoney(User user) throws Exception {
    // 查找用户
    User queryUser = null;
    if (!StringUtils.isEmpty(user.getId())) {
    queryUser = getUserById(user.getId());
    } else {
    queryUser = getUserByUsername(user.getUsername());
    }

    // 测试调用普通方法
    callPublicMethod(queryUser);
    callPrivateMethod(queryUser);

    // 支付收款
    payService.pay(queryUser, new BigDecimal("88.8"));
    }
  4. 使用mc命令内存编译修改后的java代码,-d 指定输出class的目录

    1
    mc /usr/arthas/UserServiceImpl.java -d /usr/arthas

    注意mc命令是可能编译失败的,如果编译失败,可以在本地修改java代码后将编译后的class文件上传到服务器上再使用mc命令

  5. 使用retransform命令加载修改后的字节码文件

    1
    retransform /usr/arthas/UserServiceImpl.class
  6. 使用watch命令观察接口入参,重新调用接口

    1
    watch *PayServiceImpl pay '{params[0], params[1].toString}' -n 5 -x 3
    1
    curl -H "Content-Type:application/json" -X POST -d '{"username": "1002"}' http://localhost:8088/test/user/payMoney
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 99.9 元的收款
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [arthas@10294]$ watch *PayServiceImpl pay '{params[0], params[1].toString}' -n 5 -x 3
    Press Q or Ctrl+C to abort.
    Affect(class count: 1 , method count: 1) cost in 48 ms, listenerId: 30
    method=com.codecho.test.service.PayServiceImpl.pay location=AtExit
    ts=2021-09-17 15:40:10; [cost=0.112135ms] result=@ArrayList[
    @User[
    id=@Long[2],
    username=@String[1002],
    avatar=@String[https://xxxx.com/1002.png],
    phoneNumber=@String[10010010002],
    age=@Integer[23],
    gender=@String[男],
    ],
    @String[99.9],
    ]
  7. 如果要取消retransform命令的效果,需要删除修改类的retransform entry 并重新触发retransform,如果不进行此操作,即使stop arthas后,retransform的效果仍会生效

    1
    2
    3
    4
    5
    6
    7
    8
    # 查看retransform entry
    retransform -l

    # 删除指定的retransform entry
    retransform -d 1

    # 重新触发retransform
    retransform --classPattern *UserServiceImpl

    使用watch命令观察接口入参,重新调用接口

    1
    watch *PayServiceImpl pay '{params[0], params[1].toString}' -n 5 -x 3
    1
    curl -H "Content-Type:application/json" -X POST -d '{"username": "1002"}' http://localhost:8088/test/user/payMoney
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 99.9 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 99.9 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 99.9 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [arthas@10294]$ watch *PayServiceImpl pay '{params[0], params[1].toString}' -n 5 -x 3
    Press Q or Ctrl+C to abort.
    Affect(class count: 1 , method count: 1) cost in 48 ms, listenerId: 30
    method=com.codecho.test.service.PayServiceImpl.pay location=AtExit
    ts=2021-09-17 15:40:10; [cost=0.112135ms] result=@ArrayList[
    @User[
    id=@Long[2],
    username=@String[1002],
    avatar=@String[https://xxxx.com/1002.png],
    phoneNumber=@String[10010010002],
    age=@Integer[23],
    gender=@String[男],
    ],
    @String[88.8],
    ]

quitstop

  1. quit

    退出当前arthas客户端,arthas服务端不会关闭,所做的修改不会重置

  2. stop

    关闭arthas服务端,所有arthas客户端都会退出,关闭前增强类会被重置,使用redefineredefine重加载的类不会被重置