XYIOT项目学习笔记

XYIOT项目

其实当一个程序员接手一个项目,首先最重要的不是马上把代码看懂,而是弄清楚实现这个业务为什么要用这个技术,它的优缺点是什么,再继续深究具体实现!本质上说就是需要带着目的去学习

MiniXYIot的账号密码:

influxDB

userName:fansea

passWord:fanseafansea

token:KzZcFbgR3ZuBbRrdk5ZJvWF5–x8EnRrT4eqpsceh-QoJnsCebM-XRxcGxsIu_ZhdWNBHnMqlKLifzzXKw2BFw==

token:IOOfQgGX7aiTasyGwXPjkFYZf2-5FV4R3VglJSD5OPMOl-9JgsfzCl08dVzklrxtQQyh9yI5Jct14pWg1EVRFg==

原型+设计(蓝湖/墨刀+adobe xd)—> 开发+文档(云效+钉盘)。

蓝湖设计平台:

https://lanhuapp.com/link/#/invite?sid=lxsLfrra

云效代码管理:

https://codeup.aliyun.com

钉钉团队云盘:

https://alidocs.dingtalk.com/i/desktop/team-folders

项目思考

  1. 对照界面看看页面功能
  2. 再看数据库表

PageHelper使用

  1. 导入插件
1
2
3
4
5
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
  1. 在yml中pagehelper分页插件配置
1
2
3
4
5
6
#pagehelper分页插件配置
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
  1. 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class QueryUser2 implements Query2 {
@Autowired
UserMapper userMapper;

@Override
public List<User> selectAll(int pageNum,int pageSize) {
// PageHelper 必须和 sql 语句配合使用才能生效
PageHelper.startPage(pageNum,pageSize);
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);
//pageInfo.getRow();
return pageInfo.getTotal();
}
}

minIo

上传文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public String upload(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
if (StringUtils.isBlank(originalFilename)) {
throw new RuntimeException();
}
String fileName = IdUtil.getSnowflakeNextIdStr() + originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件路径:"/2024/5/10/eaofhoa.png"
String objectName = DateUtils.datePath() + "/" + fileName;
try {
/*
这段Java代码的主要功能是用于将一个文件上传到MinIO存储系统中。以下是代码的详细解释:
PutObjectArgs objectArgs = PutObjectArgs.builder();:创建一个PutObjectArgs对象的构造器。
.bucket(minioConfig.getBucketName()):指定要上传的桶名。minioConfig.getBucketName()是从配置文件中获取桶名。
.object(objectName):指定要上传的对象名。objectName是从文件名中获取的对象名。
.stream(file.getInputStream(), file.getSize(), -1):指定要上传的文件流、文件大小和文件块大小。 file.getInputStream() 获取文件输入流,file.getSize()获取文件大小,-1表示文件块大小。
.contentType(file.getContentType()):指定文件的内容类型。file.getContentType()获取文件的内容类型。
.build():构建PutObjectArgs对象。*/
PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(minioConfig.getBucketName()).object(objectName)
.stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build();
//文件名称相同会覆盖
minioClient.putObject(objectArgs);
} catch (Exception e) {
e.printStackTrace();
return null;
}
return objectName;
}

@ConfigurationProperties

@ConfigurationProperties需要和@Configuration配合使用,我们通常在一个POJO里面进行配置:

1
2
3
4
5
6
7
8
9
@Data
@Configuration
@ConfigurationProperties(prefix = "mail")
public class ConfigProperties {

private String hostName;
private int port;
private String from;
}

上面的例子将会读取properties文件中所有以mail开头的属性,并和bean中的字段进行匹配:

1
2
3
4
#Simple properties
mail.hostname=host@mail.com
mail.port=9000
mail.from=mailer@mail.com

知识小点心

算法调用中会使用到url,这里做加密防止安全隐患!

1
2
url = StringUtils.format(url, filePath, SM4Utils.encryptHex4Time());
String doAlgorithmResult = algorithmUtils.doAlgorithmGet(url);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//StringUtils.format()方法的实现
/**
* 格式化文本, {} 表示占位符<br>
* 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
* 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
* 例:<br>
* 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
* 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
* 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
*
* @param template 文本模板,被替换的部分用 {} 表示
* @param params 参数值
* @return 格式化后的文本
*/
public static String format(String template, Object... params) {
if (isEmpty(params) || isEmpty(template)) {
return template;
}
return StrFormatter.format(template, params);
}

Guava接口限流

