2023年2月

阿粉的答案是会部署服务就行了。

关于 Docker 阿粉相信作为后端程序员小伙伴们多多少少都听过或者用过,而且很多时候也都是因为要使用的时候才会去学习,毕竟学了用不上还不如不学。

对于后端程序员来说,如果不是专门运维或者喜欢研究运维的同学,对于 Docker 的使用只要能熟练的部署服务基本上就够用了。毕竟说实话作为后端程序员要学习的东西真的太多了,一名合格的后端程序员不仅要能看得懂改得了前端代码,也要能懂运维部署和数据库设计,而且每个领域还动不动就有很多新东西要学习,只能说太难了,毕竟每个人的精力是有限的。

为了掌握一下 Docker 再加上阿粉最近搞了一台轻量服务器,想折腾点东西,但是不知道搞什么好,所以就想起来了搭建一个网站来玩玩,可以用来写一些文章和分享啥的。

搭建网站有很多种方式,不过最强大的还是要数 WordPress,但是如果要完全靠自己搭建环境还是比较复杂,所以就想着还是使用 docker 来搭建,简单快速。

0. 安装 Docker

Linux 服务器中安装 docker 非常简单,直接执行yum install docker 命令即可,阿粉这边已经安装过了,所以就提示了这个,没有安装的话,会自行安装。

1. 安装 Docker Compose

安装完了 docker 后,我们再安装一下 Docker Compose 。安装 Docker Compose 的目的主要是为了管理很多的 Docker 容器,Docker Compose 就是一个工具,通过这个工具我们可以方便的管理各个有关联关系的容器,形成一个整体。使用 Compose,我们需要在一个 YAML 文件中定义所有服务,然后使用单个命令,就可以启动或停止所有服务。

我们这里使用 Docker Compose 在隔离的容器化环境中并排运行两个容器(WordPressMySQL),一个是站点,一个是保存用户数据的。

在终端中执行下面的命令

curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

然后将可执行权限应用于二进制文件:

sudo chmod +x /usr/local/bin/docker-compose

最后可以通过检查版本来验证安装是否成功:

docker-compose --version

显示下面内容表示安装成功。

更多关于 Docker Compose 的内容可以参考官方文档

2. 站点目录

接下来我们就开始搭建站点了,首先创建或者选择一个路径,阿粉这里的路径是 /srv ,然后创建一个名为wordpress 的目录来存放 WordPress 数据。

sudo mkdir -p /srv/wordpress 
cd /srv/wordpress/

3. 创建 YAML 文件

Docker Compose 工具中,运行容器所需的所有资源都必须在名为 docker-compose.yamlYAML 文件中定义。然后 Docker Compose 将读取此类文件并与 Docker 守护程序通信以创建、配置或销毁定义的资源。

在我们的例子中,该 docker-compose.yaml 文件将包含我们的服务定义。另外 Docker Compose 允许我们将这些服务与共享网络和卷链接在一起。因为我们知道 Docker 容器本身就不会将数据持久化的,所以我们需要一个数据卷。

因此,让我们从使用 Vim 编辑器在/srv/wordpress目录中创建一个新文件docker-compose.yaml开始 。内容如下:

version: '3'
services:
  mysql:
    image: mysql:latest
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: your_root_password
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress_user
      MYSQL_PASSWORD: your_wordpress_password
    volumes:
      - mysql_data:/var/lib/mysql
  wordpress:
    image: wordpress:latest
    depends_on:
      - mysql
    ports:
      - 8080:80
    restart: always
    environment:
      WORDPRESS_DB_HOST: mysql:3306
      WORDPRESS_DB_USER: wordpress_user
      WORDPRESS_DB_PASSWORD: your_wordpress_password
    volumes:
      - ./wp-content:/var/www/html/wp-content
volumes:
  mysql_data:

这里稍微解释一下:

  • 我们定义了两个自动相互链接的容器服务、mysqlwordpress
  • 两个服务都使用 Docker 镜像,并且指定了使用最新的版本;
  • mysql 环境配置:指定 wordpress 将使用这些环境变量连接到 mysql 容器;
  • wordpress 环境配置:设置数据库连接详细信息;
  • wordpress 镜像基于 Apache 默认的端口是 80,这里我们将本机的 8080 端口映射到容器的 80 端口上;
  • mysql_data:定义一个数据卷,流入这个数据库的数据将被持久化到一个名为mysql_data 的数据卷里面,这样即使我们删除了容器,数据仍然存在于我们的机器上,并且可以再次安装在新的容器中。

另外,wordpress 下面的volumes 参数是告诉 Docker 在本地文件系统中显示 wp-content目录。这样只要我们同时拥有数据库和 wp-content 文件夹,就可以随时恢复我们的站点,即使其他所有内容都丢失了也可以恢复。

4. 使用 Docker Compose 运行 WordPress

接下来我们就可以运行 WordPress 了,首先进入 /srv/wordpress 目录,运行以下命令:

sudo docker-compose up -d

该命令将开始运行脚本,当 Docker 拉入 MySQLWordPress 镜像时,应该会在终端中看到各种“正在下载”和“正在等待”消息。4-5分钟即可。完成后我们的目录下面就会多了一个wp-content 目录,如下所示:

OK,现在带有 MySQL 容器和 WordPress 容器的 WordPress 安装已成功。

5. 访问您的 WordPress 安装

最后,在我们的容器运行的情况下,我们可以通过 WordPress Web 界面完成安装。在 Web 浏览器中,导航到http://localhost:8080http://your_ip_address:8080。如果在本地安装的话就用本地地址,如果是在服务器上面安装就用服务器的地址进行访问即可。

这里需要注意一点,如果使用的是腾讯云或者阿里云,在控制台中可能需要打开防火墙开放对应的端口,否则访问不通。访问不通的小伙伴,一定要确保自己服务器的端口是开放的。

打开后将看到语言选择页面。选择您的语言,然后单击继续,接下来就是根据页面上显示的内容进行填写就好了,主要是一些账号密码,站点名称,这些后面都是可以在重新改的,所以不用太纠结。最后成功通过身份验证后,就可以开始使用 WordPress 了。

到这里其实整个搭建就已经完成了,但是并不代表站点就可以正常使用了,我们还需要做的是购买域名,备案域名,再配置 HTTPS,等这一系列都完成了过后,整个站点才算是搭建完毕。

下面这几个都是阿粉常用的wordpress 的网站,感兴趣的小伙伴可以去看看,有条件的也可以尝试自己去搭建一个,搭建过程中如果遇到问题,可以通过文末的形式进读者群,我们一起探讨。

http://www.itmind.net/

https://www.cxy521.com/

http://itmooc.tech/

image-20220818222907308

https://yuandifly.com/

6. Docker Compose 命令

首先,确保您在/srv/wordpress目录中。

docker-compose ps // 查看容器状态
docker-compose up -d  // 启动
docker-compose stop   // 停止

注意:阿粉这里配置的端口是 9996,上面给大家演示使用的是 8080,这个完全没有任何影响,主要是阿粉这边 8080 已经被使用了。

7. 结论

最后整篇的安装文章就到这里了,通过 Docker 的安装还是非常简单的,主要网络和端口没问题,基本上都能安装成功的,剩下的就是 docker 的一些使用技巧了,不在本文讨论的范围之内就不细说了,下篇文章教大家如果给 WordPress 站点配置域名,并通过 HTTPS 的方式进行访问。

在日常开发的过程中我们经常会需要调用第三方组件或者数据库,有的时候可能会因为网络抖动或者下游服务抖动,导致我们某次查询失败,这种时候我们往往就会进行重试,当重试几次后依旧还是失败的话才会向上抛出异常进行失败。接下来阿粉就给大家演示一下通常是如何做的,以及如何更优雅的进行重试。

常规做法

