完整异常:

Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id ‘sys:role:add’ as a subtype of 'java.lang.Object': no such class found
at [Source: (byte[])“[“sys:role:add”,“sys:permission:delete”,“sys:log:list”,“sys:log:delete”,“sys:permission:list”,“sys:permission:update”,“sys:permission:add”,“sys:role:list”,“sys:user:add”,“sys:role:detail”,“sys:role:delete”,“sys:role:update”,“sys:user:delete”,“sys:user:update”,“sys:user:role:update”,“sys:user:list”,“ROLE_超级管理员”]”; line: 1, col`

很久之前写的代码的异常了,不过一直懒得管,今天尝试着手解决下。根据报错很明显是序列化的相关问题,报错说没有对应 sys:role:add 的这个对象无法进行转换。

这里代码的主要功能是整合Spring Security做了个权限校验,用户登录会去Redis中查询自己拥有的角色,并将拥有的权限返回,如果redis中没有就去数据库中查询,并保存到Redis中。看看涉及到的相关代码:

使用的序列化依赖 (Jackson):

<!--jackson相关注解,实现日期格式转换和类型格式转换并序列化等-->
 <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
</dependency>

@Cache注解缓存配置的相关代码:

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
 * @author shenyang
 * @version 1.0
 * @Date 2024/1/4 19:42
 */
@Configuration
@EnableCaching
public class CacheConfig {

    /**
     * 配置 cacheManager 代替默认的 cacheManager (缓存管理器)
     *
     * @param factory RedisConnectionFactory
     * @return CacheManager
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
//        Jackson2JsonRedisSerializer<String> stringSerializer = new Jackson2JsonRedisSerializer<>(String.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //仅仅序列化对象的属性,且属性不可为final修饰
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(objectMapper);
        // 配置key value序列化
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
                //为了解决权限数组中的元素转换成String类型
//                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(stringSerializer))
                //关闭控制存储
                .disableCachingNullValues()
                //修改前缀与key的间隔符号,默认是::
                .computePrefixWith(cacheName -> cacheName + ":");

        //设置特有的Redis配置
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        //定制化的Cache 设置过期时间 eg:以role:开头的缓存存活时间为10s
//        cacheConfigurations.put("role",customRedisCacheConfiguration(config,Duration.ofSeconds(10)));
//        cacheConfigurations.put("stock",customRedisCacheConfiguration(config,Duration.ofSeconds(3000)));
//        cacheConfigurations.put("market",customRedisCacheConfiguration(config,Duration.ofSeconds(300)));
        //构建redis缓存管理器
        //设置过期时间
        return RedisCacheManager.builder(factory)
                //Cache事务支持
                .transactionAware()
                .withInitialCacheConfigurations(cacheConfigurations)
                .cacheDefaults(config)
                .build();
    }

    /**
     * 设置RedisConfiguration配置
     *
     * @param config
     * @param ttl
     * @return
     */
    public RedisCacheConfiguration customRedisCacheConfiguration(RedisCacheConfiguration config, Duration ttl) {
        //设置缓存缺省超时时间
        return config.entryTtl(ttl);
    }

}

查询权限缓存业务相关代码(这里使用的是Redis做缓存):

import com.google.common.base.Strings;
import com.shen.stock.constant.StockConstant;
import com.shen.stock.face.PermissionCacheFace;
import com.shen.stock.mapper.SysPermissionMapper;
import com.shen.stock.mapper.SysRoleMapper;
import com.shen.stock.pojo.entity.SysPermission;
import com.shen.stock.pojo.entity.SysRole;
import com.shen.stock.service.PermissionService;
import com.shen.stock.vo.resp.MenusRespVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author shenyang
 * @version 1.0
 * @Date 2024/1/13 19:03
 */
@Component
@CacheConfig(cacheNames = StockConstant.Permission)
public class PermissionCacheFaceImpl implements PermissionCacheFace {


    @Autowired
    private SysPermissionMapper sysPermissionMapper;

    @Autowired
    private SysRoleMapper sysRoleMapper;

    @Autowired
    private PermissionService permissionService;

    /**
     * 缓存用户权限信息
     *
     * @param userId 用户id
     * @return List<String> 用户的SpringSecurity的权限标识
     */
    @Override
    @Cacheable(key = "#root.method.getName()+(':')+(#userId)")
    public String[] getPermsAndRoles(List<SysPermission> permissionList,String userId) {
        //获取权限集合
//        List<SysPermission> permissionList = sysPermissionMapper.findSysPermissionAll(userId);
        List<String> permsNameList = permissionList.stream().filter(item -> !Strings.isNullOrEmpty(item.getPerms())).map(item -> item.getPerms())
                .distinct()
                .collect(Collectors.toList());

        //获取角色集合 基于角色鉴权注解需要将角色前追加ROLE_
        List<SysRole> roleList = sysRoleMapper.getRoleByUserId(Long.valueOf(userId));
        List<String> roleNameList = roleList.stream().filter(item -> !Strings.isNullOrEmpty(item.getName()))
                .map(item -> "ROLE_" + item.getName()).collect(Collectors.toList());

        List<String> auths = new ArrayList<>();
        auths.addAll(permsNameList);
        auths.addAll(roleNameList);
        String[] psArray = auths.toArray(new String[permsNameList.size()+roleNameList.size()]);
        return psArray;
    }

    @Override
//    @CacheEvict(key = "'getPermsAndRoles'+(':')+(#userId)")
    public void updateUserPermsAndRoles(String userId) {

    }

    /**
     * 缓存侧边栏
     *
     * @param permissions 权限集合
     * @return 侧边栏权限树
     */
    @Override
    @Cacheable(key = "#root.method.getName()+(':')+(#userId)")
    public List<MenusRespVo> getMenuTreeNodes(List<SysPermission> permissions,String userId) {
        return permissionService.getTree(permissions, 0L, true);
    }

    /**
     * 更新缓存侧边栏
     */
    @Override
//    @CacheEvict(key = "'getMenuTreeNodes'+(':')+(#userId)")
    public void updateUserMenuTreeNodes(String userId) {

    }

    /**
     * 缓存用户关联的权限按钮集合
     * @param permissionList
     * @return
     */
    @Override
    @Cacheable(key = "#root.method.getName()+(':')+(#userId)")
    public List<String> getBtnPerms(List<SysPermission> permissionList,String userId) {
        if (!CollectionUtils.isEmpty(permissionList)) {
            return permissionList.stream().filter(per -> !Strings.isNullOrEmpty(per.getCode()) && per.getType() == 3)
                    .map(per -> per.getCode()).distinct().collect(Collectors.toList());
        }
        return null;
    }
}

上述 getBtnPermsgetMenuTreeNodes在被调用时都没有出现问题,只有getPermsAndRoles方法出现报错。

为了更好的查看问题的原因,去redis中看看数据的保存情况。

getBtnPerms

在这里插入图片描述

getMenuTreeNodes

在这里插入图片描述
getPermsAndRoles
在这里插入图片描述

当时乍一看没有发现有问题啊,没有乱码,好像没有出错啊!

但是,回忆一下报错,Could not resolve type id 'sys:role:add' as a subtype of 'java.lang.Object'sys:role:add 无法转换成对象。它为什么会把 sys:role:add 当成对象类型呢? 再看一下上面的几个截图,最终发现了问题的原因:

在这里插入图片描述

可以看到在Jackson在序列化时,除了保存应有的数据,还保存了一个对应的类型。但是 getPermsAndRoles 方法并没有保存,所以字符串数组的第一个值被当成了数据的类型

上述的两个返回结果为List的方法可以被正常序列化,但是 getPermsAndRoles的返回值类型是字符串数组所以出现问题。

问题的原因发现了,那么就很容易解决了。

既然 String[] 不行,我们使用List<String>作为返回值。

Spring Security中AuthorityUtils.createAuthorityList 中需要String[],我们将强转的逻辑放在调用方处就好了。

String[] psArray = permissionCacheFace.getPermsAndRoles(permissions, dbUser.getId().toString()).toArray(new String[0]);
List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(psArray);

好了,问题到这里就解决了,在工作过程中遇到问题一定不要着急,可以慢慢从各种角度分析。问题总会被解决的。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