不一样的科技宅

不一样的科技宅 查看完整档案

深圳编辑南昌大学  |  计算机科学与技术 编辑搬砖  |  JAVA端工程师 编辑 www.huangxunhui.com 编辑
编辑

每天进步一点点,体验不一样的生活。

个人动态

不一样的科技宅 发布了文章 · 1月16日

实战|省市区三级联动数据爬取

前言

  最近收到客服反应,系统的省市区数据好像不准,并且缺了一些地区。经过询问同事得知,数据库内的数据是从老项目拷贝过来的,有些年头了。难怪会缺一些数据。正好最近在对接网商银行,发现网商提供了省市区的数据的接口。这就很舒服了哇,抄起键盘就是干,很快的就把同步程序写好了。

  然后在同步的过程中,发现网商提供的数据和数据库有些对不上。于是默默的打开淘宝京东添加收货地址,看看到底是谁错了。对比到后面发现都有些差异。这就很蛋疼了。看来这个时候谁都不能相信了,只能信国家了。于是我打开了中华人民共和国民政部网站来比对异常的数据。

  对比的过程中,石锤网商数据不准。值得的是表扬淘宝京东已经同步了最新的数据了。但是呢,我并没有找到它们的数据接口。为了修正系统的数据,只能自己爬取了。

锁定爬取目标

爬取地址如下:

https://preview.www.mca.gov.c...

  爬取原理很简单,就是解析HTML元素,然后获取到相应的属性值保存下来就好了。由于使用Java进行开发,所以选用Jsoup来完成这个工作。

<!-- HTML解析器 -->
<dependency>
  <groupId>org.jsoup</groupId>
  <artifactId>jsoup</artifactId>
  <version>1.13.1</version>
</dependency>

网页数据分析

  由于需要解析HTML才能取到数据,所以需要知道数据存储在什么元素上。我们可以打开chrom的控制台,然后选中对应的数据,即可查看存储数据的元素。

  通过分析,发现每一行数据都是存储在一个<tr>标签下。我们需要的 区域码区域名称存储在第一和第二个<td>内 。与此同时还要很多空白<td>标签,在编写代码是需要将其过滤掉。

定义基础代码

  先定义好我们的爬取目标,以及Area实体类。

public class AreaSpider{

    // 爬取目标
    private static final String TARGET = "http://preview.www.mca.gov.cn/article/sj/xzqh/2020/2020/202101041104.html";

    @Data
    @AllArgsConstructor
    private static class Area {

        // 区域码
        private String code;

        // 区域名称
        private String name;

        // 父级
        private String parent;

    }
}

爬虫代码编写

public static void main(String[] args) throws IOException{
  // 请求网页
  Jsoup.connect(TARGET).timeout(10000).get()
    // 筛选出 tr 标签
    .select("tr")
    // 筛选出 tr 下的 td 标签
    .forEach(tr -> tr.select("td")
    // 过滤 值为空的 td 标签
    .stream().filter(td -> StringUtils.isNotBlank(td.text()))
    // 输出结果
    .forEach(td -> System.out.println(td.text())));
}

解析结果

代码优化

  通过上面的代码,我们已经爬取到了页面上的数据。但是并没有达到我们的预期,所以进一步处理将其转换为Area实体。

public static void main(String[] args) throws IOException{
  // 请求网页
  List<Area> areaList = Jsoup.connect(TARGET).timeout(10000).get()
    // 筛选出 tr 标签
    .select("tr")
    // 筛选出 tr 下的 td 标签
    .stream().map(tr -> tr.select("td")
    // 过滤 值为空的 td 标签,并转换为 td 列表
    .stream().filter(td -> StringUtils.isNotBlank(td.text())).collect(Collectors.toList()))
    // 前面提到,区域码和区域名称分别存储在 第一和第二个td,所以过滤掉不符合规范的数据行。
    .filter(e -> e.size() == 2)
    // 转换为 area 对象
    .map(e -> new Area(e.get(0).text(), e.get(1).text(), "0")).collect(Collectors.toList());
  
    // 遍历数据
  areaList.forEach(area -> System.out.println(JSONUtil.toJsonStr(area)));
}

解析结果

  至此,离我们想要的数据还差了父级区域码 ,我们可以通过区域码计算出来。以河北省为例:河北省:130000石家庄市:130100长安区:130102可以发现规律:0000 结尾是省份,00是市。所以就有了如下代码:

private static String calcParent(String areaCode){
    // 省 - 针对第一行特殊处理
    if(areaCode.contains("0000") || areaCode.equals("行政区划代码")){
        return "0";
    // 市
    }else if (areaCode.contains("00")) {
        return String.valueOf(Integer.parseInt(areaCode) / 10000 * 10000);
    // 区
    }else {
        return String.valueOf(Integer.parseInt(areaCode) / 100 * 100);
    }
}

最终代码

public class AreaSpider{

    // 爬取目标
    private static final String TARGET = "http://preview.www.mca.gov.cn/article/sj/xzqh/2020/2020/202101041104.html";

    @Data
    @AllArgsConstructor
    private static class Area{

        // 区域码
        private String code;

        // 区域名称
        private String name;

        // 父级
        private String parent;

    }

    public static void main(String[] args) throws IOException{
        // 请求网页
        List<Area> areaList = Jsoup.connect(TARGET).timeout(10000).get()
                // 筛选出 tr 标签
                .select("tr")
                // 筛选出 tr 下的 td 标签
                .stream().map(tr -> tr.select("td")
                // 过滤 值为空的 td 标签,并转换为 td 列表
                .stream().filter(td -> StringUtils.isNotBlank(td.text())).collect(Collectors.toList()))
                // 前面提到,区域码和区域名称分别存储在 第一和第二个td,所以过滤掉不符合规范的数据行。
                .filter(e -> e.size() == 2)
                // 转换为 area 对象
                .map(e -> new Area(e.get(0).text(), e.get(1).text(), calcParent(e.get(0).text()))).collect(Collectors.toList());

        // 去除 第一行 "行政区划代码|单位名称"
        areaList.remove(0);

        areaList.forEach(area -> System.out.println(JSONUtil.toJsonStr(area)));
    }

    private static String calcParent(String areaCode){
        // 省 - 针对第一行特殊处理
        if(areaCode.contains("0000") || areaCode.equals("行政区划代码")){
            return "0";
        // 市
        }else if (areaCode.contains("00")) {
            return String.valueOf(Integer.parseInt(areaCode) / 10000 * 10000);
        // 区
        }else {
            return String.valueOf(Integer.parseInt(areaCode) / 100 * 100);
        }
    }
}

数据修正

  由于我们需要的是省市区三级数据联动,但是了直辖市只有两级,所以我们人工的给它加上一级。以北京市为例:变成了 北京 -> 北京市- >东城区,对于其他的直辖市也是同样的处理逻辑。

  修正好的数据奉上,有需要的小伙伴可以自取哦。

对于直辖市也可以做两级的,这个主要看产品的需求吧

总结

  总体来讲,这个爬虫比较简单,只有简单的几行代码。毕竟网站也没啥反扒的机制,所以很轻松的就拿到了数据。

结尾

  嘿嘿话说,你都爬过哪些网站呢?

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

查看原文

赞 10 收藏 6 评论 0

不一样的科技宅 发布了文章 · 1月7日

你是什么级别的程序员?

前言

  最近在和领导闲聊的时候,聊到了一些关于编程思想方面的东西,让我受益匪浅。尤其是有一个问题让我印象深刻。而且对于这个问题,在过往的面试中也被问了几次。我就把我以往自认为不错的回答重新说了一遍。谁知领导听完,竟然笑了起来,这着实让我有点摸不到头脑。

你是什么级别的程序员?

  对于这个问题,相信各位在以往的面试中,应该有被问过的吧。很明显这个是一个开放式的问题,没有标准的答案。我是这样回答的:

  我觉得我是一个中级程序员。为什么这样说呢?其实我对于初级的定义是能干活。对于安排的任务能顺利完成,并解决出现的一些bug。但是对于技术呢?可能停留在使用层面,没有做到知其然而知其所以然。

  目前的我,完成日常的工作已经不是问题。对于某些技术也有一些深入的了解,能设计一些架构,所以我觉得我是一个中级程序员。那作为高级程序员来讲,需要对技术有更加深层次的一个理解,同时还需要有一个强大的架构设计能力,来应对不同场景的业务的一个考验。

领导

  听了你说的这些啊,很明显你的划分是侧重于技术方面。按照你说的,就好像会CRUD并且会使用一些工具,比如说Redis,RabbitMq就是初级程序员。然后又学习了其他的组件,并了解了一些原理,就变成了中级的程序员了。学的东西多了,就变成高级程序员了。

  按你说的这样,那我把要学的,都列举下来。一个一个学完,就成了高级程序员。要是这样的话,那高级程序员应该有很多。 很明显在我们这行,高级的程序员还是少数。你认为的高级程序员,在我这里可能只是个合格的程序员 。并不是能说是一个高级程序员。

领导:SpringBoot是一个优秀的框架是吧?

:这个是的,巴拉巴拉说了一堆的优点。

领导:那开发SpringBoot框架的程序员,应该是个高级程序员吧?

:是的。

领导:那么,他为什么是个高级程序员呢?

:呃呃呃呃呃!

领导

  一个合格的软件,并不是一蹴而就的。就以我们做的东西来说,是我们这个团队用一行一行的代码建造起来的。当我分配工作的时候,会将任务根据复杂的进行排序,然后在根据你们的能力来进行分配。比如说,把一些简单的工作,交给实习生去做。然后把复杂一点的,交给经验丰富的去做。当人员发生变动的是时候,其他人也能快速接手。

领导:那么问题来了,如何能让其他人快速接手呢?

:这个。。。。。

领导:很简单,要是所有人写的代码都一样,看别人的代码就像看自己的代码。这不就解决问题了吗?

:是的。

领导:那么问题又来了,如何让所有人写的代码都一样呢?

:我们可以用alibaba开发的idea的插件扫描代码,制定相应的代码规范,并进行实施。

领导:我们制定的规范,你遵守了多少呢?

:遵守了一部分,有一部分没有遵守。

领导:你看这不就破坏了嘛,让代码风格不一致了?

:额,这个确实是。

领导:alibaba的插件也好,公司的开发规范也罢,只能进行少量的约束,没办法实现强约束,你可以选择遵守,也可以选择不遵守。而且问起来,为啥不遵守。你们能说出一大堆的理由,比如说,项目赶,时间短。