我们先来看一下常规做法,常规做法首先会设置一个重试次数,然后通过 while 循环的方式进行遍历,当循环次数没有达到重试次数的时候,直到有正确结果后就返回,如果重试依旧失败则会进行睡眠一段时间,再次重试,直到正常返回或者达到重试次数返回。

package com.example.demo.service;

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

import java.util.Random;
import java.util.concurrent.TimeUnit;

@Service
public class HelloService {
  public String sayHello(String name) {
    String result = "";
    int retryTime = 3;
    while (retryTime > 0) {
      try {
        //
        result = name + doSomething();
        return result;
      } catch (Exception e) {
        System.out.println("send message failed. try again in 1's");
        retryTime--;
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException ex) {
          throw new RuntimeException(ex);
        }
      }
    }
    return result;
  }

  private int doSomething() {
    Random random = new Random();
    int i = random.nextInt(3);
    System.out.println("i is " + i);
    return 10 / i;
  }
}

这里为了模拟异常的情况,阿粉在 doSomething 函数里面进行了随机数的生成和使用,当随机出来的值为 0 的时候,则会触发 java.lang.ArithmeticException 异常,因为 0 不能作除数。

接下来我们再对外提供一个接口用于访问,代码如下

package com.example.demo.controller;

import com.example.demo.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

  @Autowired
  private HelloService helloService;

  @GetMapping(value = "/hello")
  public String hello(@RequestParam("name") String name) {
    return helloService.sayHello(name);
  }
}

正常启动过后,我们通过浏览器进行访问

可以看到,我们第一次方法的时候就成功的达到了我们要的效果,随机数就是 0 ,在 1 秒后重试后结果正常。在多试了几次过后,会遇到三次都是 0 的情况,这个时候才会抛出异常,说明服务是真的有问题了。

上面的代码可以看到是有效果了,虽然不是很好看,特别是在还有一些其他逻辑的情况,看上去会很臃肿,但是确实是可以正常使用的,那么有的小伙伴就要问了,有没有一种优雅的方式呢?总不能在很多地方都重复的这样写重试的代码吧。

注解重试

要知道我们普通人在日常开发的时候,如果遇到一个问题肯定是别人都遇到过的,什么时候当我们遇到的问题,没有人遇到过的时候,那说明我们是很前卫的。

因此小伙伴能想到的是不是有简单的方式来进行重试,有的人已经帮我们想好了,可以通过 @Retryable 注解来实现一样的效果,接下来阿粉就给大家演示一下如何使用这个注解。

首先我们需要在启动类上面加入 @EnableRetry 注解,表示要开启重试的功能,这个很好理解,就像我们要开启定时功能需要添加 @EnableScheduling 注解一样,Spring@Enablexxx 注解也是很有意思的,后面我们再聊。

添加完注解以后,需要加入切面的依赖,如下

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.9.2</version>
</dependency>

如下不加入这个切面依赖,启动的时候会有如下异常

添加的注解和依赖过后,我们需要改造 HelloService 里面的 sayHello() 方法,简化成如下,增加 @Retryable 注解,以及设置相应的参数值。

  @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
  public String sayHello(String name){
    return name + doSomething();
  }

再次通过浏览器访问 http://127.0.0.1:8080/hello?name=ziyou 我们看到效果如下,跟我们自己写的重试一样。

@Retryable 详解

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.retry.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
    String recover() default "";

    String interceptor() default "";

    Class<? extends Throwable>[] value() default {};

    Class<? extends Throwable>[] include() default {};

    Class<? extends Throwable>[] exclude() default {};

    String label() default "";

    boolean stateful() default false;

    int maxAttempts() default 3;

    String maxAttemptsExpression() default "";

    Backoff backoff() default @Backoff;

    String exceptionExpression() default "";

    String[] listeners() default {};
}

