Overview
Aspect-Oriented Programming enables modular handling of cross-cutting concerns. This guide demonstrates implementing automatic encryption and decryption for controller layer data using Spring Boot AOP, allowing business logic to remain clean while security concerns are handled transparently.
Core Concepts
AOP terminology includes:
- Aspect: A module encapsulating cross-cutting behavior (e.g., security, logging)
- Join Point: Specific execution points where aspects apply (method invocasions, exceptions)
- Advice: Actions executed at particular join points, categorized as:
- @Before: Executes before method invocation
- @After: Executes after method completion (regardless of outcome)
- @AfterReturning: Executes after successful method return
- @AfterThrowing: Executes when methods throw exceptions
- @Around: Wraps method execution, controlling invocation timing
- Pointcut: Expressions defining which join points trigger advice
Environment Configuration
Maven Dependencies
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.2.6.RELEASE</spring-boot.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Application Properties
server.port=8080
spring.application.name=aop-encryption-demo
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
Implementation
Data Encryption Aspect
Create an aspect class to intercept controller methods:
@Aspect
@Component
public class DataSecurityAspect {
@Pointcut("execution(public * com.example.app.api.*.*(..))")
public void controllerMethods() {}
@Before("controllerMethods()")
public void decryptRequestPayload(JoinPoint joinPoint) {
Arrays.stream(joinPoint.getArgs())
.filter(Account.class::isInstance)
.map(Account.class::cast)
.forEach(account -> {
try {
String decryptedUsername = decodeBase64(account.getUsername());
account.setUsername(decryptedUsername);
} catch (Exception ex) {
throw new RuntimeException("Decryption failed", ex);
}
});
}
@AfterReturning(pointcut = "controllerMethods()", returning = "response")
public void encryptResponsePayload(Object response) {
if (response instanceof ApiResponse) {
ApiResponse apiResponse = (ApiResponse) response;
try {
String encryptedData = encodeBase64(apiResponse.getPayload().toString());
apiResponse.setPayload(encryptedData);
} catch (Exception ex) {
throw new RuntimeException("Encryption failed", ex);
}
}
}
private String encodeBase64(String input) throws UnsupportedEncodingException {
return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8));
}
private String decodeBase64(String input) throws UnsupportedEncodingException {
return new String(Base64.getDecoder().decode(input), StandardCharsets.UTF_8);
}
}
Entity Class
public class Account {
private Long userId;
private String username;
private String email;
// Constructors, getters and setters omitted
}
Controller Layer
@RestController
@RequestMapping("/v1/accounts")
public class AccountController {
@GetMapping("/search")
public ApiResponse queryAccount(Account account) {
System.out.println("Processing request for: " + account.getUsername());
List<Account> results = new ArrayList<>();
Account sample = new Account();
sample.setUserId(1001L);
sample.setUsername("plaintextuser");
sample.setEmail("demo@example.com");
results.add(sample);
ApiResponse response = new ApiResponse();
response.setStatus("SUCCESS");
response.setPayload(results);
return response;
}
}
Response Wrapper
public class ApiResponse {
private String status;
private Object payload;
// Constructors, getters and setters omitted
}
Application Entry Point
@SpringBootApplication
public class AopEncryptionApplication {
public static void main(String[] args) {
SpringApplication.run(AopEncryptionApplication.class, args);
}
}
Testing
Start the application and test with a Base64-encoded username parameter:
Request:
http://localhost:8080/v1/accounts/search?username=dGVzdHVzZXI=
Console Output:
Decrypting request parameter: testuser
Processing request for: testuser
Encrypting response payload: [{...}]
Response:
{
"status": "SUCCESS",
"payload": "W3sidXNlcklkIjoxMDAxLCJ1c2VybmFtZSI6InBsYWludGV4dHVzZXIiLCJlbWFpbCI6ImRlbW9AZXhhbXBsZS5jb20ifV0="
}
The aspect automatically decrypts incoming parameters and encrypts outgoing responses, demonstrating transparent security handling without polluting business logic.