:哈哈哈。

领导:那么有没有什么办法来实现强约束呢?

:这个。。。应该有吧。

领导:是什么呢?

:呃呃呃!这个不知道,尴尬一笑。

领导:这个东西就是框架。

框架(framework)是一个框子——指其约束性,也是一个架子——指其支撑性。是一个基本概念上的结构,用于去解决或者处理复杂的问题。

领导:为什么这样说呢?就以你写代码来讲,应该不会出现忘记写;这个吧。

:这个肯定不可能,IDEA会提醒,并且编译都过不了。

领导:这就对了,不按照规则写,直接给你报错。如果我们把定制的各种开发规范封装成一个框架,让你们去使用,形成强约束。这样你们写出来的代码是不是就一样的了。这也就是我们封装框架的原因。想要啥,都能加上去。毕竟开源的框架,面向的是绝大数人,不能面面俱到。

:哦原来如此,学到了学到了。

:那你是如何划分程序员的等级呢?

领导:初级的程序员能根据自己掌握的技能完成相应的工作,然而中级程序员呢除了能完成工作,还能让别人快速的接手自己工作。高级程序员就是把初级变成中级。

总结

  初级的程序员能根据自己掌握的技能完成相应的工作。中级程序员除了能完成工作以外,还能让别人快速的接手自己工作。这也就意味着,写出的代码可读性要好,要保证代码可读性,就需要付出各种的努力。比如说,命名合理,进行相应的抽象,模块划分等等。高级程序员就是把初级变成中级。

  说起来很简单, 但是实现起来却很困难,封装框架只是其中的一种方法。你要问我还有啥方法,我也不知道,因为我不是一个高级程序员。按照我目前的认知,要想成为一个高级程序员,不仅要从技术的方向去考虑问题,还要更多的从的角度考虑问题。

结尾

  很感谢你能看到最后,那么问题来了你是一个什么级别的程序员呢?

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

查看原文

赞 8 收藏 5 评论 8

不一样的科技宅 发布了文章 · 2020-11-12

你的开发利器Spring自定义注解

前言

  自定义注解在开发中是一把利器,经常会被使用到。在上一篇文章中有提到了自定义校验注解的用法。 然而最近接到这样一个需求,主要是针对某些接口的返回数据需要进行一个加密操作。于是很自然的就想到了自定义注解+AOP去实现这样一个功能。但是对于自定义注解,只是停留在表面的使用,没有做到知其然,而知其所以然。所以这篇文章就是来了解自定义注解这把开发利器的。

什么是自定义注解?

官方定义

  An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

Google翻译一下

  注解是元数据的一种形式,可以添加到Java源代码中。 类,方法,变量,参数和包都可以被注释。 注解对其注释的代码的操作没有直接影响。

看完这个定义是不是有点摸不到头脑,不要慌实践出真知。

建立一个自定义注解

  我们先回顾一下需求的场景,是要针对xx接口的返回数据需要做一个加密操作。之前说到使用自定义注解+AOP来实现这个功能。所以我们先定义一个注解叫Encryption,被Encryption注解修饰后接口,返回的数据要被加密。

public @interface Encryption {
}

  你会发现创建自定义注解,就和建立普通的接口一样简单。只是所使用的关键字有所不同。在底层实现上,所有定义的注解都会自动继承java.lang.annotation.Annotation接口。

编写相应的接口

@Encryption
@GetMapping("/encrypt")
public ResultVo encrypt(){
    return ResultVoUtil.success("不一样的科技宅");
}

@GetMapping("/normal")
public ResultVo normal(){
    return ResultVoUtil.success("不一样的科技宅");
}

编写切面

@Around("@annotation(com.hxh.unified.param.check.annotation.Encryption)")
public ResultVo encryptPoint(ProceedingJoinPoint joinPoint) throws Throwable {
  ResultVo resultVo = (ResultVo) joinPoint.proceed();

  // 获取注解
  MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
  Method method = methodSignature.getMethod();
  Encryption annotation = method.getAnnotation(Encryption.class);

  // 如果被标识了,则进行加密
  if(annotation != null){
    // 进行加密
    String encrypt = EncryptUtil.encryptByAes(JSON.toJSONString(resultVo.getData()));
    resultVo.setData(encrypt);
  }

  return resultVo;
}

测试结果

  这个时候,你会发现返回的数据并没有被加密。 那么这个是为啥呢?俗话说遇到问题不要慌,先掏出手机发个朋友圈(稍微有点跑题了)。出现这个原因是,缺少了@Retention@Encryption的修饰,让我们把它加上。

@Retention(RetentionPolicy.RUNTIME)
public @interface Encryption {

}

继续测试

  这个时候返回的数据就被加密了,说明自定义注解生效了。

测试普通接口

  没有用@Encryption的接口,返回的数据没有被加密。到此需求就已经实现了,接下来就该了解原理了。

@Retention

@Retention作用是什么

  Retention的翻译过来就是"保留"的意思。也就意味着它的作用是,用来定义注解的生命周期的,并且在使用时需要指定RetentionPolicyRetentionPolicy有三种策略,分别是:

  • SOURCE - 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃。
  • CLASS - 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期。
  • RUNTIME - 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。

选择合适的生命周期

  首先要明确生命周期 RUNTIME > CLASS > SOURCE 。一般如果需要在运行时去动态获取注解信息,只能使用RUNTIME。如果要在编译时进行一些预处理操作,比如生成一些辅助代码就用CLASS。如果只是做一些检查性的操作,比如 @Override和@SuppressWarnings,则可选用 SOURCE。

我们实际开发中的自定义注解几乎都是使用的RUNTIME

  最开始@Encryption没有使用@Retention对其生命周期进行定义。所以导致AOP在获取的时候一直为空,如果为空就不会对数据进行加密。

  是不是感觉这个注解太简陋。那再给他加点东西,加上个@Target

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encryption {

}

@Target

  @Target注解是限定自定义注解可以使用在哪些地方。这就和参数校验一样,约定好规则,防止乱用而导致问题的出现。针对上述的需求可以限定它只能用方法上。根据不同的场景,还可以使用在更多的地方。比如说属性、包、构造器上等等。

  • TYPE - 类,接口(包括注解类型)或枚举
  • FIELD - 字段(包括枚举常量)
  • METHOD - 方法
  • PARAMETER - 参数
  • CONSTRUCTOR - 构造函数
  • LOCAL_VARIABLE - 局部变量
  • ANNOTATION_TYPE -注解类型
  • PACKAGE - 包
  • TYPE_PARAMETER - 类型参数
  • TYPE_USE - 使用类型

  上面两个是比较常用的元注解,Java一共提供了4个元注解。你可能会问元注解是什么?元注解的作用就是负责注解其他注解。

@Documented

  @Documented的作用是对自定义注解进行标注,如果使用@Documented标注了,在生成javadoc的时候就会把@Documented注解给显示出来。没什么实际作用,了解一下就好了。

@Inherited

  被@Inherited修饰的注解,被用在父类上时其子类也拥有该注解。 简单的说就是,当在父类使用了被@Inherited修饰的注解@InheritedTest时,继承它的子类也拥有@InheritedTest注解。

这个可以单独讲下

注解元素类型

  参照我们在定义接口的经验,在接口中能定义方法和常量。但是在自定义注解中,只能定义一个东西:注解类型元素Annotation type element

其实可以简单的理解为只能定义方法,但是和接口中的方法有区别。

定义注解类型元素时需要注意如下几点:

  • 访问修饰符必须为public,不写默认为public。
  • 元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型。
  • type()括号中不能定义方法参数,仅仅只是一个特殊的语法。但是可以通过default关键字设置"默认值"。
  • 如果没有默认值,则使用注解时必须给该类型元素赋值。

继续改造

  需求这个东西经常都在变动。原本需要加密的接口只使用AES进行加密,后面又告知有些接口要使用DES加密。针对这样的情况,我们可以在注解内,添加一下配置项,来选择使用何种方式加密。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encryption {

    /**
     * 加密类型
     */
    String value() default "AES";
  
}

调整接口

@Encryption
@GetMapping("/encrypt")
public ResultVo encrypt(){
    return ResultVoUtil.success("不一样的科技宅");
}

@Encryption(value = "DES")
@GetMapping("/encryptDes")
public ResultVo encryptDes(){
    return ResultVoUtil.success("不一样的科技宅");
}

@GetMapping("/normal")
public ResultVo normal(){
    return ResultVoUtil.success("不一样的科技宅");
}

调整AOP

@Around("@annotation(com.hxh.unified.param.check.annotation.Encryption)")
public ResultVo encryptPoint(ProceedingJoinPoint joinPoint) throws Throwable {
  ResultVo resultVo = (ResultVo) joinPoint.proceed();

  // 获取注解
  MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
  Method method = methodSignature.getMethod();
  Encryption annotation = method.getAnnotation(Encryption.class);

  // 如果被标识了,则进行加密
  if(annotation != null){
    // 进行加密
    String encrypt = null;
    switch (annotation.value()){
      case "AES":
        encrypt = EncryptUtil.encryptByAes(JSON.toJSONString(resultVo.getData()));
        break;
      case "DES":
        encrypt = EncryptUtil.encryptByDes(JSON.toJSONString(resultVo.getData()));
        break;
      default:
        break;
    }
    resultVo.setData(encrypt);
  }

  return resultVo;
}

  至此就改造完了。可以发现注解元素类型,在使用的时候,操作元素类型像在操作属性。解析的时候,操作元素类型像在操作方法。

小技巧

  • 当注解没有注解类型元素,使用时候可直接写为@Encryption@Encryption()等效。
  • 当注解只有一个注解类型元素,并且命名是value。在使用时@Encryption("DES")@Encryption(value = "DES")等效。

注意的点

  • 需要根据实际情况指定注解的生命周期@Retention
  • 使用@Target来限制注解的使用范围,防止注解被乱用。
  • 如果注解是配置在方法上的,那么我们要从Method对象上获取。如果是配置在属性上,就需要从该属性对应的Field对象上去获取。总之用在哪里,就去哪里获取。

总结

  注解可以理解为就是一个标识。可以在程序代码中的关键节点上打上这些标识,它不会改变原有代码的执行逻辑。然后程序在编译时或运行时可以检测到这些标记,在做出相应的操作。结合上面的小场景,可以得出自定义注解使用的基本流程:

  1. 定义注解 --> 根据业务进行创建。
  2. 使用注解 --> 在相应的代码中进行使用。
  3. 解析注解 --> 在编译期或运行时检测到标记,并进行特殊操作。

