一 . AOP的五种增强
AOP的增强可以分为:前置增强、后置增强、环绕增强、异常增强、最终增强。
学会AOP的五种增强方式可以帮助我们将通用的横切逻辑与核心业务逻辑分离开来,提高代码的重用性、可维护性和可扩展性,同时也方便了对非核心功能的管理和控制
增强名称 | 作用 |
前置增强(Before Advice) | 在目标方法执行之前执行的横切逻辑。可以用于执行一些预处理操作,如参数验证、权限检查等。 |
后置增强(After Advice) | 在目标方法执行之后执行的横切逻辑。可以用于执行一些清理操作,如资源释放、日志记录等。 |
环绕增强(Around Advice) | 在目标方法执行前后都可以执行的横切逻辑。环绕增强可以完全控制目标方法的执行过程,可以选择是否调用目标方法以及何时调用。 |
异常增强(After Throwing Advice) | 在目标方法抛出异常时执行的横切逻辑。可以用于处理异常、发送通知等。 |
最终增强(After finally) | 它在目标方法执行结束后(无论是否发生异常)都会执行。最终增强通常用于资源清理或日志记录等必须执行的操作。 |
① 前置增强
特点:
a. 执行时机:前置增强在目标方法执行之前执行,可以在目标方法执行之前进行一些预处理操作。例如,参数验证、权限控制等。
b. 无法阻止目标方法执行:前置增强不能中断目标方法的执行,只能在目标方法执行之前插入一些额外的逻辑操作。
c. 参数传递:前置增强可以获取目标方法的参数信息,并根据需要对参数进行修改或验证。这使得我们可以在目标方法执行之前对输入参数进行检查和修正。
d. 代码重用性:通过前置增强,我们可以将一些通用的逻辑与业务逻辑分离开来,提高代码的重用性。例如,可以将许多不同的方法都需要进行相同的权限验证的逻辑抽取出来,作为一个独立的前置增强,然后应用到需要权限验证的方法上。
e. 可扩展性:前置增强可以方便地添加新的切面逻辑,而无需修改目标方法的代码。这样,在不改变原有功能实现的情况下,可以灵活地增加新的功能。
package aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class MyAspect {
@Before("execution( * service..*.*(..))")
public void before(){
System.out.println("前置通知.... 在方法之前要执行的公共代码放在这里");
}
}
② 后置增强
特点:
a. 执行时机:后置增强在目标方法执行之后执行,无论目标方法是否发生异常都会执行。它提供了一个统一的处理点,可以进行一些与核心业务逻辑无关的后续操作
b. 不影响目标方法的返回值:后置增强不能改变目标方法的返回值,它只是在目标方法执行完毕后进行额外的处理。
c. 资源释放和清理:后置增强常用于进行资源释放、日志记录等操作。例如,在执行完数据库操作后,可以在后置增强中关闭数据库连接,释放资源,或者在后置增强中记录日志,以便后续的跟踪和分析。
d. 与异常处理相结合:后置增强可以与异常处理相结合,用于统一处理异常情况。无论目标方法是否抛出异常,后置增强都会执行,因此可以在后置增强中对异常进行记录、通知等处理。
e. 可扩展性:后置增强可以方便地添加新的切面逻辑,而不需要修改目标方法的代码。这样可以灵活地增加新的功能,而且不会影响原有的功能实现。
package aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class MyAspect {
@AfterReturning(value="execution(* service..*.*(..)))",returning = "returnVal")
public void AfterReturning(Object returnVal){
System.out.println("后置通知...."+returnVal);
}
}
③ 环绕增强
特点:
a. 灵活性:AOP可以在不修改源代码的情况下,通过配置或注解的方式对方法进行增强。这种灵活性使得开发人员能够将关注点(cross-cutting concern)从核心业务逻辑中分离出来,使代码更加模块化、可维护和易于理解。
b. 集中管理:AOP允许将多个方法间共享的逻辑集中管理。通过定义切面(Aspect)和切点(Pointcut),可以将横跨多个方法的通用操作(如日志记录、事务管理等)统一放置在一个地方,提高了代码的可重用性和可维护性。
c. 低耦合:AOP的环绕增强特点实现了将关注点与主要业务逻辑相分离,减少了代码间的耦合度。在系统演化和需求变更时,可以更加方便地对关注点进行修改和调整,而不会对核心业务逻辑产生影响。
d. 代码简洁:AOP通过将通用的横切逻辑抽象为切面和增强,使得核心业务逻辑的代码更加简洁、清晰。开发人员只需要关注业务本身,而不必关心与业务无关的细节逻辑。
package aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class MyAspect {
@Around("execution(* service..*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知前....");
Object obj= (Object) joinPoint.proceed();
System.out.println("环绕通知后....");
return obj;
}
}
④ 异常增强
特点:
a. 集中处理异常:AOP可以将异常处理逻辑集中到一个地方,从而避免在每个方法中重复编写相同的异常处理代码。通过定义切面和切点,可以将异常处理逻辑应用于多个方法,实现统一的异常管理。
b. 解耦业务逻辑和异常处理逻辑:AOP的异常增强特点将异常处理逻辑从业务逻辑中分离出来,降低了它们之间的耦合度。开发人员可以专注于业务逻辑的实现,而不必过多关注异常处理细节。
c. 异常转换和封装:AOP可以对抛出的异常进行转换和封装,使得异常信息更加友好和易于理解。通过定义切面,在捕获异常时可以对异常进行处理、包装或转换,以便于上层调用者能够更好地理解和处理异常情况。
d. 异常日志记录:AOP可以实现对异常的日志记录功能,方便开发人员进行故障排查和系统监控。通过定义切面,在捕获异常时可以记录异常信息、方法调用堆栈等相关信息,为问题定位提供有价值的线索。
package aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class MyAspect {
@AfterThrowing(value="execution(* service..*.*(..))",throwing = "e")
public void afterThrowable(Throwable e){
System.out.println("出现异常:msg="+e.getMessage());
}
}
⑤ 最终增强
特点:
a. 资源释放:在最终增强中,可以进行资源的释放操作,确保资源在方法执行完毕后得到正确释放,避免资源泄漏和内存溢出等问题。例如,关闭数据库连接、释放文件句柄等。
b. 事务处理:最终增强常被用于实现事务处理,确保在方法执行结束后,对数据进行提交或回滚操作。通过最终增强,可以保证数据库事务的一致性和完整性。
c. 日志记录:最终增强可用于记录方法的执行情况和结果,生成详细的操作日志。这对于调试和监控系统非常有帮助,可以追踪方法的执行轨迹,并记录重要的返回结果和状态信息。
d. 缓存更新:最终增强可以应用于缓存场景,当方法执行完成后,根据需要更新相关的缓存数据,以保持缓存与数据源的一致性。例如,在写操作完成后更新缓存中的数据。
package aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class MyAspect {
@After(value="execution(* service..*.*(..))")
public void after(){
System.out.println("最终通知....");
}
}
二 . P命名空间注入
在Spring框架中,P命名空间注入主要存在于使用XML配置文件进行依赖注入的情况下。通过使用P命名空间,可以将属性值直接注入到Bean的属性中,它允许我们使用更简洁的语法来进行属性注入。这种方式可以减少配置的冗余代码,使配置文件更加清晰易读。但如果没有正确地验证和过滤用户提供的属性值,就会导致安全漏洞。
使用p命名空间注入
时,我们通过在XML配置文件中使用xmlns:p="http://www.springframework.org/schema/p"
来引入命名空间,并使用p:
前缀来指定属性值。
<bean id="唯一标识" class="类的全路径" p:"属性1"="注入的值" p:"属性2"="注入的值" />
<bean id="唯一标识" class="类的全路径" p:属性-ref="注入的Bean" />
三 . 不同的数据类型注入方式
① 使用 List 注入
package com.cskt.DayTwo;
import lombok.Data;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
@Data
public class JavaCollection {
//注入 list集合特点: 有序 允许重复 有下标
List manList;
}
<!--注入不同数据类型 list-->
<bean id="JavaCollection" class="com.cskt.DayTwo.JavaCollection">
<property name="manList">
<list>
<value>List01</value>
<value>List02</value>
<value>List03</value>
<value>List04</value>
</list>
</property>
</bean>
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
JavaCollection collection = (JavaCollection) context.getBean("JavaCollection");
collection.getManList().stream().forEach(item->{
System.out.println(item);
});
② 使用 Set 注入
package com.cskt.DayTwo;
import lombok.Data;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
@Data
public class JavaCollection {
//注入 set集合的特点: 无序 不允许重复
Set manSet;
}
<!--注入不同数据类型 set-->
<bean id="JavaCollection" class="com.cskt.DayTwo.JavaCollection">
<property name="manSet">
<set>
<value>Set01</value>
<value>Set02</value>
<value>Set03</value>
<value>Set04</value>
</set>
</property>
</bean>
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
JavaCollection collection = (JavaCollection) context.getBean("JavaCollection");
collection.getManSet().stream().forEach(item->{
System.out.println(item);
});
③ 使用 Map 注入
package com.cskt.DayTwo;
import lombok.Data;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
@Data
public class JavaCollection {
//注入 Map集合特点:键值对 key值不能重复 value 可以
Map manMap;
}
<!--注入不同数据类型 map-->
<bean id="JavaCollection" class="com.cskt.DayTwo.JavaCollection">
<property name="manMap">
<map>
<entry key="1" value="Map01" />
<entry key="2" value="Map02" />
<entry key="3" value="Map03" />
<entry key="4" value="Map04" />
</map>
</property>
</bean>
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
JavaCollection collection = (JavaCollection) context.getBean("JavaCollection");
collection.getManMap().forEach((key,value)->{
System.out.println(value);
});
④ 使用 Prop 注入
package com.cskt.DayTwo;
import lombok.Data;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
@Data
public class JavaCollection {
Properties manProp;
}
<!--注入不同数据类型 Prop-->
<bean id="JavaCollection" class="com.cskt.DayTwo.JavaCollection">
<property name="manProp">
<props>
<prop key="one">Prop01</prop>
<prop key="one">Prop04</prop>
<prop key="two">Prop05</prop>
<prop key="three">Prop06</prop>
<prop key="four">Prop07</prop>
</props>
</property>
</bean>
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
JavaCollection collection = (JavaCollection) context.getBean("JavaCollection");
collection.getManProp().forEach((key,value)->{
System.out.println(value);
});
四 . 使用注解实现Spring IoC
① IoC的常用注解
② 实现
(1)@Component
:用于标记一个类为Spring的组件,通常用于自动扫描组件并将其注册到Spring容器中。示例代码如下:
@Component
public class ExampleComponent {
// 类的实现...
}
(2)@Autowired
:用于自动注入依赖对象。当一个Bean需要依赖其他Bean时,可以使用@Autowired
注解标记在字段、构造函数或Setter方法上,Spring会自动查找匹配的Bean进行注入。示例代码如下:
@Component
public class ExampleBean {
@Autowired
private ExampleDependency exampleDependency;
// 其他方法...
}
(3)@Qualifier
:与@Autowired
一起使用,用于指定具体的Bean进行注入。当存在多个类型兼容的Bean时,可以使用@Qualifier
注解指定具体的Bean名称进行注入。示例代码如下:
@Component
public class ExampleBean {
@Autowired
@Qualifier("specificDependency")
private ExampleDependency exampleDependency;
// 其他方法...
}
@Component("specificDependency")
public class SpecificDependency implements ExampleDependency {
// 类的实现...
}
(4)@Value
:用于将值注入到Bean的属性中。可以通过@Value
注解直接注入常量值,也可以通过SpEL(Spring表达式语言)注入动态值。示例代码如下:
@Component
public class ExampleBean {
@Value("18")
private int age;
@Value("${app.name}")
private String appName;
// 其他方法...
}
在上述代码中,@Value注解将值"18"注入到age属性中,${app.name}表示从配置文件中读取名为app.name的属性值。
除了上述注解,Spring还提供了许多其他的注解,如@Service、@Repository、@Controller等用于标记不同类型的组件,以及@Scope、@PostConstruct、@PreDestroy等用于管理Bean的作用域和生命周期。
使用注解实现Spring IoC具有简洁、便捷的特点,能够减少配置的工作量,并提高代码的可读性和维护性。然而,需要注意合理使用注解,避免过度使用导致代码可读性降低。
五 . 使用注解实现 Spring AOP
注解方式实现aop我们主要分为如下几个步骤
1.在切面类(为切点服务的类)前用@Aspect注释修饰,声明为一个切面类。
2.用@Pointcut注释声明一个切点,目的是为了告诉切面,谁是它的服务对象。(此注释修饰的方法的方法体为空,不需要写功能比如 public void say(){};就可以了,方法名可以被候命的具体服务功能所以引用,它可以被理解为切点对象的一个代理对象方法)
3.在对应的方法前用对应的通知类型注释修饰,将对应的方法声明称一个切面功能,为了切点而服务
4.在spring配置文件中开启aop注释自动代理。如:<aop:aspectj-autoproxy/>
代码如下:
package com.cjh.aop;
import org.springframework.stereotype.Component;
@Component("knight")
public class BraveKnight {
public void saying(){
System.out.println("我是尊嘟假嘟o.O..(切点方法)");
}
}
package com.cjh.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 注解方式声明aop
* 1.用@Aspect注解将类声明为切面(如果用@Component("")注解注释为一个bean对象,那么就要在spring配置文件中开启注解扫描,<context:component-scan base-package="com.cjh.aop"/>
* 否则要在spring配置文件中声明一个bean对象)
* 2.在切面需要实现相应方法的前面加上相应的注释,也就是通知类型。
* 3.此处有环绕通知,环绕通知方法一定要有ProceedingJoinPoint类型的参数传入,然后执行对应的proceed()方法,环绕才能实现。
*/
@Component("annotationTest")
@Aspect
public class AnnotationTest {
//定义切点
@Pointcut("execution(* *.saying(..))")
public void sayings(){}
/**
* 前置通知(注解中的sayings()方法,其实就是上面定义pointcut切点注解所修饰的方法名,那只是个代理对象,不需要写具体方法,
* 相当于xml声明切面的id名,如下,相当于id="embark",用于供其他通知类型引用)
* <aop:config>
<aop:aspect ref="mistrel">
<!-- 定义切点 -->
<aop:pointcut expression="execution(* *.saying(..))" id="embark"/>
<!-- 声明前置通知 (在切点方法被执行前调用) -->
<aop:before method="beforSay" pointcut-ref="embark"/>
<!-- 声明后置通知 (在切点方法被执行后调用) -->
<aop:after method="afterSay" pointcut-ref="embark"/>
</aop:aspect>
</aop:config>
*/
@Before("sayings()")
public void sayHello(){
System.out.println("注解类型前置通知");
}
//后置通知
@After("sayings()")
public void sayGoodbey(){
System.out.println("注解类型后置通知");
}
//环绕通知。注意要有ProceedingJoinPoint参数传入。
@Around("sayings()")
public void sayAround(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("注解类型环绕通知..环绕前");
pjp.proceed();//执行方法
System.out.println("注解类型环绕通知..环绕后");
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<!-- 开启注解扫描 -->
<context:component-scan base-package="com.cjh.aop"/>
<!-- 开启aop注解方式,此步骤s不能少,这样java类中的aop注解才会生效 -->
<aop:aspectj-autoproxy/>
</beans>
优点: 使用注解实现Spring AOP简化了配置文件的编写,使得切面的定义更加直观和灵活。同时,可以根据实际需求选择不同类型的通知和切点表达式,实现细粒度的控制和扩展。
六 . 总结