点到这个注解里面,我们可以看到这个注解的代码如下,其中有几个参数我们来解释一下

  • recover: 当前类中的回滚方法名称;
  • interceptor: 重试的拦截器名称,重试的时候可以配置一个拦截器;
  • value:需要重试的异常类型,跟下面的 include 一致;
  • include:包含的重试的异常类型;
  • exclude:不包含的重试异常类型;
  • label:用于统计的唯一标识;
  • stateful:标志表示重试是有状态的,也就是说,异常被重新抛出,重试策略是否会以相同的策略应用于具有相同参数的后续调用。如果是 false,那么可重试的异常就不会被重新抛出。
  • maxAttempts:重试次数;
  • backoff:指定用于重试此操作的属性;
  • listeners:重试监听器 bean 名称;

配合上面的一些属性的使用,我们就可以达到通过注解简单来实现方法调用异常后的自动重试,非常好用。我们可以在执行重试方法的时候设置自定义的重试拦截器,如下所示,自定义重试拦截器需要实现 MethodInterceptor 接口并实现 invoke 方法,不过要注意,如果使用了拦截器的话,那么方法上的参数就会被覆盖。

package com.example.demo.pid;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.retry.interceptor.RetryInterceptorBuilder;
import org.springframework.retry.interceptor.RetryOperationsInterceptor;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.stereotype.Component;

@Component
public class CustomRetryInterceptor implements MethodInterceptor {

  @Override
  public Object invoke(MethodInvocation invocation) throws Throwable {
    RetryOperationsInterceptor build = RetryInterceptorBuilder.stateless()
      .maxAttempts(2).backOffOptions(3000, 2, 1000).build();
    return build.invoke(invocation);
  }
}

自定义回滚方法,我们还可以在重试几次依旧错误的情况,编写自定义的回滚方法。

  @Retryable(value = Exception.class,
    recover = "recover", maxAttempts = 2,
    backoff = @Backoff(delay = 1000, multiplier = 2))
  public String sayHello(String name){
    return name + doSomething();
  }

  @Recover
  public String recover(Exception e, String name) {
    System.out.println("recover");
    return "recover";
  }

要注意:

  • 重试方法必须要使用 @Recover 注解;
  • 返回值必须和被重试的函数返回值一致;
  • 参数中除了第一个是触发的异常外,后面的参数需要和被重试函数的参数列表一致;

上面代码中的 @Backoff(delay = 1000, multiplier = 2) 表示第一次延迟 1000ms 重试,后面每次重试的延迟时间都翻倍。

总结

阿粉今天给大家介绍了一下 Spring@Retryable 注解使用,并通过几个 demo 来带大家编写了自己重试拦截器以及回滚方法的时候,是不是感觉用起来会很爽,那还在等什么赶紧用起来吧,其中还有很多细节,只有自己真正的使用过才能体会到。


更多优质内容欢迎关注公众号【Java 极客技术】,我准备了一份面试资料,回复【bbbb07】免费领取。希望能在这寒冷的日子里,帮助到大家。

layout: post
categories: Java
title: Java 中经常被提到的 SPI 到底是什么?
tagline: by 子悠
tags: 
  - 子悠

Java 程序员在日常工作中经常会听到 SPI,而且很多框架都使用了 SPI 的技术,那么问题来了,到底什么是 SPI 呢?今天阿粉就带大家好好了解一下 SPI。

SPI 概念

SPI 全称是 Service Provider Interface,是一种 JDK 内置的动态加载实现扩展点的机制,通过 SPI 技术我们可以动态获取接口的实现类,不用自己来创建。

这里提到了接口和实现类,那么 SPI 技术上具体有哪些技术细节呢?

  1. 接口:需要有一个功能接口;
  2. 实现类:接口只是规范,具体的执行需要有实现类才行,所以不可缺少的需要有实现类;
  3. 配置文件:要实现 SPI 机制,必须有一个与接口同名的文件存放于类路径下面的 META-INF/services 文件夹中,并且文件中的每一行的内容都是一个实现类的全路径;
  4. 类加载器 ServiceLoaderJDK 内置的一个类加载器,用于加载配置文件中的实现类;