上期回顾

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

查看原文

赞 10 收藏 7 评论 2

不一样的科技宅 关注了用户 · 2020-11-09

白居易 @xupei_5eb66af98fa81

生活又不是热血动漫

关注 0

不一样的科技宅 发布了文章 · 2020-09-22

SpringBoot分组校验及自定义校验注解

前言

  在日常的开发中,参数校验是非常重要的一个环节,严格参数校验会减少很多出bug的概率,增加接口的安全性。在此之前写过一篇SpringBoot统一参数校验主要介绍了一些简单的校验方法。而这篇则是介绍一些进阶的校验方式。比如说:在某个接口编写的过程中肯定会遇到,当xxType值为A,paramA值必传。xxType值为B,paramB值必须传。对于这样的,通常的做法就是在controller加上各种if判断。显然这样的代码是不够优雅的,而分组校验及自定义参数校验,就是来解决这个问题的。

PathVariable参数校验

  Restful的接口,在现在来讲应该是比较常见的了,常用的地址栏的参数,我们都是这样校验的。

/**
 * 获取电话号码信息
 */
@GetMapping("/phoneInfo/{phone}")
public ResultVo phoneInfo(@PathVariable("phone") String phone){
    // 验证电话号码是否有效
    String pattern = "^[1][3,4,5,7,8][0-9]{9}$";
    boolean isValid =  Pattern.matches(pattern, phone);
    if(isValid){
        // 执行相应逻辑
        return ResultVoUtil.success(phone);
    } else {
        // 返回错误信息
        return ResultVoUtil.error("手机号码无效");
    }
}

很显然上面的代码不够优雅,所以我们可以在参数后面,添加对应的正则表达式phone:正则表达式来进行验证。这样就省去了在controller编写校验代码了。

/**
 * 获取电话号码信息
 */
@GetMapping("/phoneInfo/{phone:^[1][3,4,5,7,8][0-9]{9}$}")
public ResultVo phoneInfo(@PathVariable("phone") String phone){
    return ResultVoUtil.success(phone);
}

虽然这样处理后代码更精简了。但是如果传入的手机号码,不符合规则会直接返回404。而不是提示手机号码错误。错误信息如下:

自定义校验注解

  我们以校验手机号码为例,虽然validation提供了@Pattern这个注解来使用正则表达式进行校验。如果被使用在多处,一旦正则表达式发生更改,则需要一个一个的进行修改。很显然为了避免做这样的无用功,自定义校验注解就是你的好帮手。

@Data
public class PhoneForm {

    /**
     * 电话号码
     */
    @Pattern(regexp = "^[1][3,4,5,7,8][0-9]{9}$" , message = "电话号码有误")
    private String phone;

}

  要实现一个自定义校验注解,主要是有两步。一是注解本身,二是校验逻辑实现类

PhoneVerify 校验注解

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
 
    String message() default "手机号码格式有误";

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

    Class<? extends Payload>[] payload() default {};

}

PhoneValidator 校验实现类

public class PhoneValidator implements ConstraintValidator<Phone, Object> {

    @Override
    public boolean isValid(Object telephone, ConstraintValidatorContext constraintValidatorContext) {
        String pattern = "^1[3|4|5|7|8]\\d{9}$";
        return Pattern.matches(pattern, telephone.toString());
    }
}

CustomForm 表单数据

@Data
public class CustomForm {

    /**
     * 电话号码
     */
    @Phone
    private String phone;

}

测试接口

@PostMapping("/customTest")
public ResultVo customTest(@RequestBody @Validated CustomForm form){
    return ResultVoUtil.success(form.getPhone());
}

注解的含义

@Target({ElementType.FIELD})

  注解是指定当前自定义注解可以使用在哪些地方,这里仅仅让他可以使用属性上。但还可以使用在更多的地方,比如说方法上、构造器上等等。

  • TYPE - 类,接口(包括注解类型)或枚举
  • FIELD - 字段(包括枚举常量)
  • METHOD - 方法
  • PARAMETER - 参数
  • CONSTRUCTOR - 构造函数
  • LOCAL_VARIABLE - 局部变量
  • ANNOTATION_TYPE -注解类型
  • PACKAGE - 包
  • TYPE_PARAMETER - 类型参数
  • TYPE_USE - 使用类型
@Retention(RetentionPolicy.RUNTIME)

  指定当前注解保留到运行时。保留策略有下面三种:

  • SOURCE - 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃。
  • CLASS - 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期。
  • RUNTIME - 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。
@Constraint(validatedBy = PhoneValidator.class)

  指定了当前注解使用哪个校验类来进行校验。

分组校验

UserForm

@Data
public class UserForm {

    /**
     * id
     */
    @Null(message = "新增时id必须为空", groups = {Insert.class})
    @NotNull(message = "更新时id不能为空", groups = {Update.class})
    private String id;

    /**
     * 类型
     */
    @NotEmpty(message = "姓名不能为空" , groups = {Insert.class})
    private String name;

    /**
     * 年龄
     */
    @NotEmpty(message = "年龄不能为空" , groups = {Insert.class})
    private String age;
    
}

Insert分组

public interface Insert {
}

Update分组

public interface Update {
}

测试接口

/**
 * 添加用户
 */
@PostMapping("/addUser")
public ResultVo addUser(@RequestBody @Validated({Insert.class}) UserForm form){
      // 选择对应的分组进行校验
    return ResultVoUtil.success(form);
}

/**
 * 更新用户
 */
@PostMapping("/updateUser")
public ResultVo updateUser(@RequestBody @Validated({Update.class}) UserForm form){
    // 选择对应的分组进行校验
    return ResultVoUtil.success(form);
}

测试结果

添加测试

更新测试

顺序校验@GroupSequence

  在@GroupSequence内可以指定,分组校验的顺序。比如说@GroupSequence({Insert.class, Update.class, UserForm.class})先执行Insert校验,然后执行Update校验。如果Insert分组,校验失败了,则不会进行Update分组的校验。

@Data
@GroupSequence({Insert.class, Update.class, UserForm.class})
public class UserForm {

    /**
     * id
     */
    @Null(message = "新增时id必须为空", groups = {Insert.class})
    @NotNull(message = "更新时id不能为空", groups = {Update.class})
    private String id;

    /**
     * 类型
     */
    @NotEmpty(message = "姓名不能为空" , groups = {Insert.class})
    private String name;

    /**
     * 年龄
     */
    @NotEmpty(message = "年龄不能为空" , groups = {Insert.class})
    private String age;

}
测试接口
/**
* 编辑用户
*/
@PostMapping("/editUser")
public ResultVo editUser(@RequestBody @Validated UserForm form){
    return ResultVoUtil.success(form);
}
测试结果

  哈哈哈,测试结果其实是个死循环,不管你咋输入都会报错,小伙伴可以尝试一下哦。上面的例子只是个演示,在实际中还是别这样做了,需要根据具体逻辑进行校验。

自定义分组校验

  对于之前提到了当xxType值为A,paramA值必传。xxType值为B,paramB值必须传这样的场景。单独使用分组校验和分组序列是无法实现的。需要使用@GroupSequenceProvider才行。

自定义分组表单

@Data
@GroupSequenceProvider(value = CustomSequenceProvider.class)
public class CustomGroupForm {

    /**
     * 类型
     */
    @Pattern(regexp = "[A|B]" , message = "类型不必须为 A|B")
    private String type;

    /**
     * 参数A
     */
    @NotEmpty(message = "参数A不能为空" , groups = {WhenTypeIsA.class})
    private String paramA;

    /**
     * 参数B
     */
    @NotEmpty(message = "参数B不能为空", groups = {WhenTypeIsB.class})
    private String paramB;

    /**
     * 分组A
     */
    public interface WhenTypeIsA {

    }

    /**
     * 分组B
     */
    public interface WhenTypeIsB {

    }

}

CustomSequenceProvider

public class CustomSequenceProvider implements DefaultGroupSequenceProvider<CustomGroupForm> {

    @Override
    public List<Class<?>> getValidationGroups(CustomGroupForm form) {
        List<Class<?>> defaultGroupSequence = new ArrayList<>();

        defaultGroupSequence.add(CustomGroupForm.class);

        if (form != null && "A".equals(form.getType())) {
            defaultGroupSequence.add(CustomGroupForm.WhenTypeIsA.class);
        }

        if (form != null && "B".equals(form.getType())) {
            defaultGroupSequence.add(CustomGroupForm.WhenTypeIsB.class);
        }

        return defaultGroupSequence;
    }
}

测试接口

/**
 * 自定义分组
 */
@PostMapping("/customGroup")
public ResultVo customGroup(@RequestBody @Validated CustomGroupForm form){
    return ResultVoUtil.success(form);
}

测试结果

Type类型为A

Type类型为B

小结一下

  GroupSequence注解是一个标准的Bean认证注解。正如之前,它能够让你静态的重新定义一个类的,默认校验组顺序。然而GroupSequenceProvider它能够让你动态的定义一个校验组的顺序。

注意的一个点

SpringBoot 2.3.x 移除了validation依赖需要手动引入依赖。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

总结

  个人的一些小经验,参数的非空判断,这个应该是校验的第一步了,除了非空校验,我们还需要做到下面这几点:

  • 普通参数 - 需要限定字段的长度。如果会将数据存入数据库,长度以数据库为准,反之根据业务确定。
  • 类型参数 - 最好使用正则对可能出现的类型做到严格校验。比如type的值是【0|1|2】这样的。
  • 列表(list)参数 - 不仅需要对list内的参数是否合格进行校验,还需要对list的size进行限制。比如说 100。
  • 日期,邮件,金额,URL这类参数都需要使用对于的正则进行校验。
  • 参数真实性 - 这个主要针对于 各种Id 比如说 userIdmerchantId,对于这样的参数,都需要进行真实性校验,判断系统内是有含有,并且对应的状态是否正常。

  参数校验越严格越好,严格的校验规则不仅能减少接口出错的概率,同时还能避免出现脏数据,从而来保证系统的安全性和稳定性。

错误的提醒信息需要友好一点哦,防止等下被前端大哥吐槽哦。

上期回顾

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

查看原文

赞 22 收藏 17 评论 0

不一样的科技宅 发布了文章 · 2020-09-15

Beanutils造成dubbo反序列化失败?

