一、SpringBoot相关
一、SpringBoot相关
SpringBoot自定义注解实现接口限流
1.1、在高并发系统中,保护系统的三种方式分别为:缓存,降级和限流。
限流的目的是通过对并发访问请求进行限速或者一个时间窗口内的的请求数量进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待。
1. 自定义限流注解
import com.asurplus.common.enums.LimitType;
import java.lang.annotation.*;
/**
* 限流注解
*
* @author Lizhou
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limit {
/**
* 限流key前缀
*/
String prefix() default "limit:";
/**
* 限流时间,单位秒
*/
int time() default 60;
/**
* 限流次数
*/
int count() default 10;
/**
* 限流类型
*/
LimitType type() default LimitType.DEFAULT;
}
2. 限流类型枚举类
/**
* 限流类型
*
* @author Lizhou
*/
public enum LimitType {
/**
* 默认策略全局限流
*/
DEFAULT,
/**
* 根据请求者IP进行限流
*/
IP
}
我们定义了两种限流类型,分别为全局限流和 IP 限流,全局限流对访问接口的所有用户进行限流保护,IP 限流对每个 IP 请求用户进行单独限流保护。
3. 限流 Lua 脚本
1、由于我们使用 Redis 进行限流,我们需要引入 Redis 的 maven 依赖,同时需要引入 aop 的依赖
<!-- aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、限流脚本
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* 接口限流
*/
@Slf4j
@Component
public class RedisLimitUtil {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 限流
*
* @param key 键
* @param count 限流次数
* @param times 限流时间
* @return
*/
public boolean limit(String key, int count, int times) {
try {
String script = "local lockKey = KEYS[1]\n" +
"local lockCount = KEYS[2]\n" +
"local lockExpire = KEYS[3]\n" +
"local currentCount = tonumber(redis.call('get', lockKey) or \"0\")\n" +
"if currentCount < tonumber(lockCount)\n" +
"then\n" +
" redis.call(\"INCRBY\", lockKey, \"1\")\n" +
" redis.call(\"expire\", lockKey, lockExpire)\n" +
" return true\n" +
"else\n" +
" return false\n" +
"end";
RedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);
List<String> keys = Arrays.asList(key, String.valueOf(count), String.valueOf(times));
return redisTemplate.execute(redisScript, keys);
} catch (Exception e) {
log.error("限流脚本执行失败:{}", e.getMessage());
}
return false;
}
}
通过 Lua 脚本,根据 Redis 中缓存的键值判断限流时间(也是 key 的过期时间)内,访问次数是否超出了限流次数,没超出则访问次数 +1,返回 true,超出了则返回 false。
脚本详细解释:
-- 创建lockKey变量,存放key
local lockKey = KEYS[1]
-- 创建lockCount变量,存放可放行的数量
local lockCount = KEYS[2]
-- 创建lockExpire变量,存放该key的guo'qi's
local lockExpire = KEYS[3]
-- 获取redis中当前key的数量(redis.call('get', lockKey) or "0" 如果获取不到key为lockKey的数据就设为0)、tonumber:转化为数字
local currentCount = tonumber(redis.call('get', lockKey) or "0")
-- redis中的该key的值小于设置的放行数量就返回true
if currentCount < tonumber(lockCount)
then
-- lockKey增加1
redis.call("INCRBY", lockKey, "1")
-- 重新修改lockKey过期时间为设置的过期时间
redis.call("expire", lockKey, lockExpire)
return true
else
return false
end
4. 限流切面处理类
import com.asurplus.common.annotation.Limit;
import com.asurplus.common.enums.LimitType;
import com.asurplus.common.exception.CustomException;
import com.asurplus.common.ip.IpUtil;
import com.asurplus.common.redis.RedisLimitUtil;
import com.asurplus.common.utils.HttpRequestUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 限流处理
*
* @author Lizhou
*/
@Slf4j
@Aspect
@Component
public class LimitAspect {
@Autowired
private RedisLimitUtil redisLimitUtil;
/**
* 前置通知,判断是否超出限流次数
*
* @param point
*/
@Before("@annotation(limit)")
public void doBefore(JoinPoint point, Limit limit) {
try {
// 拼接key
String key = getCombineKey(limit, point);
// 判断是否超出限流次数
if (!redisLimitUtil.limit(key, limit.count(), limit.time())) {
throw new CustomException("访问过于频繁,请稍候再试");
}
} catch (CustomException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("接口限流异常,请稍候再试");
}
}
/**
* 根据限流类型拼接key
*/
public String getCombineKey(Limit limit, JoinPoint point) {
StringBuilder sb = new StringBuilder(limit.prefix());
// 按照IP限流
if (limit.type() == LimitType.IP) {
sb.append(IpUtil.getIpAddr(HttpRequestUtil.getRequest())).append("-");
}
// 拼接类名和方法名
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
sb.append(targetClass.getName()).append("-").append(method.getName());
return sb.toString();
}
}
1、使用我们刚刚的 Lua 脚本判断是否超出了限流次数,超出了限流次数后返回一个自定义异常,然后在全局异常中去捕捉异常,返回 JSON 数据。
2、根据注解参数,判断限流类型,拼接缓存 key 值
5. 使用与测试
1、测试方法
@Limit(type = LimitType.DEFAULT, time = 10, count = 2)
@GetMapping("test")
public String test() {
return "请求成功:" + System.currentTimeMillis();
}
使用自定义注解 @Limit,限制为 10 秒内,允许访问 2 次
2、测试结果
第一次

