2023年2月

关于 SpringBoot 的自动装配功能,相信是每一个 Java 程序员天天都会用到的一个功能,但是它究竟是如何实现的呢?今天阿粉来带大家看一下。

自动装配案例

首先我们通过一个案例来看一下自动装配的效果,创建一个 SpringBoot 的项目,在 pom 文件中加入下面的依赖。

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

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

其中 web 的依赖表示我们这是一个 web 项目,redis 的依赖就是我们这边是要验证的功能依赖。随后在 application.properties 配置文件中增加 redis 的相关配置如下

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123456

再编写一个 ControllerService 类,相关代码如下。

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);
  }

}

service 代码如下

package com.example.demo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class HelloService {

  @Autowired
  RedisTemplate<String, String> redisTemplate;

  public String sayHello(String name) {
    String result = doSomething(name);
    redisTemplate.opsForValue().set("name", result);
    result = redisTemplate.opsForValue().get("name");
    return "hello: " + result;
  }

  private String doSomething(String name) {
    return name + " 欢迎关注 Java 极客技术";
  }

}

启动项目,然后我们通过访问 http://127.0.0.1:8080/hello?name=ziyou,可以看到正常访问。接下来我们再通过 Redis 的客户端,去观察一下数据是否正确的写入到 Redis 中,效果跟我们想象的一致。

自动装配分析

看到这里很多小伙伴就会说,这个写法我天天都在使用,用起来是真的爽。虽然用起来是很爽,但是大家有没有想过一个问题,那就是在我们的 HelloService 中通过 @Autowired 注入了一个 RedisTemplate 类,但是我们的代码中并没有写过这个类,也没有使用类似于@RestController,@Service 这样的注解将 RedisTemplate 注入到 Spring IoC 容器中,那为什么我们就可以通过 @Autowired 注解从 IoC 容器中获取到 RedisTemplate 这个类呢?这里就是常说的自动装配的功能了。

首先我们看下项目的启动类,

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(value = "com.example.demo.*")
public class DemoApplication {

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

在启动类上面有一个 @SpringBootApplication 注解,我们点进去可以看到如下内容

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
   // 省略
}

在这个注解中,其中有一个 @EnableAutoConfiguration 注解,正是因为有了这样一个注解,我们才得以实现自动装配的功能。继续往下面看。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

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

    String[] excludeName() default {};
}

可以看到 @EnableAutoConfiguration 注解中有一个 @Import({AutoConfigurationImportSelector.class}),导入了一个 AutoConfigurationImportSelector 类,该类间接实现了 ImportSelector 接口,实现了一个 String[] selectImports(AnnotationMetadata importingClassMetadata); 方法,这个方法的返回值是一个字符串数组,对应的是一系列主要注入到 Spring IoC 容器中的类名。当在 @Import 中导入一个 ImportSelector 的实现类之后,会把该实现类中返回的 Class 名称都装载到 IoC 容器中。

一旦被装载到 IoC 容器中过后,我们在后续就可以通过 @Autowired 来进行使用了。接下来我们看下 selectImports 方法里面的实现,当中引用了 getCandidateConfigurations 方法 ,其中的 ImportCandidates.load 方法我们可以看到是通过加载 String location = String.format("META-INF/spring/%s.imports", annotation.getName()); 对应路径下的 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,其中就包含了很多自动装配的配置类。

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
        ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

我们可以看到这个文件中有一个 RedisAutoConfiguration 配置类,在这个配置中就有我们需要的 RedisTemplate 类的 Bean,同时也可以看到,在类上面有一个 @ConditionalOnClass({RedisOperations.class}) 注解,表示只要在类路径上有 RedisOperations.class 这个类的时候才会进行实例化。这也就是为什么只要我们添加了依赖,就可以自动装配的原因。

通过 org.springframework.boot.autoconfigure.AutoConfiguration.imports 这个文件,我们可以看到有很多官方帮我们实现好了配置类,这些功能只要我们在 pom 文件中添加对应的 starter 依赖,然后做一些简单的配置就可以直接使用。

其中本质上自动装配的原理很简单,本质上都需要实现一个配置类,只不过这个配置类是官方帮我们创建好了,再加了一些条件类注解,让对应的实例化只发生类类路径存在某些类的时候才会触发。这个配置类跟我们平常自己通过 JavaConfig 形式编写的配置类没有本质的区别。

自动装配总结

从上面的分析我们就可以看的出来,之所以很多时候我们使用 SpringBoot 是如此的简单,全都是依赖约定优于配置的思想,很多复杂的逻辑,在框架底层都帮我们做了默认的实现。虽然用起来很爽,但是很多时候会让程序员不懂原理,我们需要做的不仅是会使用,而更要知道底层的逻辑,才能走的更远。

基于上面的分析,我们还可以知道,如果我们要实现一个自己的 starter 其实也很简单,只要安装上面的约定,编写我们自己的配置类和配置文件即可。后面的文章阿粉会带你手写一个自己的 starter 来具体实现一下。


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

用了这么多年的 SpringBoot 那么你知道什么是 SpringBootweb 类型推断吗?

