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.
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:
Removing PermissionConstants.SYS_USER_LIST from PermissionContext causes the /list request to be denied, as shown below:
Testing the /create endpoint demonstrates that the SpEL‑based check works correctly:
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