第二次

第三次

可以看出,前面两次都成功返回了请求结果,第三次超出了接口限流次数,返回了自定义异常信息。
注解编程 之 注解合并
1.2、1. 组合注解
Spring4.2
之后,就提供了组合注解的实现方式,就是将多个注解作用于一个注解,用一个注解就和依赖实现多个注解的功能。是作用的注解元素看上去更简洁美观,更强大之处是属性覆盖功能。
例如:Spring
的@RestController
,它将@ResponseBody
和@Controller
两个注解组合为一个,那么在Controller
类上只需加上@RestController
即可实现加两个注解才能实现的功能。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller //组合Controller使其实现Bean注册
@ResponseBody //组合ResponseBody使其支持将结果转化为json
public @interface RestController {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
* @since 4.0.1
*/
@AliasFor(annotation = Controller.class)
String value() default "";
}
2. 自定义注解
public class SelfAnnotationTest {
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@interface TestOne {
String testOne() default "testOne";
}
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@interface TestTwo {
String testTwo() default "testTwo";
}
@TestTwo
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@interface TestThree {
String testThree() default "testThree";
}
@TestThree
static class Element {}
public static void main(String[] args) {
TestTwo testTwo = AnnotatedElementUtils.getMergedAnnotation(Element.class, TestTwo.class);
TestThree testThree = AnnotatedElementUtils.getMergedAnnotation(Element.class, TestThree.class);
System.out.println(testTwo);
System.out.println(testThree);
}
}
输出:
@com.example.helloworld.annotation.SelfAnnotationTest$TestTwo(testTwo=testTwo)
@com.example.helloworld.annotation.SelfAnnotationTest$TestThree(testThree=testThree)
Process finished with exit code 0
可以看出,AnnotatedElemnetUtils.getMergedAnnotation()
方法可以返回组合注解本身,及此注解上的元注解。
3. 自定义注解合并
@TestOne
@TestTwo
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@interface TestThree { // 合并 @TestOne 及 @TestTwo 注解
String testThree() default "testThree";
String testTwo(); // 自动对应 @TestTwo 中的属性
String testOne(); // 自动对应 @TestOne 中的属性
}
@TestThree(testTwo = "test2", testOne = "test1")
static class Element {}
public static void main(String[] args) {
TestTwo testTwo = AnnotatedElementUtils.getMergedAnnotation(Element.class, TestTwo.class);
System.out.println(testTwo.testTwo());
TestThree testThree = AnnotatedElementUtils.getMergedAnnotation(Element.class, TestThree.class);
System.out.println(testThree.testThree());
TestOne testOne = AnnotatedElementUtils.getMergedAnnotation(Element.class, TestOne.class);
System.out.println(testOne.testOne());
}
输出:
test2
testThree
test1
Process finished with exit code 0
4. 组合注解实现属性值覆盖
Spring
组合注解中的属性覆盖功能,即更底层的注解属性方法覆盖高层次注解的属性方法。实现该功能需要Spring
提供的另外一个注解@AliasFor
配合完成。
public class SelfAnnotationTest {
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@interface TestOne {
String testOne1() default "testOne";
}
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@TestOne
@interface TestTwo {
String testTwo2() default "testTwo";
}
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@TestTwo
@interface TestThree {
@AliasFor(annotation = TestTwo.class, attribute = "testTwo2")
String testThree() default "testThree";
@AliasFor(annotation = TestOne.class, attribute = "testOne1")
String testThree2() default "testThree2";
}
@TestThree(testThree = "testThree 覆盖了 testTwo2", testThree2 = "testThree2 覆盖了 testOne1")
static class Element {}
public static void main(String[] args) {
TestTwo testTwo = AnnotatedElementUtils.getMergedAnnotation(Element.class, TestTwo.class);
System.out.println(testTwo.testTwo2());
TestOne testOne = AnnotatedElementUtils.getMergedAnnotation(Element.class, TestOne.class);
System.out.println(testOne.testOne1());
}
}
输出:
testThree 覆盖了 testTwo2
testThree2 覆盖了 testOne1
Process finished with exit code 0
以上为三层属性覆盖,支持无限层覆盖。
Spring @Value 设置默认值
1.3、这篇文章主要介绍了Spring @Value 设置默认值的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
概览
Spring 的 @Vaule 注解提供了一种便捷的方法可以让属性值注入到组件中,当属性值不存在的时候提供一个默认值也是非常好用的
这就是我们这篇文章所专注的,如何给 @Vaule 注解指定一个默认值。对于更多的关于 @Vaule 的教程看这篇文章
1. String 默认值
让我们看看对于 String 类型的值,给定一个默认值得基础语法
@Value("${some.key:my default value}")
private String stringWithDefaultValue;
如果 some.key 无法解析,那么 stringWithDefaultValue 的值会被设置为默认值 "my default value".
相似的,我们也可以用如下方法,设置一个空字符串作为默认值
@Value("${some.key:})"
private String stringWithBlankDefaultValue;
2. 原始类型
给像 int 或者 boolean 的原始类型赋一个默认值,我们使用文字值:
@Value("${some.key:true}")
private boolean booleanWithDefaultValue;
@Value("${some.key:42}")
private int intWithDefaultValue;
如果愿意,可以用原始类型的包装类型来代替,例如 Boolean 和 Integer
3. 数组
我们可以使用逗号分隔的 list 来用于数组的注入,如下
@Value("${some.key:one,two,three}")
private String[] stringArrayWithDefaults;
@Value("${some.key:1,2,3}")
private int[] intArrayWithDefaults;
在上面第一个例子, 值为 "one", "two", 和 "three" 的数组将被注入到 stringArrayWithDefaults 中
在上面第二个例子, 值为 1, 2, 和 3 的数组将被注入 intArrayWithDefaults 中
4. 使用SpEL表达式
我们也可以使用 Spring Expression Language (SpEL) 去指定一个表达式或者默认值
在下面的例子中,我们期望 some.system.key 被设置为系统值,如果他不存在则我们想用 "my default system property value"
@Value("#{systemProperties['some.key'] ?: 'my default system property value'}")
private String spelWithDefaultValue;
总结
在这篇文章中,我们研究了如何为使用Spring的@Value注释注入的属性设置默认值。
像往常一样,本文中使用的所有代码示例都可以在GitHub项目中找到。
WebSocket实现三种模式发送消息
1.4、项目地址:spring-boot-websocket-demo
简介
WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。
WebSocket的出现是为了解决Http协议只能在客户端发送请求后服务端响应请求的问题,它允许服务端主动向客户端发送请求。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在大多数情况下,为了实现消息推送,往往采用Ajax轮询方式,它遵循的是Http协议,在特定的时间内向服务端发送请求,Http协议的请求头较长,可能仅仅需要获取较小的数据而需要携带较多的数据,而且对于消息不是特别频繁的时候,大部分的轮询都是无意的,造成了极大的资源浪费。
HTML5定义的WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。


本文简要
本文基于SpringBoot框架整合WebSocket,实现三种模式发送消息:
- 自己给自己发送消息
- 自己给其他用户发送消息
- 自己给指定用户发送消息
示例代码
创建工程
修改pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.c3stones</groupId> <artifactId>spring-boot-websocket-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-boot-websocket-demo</name> <description>Spring Boot WebSocket Demo</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.8.RELEASE</version> </parent> <dependencies> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.4.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
创建配置文件
在resources目录下创建application.yml。
server:
port: 8080
spring:
thymeleaf:
prefix: classpath:/view/
suffix: .html
encoding: UTF-8
servlet:
content-type: text/html
# 生产环境设置true
cache: false
- 添加WebSocket配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket配置类
*
* @author CL
*
*/
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter
* <p>
* 该Bean会自动注册添加@ServerEndpoint注解的WebSocket端点
* </p>
*
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
- 创建启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动类
*
* @author CL
*
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
1. 自己给自己发送消息
- 创建服务端点
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* <b style="color: blue"> 自己给自己发送消息 </b>
*
* @author CL
*
*/
@Slf4j
@Component
@ServerEndpoint(value = "/selfToSelf")
public class SelfToSelfServer {
/**
* 在线数
* <p>
* 多线程环境下,为了保证线程安全
* </p>
*/
private static AtomicInteger online = new AtomicInteger(0);
/**
* 建立连接
*
* @param session 客户端连接对象
*/
@OnOpen
public void onOpen(Session session) {
// 在线数加1
online.incrementAndGet();
log.info("客户端连接建立成功,Session ID:{},当前在线数:{}", session.getId(), online.get());
}
/**
* 接收客户端消息
*
* @param message 客户端发送的消息内容
* @param session 客户端连接对象
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("服务端接收消息成功,Session ID:{},消息内容:{}", session.getId(), message);
// 处理消息,并响应给客户端
this.sendMessage(message, session);
}
/**
* 处理消息,并响应给客户端
*
* @param message 客户端发送的消息内容
* @param session 客户端连接对象
*/
private void sendMessage(String message, Session session) {
try {
String response = "Server Response ==> " + message;
session.getBasicRemote().sendText(response);
log.info("服务端响应消息成功,接收的Session ID:{},响应内容:{}", session.getId(), response);
} catch (IOException e) {
log.error("服务端响应消息异常:{}", e.getMessage());
}
}
/**
* 关闭连接
*
* @param session 客户端连接对象
*/
@OnClose
public void onClose(Session session) {
// 在线数减1
online.decrementAndGet();
log.info("客户端连接关闭成功,Session ID:{},当前在线数:{}", session.getId(), online.get());
}
/**
* 连接异常
*
* @param session 客户端连接对象
* @param error 异常
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("连接异常:{}", error);
}
}