[使用 Spring AOP 和 Guava RateLimiter 实现 API 限流-CSDN博客](https://blog.csdn.net/mbh12333/article/details/137879894#:~:text=总结 通过简单的注解和 AOP 切面%2C就可以实现 API,限流功能%2C并支持自定义限流速率和限流超时时间。 这种实现方式无侵入性%2C添加或移除限流只需要在方法上增加或移除注解即可%2C降低了维护成本。 值得注意的是%2C在分布式环境下%2C单机限流的方式可能无法满足需求%2C我们需要结合分布式限流组件如 Redis 等来实现全局限流。)

设置一个全局接口限流对象:RateLimiter

结合代理增强获取到方法名,一个接口对应一个RateLimiter对象,RateLimiter抽取@ApiRateLimit对象限流属性,并调用tryAcquire方法,判断是否满足QPS需要来决定是否返回true,最后根据判断结果来选择是否放行!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Aspect
@Component
public class ApiRateLimitAspect {

private final Map<String, RateLimiter> rateLimiters = new ConcurrentHashMap<>();

@Before("@annotation(apiRateLimit)")
public void limit(JoinPoint joinPoint, ApiRateLimit apiRateLimit) {
String methodName = joinPoint.getSignature().toLongString();
double qps = apiRateLimit.qps();
RateLimiter limiter = rateLimiters.computeIfAbsent(methodName, k -> RateLimiter.create(qps));
long timeout = apiRateLimit.timeout();
TimeUnit timeUnit = apiRateLimit.timeUnit();
if (timeout > 0) {
if (!limiter.tryAcquire(timeout, timeUnit)) {
throw new RuntimeException("API rate limit exceeded");
}
} else {
if (!limiter.tryAcquire()) {
throw new RuntimeException("API rate limit exceeded");
}
}
}
}

这个方法首先检查键是否存在于映射中,如果不存在,则调用mappingFunction来计算新的值。如果计算出的值不为null,则将新的值存储到映射中,并返回该值。否则,返回原始映射中与该键关联的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
default V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V v;
if ((v = get(key)) == null) {
V newValue;
if ((newValue = mappingFunction.apply(key)) != null) {
put(key, newValue);
return newValue;
}
}

return v;
}

@PostConstruct注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private volatile static MinioClient minioClient;

@PostConstruct
public MinioClient getMinioClient() {
if (!isMinioFileService()) {
log.error("minio服务未配置");
return null;
}
if (minioClient == null) {
synchronized (MinioClientUtils.class) {
if (minioClient == null) {
minioClient = MinioClient.builder()
.endpoint(minioConfig.getEndpoint())
.credentials(minioConfig.getAccessKey(), minioConfig.getSecretKey())
.build();
log.debug("minio文件服务连接成功");
}
}
}
//初始化默认桶
if (!bucketExists(minioConfig.getBucketName())) {
makeBucket(minioConfig.getBucketName());
}
return minioClient;
}

shiro

(1)Subject:任何可以与应用交互的“用户”

SecurityUtils.getSubject()获取到的是当前线程独有的subject,原理是shiro维护了一个ThreadContext,在其中包含了一个ThreadLocal,这里面保存了处理该线程的SecurityManager,Subject对象

1
2
3
4
5
6
7
8
Subject subject = SecurityUtils.getSubject();

public abstract class ThreadContext {
private static final Logger log = LoggerFactory.getLogger(ThreadContext.class);
public static final String SECURITY_MANAGER_KEY = ThreadContext.class.getName() + "_SECURITY_MANAGER_KEY";
public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY";
private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap();
}

(2)SecurityManager:相当于Shiro的心脏,所有的具体交互都要通过SecurityManager来进行管理,它管理着所有 Subject、且负责认证、授权、会话以及缓存的管理。
(3)Authentication:负责Subject认证,可以自定义实现,可以使用认证策略,及什么情况下该用户算通过认证
(4)Authorizer:授权器,控制着用户能够访问那些应用,是用户鉴权的核心
(5)Realm:可以有1个或者多个Realm,可以认为是安全的实体数据源,用与获取安全实体的,可以是JDBC实现,也可以是内存实现,由用户提供;所以一般在应用中都需要实现自己的Realm;
(6)SessionManager:管理Session生命周期的组件,
(7)CacheManager:缓存
(8)Cryptography:密码加密模块,对数据库中的用户密码进行加密处理

主要应用代码:

  1. 获取到subjct对象,封装 AuthenticationToken 再调用login方法
