0%

对于SpringBoot我们一定不会陌生,由于其快速方便的开发方式让我们从各种繁琐的配置中解放出来,让我们的开发效率得到了大大提升,而各种starter则是其最重要的组成部分之一

什么是starter?

我觉得可以简单认为starter是不同场景下(如MySQL、Redis、RocketMQ等)需要的一系列依赖的集合,就像是一个个可以单独引用的模块,有了starter,我们就可以使用其内置的各种依赖完成当前场景下的需求开发

starter的结构?

  1. 在IDEA中打开一个SpringBoot项目,选择一个starter依赖展开查看,这里以MyBatis为例

    • pom.properties:记录了starter的artifactId、groupId和version信息

    • pom.xml:是starter所需的依赖,比如mybatis-spring、mybatis-spring-boot-autoconfigure

    • MANIFEST.MF:记录了jar包的构建信息,比如jdk版本等

  2. 用同样的方式打开mybatis-spring-boot-autoconfigure的依赖

    • additional-spring-configuration-metadata.json:手动添加IDE配置提示
    • spring.factories:key-value键值对形式,记录需要被加载的配置类信息
    • spring-configuration-metadata.json:自动生成的IDE配置提示
    • spring-autoconfigure-metadata.properties:自动配置类是否被加载的条件
    • 除了上述,还有若干Class,如MybatisAutoConfiguration配置类,MybatisProperties属性类等等

如何创建自己的springboot-starter?

简单来说,分成如下几个步骤

1.创建一个maven项目,名字为xxx-springboot-autoconfigure(不固定),里面包含XxxAutoConfiguration配置类、XxxProperties属性类(非必须)、spring.factories文件(key为org.xxx.EnableAutoConfiguration,value为XxxAutoConfiguration)

2.创建一个空的maven项目,名字为xxx-springboot-starter(不固定),在pom.xml中引入上述的xxx-springboot-autoconfigure项目

2.创建一个springboot项目,引入xxx-springboot-starter项目,创建测试类使用starter中提供的功能类验证是否成功

实例演示

  1. 创建一个名为redisson-util-springboot-autoconfigure的maven项目,结构如下

    RedissonUtilAutoConfiguration.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
    @Configuration
    @EnableConfigurationProperties(RedissonUtilProperties.class)
    public class RedissonUtilAutoConfiguration {

    @Resource
    private RedissonUtilProperties properties;

    // 创建RedissonClient
    @Bean
    @ConditionalOnMissingBean
    public RedissonClient redissonClient() {
    String password = properties.getPassword();
    List<String> list = new ArrayList<>();
    List<String> nodes = properties.getNodes();
    for (String node : nodes) {
    list.add("redis://" + node);
    }
    Config config = new Config();
    config.useClusterServers().setPassword(password).setScanInterval(5000).setNodeAddresses(list);

    return Redisson.create(config);
    }

    }

    RedissonUtilProperties.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
    @ConfigurationProperties(prefix = RedissonUtilProperties.PREFIX)
    public class RedissonUtilProperties {

    public static final String PREFIX = "redisson";

    // redis密码
    private String password;

    // redis集群地址(ip:port)
    private List<String> nodes;

    public String getPassword() {
    return password;
    }

    public void setPassword(String password) {
    this.password = password;
    }

    public List<String> getNodes() {
    return nodes;
    }

    public void setNodes(List<String> nodes) {
    this.nodes = nodes;
    }
    }

    spring.factories

    1
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.codecho.RedissonUtilAutoConfiguration
  2. 创建一个名为redisson-util-springboot-starter的maven项目,结构如下

  3. 创建一个名为test-starter的springboot项目,引入redisson-util-springboot-starter项目依赖,结构如下

    application.properties(redis配置信息自填)

    1
    2
    redisson.password=xxx
    redisson.nodes=xxx.xxx.xxx.xxx:6379,xxx.xxx.xxx.xxx:6389,xxx.xxx.xxx.xxx:6399

    测试类代码如下

    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
    @Resource
    private RedissonClient redissonClient;

    @Test
    public void run() {
    Thread t1 = new Thread(() -> {
    winPrize();
    }, "张三");
    Thread t2 = new Thread(() -> {
    winPrize();
    }, "李四");

    t1.start();
    t2.start();
    }

    void winPrize() {
    String lockKey = "goods:1001";
    RLock lock = redissonClient.getLock(lockKey);
    if (lock.isLocked()) {
    System.out.println(Thread.currentThread().getName() + " 痛失大奖");
    return;
    }

    boolean lockFlag = lock.tryLock();
    if (!lockFlag) {
    System.out.println(Thread.currentThread().getName() + " 离中奖只差一步,别灰心,后面还有机会");
    return;
    }

    try {
    System.out.println(Thread.currentThread().getName()+ " 他就是天选之子!");
    TimeUnit.SECONDS.sleep(5);
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    if (lock.isHeldByCurrentThread()) {
    lock.unlock();
    }
    }
    }
  4. 运行测试方法,观察控制台输出

    使用Redis可视化工具(这里使用的Another Redis Desktop Manager)获取分布式锁的信息,发现其存活时间为30s,到期后锁被删除。如果需要多次运行测试方法,记得等待锁过期后再测试,否则会产生两个线程都获取不到锁的情况。

