面向切面编程 AOP 在 Java 语言中运用非常广泛,无论在开源框架,还是公司项目,比比皆是。本文使用 AOP 实现了日常工作中经常用到的权限验证和日志打印,权限校验具体逻辑是拦截方法验证参数的合法性,日志打印具体逻辑是打印每一个 controller 接口在被调用时的具体请求路径、执行方法名、传入的参数,返回参数及响应时间。
本文你将会获得以下知识:
- 统一验证方法入参实现权限验证
- 统一打印入参和返回参数等相关信息
- 为什么使用统一参数格式
适合人群: Java 初中级开发。
概念介绍
AOP
AOP 的英文全称是 Aspect Oriented Programming(面向切面编程),底层是利用预编译和运行期动态代理原理实现在方法的前后进行拦截处理,可以轻松实现权限验证、日志打印等功能。
@Aspect
Aspect 是切面的意思,此注解用于声明 Java 类为切面类,意味着切面类中可以定义 @Pointcut、@Before 等相关注解进行业务处理。
@Pointcut、execution
Pointcut 是切点的意思,即需要拦截的方法。execution 是用于匹配方法的表达式,可以精确匹配也可以模糊匹配。
@Before
Before 是在方法执行前,进行拦截处理,即前置通知。
@After
After 是在方法执行后,进行拦截处理,即后置通知,无论方法执行成功还是出现异常,都会执行后置方法。
@AfterRunning
AfterRunning 是在方法执行后,并且成功返回结果后,进行拦截处理,即返回通知,如果方法执行中出现异常,则不会会执行后置方法。
@AfterThrowing
AfterThrowing 是在方法执行过程中抛出异常后,进行拦截处理,即异常通知。
@Around
Around 是在方法的前后进行拦截处理,即环绕通知。
具体开发过程及代码分析
代码结构
pom.xml 文件配置
下面配置是工程需要使用的所有 jar 和 maven 打包策略,必须引入 spring-aop,代码如下:
<?xml version=\"1.0\" encoding=\"UTF-8\"?> <project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>demo-aop</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo-aop</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <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>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.51</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
工程配置文件和启动类
application.properties 仅仅配置了端口号,启动类也是非常简单,具体代码如下:
application.properties 文件内容: server.port=8080 package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoAopApplication { public static void main(String[] args) { SpringApplication.run(DemoAopApplication.class, args); } }
Controller 类、Service 类及相关类
Controller 类定义了 4 个接口,Service 类定义了 3 个方法,用于演示不同参数在切面类中如何处理,代码如下:
DemoController.java package com.example.demo.controller; import com.example.demo.base.WebResult; import com.example.demo.service.DemoService; import com.example.demo.vo.DemoVO; import com.example.demo.base.ReqParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController public class DemoController { @Autowired private DemoService demoService; @RequestMapping(value = \"/aop/test1/{companyId}\", method = RequestMethod.POST) public void test1(@PathVariable String companyId, @RequestParam(name = \"param2\") String param2) { demoService.updateTest1(companyId, param2); } @RequestMapping(value = \"/aop/test2\", method = RequestMethod.POST) public void test2(@RequestBody Map paramMap, @RequestParam(name = \"param2\") String param2) { demoService.updateTest2(paramMap, param2); } @RequestMapping(value = \"/aop/test3\", method = RequestMethod.POST) public void test3(@RequestBody DemoVO demoVO) { demoService.saveTest3(demoVO); } @RequestMapping(value = \"/aop/test4\", method = RequestMethod.POST) public WebResult test4(@RequestBody ReqParam<DemoVO> reqParamVO) { demoService.saveTest3(reqParamVO.getData()); //为了演示返回结果的日志打印效果,此处把入参当成返回结果。 return WebResult.ok(reqParamVO.getData()); } } DemoService.java package com.example.demo.service; import com.example.demo.vo.DemoVO; import org.springframework.stereotype.Service; import java.util.Map; @Service public class DemoService { public void updateTest1(String companyId, String param2){ // do something.... } public void updateTest2(Map paramMap, String param2){ // do something.... } public void saveTest3(DemoVO demoVO){ // do something.... } } DemoVO.java package com.example.demo.vo; public class DemoVO { private String companyId; public String getCompanyId() { return companyId; } public void setCompanyId(String companyId) { this.companyId = companyId; } @Override public String toString() { return \"DemoVO [ \" + \"companyId=\'\" + companyId + \'\\\'\' + \']\'; } } ReqParam.java package com.example.demo.base; import java.io.Serializable; public class ReqParam<T> implements Serializable { private static final long serialVersionUID = 6306747792003091002L; private String sign; private Long timestamp; private String sysCode; private String version; private String token; private T data; public T getData() { return data; } public void setData(T data) { this.data = data; } public String getSign() { return sign; } public void setSign(String sign) { this.sign = sign; } public Long getTimestamp() { return timestamp; } public void setTimestamp(Long timestamp) { this.timestamp = timestamp; } public String getSysCode() { return sysCode; } public void setSysCode(String sysCode) { this.sysCode = sysCode; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } @Override public String toString() { return \"ReqParamBody [sign=\" + sign + \", timestamp=\" + timestamp + \", sysCode=\" + sysCode + \", version=\" + version + \", token=\" + token + \", data=\" + data + \"]\"; } } WebResult.java package com.example.demo.base; import java.io.Serializable; import java.util.List; public class WebResult implements Serializable { private static final long serialVersionUID = 372525545013064618L; /** * 结果描述 */ private String message; /** * 结果编码 */ private String code; /** * 业务数据 */ private Object data; public WebResult() { } /** * 操作成功 * @return */ public static WebResult ok() { return ok(null); } public WebResult(String code, String message, Object data) { this.code = code; this.message = message; this.data = data; } /** * 操作成功 * @param data * @return */ public static WebResult ok(Object data) { return new WebResult(\"200\", \"操作成功\", data); } /** * 成功 * @param code * @param message * @return */ public static WebResult ok(String code, String message) { return new WebResult(code, message, null); } public static <T> WebResult okAndListData(List<T> data) { return ok(data); } /** * 失败 * @param code * @param message * @return */ public static WebResult fail(String code, String message) { WebResult result = new WebResult(); result.setCode(code); result.setMessage(message); return result; } /** * 失败 * @param message 消息,code 默认为 400 * @return */ public static WebResult fail(String message) { return WebResult.fail(\"400\", message); } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } } AopException.java package com.example.demo.base; public class AopException extends Exception { private static final long serialVersionUID = 1L; public AopException(String message) { super(message); } }
两个核心 AOP 类
DemoAspect 主要逻辑是获取拦截的 service 方法的第一个参数,并检验这个参数的正确性而达到权限验证效果,因第一个参数可能存在多个类型,比如:字符串类型、Map 类型、自定义类型等等,所以需要先判断类型再获取待校验的值,否则获取不到相应的值导致校验出问题。CommonLoggerAspect 主要逻辑是打印每一个 controller 接口的具体请求路径、执行方法名、传入的参数,返回参数及响应时间。
DemoAspect.java package com.example.demo.aop; import com.example.demo.base.AopException; import com.example.demo.vo.DemoVO; import com.example.demo.base.ReqParam; import org.apache.commons.lang.StringUtils; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import java.util.Map; @Aspect @Component public class DemoAspect { private Logger logger = LogManager.getLogger(getClass()); @Pointcut(\"execution(public * com.example.demo.service.DemoService.update*(..)) || execution(public * com.example.demo.service.DemoService.saveTest3(..))\") public void checkCompanyId() { } @Before(\"checkCompanyId()\") public void checkCompanyId(JoinPoint joinPoint) throws Throwable { Object[] args = joinPoint.getArgs(); String companyId = \"\"; try { //从参数中获取 companyId,args[0]代表第一个参数 if(args != null && args.length != 0){ if(args[0] instanceof String){ //假如是 String 类型 companyId = (String)args[0]; }else if(args[0] instanceof Map){ //假如是 Map 类型 Map map = (Map)args[0]; companyId = map.get(\"companyId\") != null ? map.get(\"companyId\").toString() : \"\"; }else if(args[0] instanceof DemoVO){ //假如是自定义 DemoVO 类 DemoVO demoVO = (DemoVO)args[0]; companyId = demoVO.getCompanyId(); }else if(args[0] instanceof ReqParam){ //假如是自定义 ReqParamVO 类 ReqParam reqParamVO = (ReqParam)args[0]; DemoVO demoVO = (DemoVO)reqParamVO.getData(); companyId = demoVO.getCompanyId(); } } } catch (Exception e) { logger.log(Level.ERROR, \"获取 companyId 出现异常,异常信息:{}\", e); throw new AopException(\"获取 companyId 出现异常\"); } if(StringUtils.isEmpty(companyId)){ logger.log(Level.ERROR, \"companyId 校验不通过,companyId 为空\"); throw new AopException(\"companyId 校验不通过,companyId 为空\"); } if (!verifyCompanyId(companyId)) { logger.log(Level.INFO, \"companyId({})校验不通过,简单说明校验不通过的原因,方便排查问题\", companyId); throw new AopException(\"companyId(\"+companyId+\")校验不通过,简单说明校验不通过的原因,方便排查问题\"); } } private boolean verifyCompanyId(String companyId){ //根据业务要求校验 companyId,校验通过返回 true,否则返回 false return true; } } CommonLoggerAspect.java package com.example.demo.aop; import com.alibaba.fastjson.JSONObject; import com.example.demo.base.AopException; import com.example.demo.base.ReqParam; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; @Aspect @Component public class CommonLoggerAspect { private static final Logger log = LoggerFactory.getLogger(CommonLoggerAspect.class); @Pointcut(\"execution (* com.example.demo.controller.*Controller.*(..))\") public void controllerAspect() { } @SuppressWarnings(\"rawtypes\") @Around(\"controllerAspect()\") public Object controllerAround(ProceedingJoinPoint joinPoint) throws Throwable{ /** * 接口增加日志 */ String classAndMethodName = null; //获取当前请求属性集 ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); //获取请求 HttpServletRequest request = sra.getRequest(); //获取请求地址 String requestUrl = request.getRequestURL().toString(); //记录请求地址 log.info(\"请求开始:路径[{}]\", requestUrl); //记录请求开始时间 long startTime = System.currentTimeMillis(); try { Class<?> target = joinPoint.getTarget().getClass(); Class<?>[] par = ((MethodSignature) joinPoint.getSignature()).getParameterTypes(); String methodName = joinPoint.getSignature().getName(); //获取当前执行方法 Method currentMethod = target.getMethod(methodName, par); //拼接输出字符串 classAndMethodName = target.getName() + \"的\" + currentMethod.getName() + \"方法\"; log.info(\"正在执行:{}\", classAndMethodName); //获取切点参数 List<Object> list = Arrays.asList(joinPoint.getArgs()); if (list != null && list.size() > 0) { for(Object object : list){ if(object instanceof MultipartFile) { log.info(\"入参:{}\", ((MultipartFile) object).isEmpty() ? \"空文件\" : ((MultipartFile)object).getName()); } else if(object instanceof ReqParam){ log.info(\"入参:{}\", ((ReqParam) object).getData().toString()); }else if(object == null) { log.info(\"入参: \"); }else{ log.info(\"入参:{}\", object.toString()); } } } } catch (Exception e) { log.error(\"记录日志的时出现异常,异常信息:{}\", e); throw new AopException(\"记录日志的时出现异常\"); } Object object = joinPoint.proceed(); if (object != null) { log.info(\"执行方法:{},返回参数:{}\", classAndMethodName, JSONObject.toJSONString(object)); }else{ log.info(\"执行方法:{},无返回参数!\", classAndMethodName); } long endTime = System.currentTimeMillis(); log.info(\"请求结束:路径[{}]响应时间{}毫秒\", requestUrl, endTime-startTime); return object; } }
接口测试
1. test1 接口,测试权限校验,第一个参数是字符串类型。
url:
http://127.0.0.1:8080/aop/test1/a028b8817b060b6b017b060d86230001?param2=aaa
2. test2 接口,测试权限校验,第一个参数是 Map 类型。
url:
http://127.0.0.1:8080/aop/test2?param2=bbb
Body 参数:
{ \"companyId\": \"a028b8817b060b6b017b060d86230001\" }
3. test3 接口,测试权限校验,第一个参数是自定义 DemoVO 类。
url:
http://127.0.0.1:8080/aop/test3
Body 参数:
{ \"companyId\": \"a028b8817b060b6b017b060d86230001\", \"param2\": \"ccc\" }
4. test3 接口,测试权限校验,模拟 A 校 OP 验不通过,注意 body 中的参数名是 companyId2。
url:
http://127.0.0.1:8080/aop/test3
Body 参数:
{ \"companyId2\": \"a028b8817b060b6b017b060d86230001\", \"param2\": \"zzz\" }
5. test4 接口,测试日志打印。
url:
http://127.0.0.1:8080/aop/test4
Body 参数:
{ \"data\":{ \"companyId\": \"a028b8817b060b6b017b060d86230001\", \"param2\": \"ddd\" }, \"sign\":\"\", \"sysCode\":\"demo-aop\", \"timestamp\":\"\", \"token\":\"\", \"version\":\"\" }
返回结果:
{ \"message\": \"操作成功\", \"code\": \"200\", \"data\": { \"companyId\": \"a028b8817b060b6b017b060d86230001\" } }
控制台打印结果:
2021-10-12 17:53:15.105 INFO 14900 --- [nio-8080-exec-5] com.example.demo.aop.CommonLoggerAspect : 请求开始:路径[http://127.0.0.1:8080/aop/test4] 2021-10-12 17:53:15.106 INFO 14900 --- [nio-8080-exec-5] com.example.demo.aop.CommonLoggerAspect : 正在执行:com.example.demo.controller.DemoController 的 test4 方法 2021-10-12 17:53:15.106 INFO 14900 --- [nio-8080-exec-5] com.example.demo.aop.CommonLoggerAspect : 入参:DemoVO [ companyId=\'a028b8817b060b6b017b060d86230001\'] 2021-10-12 17:53:20.682 INFO 14900 --- [nio-8080-exec-5] com.example.demo.aop.CommonLoggerAspect : 执行方法:com.example.demo.controller.DemoController 的 test4 方法,返回参数:{\"code\":\"200\",\"data\":{\"companyId\":\"a028b8817b060b6b017b060d86230001\"},\"message\":\"操作成功\"} 2021-10-12 17:53:20.682 INFO 14900 --- [nio-8080-exec-5] com.example.demo.aop.CommonLoggerAspect : 请求结束:路径[http://127.0.0.1:8080/aop/test4]响应时间 5576 毫秒
总结
通过这次的使用 AOP 实现权限验证和日志打印实战,让我们掌握了如何使用 AOP 技术实现统一验证参数合法性、如何实现统一打印入参和返回参数等相关信息,关键点在于使用使用统一参数格式。
PS:如有写错请指正,感谢您阅读。