项目XYIOT项目学习笔记
FANSEAXYIOT项目
其实当一个程序员接手一个项目,首先最重要的不是马上把代码看懂,而是弄清楚实现这个业务为什么要用这个技术,它的优缺点是什么,再继续深究具体实现!本质上说就是需要带着目的去学习
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
项目思考
- 对照界面看看页面功能
- 再看数据库表
PageHelper使用
- 导入插件
1 2 3 4 5
| <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency>
|
- 在yml中pagehelper分页插件配置
1 2 3 4 5 6
| pagehelper: helperDialect: mysql reasonable: true supportMethodsArguments: true params: count=countSql
|
- 实现
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.startPage(pageNum,pageSize); List<User> users = userMapper.selectAll(); PageInfo<User> pageInfo = new PageInfo<>(users); 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(".")); String objectName = DateUtils.datePath() + "/" + fileName; try {
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
| 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
|
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:密码加密模块,对数据库中的用户密码进行加密处理
主要应用代码:
- 获取到subjct对象,封装 AuthenticationToken 再调用login方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| Subject subject = SecurityUtils.getSubject(); AuthenticationToken token=new UsernamePasswordToken(username,password); subject.login(token); if (subject.isAuthenticated()){ System.out.println("登录成功"); httpSession.setAttribute("userName",user.getUserName()); String tokens = tokenService.createToken(userService.getOne(new QueryWrapper<User>().eq("user_name", username))); return Result.ok(tokens); }
|
- 配置 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;
@Bean public SecurityManager securityManager() { DefaultSecurityManager securityManager = new DefaultSecurityManager(); ThreadContext.bind(securityManager); securityManager.setRealm(myRealm); return securityManager; }
@Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager);
Map<String, String> filterChainDefinitionMap = new HashMap<>();
filterChainDefinitionMap.put("/user/login", "anon"); filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/swagger-ui.html", "anon"); filterChainDefinitionMap.put("/swagger/**", "anon");
filterChainDefinitionMap.put("/**/**", "authc"); filterChainDefinitionMap.put("/**", "authc");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; }
}
|
- 重写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;
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String name = token.getPrincipal().toString(); System.out.println("name = " + name); User user = userService.getOne(new QueryWrapper<User>().eq("user_name",name)); if (user != null) { AuthenticationInfo info = new SimpleAuthenticationInfo( token.getPrincipal(), user.getPassword(),
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); }
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(); 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; }
|
注解实现分布式加锁
方案
- 定义注解
- 环绕处理
定义的注解可以在AOP拦截中定位到方法字段,然后通过反射获取其属性值,然后再对不同注解做不同的业务处理
技术点
@Retention(RetentionPolicy.RUNTIME)
首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。
自定义注解之运行时注解(RetentionPolicy.RUNTIME)-CSDN博客
- 导入所需要的依赖
1 2 3 4 5 6
| <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.6</version> </dependency>
|
- 定义加锁注解,包含分布式锁各种属性,并设定作用范围
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface RedisLock { String keyName() default ""; int waitTime() default 1; int leaseTime() default -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()); String lockName = this.getLockName(joinPoint,redisLock); System.out.println("lockName = " + lockName); RLock lock = redissonClient.getLock(lockName); boolean isLocked = false; try { isLocked = lock.tryLock(redisLock.waitTime(), redisLock.leaseTime(), TimeUnit.SECONDS); System.out.println("isLocked = " + isLocked); if (!isLocked) { throw new RuntimeException("获取锁失败"); } 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
| 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 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){ productsMapper.buyProduct(id,count); 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")); 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
的原因:
- CORS 预检请求(Preflight Request):当浏览器发送一个跨域请求时,如果请求满足CORS的某些条件(例如,使用了非简单方法如PUT、DELETE,或发送了自定义头),浏览器会首先发送一个
OPTIONS
请求,这被称为“预检请求”。这个请求的目的是询问服务器是否允许跨域请求。
- 响应预检请求:服务器需要响应这个
OPTIONS
请求,告知浏览器哪些方法、头以及源是允许的。在上面的代码中,通过设置Access-Control-Allow-Origin
、Access-Control-Allow-Methods
和Access-Control-Allow-Headers
响应头来完成这一点。
- 结束请求处理:一旦服务器响应了
OPTIONS
请求,浏览器就不需要(也不应该)继续发送实际的请求。因此,在preHandle
方法中,我们返回false
来表示不需要继续处理这个请求(即,不调用控制器方法)。如果返回true
,则Spring会继续处理这个请求,这通常不是我们所期望的,因为我们只是想响应OPTIONS
请求并告诉浏览器它是允许的,而不是处理实际的请求。
- 对于非OPTIONS请求:对于其他类型的请求(如GET、POST等),我们返回
super.preHandle(request, response)
的结果。这允许Spring继续其正常的请求处理流程(即,调用控制器方法等)。