在使用CentOS时,有时候我们需要执行某个时间之前的命令,但是这些命令有时候参数过多,一时不容易想起,这时候很需要一个工具来帮我们记录执行过哪些命令

使用ctrl+R反向查找|搜索历史命令

1
2
3
4
5
6
7
8
9
(reverse-i-search)`':

# 输入要查找的命令,如docker
(reverse-i-search)`docker': docker logs 6316724f7425

# 使用ctrl+R向前连续查找

# 使用方向键 < or > 选取命令
[root@centos7 codecho]# docker logs 6316724f7425

在使用GitHub Page搭建博客后,买了一个域名和xxx.github.io绑定,域名过期后,直接访问xxx.github.io发现每次都重定向到之前的域名,然而在更换了浏览器重新访问发现可以正常访问,因此想到可能是浏览器的缓存信息没有更新

解决方法:清除浏览器缓存(以Google Chrome为例)

打开浏览器设置,点击隐私和安全,选择清除浏览数据建议通过google账号同步数据后退出,然后勾选下面的浏览记录、cookie、缓存文件、网站设置等,点击清除数据,重新访问GitHub Page网址,发现可以正常访问博客内容

自定义配置类,注入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. 使用Object.clone()方法

    • 代码示例

      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
      public class Order implements Cloneable {
      private String orderName;
      private Long orderNo;
      private Address address;

      // 省略getter/setter

      @Override
      protected Object clone() throws CloneNotSupportedException {
      return super.clone();
      }
      }

      public class Address {
      private String country;
      private String province;
      private String city;
      private String district;
      private String detailAddress;

      // 省略getter/setter
      }

      public class TestCopy {
      public static void main(String[] args) throws CloneNotSupportedException {
      Address address = new Address();
      address.setCountry("中国");
      address.setProvince("浙江省");
      address.setCity("杭州市");
      address.setDistrict("西湖区");
      address.setDetailAddress("凤起路");

      Order order = new Order();
      order.setOrderName("双十一好物订单");
      order.setOrderNo(202300156L);
      order.setAddress(address);

      System.out.println("order: " + order);
      System.out.println("address: " + address);
      System.out.println("detailAddress: " + address.getDetailAddress());

      Order copyOrder = (Order) order.clone();

      System.out.println("copyOrder: " + copyOrder);
      System.out.println("copyAddress: " + copyOrder.getAddress());

      copyOrder.getAddress().setDetailAddress("武林广场");
      System.out.println("detailAddress: " + address.getDetailAddress());
      }
      }
    • 运行结果

      1
      2
      3
      4
      5
      6
      order: com.codecho.copy.Order@677327b6
      address: com.codecho.copy.Address@14ae5a5
      detailAddress: 凤起路
      copyOrder: com.codecho.copy.Order@7f31245a
      copyAddress: com.codecho.copy.Address@14ae5a5
      detailAddress: 武林广场

      ==可以看出,使用Object.clone()方法复制出了一个新的Order对象,但是Order对象内部的Address还是原来的Address对象,并且对新Order对象内部Address进行修改会影响到原Address对象,说明Object.clone()方法只是拷贝了对象的引用,并没有拷贝对象本身。==

  2. 使用apache或spring提供的BeanUtils工具类

    • 代码示例

      1
      2
      3
      4
      5
      <dependency>
      <groupId>commons-beanutils</groupId>
      <artifactId>commons-beanutils</artifactId>
      <version>1.9.4</version>
      </dependency>
      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
      public class TestCopy {

      public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
      Address address = new Address();
      address.setCountry("中国");
      address.setProvince("浙江省");
      address.setCity("杭州市");
      address.setDistrict("西湖区");
      address.setDetailAddress("凤起路");

      Order order = new Order();
      order.setOrderName("双十一好物订单");
      order.setOrderNo(202300156L);
      order.setAddress(address);

      System.out.println("order: " + order);
      System.out.println("address: " + address);
      System.out.println("detailAddress: " + address.getDetailAddress());

      Order copyOrder = new Order();
      BeanUtils.copyProperties(copyOrder, order);

      System.out.println("copyOrder: " + copyOrder);
      System.out.println("copyAddress: " + copyOrder.getAddress());

      copyOrder.getAddress().setDetailAddress("武林广场");
      System.out.println("detailAddress: " + address.getDetailAddress());
      }

      }
    • 运行结果

      1
      2
      3
      4
      5
      6
      order: com.codecho.copy.Order@330bedb4
      address: com.codecho.copy.Address@2503dbd3
      detailAddress: 凤起路
      copyOrder: com.codecho.copy.Order@685f4c2e
      copyAddress: com.codecho.copy.Address@2503dbd3
      detailAddress: 武林广场

      ==和Object.clone()方法一样,BeanUtils.copyProperties()方法也是浅拷贝==

