1.AOP的简单介绍
❝AOP:面向切面编程,相当于OOP面向对象编程;是一种编程思想。
❞
AOP与IOC是Spring框架的两大核心,SpringAOP的存在目的是为了解藕。AOP可以让一组类共享相同行为。
在OOP中只能通过继承类和实现接口,来使代码的耦合度增强,且类继承只能为单继承,阻碍了更多行为添加到一组类上,AOP弥补了OOP的不足。
AOP基于代理思想,对原来目标对象,创建代理对象;在不修改原对象的情况下对原有方法进行增强。
2.AOP的使用
「Spring支持AspectJ的注解式切面编程」
❝1.在类上使用
❞@Aspect
注解声明该类为一个切面类
2.在类中方法上使用@Before
、@After
、@Around
、@AfterReturning
、@AfterThrowing
注解定义建言,可直接将拦截规则(切点)作为参数。
3.@Before
、@After
、@Around
、@AfterReturning
、@AfterThrowing
参数的拦截规则为切点(PointCut),为了方便使切入点复用,可以使用@PointCut
注解定义一个拦截规则,然后在@Before
、@After
、@Around
、@AfterReturning
、@AfterThrowing
的参数中调用;其中符合条件的每一个被拦截处为连接点(JoinPoint)
3.切面的简单介绍
❝描述切面必须先了解以下几个概念:
❞
1.目标类:需要被增强的类。
2.连接点:看你被增强的点;泛指目标类中的所有方法。
3.切入点:将会被增强的连接点,目标类中被增强的方法。
4.通知(增强):对切入点增强的内容;增强的内容通常以方法的形式体现,增强执行的位置不同,称呼不同。(前置通知、后置通知、环绕通知、抛出异常通知、最终通知)
5.切面:通知和切入点的组合;一个通知对应一个切入点形成一条线,多个通知对应多个切入点对应多条线,多条线形成一个面,这个面就是切面。
4.切面类中的注解介绍
❝❞
@Aspect
:声明切面类@Pointcut
:声明切入点@Before
:前置通知,在方法执行前被调用@After
:后置通知,在方法执行之后执行@AfterReturning
:最终通知,在方法正常执行完返回后执行@AfterThrowing
:异常通知,在方法发生异常会被调用@Around
:环绕通知:在方法的执行前、后都会被调用
切面类中的@Before
、@After
、@Around
、@AfterReturning
、@AfterThrowing
这些都统称为通知,在切面类中通知的执行顺序为:
没有发生异常的情况:
环绕通知 >> 前置通知 >> 环绕通知 >> 后置通知 >> 最终通知
发生异常的情况:
环绕通知 >> 前置通知 >> 环绕通知 >> 后置通知 >> 异常通知
5.演示如何基于注解和基于方法规则拦截两种方式,输出记录操作的日志
「1.相关依赖」
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
「2.编写拦截规则注解」
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Action {
//方法描述
String description();
}
❝注意:
❞
注解本身时没有功能的,注解是一种原数据,就是用来解释数据的数据,这就是所谓的配置。
注解的功能来自用这个注解的地方。
「3.编写使用注解的被拦截类」
@Service
public class TestAnnotationService {
@Action(description = "注解式拦截一个add操作")
public void add(){}
}
「4.编写使用方法规则被拦截类」
@Service
public class TestMethodService {
public void add(){}
}
「5.编写切面」
@Slf4j
@Aspect
@Component
public class LogAspect {
/**
* 切入点
*/
@Pointcut("@annotation(com.scholartang.anno.Action)")
public void annotationPointCut() {
}
/**
* 前置通知,用于测试方法式拦截规则
*
* @param joinPoint
*/
@Before("execution(* com.scholartang.service.TestMethodService.*(..))")
public void before(JoinPoint joinPoint) {
//获取连接点的方法签名对象
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//通过签名对象获取类中的方法对象
Method method = signature.getMethod();
log.info("方法式拦截规则:" + method.getName());
}
/**
* 后置通知,用于测试注解式拦截规则
*
* @param joinPoint
*/
@After("annotationPointCut()")
public void after(JoinPoint joinPoint) {
//获取连接点的方法签名对象
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//通过签名对象获取类中的方法对象
Method method = signature.getMethod();
//通过方法对象获取方法上的注解对象
Action action = method.getAnnotation(Action.class);
log.info("注解式拦截:" + action.description());
}
}
「6.测试」
❝在SpringBoot测试动类中注入两个Service层的对象,在测试方法中分别调用对应的方法进行测试
❞
@SpringBootTest
class SpringAopApplicationTests {
@Autowired
TestAnnotationService annotationService;
@Autowired
TestMethodService methodService;
@Test
void aopTest() {
annotationService.add();
methodService.add();
}
}
「7.运行效果」