估计很多小伙伴都不知道,毕竟平时开发做项目的时候做的都是普通的 web 项目并不需要什么特别的了解,不过抱着学习的心态,阿粉今天带大家看一下什么是 SpringBootweb 类型推断。

SpringBoot 的 web 类型有哪些

既然是web 类型推断,那我们肯定要知道 SpringBoot 支持哪些类型,然后才能分析是怎样进行类型推断的。

根据官方的介绍 SpringBootweb 类型有三种,分别是,NONESERVLETREACTIVE,定义在枚举 WebApplicationType 中,这三种类型分别代表了三种含义:

  1. NONE:不是一个 web 应用,不需要启动内置的 web 服务器;
  2. SERVLET:基于 servletweb 应用,需要启动一个内置的 servlet 服务器;
  3. REACTIVE:一个 reactiveweb 应用,需要启动一个内置的 reactive 服务器;
public enum WebApplicationType {
	NONE,
	SERVLET,
	REACTIVE;
}

web 类型推断

上面提到了 SpringBoot 的三种 web 类型,接下来我们先通过代码验证一下,然后再分析一下 SpringBoot 是如何进行类型推断的。

首先我们通过在 https://start.spring.io/ 快速的构建三种类型的项目,三种类型的项目配置除了依赖不一样之外,其他都一样,如下所示

None web

none

下载后的项目文件 pom 中对应的依赖为

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

Servlet web

servlet

下载后的项目文件 pom 中对应的依赖为

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

Reactive web

reactive

下载后的项目文件 pom 中对应的依赖为

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

接下来我们依次启动三个项目看看有什么区别,

启动 None web

none-web

通过启动日志我们可以看到,在 None web 类型下,应用启动运行后就自动关闭了,并没有启动内置的 web 服务器,也没有监听任何端口。接下来我们看看其他两种类型 web 的启动日志都是怎么样的。

启动 Servlet web

servelt-web

通过启动日志我们可以看到这里启动了内置的 Tomcat Servlet 服务器,监听了 8080 端口,应用程序并不会像 None 类型一样,启动后就自动关闭。

启动 Reactive web

reactive-web

通过启动日志我们可以看到,这里启动了内置的 Netty 服务器,并监听在 8080 端口上(如果启动失败记得把上面 servlet web 关闭,不然端口会冲突)。

三种类型的服务我们都成功启动了,那么接下来的问题就是 SpringBoot 是如何判断出该使用哪种类型的呢?

这三个服务我们只有依赖不一样,很明显肯定和依赖有关系,接下来我们就来研究一下 SpringBoot 是如何实现的。

SpringBoot Web 类型推断原理

我们在 main 方法中点击 run 方法,

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
		return new SpringApplication(primarySources).run(args);
}

在构造函数中我们可以看到其中有这么一行 this.webApplicationType = WebApplicationType.deduceFromClasspath();根据属性名称我们可以推断,web 类型就是根据 WebApplicationType.deduceFromClasspath(); 这个静态方法来判断的。接下来我们看下这个方法的细节。

如上图所示,可以看到 SpringBoot 底层是通过 ClassUtils.isPresent() 方法来判断对应的 web 类型类是否存在来判断 web 类型的。

在前类路径下面如果当 org.springframework.web.reactive.DispatcherHandler 存在而且 org.springframework.web.servlet.DispatcherServletorg.glassfish.jersey.servlet.ServletContainer 都不存在的时候说明当前应用 web 类型为 Reactive

javax.servlet.Servletorg.springframework.web.context.ConfigurableWebApplicationContext 任何一个不存在的时候,就说明当前应用是 None 类型非 web 应用。否则当前应用就为 Servlet 类型。

而我们再看这个 ClassUtils.isPresent() 方法,可以发现底层是通过 className 在类路径上加载对应的类,如果存在则返回 true,如果不存在则返回 false
image-20221224154733977

因此这也解释了为什么我们在 pom 文件中只要加入对应的依赖就可以直接得到相应的 web 类型了,因为当我们在 pom 中加入相应的依赖过后,类路径里面就存在了前面判断的对应的类,再通过 ClassUtils.isPresent() 就判断出来当前应用属于那种 web 类型了。

内置服务器是如何创建的

知道了 SpringBoot 是如何进行 web 类型推断的,那么接下来一个问题就是 SpringBoot 是如何根据 web 类型进行相应内置 web 服务器的启动的呢?这里我们以 Reactive web 为例进行调试追踪。

首先我们在 SpringApplicationrun 方法 createApplicationContext() 下一行打断点,可以发现创建成功的 context 类型为 AnnotationConfigReactiveWebServerApplicationContext 很明显在这一步的时候就已经根据类型推断得到了当前的应用 web 类型为 Reactive,并且根据 web 类型创建出了对应的 ApplicationContext

reactive-web

紧接着我们进入 org.springframework.boot.SpringApplication#refreshContext 方法,最后我们可以进入到 org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#refresh 方法中,因为 AnnotationConfigReactiveWebServerApplicationContext 继承了 ReactiveWebServerApplicationContext