举个栗子

上面说了 SPI 的几个概念,接下来阿粉就通过一个栗子来带大家感受一下具体的用法。

第一步

创建一个接口,这里我们创建一个解压缩的接口,其中定义了压缩和解压的两个方法。

package com.example.demo.spi;

/**
 * <br>
 * <b>Function:</b><br>
 * <b>Author:</b>@author ziyou<br>
 * <b>Date:</b>2022-10-08 21:31<br>
 * <b>Desc:</b>无<br>
 */
public interface Compresser {
  byte[] compress(byte[] bytes);
  byte[] decompress(byte[] bytes);
}

第二步

再写两个对应的实现类,分别是 GzipCompresser.javaWinRarCompresser.java 代码如下

package com.example.demo.spi.impl;

import com.example.demo.spi.Compresser;

import java.nio.charset.StandardCharsets;

/**
 * <br>
 * <b>Function:</b><br>
 * <b>Author:</b>@author ziyou<br>
 * <b>Date:</b>2022-10-08 21:33<br>
 * <b>Desc:</b>无<br>
 */
public class GzipCompresser implements Compresser {
  @Override
  public byte[] compress(byte[] bytes) {
    return"compress by Gzip".getBytes(StandardCharsets.UTF_8);
  }
  @Override
  public byte[] decompress(byte[] bytes) {
    return "decompress by Gzip".getBytes(StandardCharsets.UTF_8);
  }
}
package com.example.demo.spi.impl;

import com.example.demo.spi.Compresser;

import java.nio.charset.StandardCharsets;

/**
 * <br>
 * <b>Function:</b><br>
 * <b>Author:</b>@author ziyou<br>
 * <b>Date:</b>2022-10-08 21:33<br>
 * <b>Desc:</b>无<br>
 */
public class WinRarCompresser implements Compresser {
  @Override
  public byte[] compress(byte[] bytes) {
    return "compress by WinRar".getBytes(StandardCharsets.UTF_8);
  }

  @Override
  public byte[] decompress(byte[] bytes) {
    return "decompress by WinRar".getBytes(StandardCharsets.UTF_8);
  }
}

第三步

创建配置文件,我们接着在 resources 目录下创建一个名为 META-INF/services 的文件夹,在其中创建一个名为 com.example.demo.spi.Compresser 的文件,其中的内容如下:

com.example.demo.spi.impl.WinRarCompresser
com.example.demo.spi.impl.GzipCompresser

注意该文件的名称必须是接口的全路径,文件里面的内容每一行都是一个实现类的全路径,多个实现类就写在多行里面,效果如下。

第四步

有了上面的接口,实现类和配置文件,接下来我们就可以使用 ServiceLoader 动态加载实现类,来实现 SPI 技术了,如下所示:

package com.example.demo;

import com.example.demo.spi.Compresser;

import java.nio.charset.StandardCharsets;
import java.util.ServiceLoader;

public class TestSPI {
  public static void main(String[] args) {
    ServiceLoader<Compresser> compressers = ServiceLoader.load(Compresser.class);
    for (Compresser compresser : compressers) {
      System.out.println(compresser.getClass());
    }
  }
}

运行的结果如下

可以看到我们正常的获取到了接口的实现类,并且可以直接使用实现类的解压缩方法。

原理

知道了如何使用 SPI 接下来我们来研究一下是如何实现的,通过上面的测试我们可以看到,核心的逻辑是 ServiceLoader.load() 方法,这个方法有点类似于 Spring 中的根据接口获取所有实现类一样。

点开 ServiceLoader 我们可以看到有一个常量 PREFIX,如下所示,这也是为什么我们必须在这个路径下面创建配置文件,因为 JDK 代码里面会从这个路径里面去读取我们的文件。

同时又因为在读取文件的时候使用了 class 的路径名称,因为我们使用 load 方法的时候只会传递一个 class,所以我们的文件名也必须是接口的全路径。

