Boost Spring Security Performance with Redis Caching and AOP Exception Handling

Learn how to resolve performance bottlenecks in a Spring‑based mall project by caching user and permission data in Redis, adding cache logic with RedisTemplate, and using AOP to gracefully handle Redis failures, ensuring stable authentication even when the cache is unavailable.

macrozheng
macrozheng
macrozheng
Boost Spring Security Performance with Redis Caching and AOP Exception Handling

Prerequisite Knowledge

Familiarity with Spring Data Redis (see "Spring Data Redis Best Practices").

Understanding of Spring AOP (see "Using AOP to Record Interface Access Logs in SpringBoot").

Problem Reproduction

In the mall-security module a filter authenticates users via a token. The filter loads user details from the database on every request, causing repeated DB queries and performance degradation.

/**
 * JWT login authentication filter
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String authHeader = request.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            LOGGER.info("checking username:{}", username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                // This line queries the database for user info
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    LOGGER.info("authenticated user:{}", username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

Each request triggers SQL statements similar to the following, confirming the DB hit:

2020-03-17 16:13:02.623 DEBUG  c.m.m.m.UmsAdminMapper.selectByExample   : ==>  Preparing: select ... from ums_admin WHERE ( username = ? )
2020-03-17 16:13:02.624 DEBUG  c.m.m.m.UmsAdminMapper.selectByExample   : ==>  Parameters: admin(String)
2020-03-17 16:13:02.625 DEBUG  c.m.m.m.UmsAdminMapper.selectByExample   : <==      Total: 1
... (additional queries for role and menu data) ...

Using Redis as Cache

Store user and permission data in Redis to avoid frequent DB queries.

The loadUserByUsername method performs two DB calls: one for user info and another for resource list. We add caching for both using RedisTemplate:

@Service
public class UmsAdminServiceImpl implements UmsAdminService {
    @Autowired
    private UmsAdminCacheService adminCacheService;

    @Override
    public UmsAdmin getAdminByUsername(String username) {
        // Try cache first
        UmsAdmin admin = adminCacheService.getAdmin(username);
        if (admin != null) return admin;
        // Fallback to DB
        UmsAdminExample example = new UmsAdminExample();
        example.createCriteria().andUsernameEqualTo(username);
        List<UmsAdmin> adminList = adminMapper.selectByExample(example);
        if (adminList != null && !adminList.isEmpty()) {
            admin = adminList.get(0);
            adminCacheService.setAdmin(admin);
            return admin;
        }
        return null;
    }

    @Override
    public List<UmsResource> getResourceList(Long adminId) {
        List<UmsResource> resourceList = adminCacheService.getResourceList(adminId);
        if (CollUtil.isNotEmpty(resourceList)) return resourceList;
        resourceList = adminRoleRelationDao.getResourceList(adminId);
        if (CollUtil.isNotEmpty(resourceList)) {
            adminCacheService.setResourceList(adminId, resourceList);
        }
        return resourceList;
    }
}

The cache service interface defines methods for retrieving, storing, and invalidating cached user and resource data:

public interface UmsAdminCacheService {
    void delAdmin(Long adminId);
    void delResourceList(Long adminId);
    void delResourceListByRole(Long roleId);
    void delResourceListByRoleIds(List<Long> roleIds);
    void delResourceListByResource(Long resourceId);
    // plus get/set methods used above (omitted for brevity)
}

After adding these caches, the performance issue disappears. However, a new problem arises: if Redis goes down, authentication fails.

Handling Cache Exceptions with AOP

Use AOP to wrap all cache‑service methods with a try‑catch, so Redis outages do not break business logic.

Define an aspect that intercepts all public methods of classes ending with CacheService:

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

    @Pointcut("execution(public * com.macro.mall.portal.service.*CacheService.*(..)) || execution(public * com.macro.mall.service.*CacheService.*(..))")
    public void cacheAspect() {}

    @Around("cacheAspect()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            LOGGER.error(throwable.getMessage());
        }
        return result;
    }
}

Some cache operations (e.g., verification code storage) must still fail loudly. Create a custom annotation @CacheException and modify the aspect to re‑throw exceptions for methods annotated with it:

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheException {}
@Aspect
@Component
@Order(2)
public class RedisCacheAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(RedisCacheAspect.class);

    @Pointcut("execution(public * com.macro.mall.portal.service.*CacheService.*(..)) || execution(public * com.macro.mall.service.*CacheService.*(..))")
    public void cacheAspect() {}

    @Around("cacheAspect()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            if (method.isAnnotationPresent(CacheException.class)) {
                throw throwable;
            } else {
                LOGGER.error(throwable.getMessage());
            }
        }
        return result;
    }
}

Apply @CacheException to the verification‑code cache methods so that failures are propagated:

@Service
public class UmsMemberCacheServiceImpl implements UmsMemberCacheService {
    @Autowired
    private RedisService redisService;

    @CacheException
    @Override
    public void setAuthCode(String telephone, String authCode) {
        String key = REDIS_DATABASE + ":" + REDIS_KEY_AUTH_CODE + ":" + telephone;
        redisService.set(key, authCode, REDIS_EXPIRE_AUTH_CODE);
    }

    @CacheException
    @Override
    public String getAuthCode(String telephone) {
        String key = REDIS_DATABASE + ":" + REDIS_KEY_AUTH_CODE + ":" + telephone;
        return (String) redisService.get(key);
    }
}

Conclusion

Frequent database queries that hurt performance can be mitigated by caching user and permission data in Redis. To keep the application resilient, use AOP to centralize exception handling for cache operations, allowing normal business logic to continue even when Redis is unavailable.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Performance OptimizationaopBackend DevelopmentrediscachingSpring Boot
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.