1. Preparation
First, a simple springboot project is created here:
The contents of each class are as follows:
@Data @AllArgsConstructor @NoArgsConstructor public class User { private Integer id; private String name; }
@Component public class UserDao { public User findUserById(Integer id) { if(id > 10) { return null; } return new User(id, "user-" + id); } }
@Service public class UserService { private final UserDao userDao; public UserService(UserDao userDao) { this.userDao = userDao; } public User findUserById(Integer id) { return userDao.findUserById(id); } }
@RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @RequestMapping("user/{id}") public User findUser(@PathVariable("id") Integer id) { return userService.findUserById(id); } }
2. Use annotations to perform fixed operations
Now we have such a simple web project. After directly accessing localhost:8080/user/6, we will obviously get the following json string
{ "id": 6, "name": "user-6" }
However, we are not satisfied with this. This project is too simple. Now let's add a log function to it (not to mention using log frameworks such as log4j, our purpose is to learn custom annotations)
Suppose our goal now is to output a sentence on the console before calling the findUser method in the controller. OK, let's start. Let's create an annotation package, which creates our custom annotation class KthLog:
package com.example.demo.annotation; import java.lang.annotation.*; @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface KthLog { String value() default ""; }
Here, the three annotations on the annotation class are called meta annotations, which respectively represent the following meanings:
- @Documented: annotation information is added to the Java document
- @Retention: the life cycle of annotations, which indicates the stage to which annotations will be retained. You can choose compilation stage, class loading stage, or running stage
- @Target: the location of the annotation, ElementType Method indicates that the annotation can only act on methods
Then we can add annotations to the method:
@KthLog("This is the log content") @RequestMapping("user/{id}") public User findUser(@PathVariable("id") Integer id) { return userService.findUserById(id); }
This annotation has no effect at present, because we only declare the annotation and do not use it anywhere. The essence of annotation is also a generalized syntax sugar. Finally, we should use Java reflection to operate
However, Java provides us with an AOP mechanism that can dynamically extend classes or methods. For more in-depth understanding of this mechanism, please refer to my article: Interpreting Spring AOP from source code
We create facet classes as follows:
@Component @Aspect public class KthLogAspect { @Pointcut("@annotation(com.example.demo.annotation.KthLog)") private void pointcut() {} @Before("pointcut() && @annotation(logger)") public void advice(KthLog logger) { System.out.println("--- Kth The content of the log is[" + logger.value() + "] ---"); } }
Where @ Pointcut declares the Pointcut (the Pointcut here is our custom annotation class), and @ Before declares the notification content. In the specific notification, we get the custom annotation object through @ annotation(logger), so we can get the value we give when using the annotation. If you don't understand the concepts of Pointcut and notification, it's better to consult some aop knowledge first, and then come back to this article. This article focuses on practice rather than explanation of concepts
Then let's start the web service now. Enter localhost:8080/user/6 in the browser (you can also use JUnit unit test). You will find that the console successfully outputs:
3. Use annotations for more detailed information
Just now, we used the custom annotation to output a log before the method call, but we don't know which method or class outputs it. If two methods are annotated with this annotation and the value is the same, how can we distinguish the two methods? For example, now we add a method to the UserController class:
@RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @KthLog("This is the content of the log") @RequestMapping("user/{id}") public User findUser(@PathVariable("id") Integer id) { return userService.findUserById(id); } @KthLog("This is the log content") @RequestMapping("compared") public void comparedMethod() { } }
If we call the comparedMethod() method, we will obviously get the same output as just now. At this time, we need to further modify the annotation. In fact, it is very simple. We only need to add a JoinPoint parameter to the advice() method of the aspect class, as follows:
@Before("pointcut() && @annotation(logger)") public void advice(JoinPoint joinPoint, KthLog logger) { System.out.println("Method name of annotation function: " + joinPoint.getSignature().getName()); System.out.println("Simple class name of the class: " + joinPoint.getSignature().getDeclaringType().getSimpleName()); System.out.println("The full class name of the class: " + joinPoint.getSignature().getDeclaringType()); System.out.println("Declaration type of the target method: " + Modifier.toString(joinPoint.getSignature().getModifiers())); }
Then let's run the process again and see what results will be output:
Now let's put these contents into the log and modify the format of the log as follows:
@Before("pointcut() && @annotation(logger)") public void advice(JoinPoint joinPoint, KthLog logger) { System.out.println("[" + joinPoint.getSignature().getDeclaringType().getSimpleName() + "][" + joinPoint.getSignature().getName() + "]-Log content-[" + logger.value() + "]"); }
4. Use annotations to modify parameters and return values
We delete the compare() method we added before. Now our annotation needs to modify the parameters of the method. Take the findUser() method as an example. Suppose that the user id we pass in starts from 1 and the back end starts from 0. The developer of our @ KthLog annotation likes to "meddle" and wants to help others reduce the pressure. What should we do?
In this application scenario, we need to do two things: subtract the incoming id by 1 and add 1 to the id in the returned user class. This involves how to get the parameters. Because we need to manage the operations before and after the method execution, we use @ Around to surround the annotation, as follows:
@Around("pointcut() && @annotation(logger)") public Object advice(ProceedingJoinPoint joinPoint, KthLog logger) { System.out.println("[" + joinPoint.getSignature().getDeclaringType().getSimpleName() + "][" + joinPoint.getSignature().getName() + "]-Log content-[" + logger.value() + "]"); Object result = null; try { result = joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } return result; }
Here, in addition to changing @ Before to @ Around, the JoinPoint in the parameter is also changed to ProceedingJoinPoint. However, don't worry, what JoinPoint can do can be done by ProceedingJoinPoint. Here, the actual operation is performed by calling the proceed() method and the return value is obtained. I believe I don't need to say more about the operation of the return value. Now the problem is how to obtain the parameters
ProceedingJoinPoint inherits the JoinPoint interface. In JoinPoint, there is a getArgs() method to obtain method parameters, which returns an Object array and matches the processed (args) method. The combination of these two methods can achieve our purpose:
@Around("pointcut() && @annotation(logger)") public Object advice(ProceedingJoinPoint joinPoint, KthLog logger) { System.out.println("[" + joinPoint.getSignature().getDeclaringType().getSimpleName() + "][" + joinPoint.getSignature().getName() + "]-Log content-[" + logger.value() + "]"); Object result = null; Object[] args = joinPoint.getArgs(); for (int i = 0; i < args.length; i++) { if(args[i] instanceof Integer) { args[i] = (Integer)args[i] - 1; break; } } try { result = joinPoint.proceed(args); } catch (Throwable throwable) { throwable.printStackTrace(); } if(result instanceof User) { User user = (User) result; user.setId(user.getId() + 1); return user; } return result; }
Here, we do two parameter type checks for the robustness of the code, and then we re execute the previous tests. Here, in order to make the results more obvious, we add some outputs at UserDao to display the actual parameters and returned values:
@Component public class UserDao { public User findUserById(Integer id) { System.out.println("query id by[" + id + "]User"); if(id > 10) { return null; } User user = new User(id, "user-" + id); System.out.println("The returned user is[" + user.toString() + "]"); return user; } }
Now let's visit http://localhost:8080/user/6 , see the results printed on the console:
We find that the 6 entered in the url is converted to 5 on the back end, and the final query user is also the user with id 5, indicating that our parameter conversion is successful. Then let's look at the response results obtained by the browser:
The returned user id is 6 instead of 5 in the back-end query, indicating that we have successfully modified the returned value
5. Summary
The principle of using custom annotations in Web projects (especially Spring projects) still depends on the AOP mechanism of Spring, which is different from our ordinary Java projects. Of course, if you need to use custom annotations to develop other frameworks, you need to implement a set of mechanisms yourself, but the principles are basically the same, just encapsulating some template operations
Through user-defined annotations, we can not only expand before and after method execution, but also obtain the method name, class and other information of the method. More importantly, we can also modify parameters and return values. These points basically include most of the functions of user-defined annotations. Knowing this, we can write a custom annotation to simplify our project