通过 load 方法我们可以看到底层构造了一个 java.util.ServiceLoader.LazyIterator 迭代器。

在迭代器中的 parse 方法中,就获取了配置文件中的实现类名称集合,然后在通过反射创建出具体的实现类对象存放到 LinkedHashMap<String,S> providers = new LinkedHashMap<>(); 中。

常用的框架

SPI 技术的使用非常广泛,比如在 Dubble,不过 Dubble 中的 SPI 有经过改造的,还有我们很常见的数据库的驱动中也使用了 SPI,感兴趣的小伙伴可以去翻翻看,还有 SLF4J 用来加载不同提供商的日志实现类以及 Spring 框架等。

优缺点

前面介绍了 SPI 的原理和使用,那 SPI 有什么优缺点呢?

优点

优点当然是解耦,服务方只要定义好接口规范就好了,具体的实现可以由不同的 Jar 进行实现,只要按照规范实现功能就可以被直接拿来使用,在某些场合会被进行热插拔使用,实现了解耦的功能。

缺点

一个很明显的缺点那就是做不到按需加载,通过源码我们看到了是会将所有的实现类都进行创建的,这种做法会降低性能,如果某些实现类实现很耗时了话将影响加载时间。同时实现类的命名也没有规范,让使用者不方便引用。

总结

阿粉今天给大家介绍了一个 SPI 的原理和实现,感兴趣的小伙伴可以自己去尝试一下,多动手有利于加深记忆哦,如果觉得我们的文章有帮助,欢迎点赞评论分享转发,让更多的人看到。


更多优质内容欢迎关注公众号【Java 极客技术】,我准备了一份面试资料,回复【bbbb07】免费领取。希望能在这寒冷的日子里,帮助到大家。

layout: post
categories: Java
title: 一文带你了解 Spring 的@Enablexxx 注解
tagline: by 子悠
tags: 
  - 子悠

前面的文章给大家介绍 Spring 的重试机制的时候有提到过 Spring 有很多 @Enable 开头的注解,平时在使用的时候也没有注意过为什么会有这些注解,今天就给大家介绍一下。

@Enable 注解

首先我们先看一下有哪些常用的 @Enable 开头的注解,以及都是干什么用的。

  • @EnableRetry:开启 Spring 的重试功能;
  • @EnableScheduling:开启 Spring 的定时功能;
  • @EnableAsync:开启 Spring 的异步功能;
  • @EnableAutoConfiguration:开启 Spring 的自动装配功能;

上面这几个是我们经常会用到和看到的,都知道在使用相应的功能的时候,如果没有配置上面的注解功能都是不生效的。以我们前面的文章的 Spring 重试为例,我们需要在启动类上面配置 @EnableRetry ,否则自动重试注解 @Retryable 是不会生效的,如下所示,没看过的可以去看下,https://mp.weixin.qq.com/s/U_nm92ujCGArkii5ze7uaA。

@Import 注解

那有的小伙伴就要问了,这个 @EnableRetry 注解到底有什么作用呢?不用这个注解就没办法了吗?

要知道这个注解有什么功效,我们可以点开看看源码,代码如下

package org.springframework.retry.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@Import(RetryConfiguration.class)
@Documented
public @interface EnableRetry {

	boolean proxyTargetClass() default false;
}

可以看到源码很简单,其中最有用的就一行 @Import(RetryConfiguration.class) ,我们可以尝试把这一行代码放到启动类上面看看效果,如下所示,可以看到项目可以正常启动,并且也还是有效果的,说明跟我们的 @EnableRetry 注解是一样的。

从上面的实验效果我们可以看到 @EnableRetry 注解其实就是对 @Import(RetryConfiguration.class) 的一个封装,同样的通过源码我们还可以看到 @EnableScheduling 注解就是对 @Import({SchedulingConfiguration.class}) 的一个封装。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({SchedulingConfiguration.class})
@Documented
public @interface EnableScheduling {
}

