Sun Frame:SpringBoot 的轻量级开发框架(个人开源项目推荐)

Sun Frame Banner

轻松高效的现代化开发体验

Sun Frame 是我个人开源的一款基于 SpringBoot 的轻量级框架,专为中小型企业设计。它提供了一种快速、简单且易于扩展的开发方式。

我们的开发文档记录了整个项目从0到1的任何细节,实属不易,请给我们一个Star!
您的支持是我们持续改进的动力。

亮点功能

  • 组件化开发:灵活选择,简化流程。
  • 高性能:通过异步日志和 Redis 缓存提升性能。
  • 易扩展:支持多种数据库和消息队列。

spring cloud模块概览

  • Nacos 服务:高效的服务注册与发现。
  • Feign 远程调用:简化服务间通信。
  • 强大网关:路由与限流。

常用工具

  • 日志管理:异步处理与链路追踪。
  • Redis 集成:支持分布式锁与缓存。
  • Swagger 文档:便捷的 API 入口。
  • 测试支持:SpringBoot-Test 集成。
  • EasyCode:自定义EasyCode模板引擎,一键生成CRUD。

更多信息


1.缓存一致性问题

1、更新了数据库,再更新缓存

假设数据库更新成功,缓存更新失败,在缓存失效和过期的时候,读取到的都是老数据缓存。

2、更新缓存,更新数据库

缓存更新成功了,数据库更新失败,是不是读取的缓存的都是错误的。

以上两种,全都不推荐。

3、先删除缓存,再更新数据库

有一定的使用量。即使数据库更新失败。缓存也可以会刷。

存在的问题是什么?

高并发情况下!!

比如说有两个线程,一个是 A 线程,一个是 B 线程。

A 线程把数据删了,正在更新数据库,这个时候 B 线程来了,发现缓存没了,又查数据,又放入缓存。缓存里面存的就一直是老数据了。

延迟双删。更新完数据库之后,再删一次。

扩展思路
1、消息队列补偿

删除失败的缓存,作为消息打入 mq,mq 消费者进行监听,再次进行重试刷缓存。

2、canal

监听数据库的变化,做一个公共服务,专门来对接缓存刷新。优点业务解耦,业务太多冗余代码复杂度。

2.auth微服务在用户注册时,将当前用户的角色和权限都放到redis里

1.sun-club-auth-domain
1.pom.xml 引入依赖
        <!-- 序列化 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.12.7</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.12.7</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.6</version>
        </dependency>
2.RedisConfig.java
package com.sunxiansheng.auth.domain.redis;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Description: 原生 redis 的 template 的序列化器会产生乱码问题,重写改为 jackson
 * @Author sun
 * @Create 2024/6/5 14:16
 * @Version 1.0
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer());
        return redisTemplate;
    }

    private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
        Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jsonRedisSerializer.setObjectMapper(objectMapper);
        return jsonRedisSerializer;
    }

}
3.RedisUtil.java
package com.sunxiansheng.auth.domain.redis;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Description: RedisUtil工具类
 * @Author sun
 * @Create 2024/6/5 14:17
 * @Version 1.0
 */
@Component
@Slf4j
public class RedisUtil {

    @Resource
    private RedisTemplate redisTemplate;

    private static final String CACHE_KEY_SEPARATOR = ".";

    /**
     * 构建缓存key
     */
    public String buildKey(String... strObjs) {
        return Stream.of(strObjs).collect(Collectors.joining(CACHE_KEY_SEPARATOR));
    }

    /**
     * 是否存在key
     */
    public boolean exist(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 删除key
     */
    public boolean del(String key) {
        return redisTemplate.delete(key);
    }

    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public boolean setNx(String key, String value, Long time, TimeUnit timeUnit) {
        return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit);
    }

    public String get(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    }

    public Boolean zAdd(String key, String value, Long score) {
        return redisTemplate.opsForZSet().add(key, value, Double.valueOf(String.valueOf(score)));
    }

    public Long countZset(String key) {
        return redisTemplate.opsForZSet().size(key);
    }

    public Set<String> rangeZset(String key, long start, long end) {
        return redisTemplate.opsForZSet().range(key, start, end);
    }