6.在实际开发中通过AOP来输出控制层中的接口操作的日志案例
❝通过AOP对控制层中的方法进行增强,当方法被调用时,将一些必要信息通过日志的方式输出。
❞请求URL
、请求源IP
、请求时间
、请求方法
、请求参数
、请求处理耗时
等
「1.切面类code」
@Slf4j
@Aspect
@Component
public class ApiLogAspect {
/**
* 切入点
* public:被public修饰的方法
* *:任意返回值
* com.scholartang.controller..*:目标类中的任意方法
* .*(..):任意参数
*/
@Pointcut("execution(public * com.scholartang.controller..*.*(..))")
public void apiLog() {
}
/**
* 环绕通知:在方法的执行前、后都会被调用
*
* @param point
* @return
*/
@Around(value = "apiLog()")
private Object doAfter(ProceedingJoinPoint point) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = point.proceed();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
log.info("--- <><><><><><><><><><><><><><> ---");
log.info("请求源IP:" + getIpAddr(request));
log.info("请求URL:" + request.getRequestURL());
log.info("请求时间:" + new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(new Date()));
log.info("请求方法:" + point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName());
log.info("请求参数:{}", point.getArgs());
log.info("请求处理耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");
log.info("--- <><><><><><><><><><><><><><> ---");
} catch (Throwable e) {
log.info("{} Use time : {} ms with exception : ", point, (System.currentTimeMillis() - startTime), e.getMessage());
throw e;
}
return result;
}
/**
* 获取当前请求网络ip
*
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
if (ipAddress != null && ipAddress.length() > 15) {
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
return ipAddress;
}
}
「2.当控制层的接口被请求时输出的日志效果图」

7.知识点补充
「1.自定义注解类中的注解说明」
❝
@Target
:专门用来限定某个自定义注解能够被应用在哪些Java元素上面的,它使用一个枚举类型定义如下:public enum ElementType {
/** 类,接口(包括注解类型)或枚举的声明 */
TYPE,
/** 属性的声明 */
FIELD,
/** 方法的声明 */
METHOD,
/** 方法形式参数声明 */
PARAMETER,
/** 构造方法的声明 */
CONSTRUCTOR,
/** 局部变量声明 */
LOCAL_VARIABLE,
/** 注解类型声明 */
ANNOTATION_TYPE,
/** 包的声明 */
PACKAGE
}
@Retention
:翻译为持久力、保持力。即用来修饰自定义注解的生命力。
注解的生命周期有三个阶段:
1、Java源文件阶段;
2、编译到class文件阶段;
3、运行期阶段。
同样使用了RetentionPolicy枚举类型定义了三个阶段:public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
* 注释将被编译器丢弃。
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
* 注释将由编译器记录在类文件中,但是不需要在运行时被VM保留。这是默认的的行为。
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* 注释将由编译器和在类文件中记录虚拟机在运行时保留它们,因此可以反射地读取它们。
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}❞
@Documented
:被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。@Inherited
:指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解。
注意:@Inherited
注解只对那些@Target被定义为ElementType.TYPE的自定义注解起作用。
「2.连接点JoinPoint对象的介绍:org.aspectj.lang.JoinPoint-中文简要API」
❝AspectJ使用org.aspectj.lang.JoinPoint接口表示目标类连接点对象,如果是环绕增强时,使用org.aspectj.lang.ProceedingJoinPoint表示连接点对象,该类是JoinPoint的子接口。任何一个增强方法都可以通过将第一个入参声明为JoinPoint访问到连接点上下文的信息。我们先来了解一下这两个接口的主要方法:
1.JoinPoint❝java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;
❞
Signature getSignature() :获取连接点的方法签名对象;
java.lang.Object getTarget() :获取连接点所在的目标对象;
java.lang.Object getThis() :获取代理对象本身;2.
ProceedingJoinPoint
ProceedingJoinPoint继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法:❝❞java.lang.Object proceed() throws java.lang.Throwable:通过反射执行目标对象的连接点处的方法;
❞
ava.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable:通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。
End
❝无论艳阳高照还是雨雪纷飞,从旭日东升到华灯初上,从我们用坚实的脚步丈量每一个真实的日子。过去的岁月只剩下模糊的背影,那些美好或辛酸的记忆,只不过是某种形式的纪念。未来究竟会怎样,我们不得而知,但与今天绝对息息相关。找到迷失的自我,活出真我的风采。把握今天,活在当下!
更多技术分享,可以关注我微信公众号🙂。
❞





