以下转载和参考自:集成第三方组件 - 廖雪峰的官方网站
1、集成Open API
Open API是一个标准,它的主要作用是描述REST API,既可以作为文档给开发者阅读,又可以让机器根据这个文档自动生成客户端代码等。添加Open API的支持需要在pom.xml中加入以下依赖。引入springdoc-openapi-ui
这个依赖后,它自动引入Swagger UI用来创建API文档,我们打开浏览器输入http://localhost:8080/swagger-ui.html就可以看到效果,如下所示,其中的api-controller表示ApiController这个Controller类,其下有三个API,点击某个API还可以交互,即输入API参数,点“Try it out”按钮,获得运行结果:
org.springdoc:springdoc-openapi-ui:1.4.0
还可以在Controller类中给API加入一些描述信息,如下所示,@Operation可以对API进行描述,@Parameter可以对参数进行描述:
@RestController
@RequestMapping("/api")
public class ApiController {
...
@Operation(summary = "Get specific user object by it's id.")
@GetMapping("/users/{id}")
public User user(@Parameter(description = "id of the user.") @PathVariable("id") long id) {
return userService.getUserById(id);
}
...
}
Spring Boot内置的Tomcat默认获取的服务器名称是localhost,端口是实际监听端口,如上面swagger-ui页面显示的Servers为http://localhost:8080,如果使用Nginx这样的反向代理的话,Servers显示的不是用户访问的域名https://example.com,仍然是http://localhost:8080,这样一来,就无法直接在swagger-ui页面执行API,非常不方便。要让Tomcat获取到对外暴露的Nginx域名来显示到swagger-ui,上的话,必须在Nginx配置中传入必要的HTTP Header,常用的配置如下:
# Nginx配置
server {
...
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
...
}
# Spring Boot的application.yml配置文件
server:
# 实际监听端口:
port: 8080
# 从反向代理读取相关的HTTP Header:
forward-headers-strategy: native
还可以使用Knife4j这个基于SpringBoot构建的接口文档工具,它集成了最新的OpenAPI3和Swagger2为一体,提供接口文档增强解决方案,比如生成的文档可以导出,然后给到前端开发团队,前端开发团队可以基于API接口写具体的调用。
2、集成Redis
首先,添加必要的几个依赖项,注意我们并未指定版本号,因为在spring-boot-starter-parent中已经把常用组件的版本号确定下来了:
- io.lettuce:lettuce-core
- org.apache.commons:commons-pool2
然后在配置文件application.yml
中添加Redis的相关配置:
spring:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
ssl: ${REDIS_SSL:false}
database: ${REDIS_DATABASE:0}
然后,通过RedisConfiguration这个JavaBean
来加载它,再编写一个@Bean
方法来创建RedisClient
,可以将其直接放在RedisConfiguration
中:
@Configuration
@ConfigurationProperties("spring.redis")
public class RedisConfiguration {
private String host;
private int port;
private String password;
private int database;
// getters and setters
...
@Bean
RedisClient redisClient() {
RedisURI uri = RedisURI.Builder.redis(this.host, this.port)
.withPassword(this.password)
.withDatabase(this.database)
.build();
return RedisClient.create(uri);
}
}
然后,我们用一个RedisService来封装Redis操作,如下所示,引入了Commons Pool的一个对象池,用于缓存Redis连接。因为Lettuce本身是基于Netty的异步驱动,在异步访问时并不需要创建连接池,但基于Servlet模型的同步访问时,连接池是有必要的。get()/set()用来获取和设置字符串类型的值:
@Component
public class RedisService {
@Autowired
RedisClient redisClient;
private GenericObjectPool<StatefulRedisConnection<String, String>> redisConnectionPool; //连接池
@PostConstruct
public void init() { //初始化连接池
GenericObjectPoolConfig<StatefulRedisConnection<String, String>> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(20);
poolConfig.setMaxIdle(5);
poolConfig.setTestOnReturn(true);
poolConfig.setTestWhileIdle(true);
this.redisConnectionPool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connect(),
poolConfig);
}
@PreDestroy
public void shutdown() { //关闭连接池
this.redisConnectionPool.close();
this.redisClient.shutdown();
}
public String get(String key) { //通过键获得值
//获取Redis连接,操作Redis,最后释放连接
try (StatefulRedisConnection<String, String> connection = redisConnectionPool.borrowObject()) {
connection.setAutoFlushCommands(true);
RedisCommands<String, String> commands = connection.sync();
return commands.get(key);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public String set(String key, String value) {
try (StatefulRedisConnection<String, String> connection = redisConnectionPool.borrowObject()) {
connection.setAutoFlushCommands(true);
RedisCommands<String, String> commands = connection.sync();
return commands.set(key, value);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
可以对上面代码进行优化,将常用操作封装到executeSync(),在get()、set()中传入各自的接口实现(Lambda):
@FunctionalInterface
public interface SyncCommandCallback<T> {
T doInConnection(RedisCommands<String, String> commands);
}
public class RedisService {
...
public <T> T executeSync(SyncCommandCallback<T> callback) {
try (StatefulRedisConnection<String, String> connection = redisConnectionPool.borrowObject()) {
connection.setAutoFlushCommands(true);
RedisCommands<String, String> commands = connection.sync();
return callback.doInConnection(commands);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public String get(String key) {
return executeSync(commands -> commands.get(key));
}
public String set(String key, String value) {
return executeSync(commands -> commands.set(key, value));
}
public String set(String key, String value, Duration timeout) { //添加超时设置
return executeSync(commands -> commands.setex(key, timeout.toSeconds(), value));
}
//对redis中map类型的值进行操作
public boolean hset(String key/*map名称*/, String field/*key*/, String value/*value*/) {
return executeSync(commands -> commands.hset(key, field, value));
}
public String hget(String key, String field) {
return executeSync(commands -> commands.hget(key, field));
}
public Map<String, String> hgetall(String key) {
return executeSync(commands -> commands.hgetall(key));
}
}
完成了RedisService后,就可以通过它来使用Redis了,如下,在UserController中对Redis进行读写操作:
@Controller
public class UserController {
public static final String KEY_USER_ID = "__userid__";
public static final String KEY_USERS = "__users__";
@Autowired ObjectMapper objectMapper; //序列化、反序列化JSON:json2string/string2json
@Autowired RedisService redisService;
// 把User类写入Redis:
private void putUserIntoRedis(User user) throws Exception {
redisService.hset("userMap", user.getId(), objectMapper.writeValueAsString(user));
}
// 从Redis读取数据到User:
private User getUserFromRedis(HttpSession session) throws Exception {
Long id = (Long) session.getAttribute(“userID”); //从Session中获取用户的ID
if (id != null) {
String s = redisService.hget(userMap, id.toString());
if (s != null) {
return objectMapper.readValue(s, User.class);
}
}
return null;
}
...
}
3、集成RabbitMQ
使用RabbitMQ需要先安装,安装成功后可以访问RabbitMQ的管理后台http://localhost:15672,如下为管理后台的登录界面(RabbitMQ后台管理的默认用户名和口令均为guest):
相比JMS,RabbitMQ只有Queue,没有Topic,并且多了Exchange来定义规则,当Producer想要发送消息的时候,它将消息发送给Exchange,由Exchange将消息根据各种规则投递到一个或多个Queue,如下所示。当Producer发消息的时候,可以指定使用哪个Exchange,比如使用的Exchange的规则是只将消息发送给Queue1,那么消息就只会发送给Queue1。Exchange的这些路由规则称之为Binding,通常都在RabbitMQ的管理后台设置。如果某个Exchange把消息发送到多个Queue,那么这个消息通道就相当于JMS的Topic。
比如我们在RabbitMQ中,先创建3个Queue,如下所示,创建Queue时注意到可配置为持久化(Durable)和非持久化(Transient),当Consumer不在线时,持久化的Queue会暂存消息,非持久化的Queue会丢弃消息。
然后在Exchanges标签页下创建两个Direct类型的Exchange,命名为login和registration,login的Binding的规则是:如果发送的消息没有指定Routing Key,则被投递到q_app和q_mail,如果消息指定了Routing Key="login_failed",那么消息被投递到q_sms。registration没有设置特别的Binding规则,所以凡是发送到registration这个Exchange的消息,均会被发送到q_mail和q_sms这两个Queue。
在RabbitMQ后台配置好Exchange和Queue后,就可以集成RabbitMQ来开发程序。RabbitMQ的相关依赖及application.yml中的配置如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
然后,对于发送方,可以在Spring Boot启动类中添加生成MessageConverter对象的方法,MessageConverter
用于将Java对象转换为RabbitMQ的消息。如果不提供该方法的话,Spring Boot默认会使用SimpleMessageConverter,只能发送String和byte[]类型的消息,使用Jackson2JsonMessageConverter的话就可以发送JavaBean对象,由Spring Boot自动序列化为JSON并以文本消息传递:
@SpringBootApplication
public class Application {
...
@Bean
MessageConverter createMessageConverter() {
return new Jackson2JsonMessageConverter();
}
}
然后,在发送方就可以注入一个RabbitTemplate来发送消息,如下所示的sendRegistrationMessage()方法会将消息发送到registration这个Exchange,而且没有指定Routing Key,所以消息会发送到q_mail和q_sms这两个Queue,sendLoginMessage()方法会根据消息的success标志来判断是否设置Routing Key为"login_failed",也就是成功的消息会发送到q_app和q_mail,失败的消息会发送到q_sms。也可以发送消息到一个指定的Queue,此时指定Routing Key为Queue的名称即可。
@Component
public class MessagingService {
@Autowired
RabbitTemplate rabbitTemplate;
public void sendRegistrationMessage(RegistrationMessage msg) {
//发送消息
rabbitTemplate.convertAndSend("registration"/*要发送到的Exchange*/, ""/*Routing Key*/, msg/*要发送的消息*/);
}
public void sendLoginMessage(LoginMessage msg) {
String routingKey = msg.success ? "" : "login_failed";
rabbitTemplate.convertAndSend("login", routingKey, msg);
}
}
对于接收方接收消息时,需要在消息处理的方法上标注@RabbitListener
:
@Component
public class QueueMessageListener {
@RabbitListener(queues = "q_mail") //发送到"q_mail" Queue的消息
public void onRegistrationMessageFromMailQueue(RegistrationMessage message/*发送到registration Exchange的消息*/) throws Exception {
}
@RabbitListener(queues = "q_sms")
public void onRegistrationMessageFromSmsQueue(RegistrationMessage message) throws Exception {
}
@RabbitListener(queues = "q_mail")
public void onLoginMessageFromMailQueue(LoginMessage message) throws Exception {
}
@RabbitListener(queues = "q_sms")
public void onLoginMessageFromSmsQueue(LoginMessage message) throws Exception {
}
@RabbitListener(queues = "q_app")
public void onLoginMessageFromAppQueue(LoginMessage message) throws Exception {
}
}
RabbitMQ还提供了使用Topic的Exchange(此Topic指消息的标签,并非JMS的Topic概念),可以使用*
进行匹配并路由。可见,掌握RabbitMQ的核心是理解其消息的路由规则。
4、集成Kafka
Kafka是Scala编写的,运行在JVM之上,相对于RabbitMQ,Kafka设计比较简单,它只有一种类似JMS的Topic的消息通道:
Kafka的特点一是快,二是有巨大的吞吐量,它通过将Topic分区成多个Partition来支持十万甚至百万的高并发,如下所示,多个Partition还可以分布到多台机器上。默认情况下Kafka自动创建Topic,创建Topic时默认的分区数量是2,可以通过server.properties修改默认分区数量。在生产环境中通常会关闭自动创建功能,Topic需要由运维人员先创建好。 需要注意的是,在存在多个Partition的情况下,Consumer从多个Partition接收的消息并不一定是Producer发送的顺序,也就是Kafka只保证在一个Partition内部,消息是有序的。
Kafka的另一个特点是消息发送和接收都尽量使用批处理,一次处理几十甚至上百条消息,比一次一条效率要高很多。Kafka总是将消息写入Partition对应的文件,可以配置按照时间删除保存的消息(默认3天),也可以按照文件大小删除,只要Consumer在离线期内的消息还没有被删除,再次上线仍然可以接收到完整的消息流(客户端会保存收到消息的计量值到offsetId,客户端上线后会按上次的offsetId进行查询)。
启动Kafka之前需要先启动ZooKeeper,下载最新版Kafaka解压后,在bin目录下依次找到如下两个xxx-start.sh文件来启动服务:
如果要关闭Kafka和ZooKeeper,依次按Ctrl-C退出即可。注意这是在本地开发时使用Kafka的方式,线上Kafka服务推荐使用云服务厂商托管模式(AWS的MSK,阿里云的消息队列Kafka版)。和RabbitMQ相比,Kafka并不提供网页版管理后台,管理Topic需要使用命令行,比较繁琐,只有云服务商通常会提供更友好的管理后台。
在Spring Boot中使用Kafka的话,首先要引入依赖,这个依赖是spring-kafka
项目提供的:
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
在application.yml配置文件中添加Kafka的相关配置,如下所示,除了bootstrap-servers必须指定外,consumer相关的配置项均为调优选项。例如,max-poll-records表示一次最多抓取100条消息。其它的consumer配置项可以在代码中定义一个KafkaProperties.Consumer类型的对象,然后查看其类源码即可。
spring:
kafka:
bootstrap-servers: localhost:9092
consumer:
auto-offset-reset: latest
max-poll-records: 100
max-partition-fetch-bytes: 1000000
下面我们在代码中向Kafka发送消息,通过KafkaTemplate, 这是一个泛型类,而默认配置总是使用String作为Kafka消息的类型,所以注入KafkaTemplate<String, String>即可。使用KafkaTemplate的send()来发送消息,其参数类型为ProducerRecord,这里没有像RabbitMQ那样使用MessageConverter来转换JavaBean,而是直接把消息类型作为Header添加到消息中,消息正文是序列化的JSON:
@Component
public class MessagingService {
@Autowired ObjectMapper objectMapper;
@Autowired KafkaTemplate<String, String> kafkaTemplate;
public void sendRegistrationMessage(RegistrationMessage msg) throws IOException {
send("topic_registration", msg);
}
public void sendLoginMessage(LoginMessage msg) throws IOException {
send("topic_login", msg);
}
private void send(String topic, Object msg) throws IOException {
ProducerRecord<String, String> pr = new ProducerRecord<>(topic/*指定topic名称*/,
objectMapper.writeValueAsString(msg)/*消息正文:JavaBean序列化后的JSON*/);
pr.headers().add("type", msg.getClass().getName().getBytes(StandardCharsets.UTF_8));
kafkaTemplate.send(pr);
}
}
接收方使用@KafkaListener注解来接收消息,其中,topics用来指定接收的Topic名称,groupId相当于是Consumer名。如下所示,在接收消息的方法中,使用@Payload表示传入的是消息正文,使用@Header可传入消息的指定Header。当发送方使用上面的sendRegistrationMessage()方法发送消息后,下面的onRegistrationMessage()方法会被调用,当发送方使用上面的sendLoginMessage()方法发送消息后, 下面的onLoginMessage()方法和processLoginMessage()方法会被先后调用,因为这两个方法的Topic名称虽然相同,但groupId不同,Kafka会将消息分别投送给这两个方法。如果两个接收方法的Topic名称相同,groupId也相同,假设Producer发送的消息流是A、B、C、D,那么这两个接收方法各自收到的很可能是A、C和B、D(这种情况下多个Consumer被视作一个Consumer)。
@Component
public class TopicMessageListener {
@Autowired
ObjectMapper objectMapper;
@KafkaListener(topics = "topic_registration", groupId = "group1")
public void onRegistrationMessage(@Payload String message, @Header("type") String type) throws Exception {
RegistrationMessage msg = objectMapper.readValue(message, getType(type)); //反序列化JSON消息获得JavaBean
}
@KafkaListener(topics = "topic_login", groupId = "group1")
public void onLoginMessage(@Payload String message, @Header("type") String type) throws Exception {
LoginMessage msg = objectMapper.readValue(message, getType(type));
}
@KafkaListener(topics = "topic_login", groupId = "group2")
public void processLoginMessage(@Payload String message, @Header("type") String type) throws Exception {
LoginMessage msg = objectMapper.readValue(message, getType(type));
}
@SuppressWarnings("unchecked")
private static <T> Class<T> getType(String type) { //根据类名获得类的Class
// TODO: use cache:
try {
return (Class<T>) Class.forName(type);
}
catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
5、集成WebSocket
Spring中集成WebSocket有好几种方式,可以使用javax、WebMVC(Spring websocket)、WebFlux,也可以使用Netty或Java-WebSocket、SocketIO等第三方库来实现WebSocket功能。如下为使用javax,一般使用注解的方式来对websocket服务进行配置。
<!-- 从spring-boot-starter-parent继承 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<!-- 设置使用的版本 -->
<java.version>11</java.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
@SpringBootApplication
public class WebSocketApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(WebSocketApplication.class, args);
}
/**
* 注入ServerEndpointExporter
* 这个Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
@Component
@ServerEndpoint("/websocket/{name}")
public class WebSocket {
private Session session;
private String name;
//用于存所有的连接服务的客户端,springboot会为每个websocket连接初始化一个WebSocket bean,所以可以用一个静态set保存起来
private static ConcurrentHashMap<String,WebSocket> webSocketSet = new ConcurrentHashMap<>();
@OnOpen
public void OnOpen(Session session/*与客户端的连接对话*/, @PathParam(value = "name") String name){
this.session = session;
this.name = name;
webSocketSet.put(name,this); //name用来标识唯一客户端
System.out.println("新的连接,当前连接数: " + webSocketSet.size());
}
@OnClose
public void OnClose(){
webSocketSet.remove(this.name);
System.out.println("连接关闭,当前连接数: " + webSocketSet.size());
}
@OnMessage
public void OnMessage(String message){
System.out.println("收到消息: " + message);
try{
this.session.getBasicRemote().sendText("hello2");
}catch (Exception e){
e.printStackTrace();
}
}
//单发
public void AppointSending(String name,String message){
try {
webSocketSet.get(name).session.getBasicRemote().sendText(message);
}catch (Exception e){
e.printStackTrace();
}
}
//群发
public void GroupSending(String message){
for (String name : webSocketSet.keySet()){
try {
webSocketSet.get(name).session.getBasicRemote().sendText(message);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
可以使用在线websocket测试工具来作为客户端,如:在线websocket测试-在线工具-postjson。也可以自己来实现客户端:
package xsl;
import javax.websocket.*;
import java.nio.ByteBuffer;
@ClientEndpoint
public class JavaxWebSocketClientEndpoint {
@OnOpen
public void onOpen(Session session) {
//连接建立
System.out.println("connect open");
//发送文本消息
session.getAsyncRemote().sendText("hi");
//发送二进制消息
//session.getAsyncRemote().sendBinary(ByteBuffer message);
//发送对象消息,会尝试使用Encoder编码
//session.getAsyncRemote().sendObject(Object message);
//发送ping
//session.getAsyncRemote().sendPing(ByteBuffer buffer);
//发送pong
//session.getAsyncRemote().sendPong(ByteBuffer buffer);
}
@OnClose
public void onClose(Session session, CloseReason reason) {
//连接关闭
System.out.println("connect close");
}
@OnMessage
public void onMessage(Session session, String message) {
//接收文本消息
String msg = "receive text message: " +message;
System.out.println(msg);
}
@OnMessage
public void onMessage(Session session, PongMessage message) {
//接收pong消息
System.out.println("receive pong");
}
@OnMessage
public void onMessage(Session session, ByteBuffer message) {
//接收二进制消息
System.out.println("receive binary message");
}
@OnError
public void onError(Session session, Throwable e) {
//异常处理
System.out.println("error!");
}
}
package xsl;
import javax.websocket.*;
import java.net.URI;
public class WsClientApp {
public static void main(String[] args) throws Exception {
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
URI uri = new URI("ws://192.168.70.32:8080/websocket/test");
Session session = container.connectToServer(JavaxWebSocketClientEndpoint.class, uri);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
session.close();
}
}
上面通过ContainerProvider.getWebSocketContainer获得WebSocketContainer其实是基于SPI实现的,在Spring的环境中推荐使用ServletContextAware来获得:
@Component
public class JavaxWebSocketContainer implements ServletContextAware {
private volatile WebSocketContainer container;
public WebSocketContainer getContainer() {
if (container == null) {
synchronized (this) {
if (container == null) {
container = ContainerProvider.getWebSocketContainer();
}
}
}
return container;
}
@Override
public void setServletContext(@NonNull ServletContext servletContext) {
if (container == null) {
container = (WebSocketContainer) servletContext
.getAttribute("javax.websocket.server.ServerContainer");
}
}
}
作者:不够优雅
链接:https://juejin.cn/post/7111132777394733064
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。