1
2
3
4
5
6
7
8
9
10
11
12
13
//2.获取Subject对象
Subject subject = SecurityUtils.getSubject();
//3.创建token对象,web应用从前端传递用户名和密码
AuthenticationToken token=new UsernamePasswordToken(username,password);
//4.完成登录
subject.login(token);
if (subject.isAuthenticated()){
System.out.println("登录成功");
//将用户名存储到session对象中
httpSession.setAttribute("userName",user.getUserName());
String tokens = tokenService.createToken(userService.getOne(new QueryWrapper<User>().eq("user_name", username)));
return Result.ok(tokens);
}
  1. 配置 Manager 和 线程Manager绑定,并配置myrealm,配置拦截
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Configuration
public class ShiroConfig {

@Autowired
MyRealm myRealm;

/**
* shiro过滤器配置
*
* @param securityManager 安全管理器
* @return Shiro过滤器
*/
@Bean
public SecurityManager securityManager() {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
ThreadContext.bind(securityManager);//加上这句代码手动绑定
// 配置Realm等组件
securityManager.setRealm(myRealm);
return securityManager;
}

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);

// 定义过滤器链
Map<String, String> filterChainDefinitionMap = new HashMap<>();

// 配置匿名访问的URL
filterChainDefinitionMap.put("/user/login", "anon");
filterChainDefinitionMap.put("/static/**", "anon"); // 允许匿名访问静态资源
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger/**", "anon");

// 需要认证的URL
filterChainDefinitionMap.put("/**/**", "authc"); // 除了上述匿名路径,其余都需要认证
filterChainDefinitionMap.put("/**", "authc"); // 除了上述匿名路径,其余都需要认证

factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}

}
  1. 重写MyRealm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Component
public class MyRealm extends AuthorizingRealm {

@Autowired
private UserService userService;
//用于鉴权

/**
* 当前登录用户,我们可以从数据库中查询到该用户的权限,并通过该方法,将我们查询到的
* 该用户拥有的角色,保存在shiro框架中,后面我们使用shiro框架去核对用户权限时,所比较的
* 就是我们在这里查询、并且保存到的权限
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}

/**
* 这个方法是用于登录认证
* 我们要自定义自己的登录逻辑,就需要在该方法中编写自己的登录逻辑
*
* @param
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//首先,从token中获取登录时保存的用户信息,注意,该方法是登录时shiro会调用的方法
//也就是说我们在登录时,会将名称和密码封装成token,所以这里不要疑惑为什么token中可以
//获取到用户的信息,后续我们编写登录时,你就会恍然大悟
/*public Object getPrincipal() {
return this.getUsername();
}*/
String name = token.getPrincipal().toString();//得到的就是用户名称
System.out.println("name = " + name);//可以打印出来看看
//从数据库中查询用户信息,根据name查询
User user = userService.getOne(new QueryWrapper<User>().eq("user_name",name));
//查到用户信息之后,将登录时的密码,和现在从数据库中查到的密码进行比较
//我们将相关的数据封装到AuthenticationInfo中,知道密码的盐,用户名、密码,
//该对象会帮我们进行校验,并且将校验结果返回给shiro框架 AuthenticationManager
if (user != null) {
//创建AuthenticationInfo对象,在构造器中传递相关参数
AuthenticationInfo info = new SimpleAuthenticationInfo(
token.getPrincipal(),
user.getPassword(),//用户密码
// ByteSource.Util.bytes("salt"),//密码使用MD5加密时指定的盐
token.getPrincipal().toString()
);
return info;
}
return null;
}
}

实现图片访问保护功能

条件查询模板封装

封装page类传入对象处理

1
IPage<SysLog> sysLogIPage = sysLogService.page(page,sysLog);

page类具体实现

1
2
3
4
5
6
@Override
public IPage<SysLog> page(IPage<SysLog> iPage, SysLog sysLog) {
QueryWrapper queryWrapper = QueryWrapperUtils.entity2Wrapper(sysLog);
queryWrapper.orderByDesc("oper_date");
return page(iPage,queryWrapper);
}

