什么是AOP?
AOP:Aspect Oriented Programming,中文翻译为”面向切面编程“。面向切面编程是一种编程范式,它作为OOP面向对象编程的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、权限控制、缓存控制、日志打印等等。AOP采取横向抽取机制,取代了传统纵向继承体系的重复性代码
AOP把软件的功能模块分为两个部分:核心关注点和横切关注点。业务处理的主要功能为核心关注点,而非核心、需要拓展的功能为横切关注点。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点进行分离
使用AOP有诸多好处,如:
1.集中处理某一关注点/横切逻辑
2.可以很方便的添加/删除关注点
3.侵入性少,增强代码可读性及可维护性
1 在不改变原有的逻辑的基础上,增加一些额外的功能。代理也是这个功能,读写分离也能用aop来做。 2 AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能,日志代码往往横向地散布在所有对象层次中,而且它与对象的核心代码往往毫无关系。除了日志还有安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。 3 AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。 4 使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
代码实现
本文基于SpringBoot编写了一个简单的Spring AOPDemo。
maven依赖添加如下 <!--引入SpringBoot的Web模块--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--引入AOP依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
注意:在完成了引入AOP依赖包后,不需要去做其他配置。AOP的默认配置属性中,spring.aop.auto属性默认是开启的,也就是说只要引入了AOP依赖后,默认已经增加了@EnableAspectJAutoProxy,不需要在程序主类中增加@EnableAspectJAutoProxy来启用。
web请求入口
对应系统纵向的核心业务模块。
@RestController public class RedisController { @Reference(version = "1.0.0") private GoodsInfoService goodsInfoService; @Autowired private CacheService cacheService; @GetMapping("/detail") public GoodsInfo detail(Integer id){ GoodsInfo goods = (GoodsInfo)cacheService.getCacheByKey("mall_goods:"+id); if(null == goods){ goods = goodsInfoService.getById(id); cacheService.setCacheToRedis("mall_goods:"+id,goods,1800); } return goods; } }
定义切面类
@Component @Aspect public class WebAspect { private final static Logger LOGGER = LoggerFactory.getLogger(WebAspect.class); @Autowired private ExceptionHandler exceptionHandler; private final String pointcut = "execution(* com.mall.web.controller.*.*(..))"; @Pointcut(pointcut) public void executeService(){} /** * AOP 前置通知 * * @param joinPoint */ @Before("executeService()") public void doBeforeAdvice(JoinPoint joinPoint){ ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); LOGGER.info("【AOP前置通知】url:" + request.getRequestURI()+", " + "ip:" + DeviceUtils.getIpAddr(request)+ ", " + "method:" + request.getMethod()+ ", " + "class_method:" + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); } /** * 环绕通知 * * @param pjp */ @Around("executeService()") public Object doAroundAdvice(ProceedingJoinPoint pjp) throws Throwable{ StopWatch clock = new StopWatch(); //返回的结果 Object result = null; //方法名称 String className = pjp.getTarget().getClass().getName(); String methodName = pjp.getSignature().getName(); try { clock.start(); //处理入参特殊字符和sql注入攻击 checkRequestParam(pjp); //执行访问接口操作 result = pjp.proceed(); clock.stop(); //后置通知 if (!methodName.equalsIgnoreCase("initBinder")) { long constTime = clock.getTotalTimeMillis(); RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST); LOGGER.info("【AOP环绕通知】 接口[" + request.getRequestURI() + "]" + "-" + "[" + request.getMethod() + "]" + " 请求耗时:" + constTime + "ms"); } }catch (Exception e){ LOGGER.error(e.getMessage(),e); result = exceptionHandler.exceptionGet(e); } return result; } @AfterReturning(value = "executeService()", returning = "result") public void doAfterReturning(Object result) { LOGGER.info("【AOP后置通知】 返回值:" + result); } /** * 检查防SQL注入和非法字符 * @param pjp */ private void checkRequestParam(ProceedingJoinPoint pjp){ Object[] args = pjp.getArgs(); RequestFacade facade= (RequestFacade) args[0]; String str = (String) facade.getAttribute("RequestStr"); if (!IllegalStrFilterUtils.sqlStrFilter(str)) { LOGGER.info("访问接口:" + pjp.getSignature() + ",输入参数存在SQL注入风险!"); throw new DescribeException(ExceptionEnum.REQUEST_ERROR); } if (IllegalStrFilterUtils.isIllegalStr(str)) { LOGGER.info("访问接口:" + pjp.getSignature() + ",输入参数含有非法字符!"); throw new DescribeException(ExceptionEnum.REQUEST_ERROR); } } }
测试
请求http://192.168.0.101:9001/detail?id=1
环绕通知
这里单独讲解一下功能强大的环绕通知,环绕通知可以将你所编写的逻辑将被通知的目标方法完全包装起来。我们可以使用一个环绕通知来代替之前多个不同的前置通知和后置通知。如下所示,前置通知和后置通知位于同一个方法中,不像之前那样分散在不同的通知方法里面。
/** * @description 使用环绕通知 */ @Around("BrokerAspect()") public void doAroundGame(ProceedingJoinPoint pjp) throws Throwable { try{ System.out.println("方法执行前!"); pjp.proceed(); System.out.println("方法执行后"); } catch(Throwable e){ System.out.println("异常通知:*****!"); } }
环绕通知接受ProceedingJoinPoint作为参数,它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,需要调用ProceedingJoinPoint的proceed()方法。当你不调用proceed()方法时,将会阻塞被通知方法的访问。