目录
1.基本类型:HappyComponent的注解实现IoC (@Component @Value)
2.引用类型:HappyMachine的注解实现IoC (@Autowired)
四、Spring5整合Junit4 @RunWith @ContextConfiguration ★
四、AOP第一个示例:添加日志(实现)@Aspect @Before..
七、切入点表达式语法 execution(* ... .method())
总结
- spring.xml 标签配置:
<!--指定注解扫描基准路径 会自动扫描base-package及其子包下的注解-->
<context:component-scan base-package="com.atguigu"></context:component-scan>
<!-- 开启基于注解的AOP功能 -->
<aop:aspectj-autoproxy/>
<!-- 启动AOP自动代理
proxy-target-class:底层采用什么动态代理技术
false: 默认 有接口使用JDK动态代理,没有接口使用CGLIB
true:不管是否有接口,都使用CGLIB-->
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
- 创建Bean的注解:
@Component 标记该类,spring配置文件中base-package 扫描后会创建对象并装配到IoC容器
@Controller 标记控制器组件(控制层)
@Service 标记业务逻辑组件(业务逻辑层)
@Repository 标记持久化层组件(持久化、数据访问层)
@Configuration 说明这是一个配置类,要达到全注解开发还需要使用 @ComponentScan
以上注解本质上相同,只是在@Component 基础上换了名字。提高了代码的可读性。
- 注入Bean的注解:
@Value 给基本类型的参数注入赋值,可以用在属性、set方法上
@Autowired 给引用类型参数注入赋值,可以用在属性、set、构造方法上
@Qualifier 指定Bean的name;不会单独使用,而是和@Autowired配合使用
- 全注解开发:
@ComponentScan() 组件扫描,根据定义的扫描路径,把符合扫描规则的类装配到IoC容器中。
- 整合Junit4:
@RunWith(SpringJUnit4ClassRunner.class) 指定Spring为Junit提供的运行器
@ContextConfiguration 指定Spring配置文件的位置
单个文件
@ContextConfiguration(locations = {"classpath:spring.xml"})
@ContextConfiguration("classpath:spring.xml") (locations{}也可省略)
@ContextConfiguration(classes = SimpleConfiguration.class)
多个文件
@ContextConfiguration(locations = {"classpath:spring.xml","classpath:spring2.xml"})
- AOP 面向切面编程:声明切面类与通知方法
@Aspect 表示这个类是一个切面类
@Before(value) 前置通知方法
@AfterReturning(value) 返回通知方法
@AfterThrowing(value) 异常通知方法
@After(value) 后置通知方法
@Around(value):环绕通知方法 对应整个try...catch...finally结构,包括四种通知的所有功能。
需要在对应方法声明 ProceedingJoinPoint 类型的形参,proceed() 调用切入点的方法。
value()属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
@Aspect//这是一个切面类
public class LogAspect {
//前置通知: 指定的方法在执行之前要做什么 (切入点,表达式)
@Before("execution(* com.atguigu.service.impl.CalculatorImpl.*(..))")
public void beforeLog(){
System.out.println("[日志] xxx 方法开始执行,参数是:");
}
- 创建IOC容器对象
BeanFactory IoC容器的基本实现类
ApplicationContext BeanFactory 的子接口,更多功能。
ClassPathXmlApplicationContext 根据XML配置文件创建IoC容器对象
// ClassPathXmlApplicationContext根据XML配置文件创建IOC容器对象
private ApplicationContext iocContainer = new ClassPathXmlApplicationContext("applicationContext.xml");
AnnotationConfigApplicationContext 根据XML配置类创建IoC容器对象 (全注解开发)
// AnnotationConfigApplicationContext根据配置类创建IOC容器对象
private ApplicationContext iocContainerAnnotation = new AnnotationConfigApplicationContext(MyConfiguration.class);
WebApplicationContext Web项目
- 切入点表达式语法
execution (* com.atguigu...method(int i))
- 重用切入点
@Pointcut ("execution(切入点表达式)")
需要使用在一个方法上,使用时直接传入该方法即可
- 获取连接点信息
获取方法信息:通知方法加入形参JoinPoint joinPoint,方法内调用 getSignature()
获取方法返回值:@AfterReturning中通过 returning属性设置名称,通知方法中声明对应名称Object类型 形参
获取方法抛出异常:@AfterThrowing中声明 throwing 属性设定名称,通知方法中声明对应名称声明 Exception类型 形参
环绕通知:声明 ProceedingJoinPoint 类型的形参,里面包含各种方法,可通过proceed() 调用切入点的方法。
- 多个切面优先级
@Order(int i) 数值越小越早执行
使用注解开发
1、注解的作用
①注解
和 XML 配置文件一样,注解本身并不能执行,注解本身仅仅只是做一个标记,具体的功能是框架检测到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作。
本质上:所有一切的操作都是Java代码来完成的,XML和注解只是告诉框架中的Java代码如何执行。
②扫描
Spring 为了知道程序员在哪些地方标记了什么注解,就需要通过扫描的方式,来进行检测。然后根据注解进行后续操作。
一、标记与扫描 ★
1.基本类型:HappyComponent的注解实现IoC (@Component @Value)
使用@Component 标记该类,默认名称是该类名称首字母小写
使用@Value 给基本类型的参数(包括String)赋值,位置可以在setter方法或者成员变量上。
如果在setter方法上,就会使用反射调用setter方法。
如果在成员变量上,就是使用反射直接操作Field成员变量。
不能写在构造方法上在spring配置文件中指定注解的扫描基准包,base-package,会扫描该包及其子包下的注解,比如@Component,创建对象并放入IoC容器。
//@Component //默认类名首字母小写
@Component(value = "happyComponent")
public class HappyComponent{
@Value("engine")//给基本数据类型,包括String
private String componentName;
@Value("engine")
public void setComponentName(String componentName) {
this.componentName = componentName;
}
public void doWork() {
System.out.println("doWork01--componentName:"+componentName);
}
}
<!--指明注解扫描基准路径
base-package:会扫描该包及其子包下的注解-->
<context:component-scan base-package="com.atguigu.pojo"></context:component-scan>
2.引用类型:HappyMachine的注解实现IoC (@Autowired)
使用@Autowired 标记引用类型的成员变量(除了String),实现自动装配。
@Autowired可以写在成员变量、setter方法、构造方法上,均是通过反射实现注入
建议写在成员变量上
@Component
public class HappyMachine {
@Value("BYD")
private String machineName;//简单属性
@Autowired //自动装配
private HappyComponent happyComponent;//复杂属性
@Autowired
public void setHappyComponent(HappyComponent happyComponent) {
this.happyComponent = happyComponent;
}
@Autowired //构造器中不能有非引用参数
public HappyMachine(HappyComponent happyComponent) {
this.happyComponent = happyComponent;
}
public HappyMachine() {
}
public HappyMachine(String machineName, HappyComponent happyComponent) {
this.machineName = machineName;
this.happyComponent = happyComponent;
}
}
二、分层开发自动装配 ★
控制层
@Controller //底层就是Component注解,为了可读性,应用于控制层
public class UserController {
@Autowired //自动装配
private UserService userService;
public void addUser(){
userService.addUser();
}
}
业务逻辑层
@Service(value = "userService") //默认的名称:userServiceImpl
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void addUser() {
userDao.saveUser();
}
}
持久化层
@Repository(value = "userDao") //底层就是Component注解,为了可读性,应用于Dao层
public class UserDaoImpl implements UserDao {
@Override
public void saveUser() {
System.out.println("UserDaoImpl: saveUser");
}
}
spring.xml
<!--指定注解扫描基准路径
会自动扫描base-package及其子包下的注解-->
<context:component-scan base-package="com.atguigu"></context:component-scan>
测试
@Test
public void testIoC2_2(){
//创建IoC的容器
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring.xml");
//从IoC获取指定Bean
UserController userController = context.getBean("userController",UserController.class);
userController.addUser();
}
异常:没有这样的对象定义。
NoSuchBeanDefinitionException: No bean named 'userController' available
原因:配置文件之扫描了pojo包,要改一下,(包下找不到userController对象)
注解管理IoC的总结
1. @Autowired的过程 ★
-
按照ByType进行自动装配(如果只有一个,肯定可以装配成功)
-
如果存在多个同类型的Bean,就会尝试ByName(变量名与类名)。如果有匹配的name,自动装配成功。
-
如果没有匹配的name,就会尝试@Qualifier,匹配bean的name。
-
如果@Qualifier也不匹配,报异常。
2. <context:component-scan base-package>
-
base-package指定扫描的基准包
-
base-package 可以写多个,以逗号隔开
-
扫描创建Bean的注解:扫描org.springframework.stereotype包下的四个注解
3. 创建Bean的注解
-
@Component 通用性的注解,实际开发中可以替代@Controller @Service @Repository,但
-
@Controller 用在控制层
-
@Service 用在业务层
-
@Repository 用在DAO层
注解都写在实现类上,不能写在接口上。Bean的默认名称是类型首字母小写。
这四个注解本质是相同的,都是@Component,只是为了代码可读性。
4. 注入Bean的注解
-
@Value:注入基本类型的参数(setter方法 属性)
-
@Autowired:注入引用类型的Bean(setter方法 构造方法 属性)
-
@Qualifier:指定Bean的name;不会单独使用,而是和@Autowired配合使用
5. @Autowired的位置
-
setter方法:底层会使用反射调用setter方法 getMethod(“setUserDao”)
-
构造方法:底层会使用反射调用构造方法 getContructor()
-
成员变量:底层使用反射直接操作成员变量 getDeclaredField()
三、全注解开发 @ComponentScan
体验完全注解开发,是为了学习SpringBoot打基础。因为在SpringBoot中,就是完全舍弃XML配置文件,全面使用注解来完成主要的配置。
使用注解开发,需要实现两个功能,目前XML的作用主要是两个:组件扫描、配置外部Bean。
定义一个配置类
@Configuration //说明这是一个配置类
@ComponentScan(value = "com.atguigu")//组件扫描
public class SpringConfig {
//一般用于第三方Bean操作。第三方的Bean无法使用注解
@Bean //将当前方法中创建的Bean放入到IoC容器中,Bean的名称就是方法名
public DataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mybatis-example?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
dataSource.setUsername("root");
dataSource.setPassword("*******");
return dataSource;
}
}
测试类
//使用全注解开发
@Test
public void testAllAnnotation() throws SQLException {
//1.创建IoC容器 (使用全注解,此时xml已经不存在了)
//ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring.xml");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
//2.congIoC容器中获取Bean
DataSource dataSource = context.getBean("dataSource", DataSource.class);
UserController userController = context.getBean("userController", UserController.class);
//3.使用Bean
System.out.println(dataSource);
userController.addUser();
}
四、Spring5整合Junit4 @RunWith @ContextConfiguration ★
- 好处1:不需要自己创建IOC容器对象了 (ClassPathXmlApplicationContext.getBean())
- 好处2:任何需要的bean都可以在测试类中直接享受自动装配
1. 添加依赖
<!-- Spring的测试包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.1</version>
</dependency>
2.创建测试类
// junit的@RunWith注解:指定Spring为Junit提供的运行器
@RunWith(SpringJUnit4ClassRunner.class)
// Spring的@ContextConfiguration指定Spring配置文件的位置
@ContextConfiguration(locations = {"classpath:spring.xml","classpath:spring2.xml"})
public class TestIoC3 {
@Autowired
private DataSource dataSource;
@Autowired
private UserController userController;
@Test
public void testIntegration() throws SQLException {
//减少了创建IoC容器、装配的操作,直接使用
System.out.println(dataSource.getConnection());
userController.addUser();
}
}
AOP (面向切面编程) ★★★
AOP (Aspect Oriented Programming) 面向切面编程。
通过预编译方式和运行期间动态代理,在不修改源代码的情况下,给程序动态统一的添加功能的一种技术,简称AOP。是spring框架的一个重要内容,可以说是对OOP (面向对象编程) 的补充和完善。
OOP解决纵向的业务问题。
AOP解决横向的问题,比如日志、安全验证、事务、异常
一、代理模式 ★★
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法时,不再是直接对目标方法进行调用,而是通过代理类间接调用,解耦。
代理类符合了开闭原则:对修改关闭,对扩展开放。
-
目标类(TargetSubject):具体实现业务功能的类 (核心逻辑代码);
-
代理类(ProxyObject):提供一些被代理类功能(核心逻辑代码)之外的额外功能的类;
-
请求类(RequestObject):业务调用类。
Day34.反射-动态代理、动态代理与AOP_焰火青年·的博客-CSDN博客
1. 静态代理
优点:
不需要去修改目标类代码,就可以增加新的功能(比如日志)。目标类只关注业务即可
代理所有的实现了该接口的目标类。
缺点:
在编译时就已经将接口、被代理类、代理类等确定下来,只能代理一个接口的目标类。代码都写死了,不具备任何的灵活性,共同方法没有进行统一管理。
静态代理类是需要开发者亲自写代码开发出来的,看得见摸得着的。
定义静态代理类,要求代理类实现和目标类实现一样的接口
/*
* 静态代理: 实现了角色分离
* 关键点:
* 关联和目标类一样的接口,可以来指向真正的目标类,从而调用目标类方法
* */
public class ShoesFactoryStaticProxy implements ShoesFactory {
//相同的接口
ShoesFactory shoesFactory;
//构造器,set方法,指定目标类
public ShoesFactoryStaticProxy(ShoesFactory shoesFactory) {
this.shoesFactory = shoesFactory;
}
public void setShoesFactory(ShoesFactory shoesFactory) {
this.shoesFactory = shoesFactory;
}
//调用方法
@Override
public Shoes production() {
beforeLog(); //日志方法
Shoes shoes = shoesFactory.production();
afterLog();
return shoes;
}
//日志方法
public void beforeLog(){
System.out.println("[日志] 生产开始了!");
}
public void afterLog(){
System.out.println("[日志] 生产结束了!");
}
}
测试静态代理类
public class Test {
public static void main(String[] args) {
//创建一个经纪人
CalculatorLogStaticProxy proxy = new CalculatorLogStaticProxy();
//指定代理类代理谁
Calculator calculator = new CalculatorPureImpl();
proxy.setCalculator(calculator);
//调用代理类的方法
int result = proxy.add(10, 20);
System.out.println(result);
}
}
目标类
//鞋子
public class Shoes {
public String name;
}
//鞋子制造厂
public interface ShoesFactory {
public Shoes production();
}
//阿迪达斯制造厂
public class AdidasShoesFactoryImpl implements ShoesFactory {
@Override
public Shoes production() {
Shoes shoes = new Shoes("阿迪达斯");
return shoes;
}
}
//耐克制造厂
public class NikeShoesFactoryImpl implements ShoesFactory {
@Override
public Shoes production() {
Shoes shoes = new Shoes("耐克");
return shoes;
}
}
2. 动态代理
代理类在程序运行时创建的代理方式被成为动态代理。
相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法 (共同方法解耦)。
AOP底层原理就是动态代理技术。
定义动态代理工厂类
/*
* 日志代理类的工厂
* 代理日志功能,都是接口可以代理
* 工厂: 由这个类来产生代理对象并返回
* */
public class LogDynamicProxyFactory<T> {//泛型接口,可以传入目标接口
private T target; //指向目标类
public void setTarget(T target){
this.target = target;
}
//创建代理对象并返回
public T getProxy(){
//目标类的类加载器
ClassLoader classLoader = target.getClass().getClassLoader();
//目标类实现的接口
Class<?>[] interfaces = target.getClass().getInterfaces();
/*
* 代理对象要干什么?
* Object proxy: 代理对象
* Method method: 目标类的方法 比如add(int i,int j) saveUser()
* Object[] args: 目标类方法的参数: (int i,int j)
* */
InvocationHandler invocationHandler = (Object proxy, Method method, Object[] args) -> {
Object result = null;
try{
//新皇登基: 前置通知
System.out.println("[日志]" +method.getName()+ " 方法开始执行了,方法的参数是:"+ Arrays.toString(args));
result = method.invoke(target, args);//让大明星拍广告
//寿终正寝: 返回通知
System.out.println("[日志]" +method.getName()+ " 方法执行结束了,结果是:"+result);
}catch (Exception e){
//死于非命: 异常通知
System.out.println("[日志]" +method.getName()+ "方法出现了异常,异常信息:"+e.toString());
}finally {
//风光大葬: 后置通知
System.out.println("[日志] finally 收尾工作");
}
return result;
};
return (T) Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
}
}
测试类
public class TestDynamicProxy {
@Test
public void testCalculator(){
//创建动态代理工厂类的对象
LogDynamicProxyFactory<Calculator> factory = new LogDynamicProxyFactory();
factory.setTarget(new CalculatorPureImpl());
//由工厂创建动态代理对象
Calculator proxy = factory.getProxy();
int add = proxy.add(10, 20);
System.out.println(add);
}
@Test
public void testUser(){
//创建动态代理工厂类的对象
LogDynamicProxyFactory<UserDao> factory = new LogDynamicProxyFactory();
factory.setTarget(new UserDaoImpl());
//由工厂创建动态代理对象
UserDao proxy = factory.getProxy();
proxy.saveUser();
}
}
二、AOP术语 (核心概念) ★
概念 | 描述 | Spring中 |
---|---|---|
横切关注点 | 对哪些方法进行拦截,拦截后怎么处理 | |
通知|增强 advice | 拦截到连接点之后要做的事情。 | @Before 表现为一个方法。分为前置、后置、异常、最终、环绕五类 |
连接点 joinpoint | 在哪些地方加入通知? | 被拦截到的目标类的方法。add() sub) |
切入点 pointcut | 定位连接点的方式。 | 一个表达式,能涵盖所有的连接点。 |
切面 aspect | 类是对物体特征的抽象,切面就是对横切关注点的抽象。 | @Aspect 修饰的类切面是一个类,包括两部分:通知(做什么)+ 切入点(在哪做)。 |
目标 target | 被代理的目标对象。 | 被代理的目标对象。(如 NikeFactory) |
代理 proxy | 向目标对象应用通知之后创建的代理对象。 | (...DynamicProxy) |
织入 weaving | 把切面应用到目标上,生成代理对象的过程。 | 可以在编译期织入,也可以在运行期织入,Spring采用后者。 |
1、横切关注点
-
纵向关注点(核心关注点 业务关注点):add() sub() findAll() insert() OOP解决纵向关注点
-
横切关注点(非业务的功能) 日志、安全验证、事务、异常。AOP解决横切关注点
2、通知|增强 (advice)
每一个横切关注点上要做的事情称为通知。通知需要写一个方法来实现。也被称为增强。
就是AOP的要做的事情,比如写日志,比如要进行事务处理。在Spring中,通知表现为一个方法。
-
前置通知:在被代理的目标方法前执行(新皇登基)@Before
-
返回通知:在被代理的目标方法成功结束后执行(寿终正寝)@AfterReturning
-
异常通知:在被代理的目标方法异常结束后执行(死于非命)@AfterThrowing
-
后置通知:在被代理的目标方法最终结束后执行(风光大葬)@After
-
环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置和功能。
3、连接点 (joinpoint)
在什么地方加入通知呢? 在add() sub() findAll() 在Spring中,连接点是一个方法
这也是一个纯逻辑概念,不是语法定义的。指那些被拦截到的点。在 Spring 中,可以被动态代理拦截目标类的方法。
4、切入点 (pointcut)
一个表达式,能涵盖所有的连接点的表达式。
execution(* com.atguigu.service.CalculatorImpl.*(..)) 作用与所有方法
execution(* com.atguigu.service.CalculatorImpl.add(..)) 只作用域add方法
过滤器:<url-pattern>*.html</url-pattern> 切入点 (过滤以html结尾的)
连接点:http://www.atguigui.com/adfa/adfa/index.html 连接点 (过滤具体的请求路径)
<filter-mapping>
<filter-name>AdminFilter</filter-name>
<url-pattern>*.html</url-pattern>
</filter-mapping>
5、切面 (aspect)
切入点和通知的结合。是一个类,通知(干什么)+切入点(在哪里干)
类比过滤器:定义一个类(EncodingFilter)+过滤路径(<url-pattern>*.html</url-pattern>)
6、目标( target)
被代理的目标对象。(UserDaoImpl CalculatorPureImpl )
7、代理 (proxy) (静态代理和动态代理)
向目标对象应用通知之后创建的代理对象。
8、织入 (weave) (目标 + 通知 --> 代理对象)
指把通知应用到目标上,生成代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。
9、切面和过滤器有什么共同点和不同点
共同点:都可以用来解决横切性问题,比如日志、事务
不同点:
Filter必须是Web项目,离不开Servlet容器。 AOP没有这个要求
Filter过滤是http请求的路径,AOP过滤的是方法(哪些包下哪些类的哪些方法)
三、AOP第一个示例:添加日志(准备)
总体思路:使用OOP实现计算器的业务功能,使用AOP在不修改计算器代码的基础上增强日志功能。
1. 添加依赖
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- Spring的测试包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.1</version>
</dependency>
2. 定义目标接口和目标类
//计算器接口
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
//实现类
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
3. 配置(扫描注解)
<!--指明注解扫描基准路径 base-package:会扫描该包及其子包下的注解-->
<context:component-scan base-package="com.atguigu"></context:component-scan>
4. 测试
// junit的@RunWith注解:指定Spring为Junit提供的运行器
@RunWith(SpringJUnit4ClassRunner.class)
// Spring的@ContextConfiguration指定Spring配置文件的位置
@ContextConfiguration(locations = "classpath:spring.xml")
public class TestAOP {
@Autowired//引用类型自动匹配
Calculator calculator;
@Test
public void test(){
int result = this.calculator.add(10, 20);
System.out.println(result);
System.out.println("---------------------------");
result = this.calculator.div(10, 0);
System.out.println(result);
}
}
四、AOP第一个示例:添加日志(实现)@Aspect @Before..
1. 添加依赖
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
2. 定义切面(通知+切入点)
@Component //组件扫描,被<context:component-scan>扫描到,创建对象并放入IoC容器
@Aspect //这是一个切面
public class LogAspect {
//前置通知: 指定的方法在执行之前要做什么 (切入点,表达式)
@Before("execution(* com.atguigu.service.impl.CalculatorImpl.*(..))")
public void beforeLog(){
System.out.println("[日志] xxx 方法开始执行,参数是:");
}
//返回通知
@AfterReturning("execution(* com.atguigu.service.impl.CalculatorImpl.*(..))")
public void afterReturningLog(){
System.out.println("[日志]" +" 方法执行结束了,结果是:");
}
//异常通知
@AfterThrowing("execution(* com.atguigu.service.impl.CalculatorImpl.*(..))")
public void afterThrowingLog(){
System.out.println("[日志]" +"方法出现了异常,异常信息:");
}
//后置通知
@After("execution(* com.atguigu.service.impl.CalculatorImpl.*(..))")
public void afterLog(){
System.out.println("[日志] finally 收尾工作");
}
}
3. 配置文件中启动AOP
<!-- 注解扫描:@Component @Service @Controller @Reposity-->
<context:component-scan base-package="com.atguigu"></context:component-scan>
<!-- 启动AOP自动代理-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
4. 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring.xml")
public class TestAOP {
@Autowired
Calculator calculator;
@Test
public void testAdd(){
int add = calculator.add(10, 20);
System.out.println(add);
System.out.println("-------");
int div = calculator.div(10, 0);
System.out.println(div);
System.out.println(div);
}
}
五、重用切入点 @Pointcut
在一处声明切入点表达式之后,其他有需要的地方引用这个切入点表达式。易于维护,一处修改,处处生效。
可以创建存放切入点表达式的类,可以把整个项目中所有切入点表达式全部集中过来,便于管理。
//重用切入点
@Pointcut("execution(* com.atguigu.service.impl.CalculatorImpl.*(..))")
public void pointcut1(){
}
同一个类中使用:
//前置通知: 指定的方法在执行之前要做什么 (切入点,表达式)
@Before("pointcut1()")
public void beforeLog(){
...
}
不同类中使用:
@Around(value = "com.atguigu.aspect.LogAspect.pointcut1()")
public Object roundAdvice(ProceedingJoinPoint joinPoint) {
}
六、获取连接点 (信息)
1、方法信息 (JoinPoint接口)
org.aspectj.lang.JoinPoint
- 要点1:JoinPoint接口通过 getSignature() 方法获取目标方法的签名
- 要点2:通过目标方法签名对象获取方法名
- 要点3:通过JoinPoint 对象获取外界调用目标方法时传入的实参列表组成的数
@Before("pointcut1()")
public void beforeLog(JoinPoint joinPoint){ //连接点
Object[] args = joinPoint.getArgs();//获取方法参数
Signature signature = joinPoint.getSignature();//获取签名
String name = signature.getName();//获取方法名
System.out.println("[日志] "+name+" 方法开始执行,参数是:"+Arrays.toString(args));
}
2、方法返回值
1.在 @AfterReturning 中通过 returning 属性设置一个名称
2.使用returning 属性设置的名称在通知方法中声明一个对应的形参
//返回通知
@AfterReturning(value = "pointcut1()",returning = "result")
public void afterReturningLog(Object result){ //返回值 注意类型是Object
System.out.println("[日志]" +" 方法执行结束了,结果是:" +result);
}
3、目标方法抛出的异常
在@AfterThrowing 中声明 throwing 属性设定形参名称
使用 throwing 属性指定的名称在通知方法声明形参
@AfterThrowing(value = "pointcut1()",throwing = "exception")
public void afterThrowingLog(JoinPoint joinPoint,Exception exception){
Signature signature = joinPoint.getSignature();//获取修饰符
String name = signature.getName();
System.out.println("[日志]"+ name +"方法出现了异常,异常信息:"+exception.toString());
}
七、切入点表达式语法 execution(* ... .method())
切入点最终会落到方法上面。
1. 某包某类的无参数的方法::execution(* com.atguigu.service.impl.Student.test())
2. 某包某类带有参数的方法: execution(* com.atguigu.service.impl.Student.test(String,int))
3. 某包某类的某个同名的所有方法:execution(* com.atguigu.service.impl.Student.test(..))
.. 表示任意个数任意类型的参数
4. 某类的所有方法:execution(* com.atguigu.service.impl.Student. * (..))
* 表示任意的类名,方法名,包名
5. 所有类的所有方法:execution(* com.atguigu.service.impl.* . *(..))
- * *.Student.test(..) 包只有一级,包名任意,com.Student,cn.Student
- * *..Student.test(..) 所有层级的包都匹配,可以是com.Student,com.atguigu.Student
- * com.atguigu.service.impl.*Service.test() 指定包下类名须是Service结尾
- execution(public int *..*Service.*(.., int)) 可以
- execution(* int *..*Service.*(.., int)) 修饰符任意 不可以
- execution(public * *..*Service.*(.., int)) 返回值任意 可以
八、环绕通知 @Around (关于事物的切面) ★
环绕通知对应整个try...catch...finally结构,包括四种通知的所有功能,属于一个通知。
注意:环绕通知需要返回 指定连接点方法的 返回值。
//环绕通知: 关于事物的切面
@Aspect //声明为一个切面类
@Component
public class TransactionAspect {
//CalculatorImpl.*(..)) 链接点为该类下的所有方法
@Around("execution(* com.atguigu.service.impl.CalculatorImpl.*(..))")
public Object doTransaction (ProceedingJoinPoint pjp){
//调用目标方法
Object proceed = null;
try{
//设置事物不在自动所提交
System.out.println("[事务] 设置事物不再自动提交 conn.setAutoCommit(false)");
//调用方法
proceed = pjp.proceed();
//提交事务
System.out.println("[事务] 成功结束,手动提交 conn.commit()");
}catch (Throwable e){ //Throwable Exception父类
e.printStackTrace();
//手动回滚事务
System.out.println("[事务] 异常结束,手动回滚 conn.rollback()");
}finally {
//关闭资源
System.out.println("[事务] 无论成功失败,关闭链接 conn.close()");
}
//返回调用方法的返回值
return proceed;
}
}
九、多个切面执行顺序 (切面优先级) @Order
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
通过加入@Order(int) 参数越小越早执行。
@Aspect //声明为切面类
@Component
@Order(2)//越小越早执行
public class TransactionAspect {
@Component //组件扫描,被<context:component-scan>扫描到,创建对象并放入IoC容器
@Aspect //这是一个切面
@Order(3)//越小越早执行
public class LogAspect {
十、有无接口的动态代理技术 (JDK、CGLIB) ★
两种动态代理技术:
JDK:SUN公司官方提供的,Proxy为JDK动态代理类的父类
CGLIB: 第三方提供的动态代理的技术,更加强大
- 情况1:没有切面:创建的当前类对象自身,直接从IoC容器获取,没有使用动态代理。
- 情况2:
使用切面,但是bean对应的类有接口(CalculatorPureImpl底层自动使用 JDK 的动态代理)
使用切面,但是bean对应的类没有接口(HappyComponent 自动的使用 CGLIB 的动态代理)
- 情况3:使用切面,不管是否有接口,都使用CGLIB (通过配置AOP自动代理)
<!-- 启动AOP自动代理
proxy-target-class:底层采用什么动态代理技术
false : 默认 有接口使用JDK动态代理,没有接口使用CGLIB
true:不管是否有接口,都使用CGLIB
-->
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
问题1:为什么使用JDK动态代理,目标Bean对象必须有接口
因为JDK动态代理生成的代理类已继承了Proxy,而Java是单继承的,所以必须有接口。
问题2:为什么使用CGLIB实现动态代理,目标Bean对象不要求有接口
因为CGLIB生成的代理类没有固定的父类,所以可以直接继承目标类,不需要接口。
十一、应用AOP 对 IoC中获取bean的影响 ★
1、情况一:没有使用AOP切面
根据bean本身、或接口的类型,正常获取到IOC容器中的那个bean对象
注意:使用接口,IoC容器中只能有一个实现类;使用实现类,IoC容器中有一个实例。
否则抛出异常:NoUniqueBeanDefinitionException,表示IOC容器中这个类型的bean有多个。
2、情况2: 使用AOP,但是使用JDK动态代理(Proxy)
<aop:aspectj-autoproxy proxy-target-class="false"/>
使用接口可以获取,使用实现类不可以。原因,JDK动态代理继承了Proxy,而java是单继承
应用了切面后,真正放在IOC容器中的是代理类的对象,目标类并没有被放到IOC容器中,所以根据目标类的类型从IOC容器中是找不到的。
从内存分析的角度来说,IOC容器中引用的是代理对象,代理对象引用的是目标对象。IOC容器并没有直接引用目标对象,所以根据目标类本身在IOC容器范围内查找不到。
3、情况3: 使用AOP,但是使用CGLIB
<aop:aspectj-autoproxy proxy-target-class="true"/>
使用接口、实现类都可以获取。
十一、AOP 使用XML配置(了解)
目前Spring的开发以注解开发为主。XML配置基本不使用。
spring.xml
<!--配置目标类 -->
<bean id="calculator" class="com.atguigu.service.impl.CalculatorPureImpl"></bean>
<!-- 配置切面类-->
<bean id="logAspect" class="com.atguigu.aspect.LogAspect"></bean>
<!-- 配置切面-->
<aop:config>
<!--配置日志切面 -->
<aop:aspect ref="logAspect" order="3">
<!-- 配置切入点-->
<aop:pointcut id="pointcut1" expression="execution(public * com.atguigu.service.impl.*.*(..))"/>
<!--配置各种通知-->
<aop:before method="beforeLog" pointcut-ref="pointcut1"></aop:before>
<aop:after-returning method="afterReturningLog"
pointcut-ref="pointcut1"
returning="resultValue"></aop:after-returning>
<aop:after-throwing method="afterThrowingLog"
pointcut-ref="pointcut1"
throwing="exception"></aop:after-throwing>
<aop:after method="afterLog" pointcut-ref="pointcut1"></aop:after>
</aop:aspect>
<!--配置事务切面...待补充 -->
</aop:config>
对应使用注解方式
@Component //组件扫描,被<context:component-scan>扫描到,创建对象并放入IoC容器
@Aspect //这是一个切面
@Order(3)//越小越早执行
public class LogAspect {
//重用切入点
@Pointcut("execution(* com.atguigu.service.impl.CalculatorImpl.*(..))")
public void pointcut1(){
}
//前置通知: 指定的方法在执行之前要做什么 (切入点,表达式)
@Before("pointcut1()")
public void beforeLog(JoinPoint joinPoint){ //连接点
Object[] args = joinPoint.getArgs();
Signature signature = joinPoint.getSignature();//获取修饰符
String name = signature.getName();
System.out.println("[日志] "+name+" 方法开始执行,参数是:"+Arrays.toString(args));
}
//返回通知
@AfterReturning(value = "pointcut1()",returning = "result")
public void afterReturningLog(Object result){ //返回值 注意类型是Object
System.out.println("[日志]" +" 方法执行结束了,结果是:" +result);
}
//异常通知
@AfterThrowing(value = "pointcut1()",throwing = "exception")
public void afterThrowingLog(JoinPoint joinPoint,Exception exception){
Signature signature = joinPoint.getSignature();//获取修饰符
String name = signature.getName();
System.out.println("[日志]"+ name +"方法出现了异常,异常信息:"+exception.toString());
}
//后置通知
@After("pointcut1()")
public void afterLog(){
System.out.println("[日志] finally 收尾工作");
}
}