基于数据库自定义UserDetailsService实现JWT认证

我的思路是,登录时使用用户凭证换取Token,Token存储在Redis中,每次请求验证Token与Redis中是否相同并续签,Redis控制Token过期时间。步骤如下:

添加依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!--身份认证-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--Redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>   
    </dependencies>

创建用户类与角色类

用户类应该实现UserDetails接口以契合SpringSecurity

//实现接口时自带方法,注意各个方法的意义,自行修改,默认为True
@Data
public class User implements UserDetails {
    private Integer id;
    private String phone;
    private Boolean enabled;
    private String username;
    private String password;
    private Integer role;
    private List<Role> roles;
    
	//JsonIgnore在接口传递时隐藏敏感信息
    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role:roles)
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        return authorities;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

在实际开发中会设置用户角色来控制接口访问,角色类的角色名要求有前缀ROLE_,例如ADMIN,则时ROLE_ADMIN

@Data
public class Role implements GrantedAuthority {
    private Integer id;
    private String roleName;

    @JsonIgnore
    @Override
    public String getAuthority() {
        return roleName;
    }
}

Redis配置类

先设置常量,例如过期时间。

public final class ConstantKit {
    public static final Integer DEL_FLAG_TRUE=1;
    public static final Integer DEL_FLAG_FALSE=0;
    /**
     * redis存储token设置的过期时间
     * 单位:秒(1h)
     */
    public static final Long TOKEN_EXPIRE_TIME= Long.valueOf(60*60);

    /**
     * 设置可以重置token过期时间的时间界限
     * 单位:毫秒(30min)
     */
    public static final Long TOKEN_RESET_TIME= Long.valueOf(1000*30*60);
}

创建与属性文件映射的配置类。

@Data
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
public class RedisConfigProperty {
    private String host;
    private String password;
    private int port;
    private int database;
    private int timeout;
}

对Jedis进行配置。

public class JedisConfig extends CachingConfigurerSupport {
    private static final Logger LOGGER = LoggerFactory.getLogger(JedisConfig.class);

    @Resource
    private RedisConfigProperty redisConfigProperty;

    @Bean(name = "jedisPoolConfig")
    @ConfigurationProperties(prefix = "spring.redis.pool-config")
    public JedisPoolConfig getRedisConfig(){
        return new JedisPoolConfig();
    }

    @Bean(name = "jedisPool")
    public JedisPool jedisPool(@Qualifier(value = "jedisPoolConfig") final JedisPoolConfig jedisPoolConfig){
        LOGGER.info("Jedis Pool build start");
        String host = redisConfigProperty.getHost();
        int timeout = redisConfigProperty.getTimeout();
        String password = redisConfigProperty.getPassword();
        int database = redisConfigProperty.getDatabase();
        int port = redisConfigProperty.getPort();
        JedisPool jedisPool = new JedisPool(jedisPoolConfig,host,port,timeout,password,database);
        LOGGER.info("Jedis Pool build success host={},port={}",host,port);
        return jedisPool;
    }
}

JWT生成验证

JWT工具类。

public class JwtUtils {

	//PayLoad密钥
    private static final String JWT_PAYLOAD_USER_KEY = "优快云";

    /**
     * 生成Token并设置过期时间(过期时间不在此处实现,在Redis中实现)
     * @param userInfo
     * @param privateKey
     * @param expire
     * @return
     */
    public static String generateTokenExpireInMillis(Object userInfo, PrivateKey privateKey,long expire){
        return Jwts.builder()
                .claim(JWT_PAYLOAD_USER_KEY,JsonUtils.toString(userInfo))
                .setId(createJTI())
//                .setExpiration(new Date(System.currentTimeMillis()+expire))//毫秒
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();

    }

    /**
     * Token解密
     * @param token
     * @param publicKey
     * @return
     */
    private static Jws<Claims> parserToken(String token, PublicKey publicKey){
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }

    /**
     * 从Token中获取个人信息
     * @param token
     * @param publicKey
     * @param userType
     * @return
     * @param <T>
     */
    public static <T>Payload<T> getInfoFromToken(String token,PublicKey publicKey,Class<T> userType){
        Jws<Claims> claimsJws = parserToken(token,publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setUserinfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(),userType));
        return claims;
    }

    /**
     * 生成ID
     * @return
     */
    private static String createJTI(){
        return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)));
    }
}

RSA密钥工具类,用来对Token再加密解密。

public class RsaUtils {
    private static final int DEFAULT_KEY_SIZE = 2048;

    /**
     * 从文件中读取公钥
     * @param fileName
     * @return
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    public static PublicKey getPublicKey(String fileName) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] bytes = readFile(fileName);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取私钥
     * @param fileName
     * @return
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    public static PrivateKey getPrivateKey(String  fileName) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] bytes = readFile(fileName);
        return getPrivateKey(bytes);
    }

    /**
     * 从字节数组中读取公钥
     * @param bytes
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    private static PublicKey getPublicKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
        bytes = Base64.getDecoder().decode(bytes);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 从字节数组中读取私钥
     * @param bytes
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
        bytes = Base64.getMimeDecoder().decode(bytes);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 生成密钥文件
     * @param publicKeyFileName
     * @param privateKeyFileName
     * @param secret
     * @param keySize
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public static void generateKey(String publicKeyFileName,String privateKeyFileName,String secret,int keySize) throws IOException, NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes(StandardCharsets.UTF_8));
        keyPairGenerator.initialize(Math.max(keySize,DEFAULT_KEY_SIZE),secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
        writeFile(publicKeyFileName,publicKeyBytes);
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
        writeFile(privateKeyFileName,privateKeyBytes);
    }

    /**
     * 读文件
     * @param fileName
     * @return
     * @throws IOException
     */
    private static byte[] readFile(String fileName) throws IOException {
        return Files.readAllBytes(new File(fileName).toPath());
    }

    /**
     * 写文件
     * @param destPath
     * @param bytes
     * @throws IOException
     */
    private static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists())
            dest.createNewFile();
        Files.write(dest.toPath(),bytes);
    }
}

RSA配置类。

Data
@ConfigurationProperties("rsa.key")
public class RsaKeyProperties {
    private String publicKeyPath;
    private String privateKeyPath;
    private PublicKey publicKey;
    private PrivateKey privateKey;

    @PostConstruct
    public void createKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        this.publicKey = RsaUtils.getPublicKey(publicKeyPath);
        this.privateKey = RsaUtils.getPrivateKey(privateKeyPath);
    }
}

配置文件

在resources文件夹下编辑application.properties

server.port=8181

# rsa
rsa.key.publicKeyPath=公钥地址
rsa.key.privateKeyPath=私钥地址

#redis 基础配置
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接密码(默认为空)
spring.redis.password=redis
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器连接端口
spring.redis.port=6379
# 连接超时时间(毫秒)
spring.redis.timeout=5000

#redis 连接池配置
#池中最大链接数
spring.redis.pool-config.max-total=256
# 连接池中的最大空闲连接
spring.redis.pool-config.max-idle=128
# 连接池中的最小空闲连接
spring.redis.pool-config.min-idle=8
# 调用者获取链接时,是否检测当前链接有效性
spring.redis.pool-config.test-on-borrow=false
# 向链接池中归还链接时,是否检测链接有效性
spring.redis.pool-config.test-on-return=false
# 调用者获取链接时,是否检测空闲超时, 如果超时,则会被移除-
spring.redis.pool-config.test-while-idle=true
# 空闲链接检测线程一次运行检测多少条链接
spring.redis.pool-config.num-tests-per-eviction-run=8

# 缓存
spring.cache.cache-names=c1,c2
spring.cache.redis.time-to-live=1800s

创建认证失败类

当用户没有有效凭证时,会用此类进行处理。

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException{
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter out = response.getWriter();
        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","401");
        resultData.put("msg", "请登录!");
        out.write(new ObjectMapper().writeValueAsString(resultData));
        out.flush();
        out.close();
    }
}

创建权限不足类

当用户试图访问未经授权的接口时,会到这里处理。