前言

  今天下午,当我经过一个小时的奋”键“疾”码“,准备好好的审查一下(摸鱼)自己写的代码,经过一段时间审查(摸的差不多了,该下班了),得出一个结论我写的代码很优雅、精简。所以大手一挥提交代码,并在API管理系统上将xxx接口点了个完成。准备收拾东西走人了准点下班。然而事与愿违,没过多久前端大哥就@我了,说xxx接口有问题,麻烦处理一下。内心第一反应(你丫的参数传错了吧)卑微的我只能默默的回个,好的、麻烦把参数给我一下,我这边检查一下[微笑脸]。

场景还原

  经过测试,发现确实是我的问题。还好没甩锅,要不然就要被打脸了。错误信息如下:

{
  "code": "010000",
  "message":"java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee",
  "data": null
}

  看到这个错误有点懵,HashMap 无法转换为AddEmployeeDTO$Employee。内心在想,没道理啊。请求参数我都是拷贝过来的,压根就没用Map进行参数传递。毕竟我都是个老手了,咋可能犯这样愚蠢的错误。俗话说遇到问题不要慌,让我们掏出手机先发个朋友圈,不对好像有点跑题了,我们先看一下调用链的数据传递。

  首先web将AddEmployeeForm数据传递到服务端,然后使用fromToDTO()方法,进行将数据转换为Dubbo请求需要的AddEmployeeDTO。Dubbo服务放接收AddEmployeeDTO后,使用 EmployeeConvert 将数据转换为AddEmployeeXmlReq再执行相关逻辑。

AddEmployeeForm类

@Data
public class AddEmployeeForm implements Serializable {

    /**
     * 职员信息列表
     */
    private List<Employee> employees;

    @Data
    public static class Employee implements Serializable {

        /**
         * 姓名
         */
        private String name;

        /**
         * 工作
         */
        private String job;

    }
}

FormToDTO()方法

public <T, F> T formToDTO(F form, T dto) {

    // 进行数据拷贝
    BeanUtils.copyProperties(form, dto);

    // 返回数据
    return dto;
}

AddEmployeeDTO类

@Data
public class AddEmployeeDTO implements Serializable {

    /**
     * 职员信息列表
     */
    private List<Employee> employees;

    @Data
    public static class Employee implements Serializable {

        /**
         * 姓名
         */
        private String name;

        /**
         * 工作
         */
        private String job;

    }

}

EmployeeConvert转换类

EmployeeConvert转换类,使用了mapstruct进行实现,没使用过的小伙伴可以简单的了解下。
@Mapper
public interface EmployeeConvert {

    EmployeeConvert INSTANCE = Mappers.getMapper(EmployeeConvert.class);
        
    AddEmployeeXmlReq dtoToXmlReq(AddEmployeeDTO dto);

}

AddEmployeeXmlReq类

@Data
public class AddEmployeeXmlReq implements Serializable {

    /**
     * 职员信息列表
     */
    private List<Employee> employees;

    @Data
    public static class Employee implements Serializable {

        /**
         * 姓名
         */
        private String name;

        /**
         * 工作
         */
        private String job;

    }
}

EmployeeController

@RestController
@AllArgsConstructor
public class EmployeeController {

    private final EmployeeRpcProvider provider;

    @PostMapping("/employee/add")
    public ResultVO employeeAdd(@RequestBody AddEmployeeForm form) {
        provider.add(formToDTO(form,new AddEmployeeDTO()));
        return ResultUtil.success();
    }
}

EmployeeRpcServiceImpl

@Slf4j
@Service
public class EmployeeRpcServiceImpl implements EmployeeService {

    @Override
    public ResultDTO add(AddEmployeeDTO dto) {
        log.info("dubbo-provider-AddEmployeeDTO:{}", JSON.toJSONString(dto));
        AddEmployeeXmlReq addEmployeeXmlReq = EmployeeConvert.INSTANCE.dtoToXmlReq(dto);
        return ResultUtil.success();
    }
}

分析原因

判断异常抛出点

  我们需要先确定异常是在consumer 抛出的还是provider抛出的。判断过程很简单,我们可以进行本地debug,看看是执行到哪里失败了就知道了。如果不方便本地调试,我们可以在关键点上打上相应的日志。比如说consumer调用前后,provider处理前后。如果请求正常 日志打印的顺序应该是:

这样通过观察日志就可以判定异常是在哪里抛出的了。

实际并没有这样麻烦,因为在consumer做了rpc异常拦截,所以我当时看了下consumer的日志就知道是provider抛出来的。

找到出错的代码

  既然找到了出问题是出在provider,那看是什么原因导致的,从前面的调用链可以知道,provider接收到AddEmployeeDTO会使用EmployeeConvert将其转换为AddEmployeeXmlReq,所以我们可以打印出AddEmployeeDTO看看consumer的传参是否正常。

  通过日志我们可以发现consumer将参数正常的传递过来了。那么问题应该就出在EmployeeConvertAddEmployeeDTO转换为AddEmployeeXmlReq这里了。由于EmployeeConvert是使用mapstruct进行实现,我们可以看看自动生成的转换类实现逻辑是咋样的。

  通过观察源代码可以发现,在进行转换的时候需要传入一个List<Employee> 而这个Employee正是AddEmployeeDTO.Employee。这个时候可能会困扰了,我明明就是传入AddEmployeeDTO,而且类里面压根就没有Map,为啥会抛出java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee这个异常呢?

让我们Debug一下看看发生了啥。

  这个时候你会发现接收到的AddEmployeeDTO.employees内存储的并不是一个AddEmployeeDTO$Employee对象,而是一个HashMap。那看来真相大白了,原来是dubbo反序列化的时候将AddEmployeeDTO$Employee 转换为HashMap了。从而导致了java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee异常的抛出。

你以为结束了?

  为啥Dubbo反序列化时会将AddEmployeeDTO$Employee变成Map呢?我们回过头看看之前打印参数的日志,有一个警告日志提示了java.lang.ClassNotFoundException:com.aixiao.inv.api.model.form.AddEmployeeForm$Employee ,找不到AddEmployeeForm$Employee这个就有点奇怪了,为啥不是AddEmployeeDTO$Employee

  在进行dubbo调用前AddEmployeeForm会使用fromToDTO()方法将其转化为AddEmployeeDTO。那么问题会不会出现在这里呢?我们继续Debug看看。

  呕吼,这下石锤了。原来是在formToDTO的时候出问题了。传递过去AddEmployeeDTO内部的Employee竟然变成了AddEmployeeForm$Employee。这也是为什么provider那边会抛出java.lang.ClassNotFoundException:com.aixiao.inv.api.model.form.AddEmployeeForm$Employee的原因了。审查一下formToDTO的代码看看为啥会发生这样的情况:

public <T, F> T formToDTO(F form, T dto) {

    // 进行数据拷贝
    BeanUtils.copyProperties(form, dto);

    // 返回数据
    return dto;
}

  fromToDTO内的代码非常精简,就一个BeanUtils.copyProperties()的方法,那毫无疑问它就是罪魁祸首了。通过在baidu的海洋里遨游,我找到了原因。原来是BeanUtils是浅拷贝造成的。浅拷贝只是调用子对象的set方法,并没有将所有属性拷贝。(也就是说,引用的一个内存地址),所以在转换的时候,将AddEmployeeDTO内的employees属性指向了AddEmployeeFormemployees的内存地址。所以将在进行调用时,Dubbo因为反序列化时找不到对应的类,就会将其转换为Map

小结一下

  上面的问题,主要是由于BeanUtils浅拷贝造成。并且引发连锁反应,造成Dubbo反序列化异常以及EmployeeConvert的转换异常,最后抛出了java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee 错误信息。

解决方法

  既然知道了问题出现的原因,那么解决起来就很简单了。对于单一的属性,那么不涉及到深拷贝的问题,适合用BeanUtils继续进行拷贝。但是涉及到集合我们可以这样处理:

  1. 简单粗暴使用foreach进行拷贝。
  2. 使用labmda实现进行转换。
AddEmployeeDTO dto = new AddEmployeeDTO();
dto.setEmployees(form.getEmployees().stream().map(tmp -> {
  AddEmployeeDTO.Employee employee = new AddEmployeeDTO.Employee();
  BeanUtils.copyProperties(tmp,employee);
  return employee;
}).collect(Collectors.toList()));
  1. 封装一个转换类进行转换。
AddEmployeeDTO dto = new AddEmployeeDTO();
dto.setEmployees(convertList(form.getEmployees(),AddEmployeeDTO.Employee.class));

public <S, T> List<T> convertList(List<S> source, Class<T> targetClass) {
return JSON.parseArray(JSON.toJSONString(source), targetClass);
}

总结

  1. 使用BeanUtils.copyProperties()进行拷贝需要注意
  2. dubbo在进行反序列化的时候,如果找不到对应类会将其转化为map。

参考

结尾

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

查看原文

赞 0 收藏 0 评论 0

不一样的科技宅 发布了文章 · 2020-08-31

由String,String Builder,String Buffer 引起的面试惨案

pexels-pixabay-60504

前言

  String,StringBuilder,StringBuffer的区别是啥?这个面试题估计每个JAVA都应该碰到过吧。依稀记得第一次面试的时候,面试官问我这个问题时,心想着能有啥区别不都是拼接字符串嘛。深入了解这个问题后,发现并不简单?

前菜

面试官:你好,你是不一样的科技宅是吧?

小宅:面试官你好,我是不一样的科技宅。

面试官:你好,麻烦做一个简单的自我介绍吧。

小宅:我叫不一样的科技宅,来自xxx,做过的项目主要有xxxx用到xxx,xxx技术。

20180719930824_lrEKIh

面试官:好的,对你的的履历有些基本了解了,那我们先聊点基础知识吧。

小宅:内心OS(放马过来吧)

开胃小菜

面试官:String,StringBuilder,StringBuffer的区别是啥?

小宅:这个太简单了吧,这是看不起我?

  • 从可变性来讲String的是不可变的,StringBuilder,StringBuffer的长度是可变的。
  • 从运行速度上来讲StringBuilder > StringBuffer > String。
  • 从线程安全上来StringBuilder是线程不安全的,而StringBuffer是线程安全的。

  所以 String:适用于少量的字符串操作的情况,StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况,StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况。

75f3cd331b7ab4ca4552847a746952da

面试官:为什么String的是不可变的?

小宅:因为存储数据的char数组是使用final进行修饰的,所以不可变。

image-20200714151350294

面试官:刚才说到String是不可变,但是下面的代码运行完,却发生变化了,这是为啥呢?

public class Demo {