    public Long removeZset(String key, Object value) {
        return redisTemplate.opsForZSet().remove(key, value);
    }

    public void removeZsetList(String key, Set<String> value) {
        value.stream().forEach((val) -> redisTemplate.opsForZSet().remove(key, val));
    }

    public Double score(String key, Object value) {
        return redisTemplate.opsForZSet().score(key, value);
    }

    public Set<String> rangeByScore(String key, long start, long end) {
        return redisTemplate.opsForZSet().rangeByScore(key, Double.valueOf(String.valueOf(start)), Double.valueOf(String.valueOf(end)));
    }

    public Object addScore(String key, Object obj, double score) {
        return redisTemplate.opsForZSet().incrementScore(key, obj, score);
    }

    public Object rank(String key, Object obj) {
        return redisTemplate.opsForZSet().rank(key, obj);
    }


}
4.AuthUserDomainServiceImpl.java register方法新增逻辑
// 要把当前用户的角色和权限都放到redis里

// 1、存储角色
// 构建一个角色的key
String roleKey = redisUtil.buildKey(authRolePrefix, authUser.getUserName());
// 构建一个角色列表作为value
List<AuthRole> roleList = new ArrayList<>();
// 向角色列表中添加角色
roleList.add(authRole);
// 将角色列表序列化并放到redis中
redisUtil.set(roleKey, new Gson().toJson(roleList));

// 2、存储权限
// 查询当前用户拥有的权限
// 1.注册的时候,用户只有一个角色,先根据这个角色id去角色权限关联表中查询多条关联的记录
AuthRolePermission authRolePermission = new AuthRolePermission();
// 设置逻辑删除
authRolePermission.setIsDeleted(IsDeleteFlagEnum.UN_DELETED.getCode());
// 设置角色id
authRolePermission.setRoleId(roleId);
// 查询出一个角色权限关联的列表
List<AuthRolePermission> authRolePermissionList = authRolePermissionService.queryByCondition(authRolePermission);
// 2.根据查询出来的列表,得到所有的权限id
List<Long> permissionIdList = authRolePermissionList.stream().map(AuthRolePermission::getPermissionId).collect(Collectors.toList());
// 根据权限id,查询所有的权限,就是根据ids批量查询
List<AuthPermission> permissionList = authPermissionService.queryByIds(permissionIdList);
// 将权限列表序列化并放到redis中
String permissionKey = redisUtil.buildKey(authPermissionPrefix, authUser.getUserName());
redisUtil.set(permissionKey, new Gson().toJson(permissionList));

image-20240607164025000

2.sun-club-auth-infra
1.AuthRolePermissionService.java
/**
 * 根据角色id查询角色权限关联表
 * @param authRolePermission
 * @return
 */
List<AuthRolePermission> queryByCondition(AuthRolePermission authRolePermission);
2.AuthRolePermissionServiceImpl.java
/**
 * 根据角色id查询角色权限关联表
 * @param authRolePermission
 * @return
 */
@Override
public List<AuthRolePermission> queryByCondition(AuthRolePermission authRolePermission) {
    return this.authRolePermissionDao.queryAllByLimit(authRolePermission);
}
3.AuthPermissionService.java
/**
 * 通过ids查询数据
 * @param ids
 * @return
 */
public List<AuthPermission> queryByIds(List<Long> ids);
4.AuthPermissionServiceImpl.java
/**
 * 通过ids查询数据
 * @param ids
 * @return
 */
public List<AuthPermission> queryByIds(List<Long> ids) {
    return authPermissionDao.queryByIds(ids);
}
5.AuthPermissionDao.java
/**
 * 通过ID批量查询
 *
 * @param ids
 * @return
 */
List<AuthPermission> queryByIds(@Param("ids") List<Long> ids);
6.AuthPermissionDao.xml
<select id="queryByIds" resultMap="AuthPermissionMap">
    select id, name, parent_id, type, menu_url, status, `show`, icon, permission_key, created_by, created_time,
    update_by, update_time, is_deleted
    from auth_permission
    where id in
    <foreach collection="ids" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</select>
3.测试