public class UserAuthAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter out = response.getWriter();
        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","403");
        resultData.put("msg", "未授权");
        out.write(new ObjectMapper().writeValueAsString(resultData));
        out.flush();
        out.close();
    }
}

创建过滤器

这里创建两个过滤器,一个用于登录颁发token,一个用于续签鉴定token。

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {;
    private AuthenticationManager authenticationManager;
    private RsaKeyProperties rsaKeyProperties;
    private JedisPool jedisPool;
    public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties,
                          JedisPool jedisPool){
        this.authenticationManager = authenticationManager;
        this.rsaKeyProperties = rsaKeyProperties;
        this.jedisPool = jedisPool;
    }

	//首先执行此函数,如果与数据库中比对不上,则会抛出异常
	//为了防止账号枚举,这里只显示账号或密码错误
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
        User user = null;
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            user = objectMapper.readValue(request.getInputStream(),User.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            user.getUsername(),
                            user.userPassword()
                    )
            );
        } catch (Exception e) {
            try {
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_OK);
                PrintWriter out = response.getWriter();
                Map<String, Object> map = new HashMap<>();
                map.put("code", HttpServletResponse.SC_UNAUTHORIZED);
                map.put("message", "账号或密码错误!");
                out.write(new ObjectMapper().writeValueAsString(map));
                out.flush();
                out.close();
                e.printStackTrace();
            }catch (Exception exception){
                exception.printStackTrace();
            }
        }
        return null;
    }

	//登录成功
    @Override
    public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                         Authentication authentication){
        User user = new User();
        user.setUsername(authentication.getName());
        user.setRoles((List<Role>) authentication.getAuthorities());
        String token = JwtUtils.generateTokenExpireInMillis(user,rsaKeyProperties.getPrivateKey(),10*60*1000);
        Jedis jedis = jedisPool.getResource();
        response.addHeader("Authorization","AttackToken "+token);
        String oldToken = jedis.get(user.getUsername());
        if (oldToken!=null)
            jedis.del(oldToken,oldToken+user.getUsername());
        jedis.set(user.getUsername(),token);
        jedis.expire(user.getUsername(), ConstantKit.TOKEN_EXPIRE_TIME);
        jedis.set(token,user.getUsername());
        jedis.expire(token,ConstantKit.TOKEN_EXPIRE_TIME);
        Long currentTime = System.currentTimeMillis();
        jedis.set(token+user.getUsername(),currentTime.toString());
        jedis.close();
        try {
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_OK);
            PrintWriter out = response.getWriter();
            Map<String,Object> map = new HashMap<>(4);
            map.put("code",HttpServletResponse.SC_OK);
            map.put("message","登陆成功!");
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

请求过滤类,对每一个请求的Token进行验证与续签。

public class JwtVerifyFilter extends BasicAuthenticationFilter {
    private RsaKeyProperties rsaKeyProperties;
    private JedisPool jedisPool;

    public JwtVerifyFilter(AuthenticationManager authenticationManager,RsaKeyProperties rsaKeyProperties,
                           JedisPool jedisPool) {
        super(authenticationManager);
        this.rsaKeyProperties = rsaKeyProperties;
        this.jedisPool = jedisPool;
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        if (header == null||!header.startsWith("AttackToken ")){
            //不应该在此处抛出异常,抛出异常会重定向至/error,但/error并未放通
            //故会出现只要是出现了异常,如404等都会被AuthenticationEntryPoint捕获从而返回401
//            throw new AccessDeniedException("未登录");
            chain.doFilter(request,response);
            return;
        }
        String token = header.replace("AttackToken ","");
        User user = JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(),User.class).getUserinfo();
        Jedis jedis = jedisPool.getResource();
        request.setAttribute("requestUser",user.getUsername());
        if (jedis.get(token) == null)
            throw new AccessDeniedException("登录凭证已废弃");
        if (user!=null){
            Authentication authentication = new UsernamePasswordAuthenticationToken
                    (user.getUsername(),null,user.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
            Long tokenBirthTime = Long.valueOf(jedis.get(token+user.getUsername()));
            logger.info("token birth time is:"+tokenBirthTime);
            Long diff = System.currentTimeMillis()-tokenBirthTime;//时间差
            logger.info("token has existed for:"+diff);
            if (jedis.get(user.getUsername())==null)
                throw new AccessDeniedException("登录过期");
            if (diff> ConstantKit.TOKEN_RESET_TIME){
                jedis.expire(user.getUsername(), ConstantKit.TOKEN_EXPIRE_TIME);
                jedis.expire(token,ConstantKit.TOKEN_EXPIRE_TIME);
                logger.info("Reset expire time success");
                Long newBirthTime = System.currentTimeMillis();
                jedis.set(token+user.getUsername(),newBirthTime.toString());
            }
            jedis.close();
            chain.doFilter(request,response);
        }else {
            throw new AccessDeniedException("未登录");
        }
    }
}

自定义UserDetailsService

UsernamePasswordAuthenticationToken以及DaoAuthenticationProvider使用UserDetailsService来查询用户名、密码和GrantedAuthority,检查用户输入的密码是否匹配。

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Autowired
    RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) {
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("username", username);
        if (userMapper.selectList(userQueryWrapper).isEmpty()) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        User user = userMapper.selectList(userQueryWrapper).get(0);
        //将角色赋予用户
        Role role = roleMapper.selectById(user.getRole());
        List<Role> roles = new ArrayList<>();
        roles.add(roles);
        user.setRoles(roles);
        return user;
    }
}