    public static void main(String[] args) {
        String str = "不一样的";
        str = str + "科技宅";
        System.out.println(str);
    }

}

很明显上面运行的结果是:不一样的科技宅

我们先使用javac Demo.class 进行编译,然后反编译javap -verbose Demo 得到如下结果:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #2                  // String 不一样的
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: aload_1
        11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: ldc           #6                  // String 科技宅
        16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        22: astore_1
        23: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        26: aload_1
        27: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        30: return

  我们可以发现,在使用+ 进行拼接的时候,实际上jvm是初始化了一个StringBuilder进行拼接的。相当于编译后的代码如下:

public class Demo {

    public static void main(String[] args) {
        String str = "不一样的";
        StringBuilder builder =new StringBuilder();
        builder.append(str);
        builder.append("科技宅");
        str = builder.toString();
        System.out.println(str);
    }

}

我们可以看下builder.toString(); 的实现。

@Override
public String toString() {
  // Create a copy, don't share the array
  return new String(value, 0, count);
}

  很明显toString方法是生成了一个新的String对象而不是更改旧的str的内容,相当于把旧str的引用指向的新的String对象。这也就是str发生变化的原因。

分享我碰到过的一道面试题,大家可以猜猜答案是啥?文末有解析哦

public class Demo {

    public static void main(String[] args) {
        String str = null;
        str = str + "";
        System.out.println(str);
    }

}

面试官:String类可以被继承嘛?

小宅:不可以,因为String类使用final关键字进行修饰,所以不能被继承,并且StringBuilder,StringBuffer也是如此都被final关键字修饰。

面试官:为什么String Buffer是线程安全的?

小宅:这是因为在StringBuffer类内,常用的方法都使用了synchronized 进行同步所以是线程安全的,然而StringBuilder并没有。这也就是运行速度StringBuilder > StringBuffer的原因了。

20181119593911_rYDslC

面试官:刚才你说到了synchronized关键字 ,那能讲讲synchronized的表现形式嘛?

小宅

  • 对于普通同步方法 ,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的class对象。
  • 对于同步方法块,锁是Synchonized括号配置的对象。

面试官:能讲讲synchronized的原理嘛?

小宅synchronized是一个重量级锁,实现依赖于JVMmonitor 监视器锁。主要使用monitorentermonitorexit指令来实现方法同步和代码块同步。在编译的是时候,会将monitorexit指令插入到同步代码块的开始位置,而monitorexit插入方法结束处和异常处,并且每一个monitorexit都有一个与之对应的monitorexit

  任何对象都有一个monitor与之关联,当一个monitor被持有后,它将被处于锁定状态,线程执行到monitorenter指令时间,会尝试获取对象所对应的monitor的所有权,即获取获得对象的锁,由于在编译期会将monitorexit插入到方法结束处和异常处,所以在方法执行完毕或者出现异常的情况会自动释放锁。

硬菜来了

面试官:前面你提到synchronized是个重量级锁,那它的优化有了解嘛?

006qir4oly1g0qzhmov3tj30gw0cgjte

小宅:为了减少获得锁和和释放锁带来的性能损耗引入了偏向锁、轻量级锁、重量级锁来进行优化,锁升级的过程如下:

  首先是一个无锁的状态,当线程进入同步代码块的时候,会检查对象头内和栈帧中的锁记录里是否存入存入当前线程的ID,如果没有使用CAS 进行替换。以后该线程进入和退出同步代码块不需要进行CAS 操作来加锁和解锁,只需要判断对象头的Mark word内是否存储指向当前线程的偏向锁。如果有表示已经获得锁,如果没有或者不是,则需要使用CAS进行替换,如果设置成功则当前线程持有偏向锁,反之将偏向锁进行撤销并升级为轻量级锁。

  轻量级锁加锁过程,线程在执行同步块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的Mark Word复制到锁记录(Displaced Mark Word)中,然后线程尝试使用CAS 将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,反之表示其他线程竞争锁,当前线程便尝试使用自旋来获得锁。

  轻量级锁解锁过程,解锁时,会使用CAS将Displaced Mark Word替换回到对象头,如果成功,则表示竞争没有发生,反之则表示当前锁存在竞争锁就会膨胀成重量级锁。

升级过程流程图

533411-20200423173203871-980115964

白话一下:

  可能上面的升级过程和升级过程图,有点难理解并且还有点绕。我们先可以了解下为什么会有锁升级这个过程?HotSpot的作者经过研究发现,大多数情况下锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。为了避免获得锁和和释放锁带来的性能损耗引入锁升级这样一个过程。理解锁升级这个流程需要明确一个点:发生了竞争才锁会进行升级并且不能降级。

  我们以两个线程T1,T2执行同步代码块来演示锁是如何膨胀起来的。我们从无锁的状态开始 ,这个时候T1进入了同步代码块,判断当前锁的一个状态。发现是一个无锁的状态,这个时候会使用CAS将锁记录内的线程Id指向T1并从无锁状态变成了偏向锁。运行了一段时间后T2进入了同步代码块,发现已经是偏向锁了,于是尝试使用CAS去尝试将锁记录内的线程Id改为T2,如果更改成功则T2持有偏向锁。失败了说明存在竞争就升级为轻量级锁了。

  可能你会有疑问,为啥会失败呢?我们要从CAS操作入手,CAS是Compare-and-swap(比较与替换)的简写,是一种有名的无锁算法。CAS需要有3个操作数,内存地址V,旧的预期值A,即将要更新的目标值B,换句话说就是,内存地址0x01存的是数字6我想把他变成7。这个时候我先拿到0x01的值是6,然后再一次获取0x01的值并判断是不是6,如果是就更新为7,如果不是就再来一遍之道成功为止。这个主要是由于CPU的时间片原因,可能执行到一半被挂起了,然后别的线程把值给改了,这个时候程序就可能将错误的值设置进去,导致结果异常。

  简单了解了一下CAS现在让我们继续回到锁升级这个过程,T2尝试使用CAS进行替换锁记录内的线程ID,结果CAS失败了这也就意味着,这个时候T1抢走了原本属于T2的锁,很明显这一刻发生了竞争所以锁需要升级。在升级为轻量级锁前,持有偏向锁的线程T1会被暂停,并检查T1的状态,如果T1处于未活动的状态/已经退出同步代码块的时候,T1会释放偏向锁并被唤醒。如果未退出同步代码块,则这个时候会升级为轻量级锁,并且由T1获得锁,从安全点继续执行,执行完后对轻量级锁进行释放。

  偏向锁的使用了出现竞争了才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。并且偏向锁的撤销需要等待全局安全点(这个时间点没有任何正在执行的字节码)。

  T1由于没有人竞争经过一段时间的平稳运行,在某一个时间点时候T2进来了,产生使用CAS获得锁,但是发现失败了,这个时候T2会等待一下(自旋获得锁),由于竞争不是很激烈所以等T1执行完后,就能获取到锁并进行执行。如果长时间获取不到锁则就可能发生竞争了,可能出现了个T3把原本属于T2的轻量级锁给抢走了,这个时候就会升级成重量级锁了。

u=628528004,774370142&fm=26&gp=0

吃完撤退

面试官:内心OS:竟然没问倒他,看来让他培训是没啥希望了,让他回去等通知吧 。

  小宅是吧,你的水平我这边基本了解了,我对你还是比较满意的,但是我们这边还有几个候选人还没面试,没办法直接给你答复,你先回去等通知吧。

小宅:好的好的,谢谢面试官,我这边先回去了。多亏我准备的充分,全回答上来了,应该能收到offer了吧。

timg

面试题解析

public class Demo {

    public static void main(String[] args) {
        String str = null;
        str = str + "";
        System.out.println(str);
    }

}

答案是 null,从之前我们了解到使用+进行拼接实际上是会转换为StringBuilder使用append方法进行拼接。所以我们看看append方法实现逻辑就明白了。

public AbstractStringBuilder append(String str) {
  if (str == null)
    return appendNull();
  int len = str.length();
  ensureCapacityInternal(count + len);
  str.getChars(0, len, value, count);
  count += len;
  return this;
}
private AbstractStringBuilder appendNull() {
  int c = count;
  ensureCapacityInternal(c + 4);
  final char[] value = this.value;
  value[c++] = 'n';
  value[c++] = 'u';
  value[c++] = 'l';
  value[c++] = 'l';
  count = c;
  return this;
}

从代码中可以发现,如果传入的字符串是null时,调用appendNull方法,而appendNull会返回null。

结尾

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

查看原文

赞 0 收藏 0 评论 0

不一样的科技宅 发布了文章 · 2020-06-24

从0搭建属于自己的Jenkins持续集成平台

前言

  Jenkins在日常工作中占据了一个非常重要的角色,帮助我们节省了大量用于构建的时间。有些公司有运维大哥对Jenkins进行维护,如果没有那只能自己动手了。俗话说的好自己动手丰衣足食,所以本文就从0开始搭建属于自己的Jenkins持续平台。主要包含,普通项目构建流水线构建多分支流水线构建并将构建结果辅以钉钉通知。

前期准备

  • centos7 服务器一台

确认是否能安装docker

 Docker要求CentOS系统的内核版本高于3.10.通过uname -r命令查看你当前的内核版本。

[root@CentOS ~]# uname -r
3.10.0-1127.8.2.el7.x86_64

更改yum源为阿里云

备份旧源

mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup

下载最新的源

wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo

生成缓存

yum makecache

更新

yum update

安装docker

官方安装文档

yum install -y yum-utils

添加docker源

yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

安装docker

yum install docker-ce

启动docker

systemctl start docker

更改docker镜像源

vim /etc/docker/daemon.json

加入阿里云源地址

{
    "registry-mirrors":["https://6kx4zyno.mirror.aliyuncs.com"]
}

重新读取配置

systemctl daemon-reload 

重启docker

systemctl restart docker

安装jenkins

下载jenkins镜像

docker pull jenkins

启动jenkins

 设置端口为9090并映射jenkins_home到宿主机/home/jenkins_home

docker run -d --name jenkins -p 9090:8080 -v /home/jenkins_home:/var/jenkins_home jenkins

 可以通过docker ps查看运行的容器。

[root@CentOS home]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                               NAMES
ec6a4da6b83f        jenkins             "/bin/tini -- /usr/l…"   About a minute ago   Up About a minute   50000/tcp, 0.0.0.0:9090->8080/tcp   jenkins
[root@CentOS home]#

把玩jenkins docker镜像遇到的volume权限问题

 在运行启动jenkins的命令时,可能会出现jenkins无法启动情况。