image-20240607164355210

image-20240607164348873

3.gateway鉴权时可以获取权限/角色列表

1.sun-club-
1.复制两个entity到这个模块
1.AuthPermission.java
package com.sunxiansheng.club.gateway.entity;

import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * (AuthPermission)实体类
 *
 * @author makejava
 * @since 2024-06-06 17:16:58
 */
@Data
public class AuthPermission implements Serializable {

    private Long id;
    /**
     * 权限名称
     */
    private String name;
    /**
     * 父id
     */
    private Long parentId;
    /**
     * 权限类型 0菜单 1操作
     */
    private Integer type;
    /**
     * 菜单路由
     */
    private String menuUrl;
    /**
     * 状态 0启用 1禁用
     */
    private Integer status;
    /**
     * 展示状态 0展示 1隐藏
     */
    private Integer show;
    /**
     * 图标
     */
    private String icon;
    /**
     * 权限唯一标识
     */
    private String permissionKey;
    /**
     * 创建人
     */
    private String createdBy;
    /**
     * 创建时间
     */
    private Date createdTime;
    /**
     * 更新人
     */
    private String updateBy;
    /**
     * 更新时间
     */
    private Date updateTime;
    /**
     * 是否被删除 0为删除 1已删除
     */
    private Integer isDeleted;

}
2.AuthRole.java
package com.sunxiansheng.club.gateway.entity;

import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * (AuthRole)实体类
 *
 * @author makejava
 * @since 2024-06-06 14:30:38
 */
@Data
public class AuthRole implements Serializable {

    private Long id;
    /**
     * 角色名称
     */
    private String roleName;
    /**
     * 角色唯一标识
     */
    private String roleKey;
    /**
     * 创建人
     */
    private String createdBy;
    /**
     * 创建时间
     */
    private Date createdTime;
    /**
     * 更新人
     */
    private String updateBy;
    /**
     * 更新时间
     */
    private Date updateTime;
    /**
     * 是否被删除 0未删除 1已删除
     */
    private Integer isDeleted;


}
2.StpInterfaceImpl.java 根据loginId和前缀获取权限/角色列表
/**
 * 根据loginId和前缀获取权限/角色列表
 * @param loginId
 * @param prefix
 * @return
 */
private List<String> getAuth(String loginId, String prefix) {
    // 得到该用户在redis中存储的key
    String authKey = redisUtil.buildKey(prefix, loginId.toString());
    // 从redis中获取列表
    String authValue = redisUtil.get(authKey);
    // 判空
    if (StringUtils.isBlank(authValue)) {
        return Collections.emptyList();
    }
    List<String> authList = new LinkedList<>();
    // 根据前缀来决定将内容反序列化为什么形式
    if (authRolePrefix.equals(prefix)) {
        // 如果是角色列表的前缀,就反序列化为角色类型的
        List<AuthRole> authRoleList = new Gson().fromJson(authValue, new TypeToken<List<AuthRole>>() {
        }.getType());
        // 得到roleKey的列表,放到authList中
        authList = authRoleList.stream().map(AuthRole::getRoleKey).collect(Collectors.toList());
    } else if (authPermissionPrefix.equals(prefix)) {
        // 如果是权限列表,就反序列化为权限类型的
        List<AuthPermission> authPermissionList = new Gson().fromJson(authValue, new TypeToken<List<AuthPermission>>() {
        }.getType());
        // 得到permissionKey,放到authList中
        authList = authPermissionList.stream().map(AuthPermission::getPermissionKey).collect(Collectors.toList());
    }
    return authList;
}
3.sun-club-auth-application-controller
在UserController.java可以设置用户登录时的token对应的loginId,这里设置成鸡翅

image-20240607171955303

3.测试
1.首先登录,生成token和loginId(这里写死为鸡翅)

image-20240607172214503

2.然后携带token进行登录,后端就可以找到对应的loginId,在验证登录成功之后会进行鉴权
1.在gateway的SaTokenConfigure.java可以配置鉴权的类型

image-20240607172332923

2.下面是分别两种方式,从redis中取出的角色列表和权限列表

image-20240607171208574

image-20240607171451314

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部