introduction
Oriented Programming (AOP) is one of the core functions of the Spring framework. It realizes unified maintenance of program functions through precompilation and run-time dynamic agents. In SpringBoot applications, AOP can help us elegantly solve cross-cutting concern issues, such as logging, permission control, performance monitoring, etc. These functions often run through the entire application but do not belong to the core logic of the business.
This article will introduce four practical AOP application scenarios in SpringBoot, including code implementation, core principles and practice.
Scenario 1: Logging and performance monitoring
Business Requirements
In enterprise-level applications, we usually need:
- Record the calling status of API requests
- Monitor the execution time of the method and discover performance bottlenecks
- Tracking the incoming parameters and return results of method calls
Implementation plan
@Aspect @Component @Slf4j public class LoggingAspect { /** * Define point cut: all methods under all controller packages */ @Pointcut("execution(* .*.*(..))") public void controllerMethods() {} /** * Surround notification: log request log and execution time */ @Around("controllerMethods()") public Object logAroundControllers(ProceedingJoinPoint joinPoint) throws Throwable { // Get the method signature MethodSignature signature = (MethodSignature) (); String methodName = (); String className = (); // Record request parameters String params = (()); ("Request to {}.{} with params: {}", className, methodName, params); // Record the start time long startTime = (); // Execute the target method Object result; try { result = (); // Calculate execution time long executionTime = () - startTime; // Record the return result and execution time ("Response from {}.{} ({}ms): {}", className, methodName, executionTime, result); // Slow recording method if (executionTime > 1000) { ("Slow execution detected! {}.{} took {}ms", className, methodName, executionTime); } return result; } catch (Exception e) { // Record exception information ("Exception in {}.{}: {}", className, methodName, (), e); throw e; } } /** * Define service layer method cutting point */ @Pointcut("execution(* .*.*(..))") public void serviceMethods() {} /** * Record key calls to the service layer method */ @Before("serviceMethods() && @annotation(logMethod)") public void logServiceMethod(JoinPoint joinPoint, LogMethod logMethod) { MethodSignature signature = (MethodSignature) (); String methodName = (); // Get the parameter name String[] paramNames = (); Object[] args = (); StringBuilder logMessage = new StringBuilder(); ("Executing ").append(methodName).append(" with params: {"); for (int i = 0; i < ; i++) { (paramNames[i]).append("=").append(args[i]); if (i < - 1) { (", "); } } ("}"); // Record logs according to the level set by the annotation switch (()) { case DEBUG: (()); break; case INFO: (()); break; case WARN: (()); break; case ERROR: (()); break; } } } /** * Custom log annotations */ @Retention() @Target() public @interface LogMethod { LogLevel level() default ; public enum LogLevel { DEBUG, INFO, WARN, ERROR } }
Example of usage
@Service public class UserService { @LogMethod(level = ) public User findById(Long id) { // Business logic return (id).orElse(null); } @LogMethod(level = ) public void updateUserStatus(Long userId, String status) { // Update user status } }
Extension: MDC implements request tracking
@Component public class RequestIdFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { // Generate a unique ID for each request String requestId = ().toString().replace("-", ""); ("requestId", requestId); if (request instanceof HttpServletRequest) { HttpServletRequest httpRequest = (HttpServletRequest) request; // Record user information Authentication auth = ().getAuthentication(); if (auth != null && ()) { ("userId", ()); } ("remoteAddr", ()); } (request, response); } finally { // Clean up the MDC after the request is completed (); } } }
Scenario 2: Permission control and security enhancement
Business Requirements
In enterprise applications, permission control is a common requirement:
- Role-based interface access control
- Fine-grained operation permission control
- Recording of sensitive data access
Implementation plan
First, create a custom annotation:
@Retention() @Target({, }) public @interface RequiresPermission { /** * The required permission code array can satisfy any of them. */ String[] value() default {}; /** * Permission logic type: AND (have all permissions at the same time), OR (just satisfy any permissions) */ LogicalType logical() default ; public enum LogicalType { AND, OR } }
Implement permissions section:
@Aspect @Component @Slf4j public class PermissionAspect { @Autowired private UserService userService; /** * Definition point cut: All methods with @RequiresPermission annotation */ @Pointcut("@annotation()") public void permissionCheck() {} /** * Permission verification pre-notification */ @Before("permissionCheck() && @annotation(requiresPermission)") public void checkPermission(JoinPoint joinPoint, RequiresPermission requiresPermission) { // Get the current user User currentUser = getCurrentUser(); if (currentUser == null) { throw new UnauthorizedException("User not logged in or session expired"); } // Get the user permission list Set<String> userPermissions = (()); // Get permissions required in the annotation String[] requiredPermissions = (); logicalType = (); // Permission verification boolean hasPermission = false; if (logicalType == ) { // Just satisfy any permissions for (String permission : requiredPermissions) { if ((permission)) { hasPermission = true; break; } } } else { // All permissions must be met at the same time hasPermission = true; for (String permission : requiredPermissions) { if (!(permission)) { hasPermission = false; break; } } } if (!hasPermission) { ("user {} Try to access unauthorized resources: {}.{}", (), ().getDeclaringTypeName(), ().getName()); throw new ForbiddenException("Insufficient permissions, unable to perform this operation"); } // Record sensitive operations ("user {} Perform an authorized operation: {}.{}", (), ().getDeclaringTypeName(), ().getName()); } /** * Definition point cut: Method with @RequiresRole annotation */ @Pointcut("@annotation()") public void roleCheck() {} /** * Role check pre-notification */ @Before("roleCheck() && @annotation(requiresRole)") public void checkRole(JoinPoint joinPoint, RequiresRole requiresRole) { // Get the current user User currentUser = getCurrentUser(); if (currentUser == null) { throw new UnauthorizedException("User not logged in or session expired"); } // Get user role Set<String> userRoles = (()); // Get the role required in the annotation String[] requiredRoles = (); // Role verification boolean hasRole = false; for (String role : requiredRoles) { if ((role)) { hasRole = true; break; } } if (!hasRole) { ("user {} Try to access unauthorized role resources: {}.{}", (), ().getDeclaringTypeName(), ().getName()); throw new ForbiddenException("Insufficient roles, unable to perform this operation"); } } /** * Data permission filtering point: for query method */ @Pointcut("execution(* .*.find*(..))") public void dataPermissionFilter() {} /** * Data permission filtering notification */ @Around("dataPermissionFilter()") public Object filterDataByPermission(ProceedingJoinPoint joinPoint) throws Throwable { // Get the current user User currentUser = getCurrentUser(); // The original method is executed by default Object result = (); // If you are an administrator, there is no need to filter data if ((())) { return result; } // Filter the query results if (result instanceof Collection) { Collection<?> collection = (Collection<?>) result; // Implement data filtering logic... } else if (result instanceof Page) { Page<?> page = (Page<?>) result; // Implement paging data filtering... } return result; } /** * Get the current logged in user */ private User getCurrentUser() { Authentication authentication = ().getAuthentication(); if (authentication == null || !()) { return null; } Object principal = (); if (principal instanceof User) { return (User) principal; } return null; } } /** * Custom role notes */ @Retention() @Target({, }) public @interface RequiresRole { String[] value(); }
Example of usage
@RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping @RequiresPermission("user:list") public List<User> listUsers() { return (); } @GetMapping("/{id}") @RequiresPermission("user:view") public User getUser(@PathVariable Long id) { return (id); } @PostMapping @RequiresPermission(value = {"user:create", "user:edit"}, logical = ) public User createUser(@RequestBody User user) { return (user); } @DeleteMapping("/{id}") @RequiresRole("ADMIN") public void deleteUser(@PathVariable Long id) { (id); } @PutMapping("/{id}/status") @RequiresPermission(value = {"user:edit", "user:manage"}, logical = ) public User updateUserStatus(@PathVariable Long id, @RequestParam String status) { return (id, status); } }
Scenario 3: Custom cache implementation
Business Requirements
Caching is a key means to improve application performance, which can be achieved through AOP:
- Custom cache policies to meet specific business needs
- Fine-grained cache control
- Flexible cache key generation and expiration strategies
Implementation plan
First define the cache annotation:
@Retention() @Target() public @interface Cacheable { /** * Cache name */ String cacheName(); /** * Cache key expression, supports SpEL expression */ String key() default ""; /** * Expiration time (seconds) */ long expireTime() default 300; /** * Whether to use method parameters as part of the cache key */ boolean useMethodParameters() default true; } @Retention() @Target() public @interface CacheEvict { /** * Cache name */ String cacheName(); /** * cache key expression */ String key() default ""; /** * Whether to clear all caches */ boolean allEntries() default false; }
Implement cache sections:
@Aspect @Component @Slf4j public class CacheAspect { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private CacheKeyGenerator keyGenerator; /** * Define cache fetch point cut */ @Pointcut("@annotation()") public void cacheableOperation() {} /** * Define cache clear cut points */ @Pointcut("@annotation()") public void cacheEvictOperation() {} /** * Cache surround notifications */ @Around("cacheableOperation() && @annotation(cacheable)") public Object handleCacheable(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable { // Generate cache key String cacheKey = generateCacheKey(joinPoint, (), (), ()); // Check whether there is data in the cache Boolean hasKey = (cacheKey); if ((hasKey)) { Object cachedValue = ().get(cacheKey); ("Cache hit for key: {}", cacheKey); return cachedValue; } // Cache misses, execute the method to get the result ("Cache miss for key: {}", cacheKey); Object result = (); // Save the result in cache if (result != null) { ().set(cacheKey, result, (), ); ("Stored in cache with key: {}, expire time: {}s", cacheKey, ()); } return result; } /** * Cache clear pre-notification */ @Before("cacheEvictOperation() && @annotation(cacheEvict)") public void handleCacheEvict(JoinPoint joinPoint, CacheEvict cacheEvict) { if (()) { // Clear all entries under this cache name String cachePattern = () + ":*"; Set<String> keys = (cachePattern); if (keys != null && !()) { (keys); ("Cleared all cache entries with pattern: {}", cachePattern); } } else { // Clear the cache of the specified key String cacheKey = generateCacheKey(joinPoint, (), (), true); (cacheKey); ("Cleared cache with key: {}", cacheKey); } } /** * Generate cache key */ private String generateCacheKey(JoinPoint joinPoint, String cacheName, String keyExpression, boolean useParams) { StringBuilder keyBuilder = new StringBuilder(cacheName).append(":"); // If a custom key expression is provided if ((keyExpression)) { String evaluatedKey = (keyExpression, joinPoint); (evaluatedKey); } else if (useParams) { //Use method signature and parameters as keys MethodSignature signature = (MethodSignature) (); String methodName = (); (methodName); // Add parameters Object[] args = (); if (args != null && > 0) { for (Object arg : args) { if (arg != null) { (":").append(()); } else { (":null"); } } } } else { // Use only method names (().getName()); } return (); } } /** * Cache key generator, supports SpEL expressions */ @Component public class CacheKeyGenerator { private final ExpressionParser parser = new SpelExpressionParser(); private final StandardEvaluationContext context = new StandardEvaluationContext(); public String generateKey(String expression, JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) (); Method method = (); Object[] args = (); String[] parameterNames = (); // Set method parameters to context variables for (int i = 0; i < ; i++) { (parameterNames[i], args[i]); } // Add extra metadata ("method", ()); ("class", ().getSimpleName()); ("target", ()); // Execute expression Expression exp = (expression); return (context, ); } }
Redis configuration
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); (connectionFactory); // Serialize values using Jackson2JsonRedisSerializer Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(); ObjectMapper om = new ObjectMapper(); (, ); (, .NON_FINAL); (om); // Set the serialization method of keys to strings (new StringRedisSerializer()); // Values are serialized using JSON (jackson2JsonRedisSerializer); // Hash key also uses strings (new StringRedisSerializer()); // Hash values are serialized using JSON (jackson2JsonRedisSerializer); (); return template; } }
Example of usage
@Service public class ProductService { @Autowired private ProductRepository productRepository; @Cacheable(cacheName = "products", expireTime = 3600) public Product getById(Long id) { return (id).orElse(null); } @Cacheable(cacheName = "products", key = "'list:category:' + #categoryId", expireTime = 1800) public List<Product> getByCategory(Long categoryId) { return (categoryId); } @CacheEvict(cacheName = "products", allEntries = true) public Product save(Product product) { return (product); } @CacheEvict(cacheName = "products", key = "'list:category:' + #") public void deleteProductFromCategory(Product product) { (product); } }
Scenario 4: Unified exception handling and retry mechanism
Business Requirements
In distributed systems or complex business scenarios, we often need:
- Handle exceptions gracefully
- Automatically retry certain operations
- Idepotency assurance of key operations
Implementation plan
First define retry and exception handling annotations:
@Retention() @Target() public @interface Retryable { /** * Maximum number of retry */ int maxAttempts() default 3; /** * Retry interval (milliseconds) */ long backoff() default 1000; /** * Specify the captured exception type */ Class<? extends Throwable>[] value() default {}; /** * Retry the policy */ RetryStrategy strategy() default ; /** * Retry the policy enumeration */ enum RetryStrategy { /** * Fixed interval */ FIXED, /** * Exponential backoff */ EXPONENTIAL } } @Retention() @Target() public @interface Idempotent { /** * Idepotential key expression */ String key(); /** * Expiration time (seconds) */ long expireSeconds() default 300; }
Implement exception handling and retrying the section:
@Aspect @Component @Slf4j public class RetryAspect { @Autowired private RedisTemplate<String, String> redisTemplate; /** * Define the retry operation point cut */ @Pointcut("@annotation()") public void retryableOperation() {} /** * Define idempotent operation cut point */ @Pointcut("@annotation()") public void idempotentOperation() {} /** * Retry the surround notification */ @Around("retryableOperation() && @annotation(retryable)") public Object handleRetry(ProceedingJoinPoint joinPoint, Retryable retryable) throws Throwable { int attempts = 0; Class<? extends Throwable>[] retryableExceptions = (); strategy = (); while (true) { attempts++; try { // Execute the target method return (); } catch (Throwable t) { // Check whether it is an exception type that needs to be retryed boolean shouldRetry = false; for (Class<? extends Throwable> exceptionType : retryableExceptions) { if ((t)) { shouldRetry = true; break; } } // If no retry is required, or the maximum number of retryes is reached, an exception is thrown if (!shouldRetry || attempts >= ()) { ("Method {} failed after {} attempts: {}", ().getName(), attempts, ()); throw t; } // Calculate the retry waiting time long waitTime; if (strategy == ) { // Exponential backoff: Basic time * 2^(Number of attempts-1) waitTime = () * (long) (2, attempts - 1); } else { // Fixed interval waitTime = (); } ("Retrying {} (attempt {}/{}) after {} ms due to: {}", ().getName(), attempts, (), waitTime, ()); // Try again after waiting for the specified time (waitTime); } } } /** * Idepotency Surround Notification */ @Around("idempotentOperation() && @annotation(idempotent)") public Object handleIdempotent(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { // parse idempotent keys String idempotentKey = resolveIdempotentKey(joinPoint, ()); String lockKey = "idempotent:" + idempotentKey; // Try setting up a distributed lock Boolean success = ().setIfAbsent( lockKey, "PROCESSING", (), ); if ((success)) { try { // Acquisition lock successfully, execute business logic Object result = (); // Save the result to Redis String resultKey = "result:" + lockKey; ().set( resultKey, new ObjectMapper().writeValueAsString(result), (), ); // Mark as processed ().set( lockKey, "COMPLETED", (), ); return result; } catch (Throwable t) { // Processing failed, marking error ().set( lockKey, "ERROR:" + (), (), ); throw t; } } else { // Acquisition of lock failed, indicating that the operation is being processed or has been processed String status = ().get(lockKey); if ("PROCESSING".equals(status)) { // Still under processing throw new ConcurrentOperationException("The operation is being processed, please do not repeat submission"); } else if (status != null && ("ERROR:")) { // An error occurred before processing throw new OperationFailedException("Operation processing failed: " + (6)); } else if ("COMPLETED".equals(status)) { // Completed, try to return the previous result String resultKey = "result:" + lockKey; String resultJson = ().get(resultKey); if (resultJson != null) { // Deserialize JSON into a response object Method method = ((MethodSignature) ()).getMethod(); Class<?> returnType = (); try { return new ObjectMapper().readValue(resultJson, returnType); } catch (Exception e) { ("Failed to deserialize cached result: {}", ()); } } // If the result is not found or the deserialization fails, the message that was successful but the last result cannot be provided throw new OperationAlreadyCompletedException("The operation has been processed successfully, but the result of the last operation cannot be provided"); } // The status is unknown, throw an exception throw new OperationFailedException("Operation status unknown"); } } /** * parse idempotent key expressions */ private String resolveIdempotentKey(JoinPoint joinPoint, String keyExpression) { MethodSignature signature = (MethodSignature) (); String[] paramNames = (); Object[] args = (); // Create expression context StandardEvaluationContext context = new StandardEvaluationContext(); // Add method parameters for (int i = 0; i < ; i++) { (paramNames[i], args[i]); } // Add class and method names ("method", ().getName()); ("class", ().getSimpleName()); // parse expression ExpressionParser parser = new SpelExpressionParser(); Expression expression = (keyExpression); return (context, ); } } // Custom exception classpublic class ConcurrentOperationException extends RuntimeException { public ConcurrentOperationException(String message) { super(message); } } public class OperationFailedException extends RuntimeException { public OperationFailedException(String message) { super(message); } } public class OperationAlreadyCompletedException extends RuntimeException { public OperationAlreadyCompletedException(String message) { super(message); } }
Example of usage
@Service public class PaymentService { @Autowired private PaymentGateway paymentGateway; @Autowired private OrderRepository orderRepository; /** * Remote payment processing, you may encounter network problems and need to try again */ @Retryable( value = {, , }, maxAttempts = 3, backoff = 2000, strategy = ) public PaymentResult processPayment(String orderId, BigDecimal amount) { ("Processing payment for order {} with amount {}", orderId, amount); // Call remote payment gateway return (orderId, amount); } /** * Order refunds are required to ensure idempotence */ @Idempotent(key = "'refund:' + #orderId", expireSeconds = 3600) public RefundResult refundOrder(String orderId) { Order order = (orderId) .orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId)); // Verify order status if (!"PAID".equals(())) { throw new InvalidOrderStatusException("Cannot refund order with status: " + ()); } // Call the payment gateway to refund RefundResult result = ((), ()); // Update order status ("REFUNDED"); (()); (order); return result; } } @Service public class StockService { @Autowired private StockRepository stockRepository; /** * Deduct inventory, retry and idempotence are required in a distributed environment */ @Retryable( value = {, }, maxAttempts = 5, backoff = 500 ) @Idempotent(key = "'deduct:' + #orderId") public void deductStock(String orderId, List<OrderItem> items) { // Check whether there is an inventory record for (OrderItem item : items) { Stock stock = (()); if (stock == null) { throw new ProductNotFoundException("Product not found: " + ()); } if (() < ()) { throw new StockInsufficientException( "Insufficient stock for product: " + () + ", requested: " + () + ", available: " + ()); } } // Implement inventory deductions for (OrderItem item : items) { ((), ()); } } }
in conclusion
AOP is a powerful programming paradigm in SpringBoot. Through these modes, we can decouple cross-cutting concerns from business logic, making the code more modular and maintainable, while improving the robustness and security of the system.
The above is the detailed content of the four AOP practical application scenarios and code implementations in SpringBoot. For more information about SpringBoot AOP application scenarios, please pay attention to my other related articles!