[root@CentOS home]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS               NAMES
b571f16dafbf        jenkins             "/bin/tini -- /usr/l…"   8 minutes ago       Exited (1) 8 minutes ago                       jenkins
[root@CentOS home]#

 可以通过docker logs 镜像名称查看启动日志。

[root@CentOS home]# docker logs jenkins
touch: cannot touch '/var/jenkins_home/copy_reference_file.log': Permission denied
Can not write to /var/jenkins_home/copy_reference_file.log. Wrong volume permissions?
[root@CentOS home]#

 查看输出的日志,如果出现 Permission denied 类似的错误。需要删除旧容器重新运行。

docker rm jenkins

 运行命令加入了-u 0重新运行。

docker run -d --name jenkins -p 9090:8080 -v /home/jenkins_home:/var/jenkins_home -u 0 jenkins
参考 https://blog.csdn.net/minicto...

Jenkins初始化

 启动成功后输入 http://服务器:9090/

如果无法访问,请检查一下防火墙端口是否开放,如果是云服务器还需要检查安全组设置

  首次启动jenkins需要输入密码,需要进入容器内获取密码。密码位于/var/jenkins_home/secrets/initialAdminPassword

进入容器

docker exec -it jenkins /bin/bash

获取密码

cat /var/jenkins_home/secrets/initialAdminPassword
[root@CentOS jenkins_home]# docker exec -it jenkins /bin/bash
root@ec6a4da6b83f:/# cat /var/jenkins_home/secrets/initialAdminPassword
68eed23ad39541949972468e4f2ce1fd
root@ec6a4da6b83f:/#

  由于我们将/var/jenkins_home -- 挂载到--> /home/jenkins_home所以也可以直接cat /home/jenkins_home/secrets/initialAdminPassword 获取密码。

  输入密码以后,安装需要的插件,在安装途中由于网络原因会出现有些插件安装失败,这个可以不用理会。

设置jenkins的默认登录账号和密码

处理插件安装失败

  进入jenkins的主页面右上角可能会出现一些报错信息,主要是提示jenkins 需要的某些插件没有安装,或者说jenkins版本太低了,插件无法使用这个时候我们需要先升级jenkins做一个升级。

自动升级

Jenkins提供了自动升级的方式

手动升级

 可以去Jenkins的官网下载好最新jar包上传到服务器,也可以使用wget命令。

wget http://jenkins新版本的下载地址
#目前最新2.239
wget http://updates.jenkins-ci.org/download/war/2.239/jenkins.war

  Jenkins的更新主要是替换jenkins镜像里面的war包 ,我们可以把下载好的war包使用docker cp直接进行复制命令如下:

docker cp jenkins.war jenkins:/usr/share/jenkins

 重新启动Jenkins即可完成升级。

docker restart jenkins

更插件源

https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json
  • 替换完源以后点击提交。
  • 然后进入插件管理页面将出错的插件重新安装。
  • 及时更新插件。

安装必要的插件

  • Localization: Chinese (Simplified) 1.0.14 汉化包 搜索关键字 chinese
  • Publish Over SSH 1.20.1 搜索关键字 ssh
  • DingTalk 钉钉通知 2.3.0

配置jenkins

全局工具配置

  主要配置 jdk、maven、git等常用环境。需要注意配置的别名,后续构建将会使用到。

配置jdk

  因为jenkins镜像自带jdk所以无需安装直接使用即可,进入Jenkins容器,使用java -verbose查看java安装路径。

docker exec -it jenkins /bin/bash
java -verbose

配置git

 进入容器内使用whereis git即可查询到git安装路径。

root@6a9fbb129cbe:~# whereis git
git: /usr/bin/git /usr/share/man/man1/git.1.gz
root@6a9fbb129cbe:~#

配置maven

 maven直接使用自动安装即可。

系统设置

配置服务器

点击新增即可添加服务器,主要配置:

  • Name 名称 - 构建的时候将会用到
  • Hostname 服务器地址
  • Username 用户名
  • Remote Directory 远程目录 - 上传文件的目录 默认配置根目录即可/

点击高级进行其他参数配置

  • 如果需要使用密码登录,则选中Use password authentication, or use a different key 复选框即可,如下图所示。

  除了配置密码还可以配置端口Port,跳板机Jump Host的参数,可以根据实际情况配置。默认可以使用密码。

  配置完成以后点击Test Configuration按钮,如果配置正常会出现Success 反之出现错误信息,可以根据错误信息,调整配置参数。

配置钉钉

  钉钉主要用于构建通知,在配置前需要在钉钉群内,添加自定义机器人。

自由风格的软件项目

  以https://gitee.com/huangxunhui/jenkins_demo.git为例。

新建项目

设置项目简介

源码管理

  • 配置仓库地址。
  • 配置凭证-主要用于拉取代码。
  • 配置需要构建的分支。

添加凭证

  如果项目是开源,则可以跳过这一步。反之需要设置凭证,要不然将无法拉取代码进行构建。

构建触发器

  可以根据实际情况选择,案例采用轮询的方式进行构建。

构建

构建后操作

  • 将jar包发送到相应的服务器。

  • Source files jar包的路径。支持通配符匹配.
  • Remove prefix 移除前缀,一般jar包的路径都存在于**/target下,如果不移除,会在目标服务器上建立相应的目录结构。
  • Remote directory 远程目录。
注意的点, 在之前配置服务器时也配置了Remote directory,这时候部署的实际目录是,服务器设置的远程目录+现在配置的远程目录。
  • Exec command 执行脚本,主要用于将jar发送到目标服务器后,执行相应的启动脚本。

配置完成点击保存即可。

点击开始构建

发送钉钉通知


流水线

  流水线构建,将上述构建步骤代码化,方便调整。

项目创建

流水线编写

  由于配置步骤类似,前面简单的步骤可以参照,自由风格的软件项目。这里主要讲流水线如何编写。

注意右下角的流水线语法,后续会用上。

  我们可以点击右上角的下拉按钮,生成一个简单的流水线。比如说hello world。

pipeline {
    
    // 表示所有机器都能运行   
   agent any

   stages {
      stage('Hello') {
         steps {
            echo 'Hello World'
         }
      }
   }
}

  通过上面的pipeline可以知道,有一个Hello的步骤,这个步骤执行的是,输出hello world。依葫芦画瓢,一次完整的构建我们可以总结出如下几个步骤:拉取代码(checkout) -> 打包(build) -> 部署(deploy)。

pipeline {
   agent any
   
   stages {
      stage('checkout') {
         steps {
            
         }
      }
      
      stage('build') {
         steps {
                     
         }
      }
      
    stage('deploy') {
         steps {

         }
      }
   }
}

  步骤梳理好了,这个时候就可以完善对应的步骤了,这就需要用到提到的,流水线语法。

将生成好的流水线脚本复制到对应的步骤即可。