Spring Security配置类

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final UserService userService;
    private final RsaKeyProperties rsaKeyProperties;
    private final JedisPool jedisPool;

    public SecurityConfig(UserServiceImpl userServiceImpl, RsaKeyProperties rsaKeyProperties, JedisPool jedisPool) {
        this.userServiceImpl = userServiceImpl;
        this.rsaKeyProperties = rsaKeyProperties;
        this.jedisPool = jedisPool;
    }

    private String[] loadExcludePath() {
        return new String[]{
                "/static/**",
                "/templates/**",
                "/img/**",
                "/js/**",
                "/css/**",
                "/lib/**"
        };
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    AuthenticationManager authenticationManager(HttpSecurity httpSecurity) throws Exception {
        AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userServiceImpl)
                .passwordEncoder(passwordEncoder())
                .and()
                .build();
        return authenticationManager;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(AuthenticationManager authenticationManager, HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable()
                .authorizeRequests()
                //放通所有静态资源
                .antMatchers(loadExcludePath()).permitAll()
                //放通注册
                .antMatchers(HttpMethod.POST,"/user/add").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/logger/**").hasAnyRole("ADMIN","LOGGER")
                .antMatchers("/user/**").hasAnyRole("ADMIN","LOGGER","USER")
                //其余请求都需要认证后访问
                .anyRequest()
                .authenticated()
                .and()
                .addFilter(new JwtLoginFilter(authenticationManager,rsaKeyProperties, jedisPool))
                .addFilter(new JwtVerifyFilter(authenticationManager, rsaKeyProperties,jedisPool))
                //已认证但是权限不够
                .exceptionHandling().accessDeniedHandler(new UserAuthAccessDeniedHandler())
                .and()
                //未能通过认证,也就是未登录
                .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//禁用session
        return httpSecurity.build();
    }
}

注意事项

  1. 在数据库中的角色名应该为ROLE_ADMIN,ROLE_LOGGER,ROLE_USER。
  2. 本文对密码进行了加盐加密处理,请先对密码加密后再存储到数据库,才能比对成功,加密示例如下
String password = "password";
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encodedPassword = bCryptPasswordEncoder.encode(password);
System.out.println("encodedPassword");
  1. 本文省略了诸多细节,例如Redis、MyBatisPlus等,无法直接使用。
<think>我们正在讨论的是在MyBatis中使用@SelectProvider注解时,如何在对应的mapper.xml文件中配置<cache>标签来优化缓存行为并防止GC风险。虽然@SelectProvider注解通常用于动态SQL,但它并不影响二级缓存的配置方式。二级缓存是Mapper级别的,因此无论SQL是通过XML还是注解(包括@SelectProvider)定义的,只要在Mapper接口上启用了缓存,就可以通过XML中的<cache>标签进行配置。 根据用户的问题,我们需要在mapper.xml中配置<cache>标签,并设置eviction(清除策略)、size(缓存大小)、flushInterval(刷新间隔)和readOnly(只读)等属性,以防止GC风险。 具体步骤: 1. 在mapper.xml文件中添加<cache>标签。注意,即使Mapper接口中使用了注解,我们仍然可以在同一个命名空间的mapper.xml文件中配置缓存。MyBatis会合并同一命名空间的配置。 2. 设置eviction属性为WEAK(弱引用)或SOFT(软引用),这样当内存不足时,垃圾回收器可以回收缓存中的对象,避免内存泄漏。例如:`eviction="WEAK"`。 3. 设置size属性,限制缓存中存储的对象数量。例如:`size="1024"`。 4. 设置flushInterval属性,定义缓存自动刷新的时间间隔(单位:毫秒)。例如,每60秒刷新一次:`flushInterval="60000"`。 5. 设置readOnly属性,根据实际需要选择。如果缓存的数据是只读的(即不会在应用程序中修改),可以设置为`readOnly="true"`,这样MyBatis会直接返回缓存实例,性能更好。但如果数据可能被修改,应设置为`readOnly="false"`,这样MyBatis会返回缓存对象的拷贝,以保证数据安全。 6. 此外,还可以设置其他属性,如`blocking`(是否使用阻塞缓存)等,但根据问题,我们主要关注防止GC风险的配置。 示例配置: ```xml <mapper namespace="com.example.mapper.UserMapper"> <cache eviction="WEAK" size="1000" flushInterval="60000" readOnly="true"/> <!-- 即使使用@SelectProvider,这里也可以配置缓存 --> </mapper> ``` 注意:如果同一个Mapper接口既有注解又有XML配置,MyBatis会合并它们。所以,在XML中配置<cache>标签是有效的。 另外,为了防止GC风险,除了配置弱引用或软引用外,还应该注意: - 避免缓存过大的对象。 - 定期清理缓存(可以通过flushInterval自动刷新,或者在必要时手动调用`clearCache`方法)。 - 监控缓存的使用情况,避免缓存无限增长。 如果使用外部缓存(如Ehcache、Redis等),则可以通过指定<cache>标签的type属性来集成,这样可以利用外部缓存的高级内存管理功能。 例如,使用Ehcache: ```xml <cache type="org.mybatis.caches.ehcache.EhcacheCache"> <!-- 可以在这里配置Ehcache的特定属性 --> <property name="timeToIdleSeconds" value="3600"/> <property name="timeToLiveSeconds" value="3600"/> <!-- 其他配置 --> </cache> ``` 最后,需要确保在mybatis-config.xml中启用了二级缓存(默认是启用的): ```xml <settings> <setting name="cacheEnabled" value="true"/> </settings> ``` 总结:在mapper.xml中配置<cache>标签,设置eviction为WEAK或SOFT,并合理设置size和flushInterval,可以有效地防止GC风险并优化缓存行为。 根据用户提供的引用,我们注意到: - 引用[1]和[3]提到了mapper.xml文件的配置和加载,所以我们需要确保mapper.xml文件被正确配置在mybatis-config.xml中(即<mappers>标签内)[^1][^3]。 - 引用[2]提到了在mapper.xml中配置<cache>标签的eviction属性[^2]。 - 引用[4]展示了mapper.xml中一个普通的select语句的配置,但与我们讨论的缓存配置无关[^4]。 因此,我们的配置方法主要基于引用[2]中提到的eviction属性,并结合其他属性进行设置。 相关问题: 1. 如何在MyBatis中配置多个Mapper命名空间的共享缓存? 2. MyBatis二级缓存使用弱引用(WEAK)和软引用(SOFT)有什么区别? 3. 如何监控MyBatis二级缓存的使用情况和命中率?</think>在使用 MyBatis 的 `@SelectProvider` 注解时,虽然 SQL 是通过 Java 代码动态生成,但仍可通过在对应的 **mapper.xml** 文件中配置 `<cache>` 标签优化缓存行为并防止 GC 风险。以下是完整配置方法: --- ### 步骤 1:在 mapper.xml 中配置缓存属性 在对应的 XML 映射文件中添加 `<cache>` 标签,关键属性配置如下: ```xml <mapper namespace="com.example.mapper.UserMapper"> <!-- 缓存配置:弱引用策略 + 大小限制 + 自动刷新 --> <cache eviction="WEAK" <!-- 使用弱引用,允许GC回收对象 --> size="1000" <!-- 限制最大缓存条目数 --> flushInterval="60000" <!-- 60秒自动刷新缓存(单位:毫秒) --> readOnly="true" <!-- 只读模式避免对象拷贝 --> /> <!-- 即使使用@SelectProvider,此处仍需声明对应方法 --> <select id="selectByCondition" resultType="User"/> </mapper> ``` --- ### 关键配置解析 | **属性** | **值** | **作用** | **防GC原理** | |-------------------|-----------|--------------------------------------------------------------------------|-----------------------------------------------------------------------------| | `eviction` | `WEAK` | 缓存淘汰策略 | 使用弱引用(WeakReference),允许 JVM 在内存不足时直接回收缓存对象[^2] | | `size` | `1000` | 最大缓存对象数量 | 限制缓存膨胀,避免存储过多对象导致 OOM | | `flushInterval` | `60000` | 自动刷新间隔(60秒) | 定期清理缓存,防止长期未使用的对象堆积 | | `readOnly` | `true` | 缓存只读模式 | 避免返回对象的拷贝,减少内存占用(仅适用于不可变对象) | --- ### 步骤 2:确保全局缓存启用 在 **mybatis-config.xml** 中启用二级缓存: ```xml <configuration> <settings> <!-- 必须开启全局缓存 --> <setting name="cacheEnabled" value="true"/> </settings> <!-- 加载包含<cache>的mapper.xml --> <mappers> <mapper resource="com/example/mapper/UserMapper.xml"/> <!-- [^1] --> </mappers> </configuration> ``` --- ### 步骤 3:防止 GC 风险的补充措施 1. **避免大对象缓存** 在 `@SelectProvider` 返回的 SQL 中,只查询必要字段: ```java public String selectUserIds() { return "SELECT id FROM user"; // 只缓存ID而非整个User对象 } ``` 2. **手动清理缓存** 在服务层适时清理: ```java // 注入Mapper @Autowired private UserMapper userMapper; public void clearCache() { userMapper.clearCache(); // 调用MyBatis生成的缓存清理方法 } ``` 3. **集成外部缓存**(如 Ehcache) 修改 `<cache>` 的 `type` 属性: ```xml <cache type="org.mybatis.caches.ehcache.EhcacheCache"> <property name="timeToLiveSeconds" value="3600"/> </cache> ``` --- ### 注意事项 1. **命名空间匹配** `<mapper namespace>` 必须与 `@SelectProvider` 接口的**全限定名**一致[^2]。 2. **事务边界** 二级缓存生效于 SqlSession 提交后,确保在事务提交后再访问缓存。 3. **监控建议** 使用 JVisualVM 监控 `org.apache.ibatis.caching` 包的内存占用,重点关注 `WeakCache` 和 `SoftCache` 对象。 > 📌 **关键点**:`eviction="WEAK"` 是防 GC 的核心配置,它确保缓存对象不被强引用持有,允许 JVM 在需要时回收内存[^2]。 --- ### 完整流程示例 ```mermaid graph TD A[mybatis-config.xml] -->|启用 cacheEnabled=true| B[UserMapper.xml] B -->|配置 <cache eviction='WEAK'>| C[二级缓存实例] D[UserMapper.java] -->|@SelectProvider 定义 SQL| E[动态 SQL 生成] E -->|查询结果| C C -->|弱引用存储| F[JVM 堆内存] G[GC 线程] -->|内存不足时回收| F ``` 通过以上配置,可显著降低 GC 风险,同时保持 `@SelectProvider` 动态 SQL 的优势。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值