那如果在没有 @Enablexxx 注解的时候,我们直接通过 @Import 注解是可以这样写的,在一个 @Import 注解里面包含多个配置类,不过这种在配置类较多的场景下还是相对不够简洁的,因而才有了各自功能对应的 @Enable 注解。

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.retry.annotation.RetryConfiguration;
import org.springframework.scheduling.annotation.SchedulingConfiguration;

@SpringBootApplication
@ComponentScan(value = "com.example.demo.*")
@Import({RetryConfiguration.class, SchedulingConfiguration.class})
public class DemoApplication {

  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }

}

为什么要使用 @Import 注解呢

那么很多的小伙伴又要问了,为啥要通过使用 @Import 注解将配置类加载进来呢?在项目中的 Spring 上下文中不是能直接获取到吗?为此我们来实验一下,通过下面的代码我们看下是否能在 Spring 的容器中获取到 RetryConfiguration Bean

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.retry.annotation.RetryConfiguration;
import org.springframework.scheduling.annotation.SchedulingConfiguration;

@SpringBootApplication
@ComponentScan(value = "com.example.demo.*")
//@Import({RetryConfiguration.class, SchedulingConfiguration.class})
public class DemoApplication {

  public static void main(String[] args) {

    ConfigurableApplicationContext applicationContext = SpringApplication.run(DemoApplication.class, args);
    Object bean = applicationContext.getBean("org.springframework.retry.annotation.RetryConfiguration");
    System.out.println(bean.toString());
  }
}

启动过后我们可以看到结果如下,提示我们在容器中找不到这个 bean,有点小伙伴会说是不是 bean 的名字写错了,其实并不是,紧接着我们再把注释的那一行放开再运行一下。

可以看到,这次我们成功的获取到了这个 Bean,这个实验就是告诉我们,其实在默认情况下,Spring 的容器中是找不到RetryConfiguration 这个 Bean 的,因此我们需要通过使用 @Import 注解,将该类加载到容器中。

那么为什么在容器中找不到这个 Bean 呢?

其实很简单,因为这个 Bean 跟我们当前环境的类是不是同一个包里面的,在项目启动的过程中并不会扫描到 RetryConfiguration 类所在的包,因此找不到是很正常的。

总结

上面通过 @EnableRetry 这个注解带大家了解了一下 Spring@Enable 开头的注解的使用原理,相信大家对这些注解有了更深入的了解。简单来说就是因为我们要使用的很多类并不在我们项目所在的包下面,我们不能将所有的依赖包都进行扫描,也不不方便将所有的配置类都通过 @Import 的方式进行导入,而是让每个功能的项目包都提供一个 @Enable 开头的注解,我们直接启用注解就可以达到效果。

这种方式我们在平时的开发中也可以自己实现,实现一个自己的 @Enable 开头的注解来实现特定的功能,下一篇文章我们来带大家实现一下。好了,今天的文章就到这里,如果觉得有帮助还请大家帮我们的文章点赞,评论,转发,一键三连走起。


更多优质内容欢迎关注公众号【Java 极客技术】,我准备了一份面试资料,回复【bbbb07】免费领取。希望能在这寒冷的日子里,帮助到大家。


layout: post
categories: Java
title: Java 中你绝对没用过的一个关键字?
tagline: by 子悠
tags:

  • 子悠

前面的文章给大家介绍了如何自定义一个不可变类,没看过的小伙伴建议去看一下,这节课给大家介绍一个 Java 中的一个关键字 Record,那 Record 关键字跟不可变类有什么关系呢?看完今天的文章你就知道了。友情提示 Record 关键字在 Java14 过后才支持的,所以是不是被阿粉说中了,还在使用 Java 8 的你一定没用过!

不可变类

我们先看一下之前定义的不可变类,代码如下。

