Spring Boot集成Spring Security系列六之缓存操作异常

257 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第18天,点击查看活动详情

大家好,我是程序员路飞,三年的下水道打杂前端,喜欢研究技术,正在往全栈发展
欢迎小伙伴们加我微信:DZHningmeng,一起讨论,期待与大家共同成长🥂

前言

这是Spring Boot集成Spring Security的第六章,我将会通过这个系列文章,包括完整的项目搭建、编码过程实现(认证和授权),关键点讲解,来帮助大家了解Spring Security在实际项目中的应用

今天这篇内容主要是AOP,首先简单介绍下AOP

AOPAspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率

在项目中AOP可以用来处理统一抛出的全局异常、接口日志处理、特定业务(Redis等)的异常处理

源码链接

往期系列

Spring Boot集成Spring Security系列一之搭建项目

Spring Boot集成Spring Security系列二之查询用户

Spring Boot集成Spring Security系列三之自定义登录

Spring Boot集成Spring Security系列四之接口鉴权

Spring Boot集成Spring Security系列五之Redis持久化缓存

功能实现

需求

本篇文章一共使用AOP要实现处理缓存操作异常

引入依赖文件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

处理缓存操作异常

在第五篇中我们讲到了用Redis实现用户数据的持久化缓存,但是在实际生产中我们可能要考虑Redis宕机的情况,需要对异常进行统一处理,和每一个调用的service进行功能解耦,所以我们可以使用到AOP,定义一个切面,防止Redis宕机影响正常业务逻辑

但是不是所有的业务都需要处理异常的,有些业务需要正常抛出异常,有些业务比如数据缓存类的就走正常数据库请求,不抛出异常,我们使用自定义注解,在不需要抛出异常的方法使用自定义注解

自定义一个Redis异常处理类

package com.example.springsecurity.common.exception;

import java.lang.annotation.*;

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheException {
}

定义切面对用了CacheException注解进行特殊异常处理

/**
 * Redis缓存切面,
 */


@Aspect
@Component
@Order(2)
public class RedisCacheAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(RedisCacheAspect.class);

    @Pointcut("execution(public * com.example.springsecurity.service.*CacheService.*(..))")
    public void cacheAspect() {};

    @Around("cacheAspect()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        Object res = null;
        try {
            res = joinPoint.proceed();
        } catch (Throwable throwable) {
            // 有CacheException注解的方法需要抛出异常
            if(method.isAnnotationPresent(CacheException.class)) {
                throw throwable;
            } else {
                LOGGER.error(throwable.getMessage());
            }
        }

        return res;
    }

}

我们还需要将获取用户信息的方法单独抽取出来,这样才能对操作Redis的方法用特定的注解

我们定义一个UserCacheServie和对应的实现类。

这边有一个注意点:要把@CacheException应用在对应方法的实现类上,不能放在接口定义上,因为isAnnotationPresent方法只能获取到当前方法上的注解,而不能获取到它实现接口方法上的注解

package com.example.springsecurity.service;

import com.example.springsecurity.entity.User;

public interface UserCacheService {
    User getUser(String username);

    void setUser(User user);
}
package com.example.springsecurity.service.Impl;

import cn.hutool.json.JSONUtil;
import com.example.springsecurity.common.exception.CacheException;
import com.example.springsecurity.entity.User;
import com.example.springsecurity.service.RedisService;
import com.example.springsecurity.service.UserCacheService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class UserCacheServiceImpl implements UserCacheService {

    @Value("${redis.key.prefix.authCode}")
    private String REDIS_KEY_PREFIX_AUTH_CODE;
    @Value("${redis.key.expire.authCode}")
    private Long AUTH_CODE_EXPIRE_SECONDS;

    @Autowired
    private RedisService redisService;

    @CacheException
    @Override
    public User getUser(String username) {
        String key = REDIS_KEY_PREFIX_AUTH_CODE + username;
        String userStr = redisService.get(key);
        User user = null;
        if(userStr != null) {
            user = JSONUtil.toBean(userStr, User.class);
        }
        return user;
    }

    @CacheException
    @Override
    public void setUser(User user) {
        String key = REDIS_KEY_PREFIX_AUTH_CODE + user.getUsername();
        redisService.set(key, JSONUtil.toJsonStr(user));
        redisService.expire(key, AUTH_CODE_EXPIRE_SECONDS);
    }
}

修改UserServiceImpl中的逻辑

@Override
public User queryUserByUsername(String username) {
    User user = null;
    try {
        user = userCacheService.getUser(username);
    } catch (Throwable throwable) {
        LOGGER.error("userCacheService.getUser error: {}", throwable.getMessage());
    }

    if (user == null) {
        user = userMapper.queryUserByName(username);
        try {
            userCacheService.setUser(user);
        } catch (Throwable throwable) {
            LOGGER.error("userCacheService.setUser: {}", throwable.getMessage());
        }
    }

    return user;
}

验证

我们启动项目验证下, 关闭Redis服务

  • 添加注解的情况

image.png

image.png

从截图中我们可以看出,错误捕获到输出到控制台,Redis没有启动,业务也不受影响,这种功能需要应用到Redis出错会影响业务的情况,需要捕获对应的异常

  • 未添加注解的情况

如果我们不需要抛出异常处理,我们把CacheException注解去掉,修改queryUserByUsername中的方法

@Override
public User queryUserByUsername(String username) {
    User user = null;
    user = userCacheService.getUser(username);
    if(user == null) {
        user = userMapper.queryUserByName(username);
        userCacheService.setUser(user);
    }
    return user;
}

image.png

image.png

Redis未开启不影响正常的业务逻辑, 比起上一种需要捕获异常代码更优雅

小结

这篇文章我们结合AOP处理的Redis缓存异常的情况,使程序的功能更具有健壮性,下一篇我们会使用AOP进行全局的业务异常捕获,返回前端。

🚴‍♀️ 结束语

通过项目实践,结合具体的步骤,输出相关的文章,给小伙伴一些有些的知识点,希望大家喜欢我的文章,希望认识到更多志同道合的伙伴,如果你也对技术感兴趣,可以加我好友互相探讨一起进步

Github: Cheering-baby

公众号: 程序员路飞

vx: DZHningmeng

最后,如果喜欢我的文章,可以给个赞👍或者关注➕都是对我最大的支持