Java中实现深拷贝

  1. 序列化和反序列化

    • 代码示例

      ==注意:Order和Address需要实现Serializable接口==

      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
      public class Order implements Serializable {
      ...
      }

      public class Address implements Serializable {
      ...
      }

      public static void main(String[] args) throws IOException, ClassNotFoundException {
      Address address = new Address();
      address.setCountry("中国");
      address.setProvince("浙江省");
      address.setCity("杭州市");
      address.setDistrict("西湖区");
      address.setDetailAddress("凤起路");

      Order order = new Order();
      order.setOrderName("双十一好物订单");
      order.setOrderNo(202300156L);
      order.setAddress(address);

      System.out.println("order: " + order);
      System.out.println("address: " + address);
      System.out.println("detailAddress: " + address.getDetailAddress());

      Order copyOrder = deepCopy(order);

      System.out.println("copyOrder: " + copyOrder);
      System.out.println("copyAddress: " + copyOrder.getAddress());

      copyOrder.getAddress().setDetailAddress("武林广场");
      System.out.println("detailAddress: " + address.getDetailAddress());
      }


      public static Order deepCopy(Order source) throws IOException, ClassNotFoundException {
      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
      ObjectOutputStream out = new ObjectOutputStream(outputStream);
      out.writeObject(source);

      ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
      ObjectInputStream in = new ObjectInputStream(inputStream);

      Order copy = (Order) in.readObject();
      return copy;
      }
    • 运行结果

      1
      2
      3
      4
      5
      6
      order: com.codecho.copy.Order@330bedb4
      address: com.codecho.copy.Address@2503dbd3
      detailAddress: 凤起路
      copyOrder: com.codecho.copy.Order@7e6cbb7a
      copyAddress: com.codecho.copy.Address@7c3df479
      detailAddress: 凤起路

