前言
本文将会介绍几个日常场景,简单的举例并利用AOP来实现并解决这些问题。
一、什么是AOP?
面向切面编程(Aspect Oriented Programming,AOP),它是一种编程思想,是一种关注点分离的技术。可以说它是对OOP的一种补充和完善,简单的说,AOP将你的场景分为核心关注点和横切关注点,核心关注点就是你的主要逻辑处理流程,这是所有程序的主要部分,也是推动整个程序运转的关键,那么像参数校验、日志记录、权限校验等等这些去掉也不影响主流程运转的功能就可以称之为横切关注点。这些功能可以作为一个切入点插入主流程中,却不影响主流程运转。
二、AOP原理
AOP采用动态代理的方式实现。Spring 中的AOP采用了两种动态代理的模式:JDK动态代理和CGLib动态代理。
1.JDK动态代理:
JDK动态代理是原生的,采用反射实现,没有任何依赖。当一个类实现接口时,AOP默认使用JDK动态代理方式。JDK实现动态代理需要两个组件:InvocationHandler接口和Proxy类,当我们要实现JDK动态代理模式的时候,我们需要定义一个类去实现InvocationHandler接口,并且重写它的invoke方法。使用的时候,调用Proxy的newProxyInstance方法获取代理类,该代理类是Proxy类的子类,当我们使用该子类调用接口定义的方法时,底层就会通过反射来调用我们实现的invoke方法。例如:
// 首先定义一个HelloWord接口
public interface HelloWord {
void sayHelloWord ();
}
// HelloWordImp去实现HelloWord接口
public class HelloWordImp implements HelloWord {
@Override
public void sayHelloWord() {
System.out.println("hello word!");
}
}
// CustomHelloWord需要实现JDK动态代理组件InvocationHandler接口
public class CustomHelloWord implements InvocationHandler {
private Object target;
public CustomHelloWord(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 切入目标方法执行之前做的事情
System.out.println("Before CustomHelloWord say hello word!");
// 切入目标方法执行
Object result = method.invoke(target, args);
// 切入目标方法执行之后做的事情
System.out.println("After CustomHelloWord say hello word!");
return result;
}
}
// 执行类
public class Main {
public static void main(String[] args) {
// 创建实现切入方法类的实例
CustomHelloWord handle = new CustomHelloWord(new HelloWordImp());
// 获取被切入方法类的代理类,该类是Proxy类的子类
HelloWord helloWoed = (HelloWord) Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{HelloWord.class}, handle);
// 打印helloWoed的父类
System.out.println(helloWord.getClass().getSuperclass());
// 执行切入方法
helloWoed.sayHelloWord();
}
}
// 结果如下:
2.CGLib动态代理:后续补充。
三、AOP使用场景
权限校验,参数校验,日志记录,多数据源切换,事务处理等。
四、AOP使用实现
1、springboot项目pom中引入以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.4.1</version>
</dependency>
2、接口参数校验,假如我们有如下测试接口:
@Controller
@ResponseBody
@RequestMapping("/demo")
public class SpringTestController {
// 模拟一个测试接口
@RequestMapping("/test")
public String toString(HttpServletRequest request, HttpServletResponse response) {
return "test";
}
}
/**
* AOP实现一个请求拦截器:
* 1、@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)"):
* @Around是@Before()和@After()的结合写法,表示手动推动方法执行。
* RequestMapping表示拦截被RequestMapping注解的所有接口。
* 2、实现参数校验功能。
*/
@Aspect
@Order
@Component
public class RequestInterceptor {
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void process (ProceedingJoinPoint point) {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String name = request.getParameter("name");
String password = request.getParameter("password");
if (Objects.isNull(name) || Objects.isNull(password)) {
System.out.println("name or password is empty!");
}
}
}
// 结果如下:
注意:我们是在SpringTestController中的/test接口中的方法是返回一个字符串(“test”)的,为什么我们的响应结果中没有返回呢?这是因为我们的请求拦截器中没有去执行被切入的方法并没有返回执行的结果导致,这一点一定要注意,否则只会执行你的切入点,而不会执行你的被切入的方法,看如下改动:
3、日志记录:
/**
* AOP实现一个请求拦截器:
* 1、@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)"):
* @Around是@Before()和@After()的结合写法,表示手动推动方法执行。
* RequestMapping表示拦截被RequestMapping注解的所有接口。
* 2、实现日志记录功能。
*/
@Aspect
@Order
@Component
public class RequestInterceptor {
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object process (ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String name = request.getParameter("name");
String password = request.getParameter("password");
if (Objects.isNull(name) || Objects.isNull(password)) {
System.out.println("name or password is empty!");
}
// 这里可以自定义实现,将操作记录插入数据库。作为审计日志使用。比如一些比较重要的
// 操作:删除记录,编辑记录,插入记录等
System.out.println(name + " Logging on to the system");
return point.proceed();
}
}
// 结果:
4、自定义注解:
(1)自定义方法注解UserRoleAuth:该注解作用与方法上,主要用来判断当前用户是否有权限操作操作此方法,比如:一般新增,删除,更新操作只有admin类型的用户才能操作,其他类型的用户将不能操作这些接口。
UserType枚举类
public enum UserType {
ADMIN("admin", "0"),
ORDINARY("ordinary", "1");
String value;
String code;
UserType(String value, String code) {
this.value = value;
this.code = code;
}
public String getValue() {
return value;
}
public String getCode() {
return code;
}
}
自定义用户权限注解UserRoleAuth
/**
* 用户权限自定义注解
* @Target(ElementType.METHOD):表明该注解作用于方法上,取决于ElementType这个枚举类,可自行
* 查看源码。
* @Retention(RetentionPolicy.RUNTIME):注解生命周期的一种,表明注解一直被保留,一般我们是在
* 运行时动态的获取注解信息,所以这里只能用RetentionPolicy.RUNTIME
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UserRoleAuth {
UserType userType();
}
在RequestInterceptor中增加如下方法。
/**
* 这里利用AOP,对所有加了自定义注解@UserRoleAuth的方法做切入点,
* 来实现用户权限拦截。
*/
@Around("@annotation(com.it.springdemo.server.UserRoleAuth)")
public Object userRoleAuth (ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String name = request.getParameter("name");
// 这里假设我们根据传入的用户名name查寻得到该用户的用户类型为ordinary。
String userType = "ordinary";
MethodSignature methodSignature = (MethodSignature) point.getSignature();
// 这两种方法都能拿到自定义注解UserRoleAuth对象
UserRoleAuth userRoleAuth = AnnotationUtils.findAnnotation(methodSignature.getMethod(), UserRoleAuth.class);
// UserRoleAuth userRoleAuth = point.getTarget().getClass().getAnnotation(UserRoleAuth.class);
assert userRoleAuth != null;
if (!userRoleAuth.userType().getValue().equals(userType)) {
System.out.println("You do not have permission to operate this interface!");
return null;
}
return point.proceed();
}
在SpringTestController增加如下接口,并给该接口增加自定义注解@UserRoleAuth(userType = UserType.ADMIN),表明该接口只有admin类型的用户才能访问
@RequestMapping("/add")
@UserRoleAuth(userType = UserType.ADMIN)
public String add(HttpServletRequest request, HttpServletResponse response) {
return "add";
}
调用结果:
(2)自定义类注解:该注解作用于类上,比如:我的某个类上的所有接口都是给admin类型用户使用的,那么只需要给这个类加上用户权限注解@UserRoleAuth(userType = UserType.ADMIN)即可,不过这里需要改动一些东西:
- UserRoleAuth自定义注解上的Target(ElementType.METHOD)应该改为Target(ElementType.TYPE)
- RequestInterceptor中的userRoleAutn上的@Around("@annotation(com.it.springdemo.server.UserRoleAuth)")应该改为@Around("@within(com.it.springdemo.server.UserRoleAuth)")
总结
以上就是对spring AOP的简单理解,本文仅仅简单介绍了AOP的几个使用场景和使用方法,从而解决我们日常开发中遇到的一些比较通俗的处理逻辑。