Spring Boot(2)【转】

  以下转载和参考自:集成第三方组件 - 廖雪峰的官方网站

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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值