继续通过引用关系,我们可以找到 org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#onRefresh 方法,而在这个方法里面我们就会发现了如下代码,此处就会创建一个 webServer

具体创建的方法在 WebServerManager 里面,跟着继续往下找我们可以找到 createHttpServer() 方法,在 createHttpServer() 方法中就创建了 HttpServer 并且绑定了默认的端口 8080。具体过程,如下几张接入所示,感兴趣的可以自行跟踪 debug,至此一个 Reactive 内置服务器就创建成功了,同样的 Servlet 服务器也是类似的。




总结

Spring 的出现给 Java 程序员带来了春天,而 SpringBoot 框架的出现又极大的加速了程序员的开发效率,然而很多时候我们在使用她的便利的同时会缺少对于底层系统实现的把握,希望这篇文章弄帮助大家对 SpringBoot 产生更多的理解。

相信大家最近被 ChatGPT 刷屏了,其实在差不多一个月前就火过一次,不会那会好像只在程序员的圈子里面火起来了,并没有被大众认知到,不知道最近是因为什么又火起来了,而且这次搞的人尽皆知。

想着这么火的 AI 完全可以好好玩一玩呀,于是就尝试着将 ChatGPT 接入到了个人微信中,实现在微信中调戏 AI

先看几个聊天截图

还可以拉入到一个群,进行群聊,可以看到 ChatGPT 的训练数据相当还是比较旧的,有些问题回答的还是历史的答案。不过从上面的 case 可以看到,目前的这个效果比以往要好很多的,最起码可以联系上下文了,不得不说,这绝对是一个里程碑。

流程

想要在微信中接入 ChatGPT 我们需要如下几个步骤

  1. 成功注册一个 OpenAi 的账号,获取的到一个 API key
  2. 基于开源项目 wechatbot 运行一个微信机器人程序;
  3. 用微信扫码登录(建议用小号);

注册

OpenAi 由于某些原因,在国内是无法正常注册账号的,并且连网站都是无法正常访问的,详细的注册流程不在本文的讨论范围之内,感兴趣的可以在 docs.qq.com/doc/DUE5NcFZQV1F5SHli 查看注册细节。

这里给大家演示一下注册成功后,如何获取一个 API key,当我们注册成功并且登录以后,可以在链接 https://platform.openai.com/account/api-keys,在页面上点击 Create new Secret key 即可生成一个 API key,复制 API key存放起来,后续备用。

运行机器人程序

对于技术出身的朋友,程序的运行有几种方式,可以基于源码来运行,也可以基于 docker 来运行,阿粉这边为了简单,就直接通过 docker 来运行,在服务上面安装好 docker 后,直接运行如下命令即可。

docker run -itd --name wechatbot --restart=always \
 -e APIKEY=刚刚生成的 API key \
 -e AUTO_PASS=false \
 -e SESSION_TIMEOUT=60s \
 -e MODEL=text-davinci-003 \
 -e MAX_TOKENS=512 \
 -e TEMPREATURE=0.9 \
 -e REPLY_PREFIX=ChatGPT: \
 -e SESSION_CLEAR_TOKEN=下一个问题 \
 docker.mirrors.sjtug.sjtu.edu.cn/qingshui869413421/wechatbot:latest

正常了说启动了过后,通过 docker ps 命令我们可以看到有相应的 docker 进程如下。

接下来我们进入的容器中,去查询二维码,并扫码登录,依次执行下面两行代码,不出意外的话,可以看到一个二维码出来,用微信扫码登录一下即可(建议用小号)。

docker exec -it wechatbot bash
tail -f -n 50 /app/run.log

扫码登录过后,我们就可以愉快的玩耍了,不过需要注意的是,API 的调用对于每个账户是有额度限制的,目前每个账户限制的是 18 美元,所以大家用来自己玩玩,调戏调戏 AI 就好,wechatbot 的项目地址在这里 https://github.com/qingconglaixueit/wechatbot ,感兴趣的也可以去研究研究。

思考

ChatGPT 的出现打破了以往的人工智障,虽然现在也还有很多不足的地方,相信在后面的迭代和升级中可以有更好的表现。同时 AI 目前已经越来越成熟,相信以后会有更多的工作岗位和技能将会被取代。

对于我们来说,唯一不变的就是变化,所以我们要做好接受新事物的准备,同时也做好应对未来的准备,尽量让自己变成不可替代的。

不会搭建的小伙伴可以根据下面的提示添加阿粉微信,阿粉会将搭建好的机器人拉进专门的读者群,可以体验一下,坑位有限先到先得。

·1、更改host文件 文件地址: C:\Windows\System32\Drivers\etc

​ 如果不能直接修改,可拷贝到桌面修改后再复制回去

2、在host文件追加

#github
192.30.253.112 github.com 

151.101.113.194 github.global.ssl.fastly.net

​ ip 地址通过以下两个网站可查

http://github.com.ipaddress.com/

http://github.global.ssl.fastly.net.ipaddress.com/

3、打开cmd执行dns刷新命令

ipconfig /flushdns