package com.example.demo.immutable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class Teacher {
  private final String name;
  private final List<String> students;
  private final Address address;
  private final Map<String, String> metadata;

  public Teacher(String name, List<String> students, Address address, Map<String, String> metadata) {
    this.name = name;
    this.students = students;
    this.address = address;
    this.metadata = metadata;
  }

  public String getName() {
    return name;
  }

  public List<String> getStudents() {
    return new ArrayList<>(students);
//    return students;
  }

  public Address getAddress() {
//    return address;
    return address.clone();
  }

  public Map<String, String> getMetadata() {
    return new HashMap<>(metadata);
//    return metadata;
  }
}

如果你复制上面代码到 IDEA 中,并且刚好你的 JDK 版本是 Java14 之后的话,那么你就会看到下面这个提示,提示我们可以将 Teacher 这个不可变类转换为 Record。怎么样是不是很懵,没关系,我们按照提示操作一下看看会发生什么。

点完之后我们的代码会变成下面的样子

package com.example.demo.immutable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public record Teacher(String name, List<String> students, Address address, Map<String, String> metadata) {

  @Override
  public List<String> students() {
    return new ArrayList<>(students);
//    return students;
  }

  @Override
  public Address address() {
//    return address;
    return address.clone();
  }

  @Override
  public Map<String, String> metadata() {
    return new HashMap<>(metadata);
//    return metadata;
  }
}

仔细一看你会发现,这是什么情况,record 是什么关键字,然后类名后面怎么还有参数?乍一看还以为变成一个方法了。此外我们之前的测试代码不用修改任何逻辑,照样可以正常运行,是不是很神奇?这就是 Record 关键字的特性。

Record 关键字

看完了 Record 关键字的 case ,我们来聊一下 Record 关键字是怎么用的,以及它有什么特性。

  1. Record 关键定义的类是不可变类;
  2. Record 定义的类需要将所有成员变量通过参数的形式定义;
  3. Record 定义的类默认会生成全部参数的构造方法;
  4. Record 定义的类中可以定义静态方法;
  5. Record 定义的类可以提供紧凑的方式进行参数校验;

上面的五点里面前三点我们在之前的例子中都可以看出来,在定义和使用的时候可以明显的看到,如下所示。

public record Teacher(String name, List<String> students, Address address, Map<String, String> metadata) {
}//1,2
 Teacher teacher = new Teacher("Java极客技术", students, address, metadata);//3

下面我们看下第四点和第五点,关于第四点我们可以在 Record 类中定义静态方法用来默认初始化对象,如下所示,通过这种方式我们可以写出更简洁的代码。

  public static Teacher of() {
    return new Teacher("Java极客技术", new ArrayList<>(), new Address(), new HashMap<>());
  }

  public static Teacher of(String name) {
    return new Teacher(name, new ArrayList<>(), new Address(), new HashMap<>());
  }

在使用的时候,我们就可以直接通过类名引用静态方法就可以了,如下所示

 Teacher teacher = Teacher.of();

接下来我们看看什么叫紧凑的方式进行参数校验,试想一下,如果我们需要校验在沟通 Teacher 对象的时候,student 成员变量不能为空,在我们以前的写法里面只要在构造方法里面进行一下判空就可以了,但是对于 Record 的形式,我们没有显示的创建构造方法,那我们应该如何进行判断呢?答案如下

  public Teacher {
    if (null == students || students.size() == 0) {
      throw new IllegalArgumentException();
    }
  }

可以看到我们通过一种紧凑的构造方法的形式来进行了参数的校验,这种写法跟我们普通的构造方法是不一样的,没有方法参数,怎么样是不是很神奇。

总结

有的人说 JavaRecord 的新特性是为了让大家不使用 Lombok 的,阿粉倒是觉得不见得,毕竟 Lombok 用起来是真的香,而且 Record 也只能是定义不可变类,在某些情况下使用还是有局限性的,不可变类的使用场景并不是很多。

更多优质内容欢迎关注公众号【Java 极客技术】,我准备了一份面试资料,回复【bbbb07】免费领取。希望能在这寒冷的日子里,帮助到大家。