注意:如果使用到maven需要将maven引入,tools相应的内容就是配置maven时配置的别名。
pipeline {
   agent any
   
    // 工具
    tools {
        maven 'maven'
        jdk 'jdk'
    }

   stages {
      stage('checkout') {
         steps {
            checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[url: 'https://gitee.com/huangxunhui/jenkins_demo.git']]])
         }
      }
      
      stage('build') {
         steps {
            sh 'mvn clean package -Dmaven.test.skip=true'            
         }
      }
      
    stage('deploy') {
         steps {
            sshPublisher(publishers: [sshPublisherDesc(configName: 'dev', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '''cd /home/project 
nohup java -jar jenkins_demo.jar > nohup.out 2>&1 &''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/home/project', remoteDirectorySDF: false, removePrefix: '/target', sourceFiles: '**/target/jenkins_demo.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
         }
      }
      
   }
}

配置完成点击应用即可。

构建测试

上面演示的是将流水线配置在jenkins内,其实我们还可以从SCM中获取,比如git

我们可以建立一个仓库专门维护不同项目的构建脚本Jenkinsfile,也可以在每个项目下,建立对应的Jenkinsfile.

  注意的点:项目中的Jenkinsfile需要和配置的一致。比如说上面的配置,是扫描项目根目录下名字为Jenkinsfile的文件。

所以我们可以在jenkins_demo仓库内添加Jenkinsfile文件。

配置点击完成,即可。


多分支流水线

  在日常开发中,通常是基于git-flow进行开发的,前面两种都是基于单分支构建,如果每个分支都去配置,那将耗费大量时间。所以多分支流水线就是用来解决这个问题的。

创建项目

配置分支源

构建配置

扫描触发器

完成上述配置,点击应用即可。

编写jenkinsfile文件

  核心思想是,根据不同的分支使用不同的打包命令,发送到不同的服务器进行运行。

pipeline {
    // 指定集群 any 表示所有
    agent any

    // 工具
    tools {
        maven 'maven'
        jdk 'jdk'
    }

    // 定义常量
    environment {

        // 钉钉机器人编号
        rebootId = 'a3c07482-d031-47a6-8542-05ac56c5f17a'

        // 开始logo
        imageOfStart = 'https://www.easyicon.net/api/resizeApi.php?id=1229977&size=128'

        // 成功logo
        imageOfSuccess = 'https://www.easyicon.net/api/resizeApi.php?id=1194837&size=128'

        // 失败logo
        imageOfFailure = 'https://www.easyicon.net/api/resizeApi.php?id=1201052&size=128'

        // 不稳定logo
        imageOfUnstable = 'https://www.easyicon.net/api/resizeApi.php?id=1219854&size=128'

        // 终止logo
        imageOfAborted = 'https://www.easyicon.net/api/resizeApi.php?id=1183198&size=128'

        // 认证Id
        credentialsId = '98e9c197-f0ae-44c3-8f67-4ca0339028a8'

        // 仓库地址
        repositoryUrl = 'https://gitee.com/huangxunhui/jenkins_demo.git'

        // 打包命令 - 项目需要配置maven多环境
        mavenProd = 'mvn clean package -P prod -Dmaven.test.skip=true'
        mavenTest = 'mvn clean package -P test -Dmaven.test.skip=true'
        mavenDev = 'mvn clean package -P dev -Dmaven.test.skip=true'

        // 服务器名称 - 案例测试-全部部署到dev环境
        devServer = 'dev'
        testServer = 'dev'
        prodServer = 'dev'

        // sshPublisher 配置
        removePrefix = '/target'
        remoteDirectory = '/home/project/jenkins_demo'
        sourceFiles = '**/target/jenkins_demo.jar'

        execCommandProd = 'cd /home/project && ./manage.sh jenkins_demo/ restart'
        execCommandTest = 'cd /home/project && ./manage.sh jenkins_demo/ restart'
        execCommandDev = 'cd /home/project && ./manage.sh jenkins_demo/ restart'

    }

    stages {

        stage('开始构建通知'){
            steps {
                dingtalk (
                        robot: "${rebootId}",
                        type: 'LINK',
                        title: "${env.JOB_NAME}",
                        text: [
                            "开始构建-编号为#${BUILD_NUMBER}"
                        ],
                        messageUrl: "${env.BUILD_URL}",
                        picUrl: "${imageOfStart}"
                )
            }
        }

        stage('拉取代码'){
            steps {
                echo "拉取 ${BRANCH_NAME} 分支的代码。"
                checkout([$class: 'GitSCM', branches: [[name: "*/${BRANCH_NAME}"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: "${credentialsId}", url: "${repositoryUrl}"]]])
            }
        }

        stage('进行打包'){
            steps {
                script {
                    if (env.BRANCH_NAME == 'master') {
                        sh "${mavenProd}"
                    } else if (env.BRANCH_NAME == 'test') {
                        sh "${mavenTest}"
                    } else if (env.BRANCH_NAME == 'dev') {
                        sh "${mavenDev}"
                    } else {
                        sh "${mavenDev}"
                    }
                }
            }
        }

        stage('项目部署'){
            steps {
                script {
                    if (env.BRANCH_NAME == 'master') {
                        // 部署生产环境
                        sshPublisher(publishers: [sshPublisherDesc(configName: "${prodServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${execCommandProd}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory:  "${remoteDirectory}", remoteDirectorySDF: false, removePrefix: "${removePrefix}", sourceFiles: "${sourceFiles}")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
                    } else if (env.BRANCH_NAME == 'test') {
                        // 部署测试环境
                        sshPublisher(publishers: [sshPublisherDesc(configName: "${testServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${execCommandTest}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory:  "${remoteDirectory}", remoteDirectorySDF: false, removePrefix: "${removePrefix}", sourceFiles: "${sourceFiles}")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
                    } else if (env.BRANCH_NAME == 'dev') {
                        // 部署开发环境
                        sshPublisher(publishers: [sshPublisherDesc(configName: "${devServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${execCommandDev}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory:  "${remoteDirectory}", remoteDirectorySDF: false, removePrefix: "${removePrefix}", sourceFiles: "${sourceFiles}")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
                    } else {
                        sshPublisher(publishers: [sshPublisherDesc(configName: "${devServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${execCommandTest}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory:  "${remoteDirectory}", remoteDirectorySDF: false, removePrefix: "${removePrefix}", sourceFiles: "${sourceFiles}")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
                    }
                }
            }
        }
    }

    // 流水线结束通知
    post {

        // 成功通知
        success {
            dingtalk (
                    robot: "${rebootId}",
                    type: 'LINK',
                    title: "${env.JOB_NAME}",
                    text: [
                        "构建成功-编号为#${BUILD_NUMBER}"
                    ],
                    messageUrl: "${env.BUILD_URL}",
                    picUrl: "${imageOfSuccess}"
                )
        }

        // 失败通知
        failure {
            dingtalk (
                    robot: "${rebootId}",
                    type: 'LINK',
                    title: "${env.JOB_NAME}",
                    text: [
                        "构建失败-编号为#${BUILD_NUMBER}"
                    ],
                    messageUrl: "${env.BUILD_URL}",
                    picUrl: "${imageOfFailure}"
            )
        }

        // 构建不稳定通知
        unstable {
            dingtalk (
                    robot: "${rebootId}",
                    type: 'LINK',
                    title: "${env.JOB_NAME}",
                    text: [
                        "构建不稳定-编号为#${BUILD_NUMBER}"
                    ],
                    messageUrl: "${env.BUILD_URL}",
                    picUrl: "${imageOfUnstable}"
            )
        }

        // 构建终止通知
        aborted {
            dingtalk (
                    robot: "${rebootId}",
                    type: 'LINK',
                    title: "${env.JOB_NAME}",
                    text: [
                        "构建终止-编号为#${BUILD_NUMBER}"
                    ],
                    messageUrl: "${env.BUILD_URL}",
                    picUrl: "${imageOfAborted}"
            )
        }
    }
}

使用到的启动脚本manage.sh

钉钉机器人插件使用文档

构建结果

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

查看原文

赞 0 收藏 0 评论 0

不一样的科技宅 发布了文章 · 2020-06-16

SpringBoot多邮件源发送邮件

前言

  最近由于业务需要,需要使用邮件对用户进行通知,但是单个邮箱有发送限制,当推送量过多时,经常会出现失败,于是考虑使用多个邮箱进行发送,由于spring-boot-starter-mail不支持同时配置多个邮件源,所以决定自己动手实现。

实现思路

  spring-boot-starter-mail会根据spring.mail.xxx相关配置对JavaMailSender进行自动配置。但是只支持单个邮箱。为了实现多邮件源,可以参照上述逻辑。在配置文件内配置好,多个邮件源。然后读取配置文件,手动对JavaMailSender进行配置,并将其初始化完毕的JavaMailSender存储容器内。然后发送时随机取出JavaMailSender进行发送。

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

邮件yml配置

替换实际的邮件配置

# 配置邮件
mail:
  configs:
    # 账号
    - username: fapiao@QQ.com
      password: 123456
      host: smtp.qq.com
      port: 25
      protocol: smtp
      default-encoding: UTF-8
    # 账号A
    - username: fapiaoa@QQ.com
      password: 123456
      host: smtp.qq.com
      port: 25
      protocol: smtp
      default-encoding: UTF-8

MailConfig

@Data
@Component
@ConfigurationProperties(prefix = "mail")
public class MailConfig {

    private List<MailProperties> configs;

    @Data
    public static class MailProperties {

        /**
         * 密码
         */
        private String username;

        /**
         * 密码
         */
        private String password;

        /**
         * host
         */
        private String host;

        /**
         * 端口
         */
        private Integer port;

        /**
         * 协议
         */
        private String protocol;

        /**
         * 默认编码
         */
        private String defaultEncoding;

    }

}

MailSenderConfig

@Slf4j
@Component
@AllArgsConstructor
public class MailSenderConfig {

    private final MailConfig mailConfig;

    private final List<JavaMailSenderImpl> senderList;

    /**
     * 初始化 sender
     */
    @PostConstruct
    public void buildMailSender(){
        List<MailConfig.MailProperties> mailConfigs = mailConfig.getConfigs();
        log.info("初始化mailSender");
        mailConfigs.forEach(mailProperties -> {

            // 邮件发送者
            JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
            javaMailSender.setDefaultEncoding(mailProperties.getDefaultEncoding());
            javaMailSender.setHost(mailProperties.getHost());
            javaMailSender.setPort(mailProperties.getPort());
            javaMailSender.setProtocol(mailProperties.getProtocol());
            javaMailSender.setUsername(mailProperties.getUsername());
            javaMailSender.setPassword(mailProperties.getPassword());

            // 添加数据
            senderList.add(javaMailSender);
        });
    }

    /**
     * 获取MailSender
     * @return CustomMailSender
     */
    public JavaMailSenderImpl getSender(){
        if(senderList.isEmpty()){
            buildMailSender();
        }
        // 随机返回一个JavaMailSender
        return senderList.get(new Random().nextInt(senderList.size()));
    }

    /**
     * 清理 sender
     */
    public void clear(){
        senderList.clear();
    }

}

MailService

public interface MailService {

    /**
     * 发送邮件
     * @return 返回 true 或者 false
     */
    boolean sendMail();

}

MailServiceImpl

@Service
@AllArgsConstructor
public class MailServiceImpl implements MailService {

    private final MailSenderConfig senderConfig;

    @Override
    public boolean sendMail() {

        JavaMailSenderImpl mailSender = senderConfig.getSender();

        //创建SimpleMailMessage对象
        SimpleMailMessage message = new SimpleMailMessage();
        //邮件发送人
        message.setFrom(Objects.requireNonNull(mailSender.getUsername()));
        //邮件接收人
        message.setTo("2583174414@qq.com");
        //邮件主题
        message.setSubject("测试邮件");
        //邮件内容
        message.setText("测试邮件内容");
        //发送邮件
        mailSender.send(message);

        return true;
    }
}

测试

@SpringBootTest
@RunWith(SpringRunner.class)
class MailServiceImplTest {

    @Autowired
    private MailServiceImpl mailService;

    @Test
    void sendMail() {
        mailService.sendMail();
    }
}

测试结果

注意的点

  • 需要检查邮箱配置相关协议是否开启。
  • 如果使用QQ邮箱进行发送,密码不是邮箱密码,而是授权码。

拓展

  上述是将邮箱配置写入配置文件,在没有使用配置中心的情况下,如果邮箱源被限制,更改起来相对麻烦。可以将配置存入数据库,在启动的时候从数据库进行加载。如果邮箱源发生更改,可直接更改数据库邮箱源配置,然后调用MailSenderConfig.clear()方法,等待下次发送的时候,会重新初始化配置。

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

查看原文

赞 1 收藏 1 评论 0

不一样的科技宅 发布了文章 · 2020-05-18

面试问到AOP就该这样回答

前言

  相信各位小伙伴在准备面试的时候,AOP都是无法绕过的一个点,经常能看到动态代理、JDK动态代理、CGLIB动态代理这样的字眼。其实动态代理是代理模式的一种。代理模式有静态代理、强制代理、动态代理。所以在认识AOP之前需要了解代理模式。

代理模式定义

  代理模式(Proxy Pattern):为其他对象提供一种代理以控制这个对象的访问。

  • Subject抽象主题角色,也叫做抽象主题类。可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求。
  • RealSubject具体主题角色,也叫做被委托角色,被代理角色,是业务逻辑的具体执行者。
  • Proxy代理主题角色,也叫做委托类、代理类。他负责对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色实现,并且在真实主题角色处理完毕前后做到预处理和善后工作。

代理模式的优点

  • 职责清晰
  • 高扩展性
  • 智能化

UML

image

我的理解

  跳板机不管是对于运维老哥还是对于我们来讲,都是日常的工作中不可或缺的一个工具。为了保证生产服务器的安全,我们是无法通过xshell等工具直接进行连接的。如果需要操作生产的服务器,则需要通过跳板机。并且跳板机还能记录我们的操作,用来做安全审计。防止出现,某位老哥一气之下反手就是个sudo rm -rf /*直接凉凉。

  我们回过头看看代理模式的定义:为其他对象提供一种代理以控制这个对象的访问。实际上跳板机就是生产服务器的一个代理Proxy,为了实现控制生产服务器的访问权限。你需要通过跳板机来操作生产服务器。

  • 为其他对象提供一种代理以控制这个对象的访问
  • 给你提供一个跳板机来访问生产服务器,目的是来控制生产服务器的访问

  Proxy的职责:Proxy是对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色实现。并且在真实主题角色处理完毕前后做到预处理善后工作。你通过操作跳板机。跳板机将你输入的命令在生产服务器上进行执行。并且能记录下执行的命令和执行的结果。

代码演示

Server

public interface Server {

    /**
     * 执行
     * @param command 命令
     */
    void exec(String command);

}

ProdServer

public class ProdServer implements Server {

    @Override
    public void exec(String command){
        System.out.println(command + ":执行成功");
    }

}

JumpServer

public class JumpServer {

    private Server server;

    public JumpServer(Server server) {
        this.server = server;
    }

    public void exec(String command){
        System.out.println("xxx在:" + LocalDateTime.now() + " 执行了:" + command);
        server.exec(command);
        System.out.println("xxx在:" + LocalDateTime.now() + " 执行完了:"+ command + " 结果是XXX");
    }

}

Client

public class Client {

    public static void main(String[] args) {
        JumpServer jumpServer = new JumpServer();
        jumpServer.exec("pwd");
    }
}

运行结果

xxx在:2020-04-04T16:43:19.277 执行了:pwd
pwd:执行成功
xxx在:2020-04-04T16:43:19.278 执行完了:pwd 结果是XXX

  通过上面的代码,简单的实现了代理模式。在网络上代理服务器设置分为透明代理普通代理

  • 透明代理就是用户不用设置代理服务器地址,就可以直接访问.也就是说代理服务器对用户来说是透明的,不用知道它存在的。
  • 普通代理则是需要用户自己设置代理服务器的IP地址,用户必须知道代理的存在。

  当运维老哥给了一台服务器的账号和密码,你成功登录,并完成了相应的操作。你以为给你的是生产的服务器。实际就可能是个跳板机。为了安全起见,是不可能将实际服务器的IP让你知道的。很显然跳板机对你来讲就是透明的。

所以我们调整一下代码,将其变成普通代理

JumpServer

public class JumpServer {

    private Server server;

    public JumpServer(Server server) {
        this.server = server;
    }

    public void exec(String command){
        server.exec(command);
    }

}

Client

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();
        JumpServer jumpServer = new JumpServer(prodServer);
        jumpServer.exec("pwd");
    }
    
}

执行结果

xxx在:2020-04-04T16:52:23.282 执行了:pwd
pwd:执行成功
xxx在:2020-04-04T16:52:23.283 执行完了:pwd 结果是XXX

强制代理

  对于现实情况,我们可以通过不开放公网访问的权限来实现,强制使用代理操作服务器。我们可以用代码简单的模拟下。

JumpServer

public class ProdServer implements Server {

    private JumpServer jumpServer;

    @Override
    public void exec(String command){
        hasProxy();
        System.out.println(command + ":执行成功");
    }

    private void hasProxy(){
        if(jumpServer == null){
            throw new RuntimeException("请使用跳板机!");
        }
    }

    public JumpServer setJumpServer() {
        this.jumpServer = new JumpServer(this);
        return this.jumpServer;
    }
}

Client未设置跳板机

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();
        prodServer.exec("pwd");
    }

}

不设置跳板机运行结果

Exception in thread "main" java.lang.RuntimeException: 请使用跳板机!
    at proxy.pattern.tmp.ProdServer.hasProxy(ProdServer.java:21)
    at proxy.pattern.tmp.ProdServer.exec(ProdServer.java:15)
    at proxy.pattern.tmp.Client.main(Client.java:13)

Client设置跳板机

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();
        prodServer.setJumpServer().exec("pwd");
    }

}

运行结果

xxx在:2020-04-05T15:01:10.944 执行了:pwd
pwd:执行成功
xxx在:2020-04-05T15:01:10.944 执行完了:pwd 结果是XXX

这个时候需要访问生产服务器,就需要先设置跳板机了,才能进行操作。

动态代理

  对静态代理来说,我们需要手动生成代理类。但是如果需要代理的类太多了,那这个肯定是不可取的。所以我们可以使用JDK动态代理来帮我们完成工作。

JDK动态代理

  JDK动态代理利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

JumpServerInvocationHandler

public class JumpServerInvocationHandler implements InvocationHandler {

    private Object target;

    public JumpServerInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target,args);
    }
}

Client

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();

        JumpServerInvocationHandler handler = new JumpServerInvocationHandler(prodServer);
        ClassLoader classLoader = prodServer.getClass().getClassLoader();
        Server proxy = (Server) Proxy.newProxyInstance(classLoader, new Class[]{Server.class}, handler);

        proxy.exec("pwd");
    }

}

测试结果

pwd:执行成功

增加前置通知和后置通知

Advice

public interface Advice {

    void exec();

}

BeforeAdvice

public class BeforeAdvice implements Advice {
    @Override
    public void exec() {
        System.out.println("执行前置通知");
    }
}

AfterAdvice

public class AfterAdvice implements Advice {
    @Override
    public void exec() {
        System.out.println("执行后置通知");
    }
}

JumpServerInvocationHandler

public class JumpServerInvocationHandler implements InvocationHandler {

    private Object target;

    public JumpServerInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 执行前置通知
        new BeforeAdvice().exec();

        Object ret = method.invoke(target, args);

        // 执行后置通知
        new AfterAdvice().exec();
        return ret;

    }
}

Client

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();

        JumpServerInvocationHandler handler = new JumpServerInvocationHandler(prodServer);
        ClassLoader classLoader = prodServer.getClass().getClassLoader();
        Server proxy = (Server) Proxy.newProxyInstance(classLoader, new Class[]{Server.class}, handler);

        proxy.exec("pwd");
    }

}

测试结果

执行前置通知
pwd:执行成功
执行后置通知
看到这里有没有点AOP的感觉了。

CGLIB动态代理

  CGLIB动态代理利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理,需要使用MethodInterceptor接口来进行实现。

JumpServerMethodInterceptor

public class JumpServerMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        new BeforeAdvice().exec();

        Object ret = methodProxy.invokeSuper(obj, args);

        new AfterAdvice().exec();

        return ret;
    }
}
注意的点:是使用invokeSuper()而不是invoke()

Client

public class Client {

    public static void main(String[] args) {

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ProdServer.class);
        enhancer.setCallback(new JumpServerMethodInterceptor());

        ProdServer proxy = (ProdServer) enhancer.create();

        proxy.exec("pwd");
    }

}

测试结果

执行前置通知
pwd:执行成功
执行后置通知

小结

  通过上面的一大堆的篇幅介绍代理模式就是为了能更加清晰的理解代理模式非常重要的一个应用场景AOP。

AOP

  AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。

AOP相关术语

连接点(Joinpoint)
  一个类或一段程序代码拥有一些具有便捷性质的特定点。比如说类的某个方法调用前/调用后、方法抛出异常后。

切点(Pointcut)
  在AOP中用于定位连接点。如果将连接点作为数据库中的记录,切点即相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。

增强(Advice)
  增强是织入目标类连接点上的一段程序代码。

在Spring中增强除了用于描述一段程序代码外,还可以拥有另一个和连接点相关的信息,这便是执行点的方位。通过执行点方位信息和切点信息,就可以找到特定的连接。正是因为增强即包含添加到连接点上的逻辑,包含定位连接点的方位信息,所以Spring提供的增强接口都是带方位名的。如BeforeAdviceAfterAdviceAroundAdvice

目标对象(Target)
  需要织入增强逻辑的目标类。比如说在使用AOP的时候配置的请求日志输出,目标对象就是对应的controller.

引介(Introduction)
  引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类没有原本没有实现某个接口,通过AOP可以动态的为某些业务类添加接口和实现方法,让业务类成为这个接口的实现类。

织入(Weaving)
  织入是将增强添加到目标类的具体连接点上的过程。AOP有3种织入方式:

  • 编译期织入,要求使用特殊的Java编译器。
  • 类装载期织入,要求使用特殊的类装载器。
  • 动态代理织入,在运行期,为目标类添加增强生成子类的方式。
    毫无疑问Spring是采用动态代理织入。

代理(Proxy)
  一个类被AOP织入增强后,就产生了一个结果类,它是融合了原类和增强逻辑的代理类。

切面(Proxy)
  切面由切点和增强(引介)组成,它即包括很切逻辑的定义,也包括连接点的定义。SpringAOP就是负责实施切面的框架,他将切面所定义的横切逻辑织入切面所指定的连接点中。

我们可以这样回答

  AOP翻译过来是:面向切面编程是一种设计思想。主要由连接点,切点,增强、切面组成。AOP依托于代理模式进行实现,所以AOP拥有代理模式的特性。可以在不改变原有类的情况下,能动态的添加某些功能。所以说比较适合来实现,打印请求日志,权限校验,等功能。针对不同的场景,AOP可以选择使用JDK动态代理或CGLIB代理来实现。由于CGLIB创建出来的代理类运行速度快于JDK动态代理,但是创建的过程太慢,所以可以将其创建出来的代理类交由IOC容器进行管理,免去了重复创建代理不必要的性能开销,来提高运行速度。

主要针对,AOP是什么、由什么组成、适合用场景、如何实现,不同实现的区别这些点去总结回答。

切点和切面的区别?

  切面包含切点,切点和增强组成了切面。SpringAOP通过切面将逻辑特定织入切面所指定的连接点中。

CGLIB 和 JDK 动态代理的区别

  jdk动态代理只能对实现了接口的类生成代理,而不能针对类。cglib是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法。简而言之就是JDK动态代理基于接口实现,cglib基于类继承。因为是继承,所以该类或方法不能使用final进行修饰。

  在性能上,有研究表明cglib所创建的代理对象的性能要比jdk创建的高10倍,但是呢cglib创建代理对象时所花费的时间要比jdk8倍。所以单例的代理对象或者具有实例池的代理,无效频繁的创建对象,比较适合采用cglib,反正适合采用jdk

参考书籍

  • 设计模式之禅道第二版
  • 精通Spring 4.x企业应用开发实战

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 62 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-01-03
个人主页被 2.7k 人浏览