entity2Wrapper(通用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static QueryWrapper entity2Wrapper(Object obj) {
return entity2Wrapper(null, obj);
}

/**
* entity转查询条件
*
* @param wrapper
* @param obj
* @return
*/

public static QueryWrapper entity2Wrapper(QueryWrapper wrapper, Object obj) {
if (Objects.isNull(wrapper)) {
wrapper = new QueryWrapper();
}
Field[] fields = ReflectUtil.getFields(obj.getClass());
//遍历属性
for (Field field : fields) {
String fieldName = field.getName();
//跳过serialVersionUID
if (fieldName.equals("serialVersionUID") || fieldName.equals("searchValue")) {
continue;
}
Object fieldValue = ReflectUtil.getFieldValue(obj, field);
if (Objects.isNull(fieldValue)) {
continue;
}
if (fieldValue instanceof Map) {
Map params = (Map) fieldValue;
if (CollUtil.isNotEmpty(params)) {
map2Wrapper(wrapper, obj.getClass(), params);
}
} else {
entity2Wrapper(wrapper, obj, field);
}
}
return wrapper;
}

注解实现分布式加锁

方案

  1. 定义注解
  2. 环绕处理

定义的注解可以在AOP拦截中定位到方法字段,然后通过反射获取其属性值,然后再对不同注解做不同的业务处理

技术点

@Retention(RetentionPolicy.RUNTIME)

首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。
自定义注解之运行时注解(RetentionPolicy.RUNTIME)-CSDN博客

  1. 导入所需要的依赖
1
2
3
4
5
6
<!--        redisson配置-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
  1. 定义加锁注解,包含分布式锁各种属性,并设定作用范围
1
2
3
4
5
6
7
8
9
10
11
12
13
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface RedisLock {
// key的值
String keyName() default "";

// 获取锁等待时间
int waitTime() default 1;

// 释放时间:默认值为-1,代表如果到默认时间了会自动续期
int leaseTime() default -1;

}
  1. 添加注解环绕加强,从注解属性中读取值组成key进行加锁操作(难点:属性值SpEL表达式转化)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Aspect
@Component
public class RedisLockAspect {
@Autowired
RedissonClient redissonClient;


private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
private static final ExpressionParser parser = new SpelExpressionParser();

@Around("@annotation(redisLock)")
public Object around(ProceedingJoinPoint joinPoint,RedisLock redisLock) throws Throwable {
System.out.println("redisLock.keyName() = " + redisLock.keyName());
//1.对锁属性进行解析获取锁key
String lockName = this.getLockName(joinPoint,redisLock);
System.out.println("lockName = " + lockName);
RLock lock = redissonClient.getLock(lockName);
//2.获取锁,如果锁获取失败则抛出异常
boolean isLocked = false;
try {
isLocked = lock.tryLock(redisLock.waitTime(), redisLock.leaseTime(), TimeUnit.SECONDS);
System.out.println("isLocked = " + isLocked);
if (!isLocked) {
throw new RuntimeException("获取锁失败");
}
//3.执行方法,并返回结果
return joinPoint.proceed();
} finally {
if (isLocked&&lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SpEL表达式转化为我们需要的key操作(难点)

private String getLockName(ProceedingJoinPoint joinPoint, RedisLock redisLock) {
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method method = resolveMethod(signature, joinPoint.getTarget());
EvaluationContext context = new MethodBasedEvaluationContext(TypedValue.NULL, method,
joinPoint.getArgs(), parameterNameDiscoverer);
Expression expression = parser.parseExpression(redisLock.keyName());
return expression.getValue(context, String.class);
}

private Method resolveMethod(MethodSignature signature, Object target) {
Class<?> aClass = target.getClass();
try{
return aClass.getMethod(signature.getName(), signature.getParameterTypes());
} catch (NoSuchMethodException e) {
throw new IllegalStateException("Cannot resolve target method" + signature.getName());
}
}
  1. 应用
1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("buy")
@RedisLock(keyName = "'product:'+ #id")
public Result buyProduct(@RequestParam Integer id,Integer count){
// UPDATE products set stock = stock - 1 where id = 1 and stock>0
productsMapper.buyProduct(id,count);
//休眠10分钟
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return Result.ok("购买成功!");
}

CORS跨域处理

前端资源访问后端资源,需要解决跨域问题,浏览器首先会发个OPTIONS请求,询问后端是否允许调自己接口,这个方法仅此而已!所以验证完毕之后直接返回false就可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception
{
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时,option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name()))
{
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}

在Spring框架中,preHandle 方法是HandlerInterceptor接口的一部分,它允许在请求处理之前执行某些操作。当你看到这个方法中针对OPTIONS请求返回false时,这通常与CORS(跨源资源共享)的处理有关。

这里是为什么针对OPTIONS请求返回false的原因:

  1. CORS 预检请求(Preflight Request):当浏览器发送一个跨域请求时,如果请求满足CORS的某些条件(例如,使用了非简单方法如PUT、DELETE,或发送了自定义头),浏览器会首先发送一个OPTIONS请求,这被称为“预检请求”。这个请求的目的是询问服务器是否允许跨域请求。
  2. 响应预检请求:服务器需要响应这个OPTIONS请求,告知浏览器哪些方法、头以及源是允许的。在上面的代码中,通过设置Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers响应头来完成这一点。
  3. 结束请求处理:一旦服务器响应了OPTIONS请求,浏览器就不需要(也不应该)继续发送实际的请求。因此,在preHandle方法中,我们返回false来表示不需要继续处理这个请求(即,不调用控制器方法)。如果返回true,则Spring会继续处理这个请求,这通常不是我们所期望的,因为我们只是想响应OPTIONS请求并告诉浏览器它是允许的,而不是处理实际的请求。
  4. 对于非OPTIONS请求:对于其他类型的请求(如GET、POST等),我们返回super.preHandle(request, response)的结果。这允许Spring继续其正常的请求处理流程(即,调用控制器方法等)。