A Faster Alternative to AOP: Spring Boot’s Elegant Annotation‑Based Permission Control

The article explains how traditional AOP‑based permission checks in Spring MVC cause performance overhead and proposes a custom annotation‑driven solution that integrates with HandlerMapping to perform fine‑grained, configurable access control efficiently.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
A Faster Alternative to AOP: Spring Boot’s Elegant Annotation‑Based Permission Control

In enterprise Spring MVC backend interfaces, permission validation is commonly implemented with AOP, which adds performance cost and separates the validation logic from the request‑mapping flow.

To address this, the author designs a custom annotation‑based permission control that leverages Spring MVC’s native HandlerMapping mechanism, performing checks during the route‑matching stage and thus avoiding AOP overhead. The solution combines a custom annotation, SpEL expressions, and configurable permission codes for precise, unified control.

Permission annotation definition

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
    // Permission codes (required), e.g., "sys:user:list"
    String[] value() default {};
    // Skip permission check (public API, login‑only, admin bypass)
    boolean ignore() default false;
    // Description for API docs or admin UI
    String description() default "";
    // Module name for grouping, e.g., "User Management"
    String module() default "";
    // Highest‑priority SpEL expression for dynamic checks
    String expression() default "";
}

The annotation can be placed on controller methods to declare required permissions, optional module grouping, descriptive text, and an optional SpEL expression.

Custom HandlerMapping

public class PermissionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        Permission annotation = method.getAnnotation(Permission.class);
        if (annotation == null) {
            return null;
        }
        if (annotation.ignore()) {
            return null;
        }
        return new PermissionRequestCondition(annotation, getApplicationContext());
    }
}

This class overrides getCustomMethodCondition to return a custom PermissionRequestCondition when the @Permission annotation is present and not ignored.

Permission request condition implementation

public class PermissionRequestCondition implements RequestCondition<PermissionRequestCondition> {
    private static final BeanExpressionResolver beanExpressionResolver = new StandardBeanExpressionResolver();
    private final Permission permission;
    private final ApplicationContext context;

    public PermissionRequestCondition(Permission permission, ApplicationContext context) {
        this.permission = permission;
        this.context = context;
    }

    @Override
    public PermissionRequestCondition combine(PermissionRequestCondition other) {
        return this;
    }

    @Override
    public PermissionRequestCondition getMatchingCondition(HttpServletRequest request) {
        String expression = permission.expression();
        if (StringUtils.hasLength(expression)) {
            ConfigurableBeanFactory beanFactory = (ConfigurableBeanFactory) context.getAutowireCapableBeanFactory();
            Object value = beanExpressionResolver.evaluate(expression, new BeanExpressionContext(beanFactory, null));
            Boolean result = beanFactory.getTypeConverter().convertIfNecessary(value, Boolean.class);
            return result ? this : null;
        }
        String[] permissions = permission.value();
        return PermissionContext.hasPermission(Set.of(permissions)) ? this : null;
    }

    @Override
    public int compareTo(PermissionRequestCondition other, HttpServletRequest request) {
        return 0;
    }
}

The condition first evaluates the optional SpEL expression; if it yields true, the request matches. Otherwise, it checks whether the current user possesses any of the declared permission codes via PermissionContext.

Registration of the custom HandlerMapping

@Component
public class WebMvcConfig implements WebMvcRegistrations {
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new PermissionRequestMappingHandlerMapping();
    }
}

Implementing WebMvcRegistrations allows Spring to use the custom mapping class throughout the application.

Permission constants and context utilities

public final class PermissionConstants {
    // User module
    public static final String SYS_USER_LIST = "sys:user:list";
    public static final String SYS_USER_ADD = "sys:user:save";
    public static final String SYS_USER_EDIT = "sys:user:update";
    public static final String SYS_USER_DELETE = "sys:user:remove";
    public static final String SYS_USER_RESET_PWD = "sys:user:resetPwd";
    // ... other constants
}

public final class PermissionContext {
    private PermissionContext() {}
    public static Set<String> getCurrentUserPermissions() {
        return Set.of(
            PermissionConstants.SYS_USER_LIST,
            PermissionConstants.SYS_USER_ADD,
            PermissionConstants.SYS_ROLE_LIST,
            PermissionConstants.SYS_MENU_LIST,
            PermissionConstants.SYS_LOG_QUERY
        );
    }
    public static boolean hasPermission(Set<String> permissions) {
        if (permissions == null || permissions.isEmpty()) {
            return true;
        }
        return permissions.stream().anyMatch(getCurrentUserPermissions()::contains);
    }
}

These classes provide a static list of permission codes and a helper method to check whether the current user (simulated here) holds any of the required permissions.

Test controller demonstrating usage

@RestController
@RequestMapping("/api")
public class ApiController {
    @GetMapping("/list")
    @Permission(value = { PermissionConstants.SYS_USER_LIST }, module = "用户管理", description = "查询用户列表")
    public String list() {
        return "用户列表查询接口";
    }

    @GetMapping("/create")
    @Permission(module = "用户管理", description = "创建用户", expression = "#{@ps.hasPerm('sys:user:save')}")
    public String create() {
        return "创建用户接口";
    }

    @GetMapping("/public")
    @Permission(ignore = true, description = "公开接口,无需权限")
    public String publicApi() {
        return "公开接口";
    }
}

The /list endpoint checks a static permission code, /create uses a SpEL expression that calls PermissionService, and /public is marked to bypass permission checks.

Permission service used by SpEL

@Service("ps")
public class PermissionService {
    public boolean hasPerm(String permission) {
        return PermissionConstants.SYS_USER_ADD.equals(permission);
    }
}

When the SpEL expression evaluates, it invokes hasPerm to decide access.

Verification results

After deploying the application, the following screenshots illustrate the behavior:

Permission check result for /list
Permission check result for /list

Removing PermissionConstants.SYS_USER_LIST from PermissionContext causes the /list request to be denied, as shown below:

Denied after removing permission
Denied after removing permission

Testing the /create endpoint demonstrates that the SpEL‑based check works correctly:

SpEL expression evaluation result
SpEL expression evaluation result

Overall, the custom annotation‑driven approach eliminates the performance penalty of AOP, keeps permission logic close to the routing layer, and offers flexible, fine‑grained control through static codes, module metadata, and dynamic SpEL expressions.

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.

javapermissionspring-bootSecurityannotationaop-alternativehandler-mapping
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

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.