需求背景

最近给电脑换桌面背景的时候,想起硬盘里还有之前大学时期下载的搜狗壁纸图片,于是从硬盘将壁纸文件夹拷贝到电脑上,打开后发现有两个目录都保存有壁纸,每个都有几百张,但是有些是各自目录独有的,也有互相重复的。为了能够获得最完整的壁纸合集,打算将所有图片进行合并去重,很明显,这么重复枯燥的工作不可能人工去一张一张地比对处理(其实我一开始真的手动处理了几张,发现实在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. 查看删除后目录

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

我们在使用云服务器时的过程中,可能经常会发现自己的主机被各种扫描端口和尝试登录,如果安全意识不够,很可能会发生服务器被别人入侵,导致自己的数据资产受损。下面介绍如何修改ssh登录默认端口,减少自己主机被暴力登录的情况发生。

查看服务器是否被暴力登录

  • 查看/var/log/secure日志,如果发现有很多登录密码错误的信息,表示服务器可能正在被别人尝试暴力登录

    1
    tail -n 100 /var/log/secure | grep "Failed password"

修改默认端口

  • 修改sshd配置文件

    1
    2
    3
    4
    5
    vim /etc/ssh/sshd_config

    # 增加另一个端口号 1234
    Port 22
    Port 1234
  • 重启sshd服务

    1
    systemctl restart sshd
  • 修改防火墙或安全组配置,增加新端口

  • 使用新端口连接服务器,修改sshd_config配置文件,将原端口注释,保留新端口

    1
    2
    # Port 22
    Port 1234

屏蔽尝试非法登录的ip

​ 通过shell脚本和定时任务,将ip加入屏蔽名单,详细参考这里

从刚工作时使用的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 {

    }
  • 作用

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

准备条件

  1. IDEA软件(安装有Docker插件,新版本会自带)
  2. 装有Docker环境的Linux服务器/虚拟机

开始

  1. 在IDEA中创建一个SpringBoot项目,并编写一个controller,用于验证服务是否部署成功

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

    @Resource
    private UserService userService;

    @GetMapping(value = "/id/{id}")
    public CommonResponse getById(@PathVariable(value = "id") Long id) {
    UserDO user = userService.getById(id);
    return CommonResponse.success(user);
    }
    }
  2. 右键项目,创建一个名为Dockerfile的文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 使用openjdk镜像
    FROM openjdk:8-jre

    # 工作目录
    WORKDIR /home/docker

    # 复制jar包到容器
    ADD ./target/user-0.0.1-SNAPSHOT.jar ./user.jar

    # 运行jar包
    ENTRYPOINT ["sh","-c","java -jar $JAVA_OPTS user.jar"]
  3. 右键Dockerfile文件,点击Modify Run Configuration,编辑运行配置

    配置docker服务器信息

    配置docker运行参数(镜像tag、容器名称、端口映射、运行前执行package命令)

  4. 右键Dockerfile文件,点击运行,在Services栏可以看到docker的镜像和正在运行的容器

  5. 验证docker镜像是否部署成功

需要提前知道的一些内容

  1. Linux下的/var/log/secure文件记录了登录Linux服务器的日志信息,如果有人尝试破解你的服务器,那么可以在这个文件中看到很多密码错误的信息,如下图

  2. Linux下的/etc/hosts.deny文件可以配置需要禁止访问的ip,外部请求进入时,会先在/etc/hosts.allow中检查ip是否存在,若存在,直接放行请求,如果没有,判断在是否在/etc/hosts.deny中,如果在,禁止请求连接

  3. Linux支持cron定时任务,搭配shell脚本可以让我们定时运行自己编写的脚本,不需要人工进行干预

本机Linux版本为CentOS7

先梳理整个流程

  1. 读取/var/log/secure文件,筛选出试图登录服务器但密码错误的信息
  2. 从每一行信息中分离出对应的外部ip
  3. 对这些ip进行排序和去重,统计每个ip尝试登录的次数
  4. 遍历所有的外部ip,判断当前ip尝试登录次数是否超过忍耐值(可能自己有时候也会输错密码或者其他原因导致登录失败),将该ip添加到/etc/hosts.deny
  5. 使用Linux自带的cron服务创建定时任务,每隔一定的时间就执行该脚本

开始动手

  1. 创建deny_illegal_ip.sh脚本文件

    1
    2
    cd /usr/shell_study
    touch deny_illegal_ip.sh
  2. 创建一个文件来保存非法访问的ip

    1
    touch illegal_ip_list.txt
  3. 创建一个文件来保存cron定时任务执行日志

    1
    touch cron_task.log
  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
    32
    33
    34
    35
    36
    37
    38
    39
    IP_LIST_PATH="/usr/shell_study/illegal_ip_list.txt"
    # hosts.deny path
    HOSTS_DENY_PATH="/etc/hosts.deny"
    # hosts.deny backup path
    HOSTS_DENY_BAK_PATH="/etc/hosts.deny.bak"
    # cron task log path
    CRON_TASK_LOG_PATH="/usr/shell_study/cron_task.log"
    # the number of illegal ip
    IP_COUNT=0

    # clean old ip file
    if [ -e $IP_LIST_PATH ]
    then
    cat /dev/null > $IP_LIST_PATH
    fi

    # extract illegal ip
    cat $SECURE_LOG_PATH | grep "Failed password" | awk -F "from" '{print $2}' | awk -F "port" '{print $1}' | sort | uniq -c > $IP_LIST_PATH

    # reset hosts.deny, as we check the ip whether existed in hosts.deny, so this step can be removed
    #if [ -e $HOSTS_DENY_BAK_PATH ]
    #then
    # cat $HOSTS_DENY_BAK_PATH > $HOSTS_DENY_PATH
    #fi

    # add illegal ip to hosts.deny which appears more than once
    while read IP_LINE
    do
    COUNT=`echo $IP_LINE | awk -F " " '{print $1}'`
    IP=`echo $IP_LINE | awk -F " " '{print $2}'`
    if [ $COUNT -ge 2 -a `grep -c $IP $HOSTS_DENY_PATH` -eq 0 ]
    then
    let IP_COUNT++
    echo "sshd:$IP" >> $HOSTS_DENY_PATH
    fi
    done < $IP_LIST_PATH

    # add cron task log
    echo "$(date "+%Y-%m-%d %H:%M:%S"): added $IP_COUNT illegal ip" >> $CRON_TASK_LOG_PATH
  5. 设置定时任务,每天执行一次

    1
    2
    crontab -e
    0 0 * * * /usr/shell_study/deny_illegal_ip.sh
  6. 查看定时任务日志

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    tail -f /usr/shell_study/cron_task.log
    2022-10-24 22:00:01: added 0 illegal ips
    2022-10-24 22:30:01: added 0 illegal ips
    2022-10-24 23:00:01: added 0 illegal ips
    2022-10-24 23:30:01: added 0 illegal ips
    2022-10-25 00:00:01: added 0 illegal ips
    2022-10-25 00:30:01: added 1 illegal ips
    2022-10-25 01:00:02: added 0 illegal ips
    2022-10-25 01:30:01: added 0 illegal ips
    2022-10-25 02:00:01: added 0 illegal ips
    2022-10-25 02:30:01: added 0 illegal ips
    2022-10-25 03:00:01: added 0 illegal ips
  7. 查看ip是否被禁止访问

    1
    vim /etc/hosts.deny