优质全套Spring全套教程一https://developer.aliyun.com/article/1491418
2.2.14、实验十四:基于xml的自动装配
补充命名规则:
- 这里接口就叫什么什么Service(Controller),如UserService,其它地方规则一样
- 实现类的话在后面加上impl
自动装配:
根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或接口类
我们不需要自己写property了
型属性赋值
给IOC容器赋值的时候,其实只需要找到IOC容器所对应的Bean对象,通过ref来引用这个Bean的id,我们就可以把这个Bean所对应的对象来为类类型或者接口类型的属性赋值
三层架构:表述层、业务层、持久层
Service是业务层,Dao是数据访问层,这样的分层是基于MVC架构来说的,分层的主要作用是解耦。 对于Spring这样的框架,(View\Web)表示层调用控制层(Controller),控制层调用业务层(Service),业务层调用数据访问层(Dao)
学了框架就不需要用Servlet了,所以说我们控制层就叫做什么什么Controller了
①场景模拟
我们后期学SpringMVC时候控制层是SpringMVC所封装的一个servlet来统一访问的,下面是模拟,自己访问UserController
学了IOC,我们就可以把类交给IOC管理,解耦合
控制层调用Service,Service层调用Dao层实现持久化操作
(这么做的意思就是:三层架构只做自己该做的事,不要越级去干别的层该做的事!!)
要把下面这些交给IOC容器管理
也要把它们之间的关系教给IOC容器
autowire就是自动装配的意思
bean里面的Class不能写接口!一定要写具体类型
别忘记创建无参构造!
创建类UserController
public class UserController { private UserService userService; public void setUserService(UserService userService) { this.userService = userService; } public void saveUser(){ userService.saveUser(); } }
创建接口UserService
public interface UserService { void saveUser(); }
创建类UserServiceImpl实现接口UserService
public class UserServiceImpl implements UserService { private UserDao userDao; public void setUserDao(UserDao userDao) { this.userDao = userDao; } @Override public void saveUser() { userDao.saveUser(); } }
创建接口UserDao
public interface UserDao { void saveUser(); }
创建类UserDaoImpl实现接口UserDao
public class UserDaoImpl implements UserDao { @Override public void saveUser() { System.out.println("保存成功"); } }
测试
public class FactoryBeanTest { @Test public void testFactoryBean(){ ApplicationContext ioc=new ClassPathXmlApplicationContext("spring-autowire-xml.xml"); UserController user = ioc.getBean(UserController.class); user.saveUser(); } }
spring-autowire-xml.xml
- 实现自动装配后下面的property就不用写了
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--把三层架构的三个主键交给IOC管理了,接下来还要管理它们的依赖关系--> <bean id="userController" class="com.atguigu.spring.controller.UserController"> <property name="userService" ref="userService"></property> </bean> <bean id="userService" class="com.atguigu.spring.service.impl.UserServiceImpl"> <!--Service里面还要管理Dao--> <property name="userDao" ref="userDao"></property> </bean> <bean id="userDao" class="com.atguigu.spring.dao.impl.UserDaoImpl"></bean> </beans>
②配置bean
使用bean标签的autowire属性设置自动装配效果
自动装配方式:byType
byType:根据类型匹配IOC容器中的某个兼容类型的bean,为属性自动赋值
若在IOC中,没有任何一个兼容类型的bean能够为属性赋值,则该属性不装配,即值为默认值
null
若在IOC中,有多个兼容类型的bean能够为属性赋值,则抛出异常
NoUniqueBeanDefinitionException
UserController中有private UserService userService; userService这是一个接口(根据类型会给userService赋值)
UserServiceImp中有private UserDao userDao; userDao这是一个接口(UserServiceImple能和userService匹配-接口和实现类)
UserDaoImpl中有覆盖了UserDao中的saveUser()方法
<bean id="userController" class="com.atguigu.autowire.xml.controller.UserController" autowire="byType"> </bean> <bean id="userService" class="com.atguigu.autowire.xml.service.impl.UserServiceImpl" autowire="byType"> </bean> <bean id="userDao" class="com.atguigu.autowire.xml.dao.impl.UserDaoImpl"></bean>
自动装配方式:byName
byName:将自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值
<bean id="userController"class="com.atguigu.autowire.xml.controller.UserController" autowire="byName"> </bean> <bean id="userService"class="com.atguigu.autowire.xml.service.impl.UserServiceImpl" autowire="byName"> </bean> <bean id="userServiceImpl"class="com.atguigu.autowire.xml.service.impl.UserServiceImpl" autowire="byName"> </bean> <bean id="userDao" class="com.atguigu.autowire.xml.dao.impl.UserDaoImpl"> </bean> <bean id="userDaoImpl" class="com.atguigu.autowire.xml.dao.impl.UserDaoImpl"> </bean>
③测试
@Test public void testAutoWireByXML(){ ApplicationContext ac = new ClassPathXmlApplicationContext("autowire-xml.xml"); UserController userController = ac.getBean(UserController.class); userController.saveUser(); }
自动装配总结:
public class AutowireByXMLTest { /** * 自动装配: * 根据指定的策略,在IOC容器中匹配某个bean(是bean,不是属性!),自动为bean中的类类型的属性或接口类型的属性赋值 * 可以通过bean标签中的autowire属性设置自动装配的策略 * 自动装配的策略: * 1、no,default:表示不装配,即bean中的属性不会自动匹配某个bean为属性赋值,此时属性使用默认值成员变量的默认值) * 2、byType:根据要赋值的属性的类型,在IOC容器中匹配某个bean,为属性赋值 * 注意: * a>若通过类型没有找到任何一个类型匹配的bean,此时不装配,属性使用默认值,默认值null则报空指针异常 * b>若通过类型找到了多个类型匹配的bean,此时会抛出异常:NoUniqueBeanDefinitionException * 总结:当使用byType实现自动装配时,IOC容器中有且只有一个类型匹配的bean能够为属性赋值 * 3、byName:将要赋值的属性的属性名作为bean的id在IOC容器中匹配某个bean,为属性赋值(很少用到) * 总结:当类型匹配的bean有多个时,此时可以使用byName实现自动装配(当byType不能用的时候再考虑byName) */ @Test public void testAutowire(){ ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-autowire-xml.xml"); UserController userController = ioc.getBean(UserController.class); userController.saveUser(); } }
自动装配autowire用的很多,但是基于xml的自动装配用的不多,因为配置到xml中是针对于我们所有的bean类型的属性和接口类型的属性
我们如果想要为其中的某些属性赋值是做不到的,它都会通过自动装配来找到bean为其赋值
所以我们延伸了基于注解的方式,想要自动装配在其成员变量上加上注解即可
2.3、基于注解管理bean
2.3.1、实验一:标记与扫描
①注解
和 XML 配置文件一样,注解本身并不能执行,注解本身仅仅只是做一个标记,具体的功能是框架检测
到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作。
本质上:所有一切的操作都是Java代码来完成的,XML和注解只是告诉框架中的Java代码如何执行。
举例:元旦联欢会要布置教室,蓝色的地方贴上元旦快乐四个字,红色的地方贴上拉花,黄色的地方贴上气球。
班长做了所有标记,同学们来完成具体工作。墙上的标记相当于我们在代码中使用的注解,后面同学们做的工作,相当于框架的具体操作。
②扫描
Spring 为了知道程序员在哪些地方标记了什么注解,就需要通过扫描的方式,来进行检测。然后根据注解进行后续操作。
- 加了注解就是实现相对应的功能,没加则不执行
- 单单加上注解还不行,还需要扫描
- 比如根据注解自动配置相应的bean标签
③新建Maven Module
<dependencies> <!-- 基于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> </dependencies>
④创建Spring配置文件
⑤标识组件的常用注解
- IOC容器中根标签是beans,beans由多个bean组成,每一个bean都是IOC中的一个组件
- 加一个注解就相当于配置了一个bean,不能加载接口上面
@Component:将类标识为普通组件
@Controller:将类标识为控制层组件
@Service:将类标识为业务层组件
@Repository:将类标识为持久层组件
问:以上四个注解有什么关系和区别?
通过查看源码我们得知,@Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。(功能都是将一个类表识为一个组件,只不过各种的含义不一样)
对于Spring使用IOC容器管理这些组件来说没有区别。所以@Controller、@Service、@Repository这
三个注解只是给开发人员看的,让我们能够便于分辨组件的作用。
注意:虽然它们本质上一样,但是为了代码的可读性,为了程序结构严谨我们肯定不能随便胡乱标记。
- 注意注解在类上面,而不是接口上面,创建bean时候class对应的不是接口!
⑥创建组件
创建控制层组件
@Controller public class UserController { }
创建接口UserService
public interface UserService { }
创建业务层组件UserServiceImpl
@Service public class UserServiceImpl implements UserService { }
创建接口UserDao
public interface UserDao { }
创建持久层组件UserDaoImpl
@Repository public class UserDaoImpl implements UserDao { }
⑦扫描组件
拓展:spring是要和springMVC一起使用的,SpringMVC扫描的是控制层,Spring扫描的是控制层以外的所有的组件
不能让Spring来扫描控制层,不然会造成多次扫描
情况一:最基本的扫描方式
- base-package扫描包下面的组件
- 将这些类作为组件交给IOC容器来作为组件进行管理
- 也可以中间加,逗号来指定扫描的组件
<context:component-scan base-package="com.atguigu"> </context:component-scan>
情况二:指定要排除的组件
下面有两个子标签 context:exclude-filter,context:include-filter
context:component-scan base-package="com.atguigu"> <!-- context:exclude-filter标签:指定排除规则 --> <!-- type:设置排除或包含的依据 type="annotation",根据注解排除,expression中设置要排除的【注解】的全类名 下面org开头的 type="assignable",根据类型排除,expression中设置要排除的【类型】的全类名 下面包名开头的 --> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> <!--<context:exclude-filter type="assignable"expression="com.atguigu.controller.UserController"/>--> </context:component-scan>
情况三:仅扫描指定组件
仅扫描谁
<context:component-scan base-package="com.atguigu" use-default-filters="false"> <!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 --> <!-- use-default-filters属性:取值false表示关闭默认扫描规则 --> <!-- 此时必须设置use-default-filters="false",因为默认规则即扫描指定包下所有类,父标签肯定大于子标签 --> <!-- 用的多的还是exclude-filter 如果仅扫描还不如在context:component-scan上直接写呢--> <!-- type:设置排除或包含的依据 type="annotation",根据注解排除,expression中设置要排除的注解的全类名 type="assignable",根据类型排除,expression中设置要排除的类型的全类名 --> <context:include-filter type="annotation"expression="org.springframework.stereotype.Controller"/> <!--<context:include-filter type="assignable"expression="com.atguigu.controller.UserController"/>--> </context:component-scan>
⑧测试
@Test public void testAutowireByAnnotation(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); UserController userController = ac.getBean(UserController.class); System.out.println(userController); UserService userService = ac.getBean(UserService.class); System.out.println(userService); UserDao userDao = ac.getBean(UserDao.class); System.out.println(userDao); }
⑨组件所对应的bean的id
在我们使用XML方式管理bean的时候,每个bean都有一个唯一标识,便于在其他地方引用。现在使用
注解后,每个组件仍然应该有一个唯一标识。
默认情况
类名首字母小写就是bean的id。例如:UserController类对应的bean的id就是userController(类的小驼峰)。
自定义bean的id 可通过标识组件的注解的value属性设置自定义的bean的id @Service("userService") //默认为userServiceImpl public class UserServiceImpl implements UserService {}
一般不会去指定它的id,我们都是根据类名获取bean的,在SpringMVC中有个文件上传信息中需要加id,不加id和没配置是一样的
2.3.2、实验二:基于注解的自动装配
①场景模拟
参考基于xml的自动装配
在UserController中声明UserService对象
在UserServiceImpl中声明UserDao对象
②@Autowired注解
在成员变量上直接标记@Autowired注解即可完成自动装配,注解装配不需要提供setXxx()方法。以后我们在项
目中的正式用法就是这样。
@Controller public class UserController { @Autowired private UserService userService; public void saveUser(){ userService.saveUser(); } } public interface UserService { void saveUser(); } @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override public void saveUser() { userDao.saveUser(); } } public interface UserDao { void saveUser(); } @Repository public class UserDaoImpl implements UserDao { @Override public void saveUser() { System.out.println("保存成功"); } }
③@Autowired注解其他细节
@Autowired注解可以标记在成员变量(添加成员变量上set就不需要创建了)构造器和set方法上
建议使用成员变量这一种即可
@Controller public class UserController { private UserService userService; @Autowired public UserController(UserService userService){ this.userService = userService; } public void saveUser(){ userService.saveUser(); } } @Controller public class UserController { private UserService userService; @Autowired public void setUserService(UserService userService){ this.userService = userService; } public void saveUser(){ userService.saveUser(); } }
④@Autowired工作流程
- 默认情况注解的类名首字母小写就是bean的id
首先根据所需要的组件类型到IOC容器中查找
- 能够找到唯一的bean:直接执行装配
- 如果完全找不到匹配这个类型的bean:装配失败
和所需类型匹配的bean不止一个
- 没有@Qualifier注解:根据@Autowired标记位置成员变量的变量名作为bean的id进行匹配
- 能够找到:执行装配
- 找不到:装配失败
- 使用@Qualifier注解:根据@Qualifier注解中指定的名称作为bean的id进行匹配
- 能够找到:执行装配
- 找不到:装配失败
@Controller public class UserController { @Autowired @Qualifier("userServiceImpl") private UserService userService; public void saveUser(){ userService.saveUser(); } }
可以将属性required的值设置为true,则表示能装就装,装不上就不装,此时自动装配的属性为
默认值
但是实际开发时,基本上所有需要装配组件的地方都是必须装配的,用不上这个属性。
拓展:
@Qualifier("userServiceImpl") 是 Spring 框架中的一个注解,用于指定注入 Bean 的名称。当一个接口有多个实现类时,使用 @Qualifier 注解可以指定要注入的 Bean 的名称,避免出现歧义。 在 Spring 中,当一个接口有多个实现类时,使用 @Autowired 注解自动注入 Bean 时会出现歧义,因为 Spring 不知道要注入哪个实现类的 Bean。这时可以使用 @Qualifier 注解指定要注入的 Bean 的名称。
例如,假设有一个 UserService 接口和两个实现类 UserServiceImpl1 和 UserServiceImpl2,如果要在另一个类中注入 UserService 的 Bean,可以使用以下代码:
@Autowired @Qualifier("userServiceImpl1") private UserService userService;
需要注意的是,@Qualifier 注解需要与 @Autowired 注解一起使用,@Qualifier 注解指定要注入的 Bean 的名称,@Autowired 注解实现自动注入。
总之,@Qualifier("userServiceImpl") 注解是 Spring 框架中的一个注解,用于指定注入
IOCByAnnotationTest.java
package com.atguigu.spring.test; import com.atguigu.spring.controller.UserController; import com.atguigu.spring.dao.UserDao; import com.atguigu.spring.service.UserService; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class IOCByAnnotationTest { /** * @Component:将类标识为普通组件 * @Controller:将类标识为控制层组件 * @Service:将类标识为业务层组件 * @Repository:将类标识为持久层组件 * * 通过注解+扫描所配置的bean的id,默认值为类的小驼峰,即类名的首字母为小写的结果(使用的是小驼峰命名法,则在查找匹配的Bean时,Spring会自动将小驼峰形式的Bean名称转换为大驼峰形式,为了清晰、便于理解) * 使用@Component注解标记一个类为Bean时,可以使用value属性来指定Bean的名称,如果不指定名称,则默认使用类名的小驼 * 峰形式作为Bean的名称。例如: * @Component(value = "userService") * public class UserServiceImpl implements UserService { ... * } * 可以通过标识组件的注解的value属性值设置bean的自定义的id * * @Autowired:实现自动装配功能的注解 * 1、@Autowired注解能够标识的位置 * a>标识在成员变量上,此时不需要设置成员变量的set方法 * b>标识在set方法上 * c>标识在为当前成员变量赋值的有参构造上 * 2、@Autowired注解的原理 * a>默认通过byType的方式,在IOC容器中通过类型匹配某个bean为属性赋值(想一想通过byName也不可能,注解默认配置的id是类的小驼峰,和类名不匹配) * 已经扫描过了又配置的bean * b>若有多个相同类型匹配的bean(同class),此时会自动转换为byName的方式实现自动装配的效果 * 即将要赋值的属性的属性名作为bean的id匹配IOC容器中的bean为属性赋值 * c>若byType和byName的方式都无妨实现自动装配,即IOC容器中有多个类型匹配的bean * 且这些bean的id和要赋值的属性的属性名都不一致,此时抛异常:NoUniqueBeanDefinitionException * d>此时可以在要赋值的属性上,添加一个注解@Qualifier(以后不会有那么多情况) * 通过该注解的value属性值,指定某个bean的id,将这个bean为属性赋值(这个是通过byName赋值的,因为有多个同类型的bean) * * 注意:若IOC容器中没有任何一个类型匹配的bean,此时抛出异常:NoSuchBeanDefinitionException * 在@Autowired注解中有个属性required,默认值为true,要求必须完成自动装配 * 可以将required设置为false,此时能装配则装配,无法装配则使用属性的默认值 */ @Test public void test(){ ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc-annotation.xml"); UserController userController = ioc.getBean("controller", UserController.class); /*System.out.println(userController); UserService userService = ioc.getBean("userServiceImpl", UserService.class); System.out.println(userService); UserDao userDao = ioc.getBean("userDaoImpl", UserDao.class); System.out.println(userDao);*/ userController.saveUser(); } }
报错分析:如果自动装配没有找到,在类上没有注解的话,会报异常NoSuchBeanDefinitionException(在自动装配过程中每没有找到任何一个能够匹配的bean),因为默认required=true,改为false之后,报空指针异常(因为不装配使用的是成员变量的默认值-这里是NULL)
- UserServiceImpl实现类没有加注解
IOC基于XML和基于注解管理bean都要熟练掌握
第三方jar包中的类交给IOC容器来管理,这个时候不能用注解,因为jar包里面放的都是class字节码文件,看到的都是idea反编译之后的结果,这是改不了的,是加不了注解的,我们只能通过XML方式
拓展:
说成员变量是属性是可以的,但实际上属性是和get、set方法有关的
3、AOP
AOP也是以IOC容器为基础的-面向方面(切面)编程(Aspect-Oriented Programming)
是OOP的一种补充和完善
spring最重要的核心肯定是IOC,这个也是在IOC基础上实现的
3.1、场景模拟
3.1.1、声明接口
声明计算器接口Calculator,包含加减乘除的抽象方法
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); }
3.1.2、创建实现类
package com.atguigu.spring.proxy; import javax.sql.rowset.CachedRowSet; /** * @author 即兴小索奇 * @version 1.0 * @date 22/12/31 20:30 * @description */ public class CalculatorImpl implements Calculator { @Override public int add(int i, int j) { System.out.println("日志,方法:add,参数:"+i+","+j); int result = i + j; System.out.println("方法内部 result = " + result); System.out.println("日志,方法:add,结果:"+result); return result; } @Override public int sub(int i, int j) { System.out.println("日志,方法:sub,参数:"+i+","+j); int result = i - j; System.out.println("方法内部 result = " + result); System.out.println("日志,方法:int,结果:"+result); return result; } @Override public int mul(int i, int j) { System.out.println("日志,方法:mul,参数:"+i+","+j); int result = i * j; System.out.println("方法内部 result = " + result); System.out.println("日志,方法:mul,结果:"+result); return result; } @Override public int div(int i, int j) { System.out.println("日志,方法:div,参数:"+i+","+j); int result = i / j; System.out.println("方法内部 result = " + result); System.out.println("日志,方法:div,结果:"+result); return result; } }
3.1.3、创建带日志功能的实现类
public class CalculatorLogImpl implements Calculator { @Override public int add(int i, int j) { System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j); int result = i + j; System.out.println("方法内部 result = " + result); System.out.println("[日志] add 方法结束了,结果是:" + result); return result; } @Override public int sub(int i, int j) { System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j); int result = i - j; System.out.println("方法内部 result = " + result); System.out.println("[日志] sub 方法结束了,结果是:" + result); return result; } @Override public int mul(int i, int j) { System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j); int result = i * j; System.out.println("方法内部 result = " + result); System.out.println("[日志] mul 方法结束了,结果是:" + result); return result; } @Override public int div(int i, int j) { System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j); int result = i / j; System.out.println("方法内部 result = " + result); System.out.println("[日志] div 方法结束了,结果是:" + result); return result; } }
3.1.4、提出问题
①现有代码缺陷
针对带日志功能的实现类,我们发现有如下缺陷:
- 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
- 附加功能分散在各个业务功能方法中,不利于统一维护
②解决思路
解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
③困难
解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决(以前面向对象是纵向继承机制,只能把一段连续的代码进行封装)。所以需要引入新的技术。
3.2、代理模式
3.2.1、概念
①介绍
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦(剥离到了代理类中)。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
使用代理后:
②生活中的代理
- 广告商找大明星拍广告需要经过经纪人
- 合作伙伴找大老板谈合作要约见面时间需要经过秘书
- 房产中介是买卖双方的代理
③相关术语
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
- 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。(这里是Calculator)
代理分为静态代理和动态代理
静态代理就是一对一的,一个目标对象对应一个代理对象
3.2.2、静态代理
创建静态代理类:
public class CalculatorStaticProxy implements Calculator { // 将被代理的目标对象声明为成员变量(声明实现类或者接口都是可以的) private CalculatorImpl target; public CalculatorStaticProxy(CalculatorImpl target) { this.target = target; } @Override public int add(int i, int j) { // 附加功能由代理类中的代理方法来实现 System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j); // 通过目标对象来实现核心业务逻辑 int addResult = target.add(i, j); System.out.println("[日志] add 方法结束了,结果是:" + addResult); return addResult; } } public class ProxyTest { @Test public void testProxy(){ CalculatorStaticProxy staticProxy=new CalculatorStaticProxy(new CalculatorImpl()); staticProxy.add(1,3); } }
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来
说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代
码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理
类来实现。这就需要使用动态代理技术了。
3.2.3、动态代理
动态代理有两种:
一种是jdk动态代理(要求必须有接口,生成的代理类和目标类实现相同的接口,最终生成的代理类在com.sun.proxy包下,类名为$proxy加一个数字)
(AOP底层就是动态代理实现的,我们自己写的不多)
一种是cglib动态代理(没有接口时)最终生成的代理类会继承目标类,并且和目标类在相同的包下(这个扫描的时候能扫描到,后面可能会用到)
指的是动态生成目标类对应的代理类
生产代理对象的工厂类:
拓展: - getMethods(): 获得类的public类型的方法
getMethod(String name, Class[] params): 获得类的特定方法,name参数指定方法的名字,params参数指定方法的参数类型(null和不写都调用无参的方法)
getDeclaredMethods(): 获取类中所有的方法(public、protected、default、private)
getDeclaredMethod(String name, Class[] params): 获得类的特定方法,name参数指定方法的名字,params参数指定方法的参数类型
单独的T 代表一个类型(表现形式是一个类名而已) ,而 Class代表这个类型所对应的类(又可以称做类实例、类类型、字节码文件), Class<?>表示类型不确定的类
Class表示T类型的字节码文件,意思是:
Class 相当于Class c=T.class,T t new T() ;
或者Class c= t.getClass();
通过以上可以获取类名为c.getName();
反射补充:
反射的基本概念: Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法(即使是private的),可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键(来自 百度百科)
除了int等基本类型外,Java的其他类型全部都是class(包括interface)。例如:仔细思考,我们可以得出结论:class(包括interface)的本质是数据类型(Type)。无继承关系的数据类型无法赋值: 而class是由JVM在执行过程中,动态加载的,JVM在第一次读取到class类型时,将其加载入内存 每加载一种class,JVM就为其创建一个Class类型的实例,并关联起来。注意:这里的Class类型是一个名叫Class的class。它长这样:
以String类为例,当JVM加载String类时,它首先读取String.class文件到内存,然后,为String类创建一个Class实例并关联起来:
这个Class实例是JVM内部创建的,如果我们查看JDK源码,可以发现Class类的构造方法是private,只有JVM能创建Class实例,我们自己的Java程序是无法创建Class实例的。 所以,JVM持有的每个Class实例都指向一个数据类型(class或interface):
一个Class实例包含了该class的所有完整信息:
由于JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息。 这种通过Class实例获取class信息的方法称为反射(Reflection)。 获取一个class的Class有三种方法:
一、 直接通过class的静态变量class获取:
二、 如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取:
三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:
因为Class实例在JVM中是唯一的,所以,上述方法获取的Class实例是同一个实例。可以用==比较两个Class实例:
Class实例与instanceof的差别:
public class ProxyFactory { private Object target;//因为是动态嘛,所以必须是Object,类型不确定 public ProxyFactory(Object target) { this.target = target; } public Object getProxy(){ /** * newProxyInstance():创建一个代理实例(通过这个方法创建的动态代理类,共有三个参数在·) * 其中有三个参数: * 1、ClassLoader:加载动态生成的代理类的类加载器 * 2、Class[] interfaces:目标对象实现的所有接口的class对象所组成的数组(动态生成的这个类,必须要和目标类实现相同的接口) * 3、InvocationHandler:调用处理的意思-设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法 */ //(要保证代理对象和目标对象实现的功能是一样的) ClassLoader classLoader = target.getClass().getClassLoader();//target改为this也可-target调用getProxy Class<?>[] interfaces = target.getClass().getInterfaces(); //getClass()返回此 Object的运行时类。 所有的类和接口都能用Class类来接收 InvocationHandler invocationHandler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { /** * proxy:代理对象 * method:代理对象需要实现的方法,即其中需要重写的方法 * args:method所对应方法的参数 * 韩顺平回顾反射:method1.invoke(o); //传统方法 对象.方法() , 反射机制 方法.invoke(对象) * Object invoke(Object obj, Object... args) 在具有指定参数的指定对象上调用此 方法对象表示的基础方法。 */ Object result = null; try {//因为是Object类型的,所以要转换为String类型的看的清 System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args)); result = method.invoke(target, args); System.out.println("[动态代理][日志] "+method.getName()+",结 果:"+ result); } catch (Exception e) { e.printStackTrace(); System.out.println("[动态代理][日志] "+method.getName()+",异常:"+e.getMessage()); } finally { System.out.println("[动态代理][日志] "+method.getName()+",方法执行完毕"); } return result; } }; //JDK代理要求必须有接口 return Proxy.newProxyInstance(classLoader, interfaces,invocationHandler); } }
3.2.4、测试
@Test public void testDynamicProxy(){ ProxyFactory factory = new ProxyFactory(new CalculatorLogImpl()); Calculator proxy = (Calculator) factory.getProxy();//这个类是我们执行代码过程中动态创建出来的,我们不知道这个类叫什么,但是它实现了接口(和目标类实现了相同的接口)可以向上转型(都是面向接口编程) proxy.div(1,0); //proxy.div(1,1); }
3.3、AOP概念及相关术语
3.3.1、概述
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程OOP的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术(底层是代理模式)
OOP是纵向继承机制,AOP叫做横向抽取机制(抽取非核心代码)
3.3.2、相关术语
①横切关注点
简要来说,就是从核心业务代码中抽取的
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
切面就是封装横向关注点(非核心业务代码)的--
把横切关注点抽取出来放到一个类中,这个类叫切面(每一个横切关注点都是一个方法,这个方法叫通知)
把横切关注点封装到一个类中,这个类就叫做切面,而这个切面,每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
前置通知:在被代理的目标方法前执行
返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
异常通知:在被代理的目标方法异常结束后执行(死于非命)
后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所
有位置
③切面
封装通知方法的类。
④目标
我们要抽取非核心代码的这个对象
被代理的目标对象。
⑤代理
AOP帮助我们来创建
向目标对象应用通知之后创建的代理对象。
⑥连接点
抽取横切关注点的位置
这也是一个纯逻辑概念,不是语法定义的。
把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。
⑦切入点
从哪儿抽出来,放到哪儿去
定位连接点的方式。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条
件。
3.3.3、作用
- 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
- 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。
3.4、基于注解的AOP
AOP是一种思想,AspectJ是AOP的具体实现方式,具体实现层就是动态代理
3.4.1、技术说明
动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因
为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
3.4.2、准备工作
IOC加的依赖有:
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包,spring-context是IOC的依赖 --> <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>
①添加依赖
在IOC所需依赖基础上再加入下面依赖即可:
<!-- spring-aspects会帮我们传递过来aspectjweaver --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.1</version> </dependency>
②准备被代理的目标资源
接口:
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 CalculatorPureImpl 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.4.3、创建切面类并配置
因为AOP是在IOC的基础上实现的,所以也要建立配置文件,把切面类和目标对象交给IOC容器管理(自己配置or注解+扫描)
不是控制层、业务层、持久层,标记为Component普通组件
// @Aspect表示这个类是一个切面类 @Aspect // @Component注解保证这个切面类能够放入IOC容器 @Component public class LoggerAspect { @Before("execution(public int com.atguigu.aop.annotation.CalculatorImpl.*(..))") public void beforeMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args); } @After("execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))") public void afterMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->后置通知,方法名:"+methodName); } @AfterReturning(value = "execution(*com.atguigu.aop.annotation.CalculatorImpl.*(..))", returning = "result") public void afterReturningMethod(JoinPoint joinPoint, Object result){ String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result); } @AfterThrowing(value = "execution(*com.atguigu.aop.annotation.CalculatorImpl.*(..))", throwing = "ex") public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){ String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex); } @Around("execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))") public Object aroundMethod(ProceedingJoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); Object result = null; try { System.out.println("环绕通知-->目标对象方法执行之前"); //目标对象(连接点)方法的执行 result = joinPoint.proceed(); System.out.println("环绕通知-->目标对象方法返回值之后"); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("环绕通知-->目标对象方法出现异常时"); } finally { System.out.println("环绕通知-->目标对象方法执行完毕"); } return result; } }
在Spring的配置文件中配置:
<!-- 基于注解的AOP的实现: 1、将目标对象和切面交给IOC容器管理(注解+扫描) 2、开启AspectJ的自动代理,为目标对象自动生成代理 3、将切面类通过注解@Aspect标识 --> <context:component-scan base-package="com.atguigu.aop.annotation"> </context:component-scan> <!--开启基于注解的AOP--> <aop:aspectj-autoproxy />
3.4.4、各种通知
必须加上这些注解才能标记为相应的通知
前置通知:使用@Before注解标识,在被代理的目标方法前执行 |必须要设置value属性(切入点表达式-通过切入点表达式定位到连接点-连接点就是抽取横切关注点的地方),否则报错
返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行(寿终正寝)
异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(死于非命)
后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行(盖棺定论)
环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包
括上面四种通知对应的所有位置
补充:
创建了代理对象就不能通过原注解方式直接访问对象了(否则报错),不然创建代理干什么(这个代理类不知道什么类型,但知道它实现了接口,我们可以向上转型)
设置了切面,不能用CalculatorImpl实现了,用代理类实现的接口Calculator(因为创建代理类的时候继承了Calculator的接口,并且是唯一的,所以可以通过接口来获取代理类的对象)
前置通知:@Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))") 要作用于目标对象的某一个方法上,public int 而add方法有那么多,怎么知道它是哪一个,所以说加上包名,这里参数可以只写类型(方法重载和参数名没关系,只和参数类型个数有关系)
下图中的半圆m就代表在目标方法执行前执行
切入点表达式语法扩充:
"execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))"这样只能作用于add方法
这个* com代表任意修饰符类型
CalculatorImpl.*表示类中所有的方法
CalculatorImpl.*()表示当前方法的参数列表,这样写表示无参的方法
CalculatorImpl.*(..)所有的方法都加前置通知的话 ..表示任意的参数列表
在类下面每隔地方都能写* ,表示当前类下面的所有的类
可以写"execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))"各种通知的执行顺序:
- Spring版本5.3.x以前:
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
- Spring版本5.3.x以后:
- 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
3.4.5、切入点表达式语法
①作用
②语法细节
用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
在包名的部分,一个“星* ”号只能代表包的层次结构中的一层,表示这一层是任意的。*
例如:.Hello匹配com.Hello,不匹配com.atguigu.Hello
在包名的部分,使用“..”表示包名任意、包的层次深度任意
*..Service:表示方法名以 Service 结尾,且方法所在的类名中包含任意数量的任意字符,例如 UserService、OrderService、ProductService 等。
UserService:符合条件,因为类名以 Service 结尾。
com.example.service.UserService:符合条件,因为类名以 Service 结尾,且 com.example.service 是 *..Service 的子包。
一个星号( *)表示匹配任意数量的任意字符,但至少要有一个字符。它可以出现在包名、类名和方法名中,但只匹配当前层级的包或子包中的类或方法。例如, * Service 表示匹配当前层级的包或子包中以 Service 结尾的类名或方法名。
两个星号()表示匹配任意数量的任意字符,包括 0 个字符。它可以出现在包名中,匹配任意层级的包或子包。例如,com.example. * *表示匹配 com.example 包及其所有子包中的所有类和方法。
com.example.** 是一个Ant风格的类路径模式。在这种模式下,** 表示任意数量的目录。
..:表示任意数量任意类型的子包或子类。例如,com.example..*Service 表示 com.example 包下的所有以 Service 结尾的类,com.example.service.. 表示 com.example.service 包及其子包下的所有类。
在Spring MVC中,**和..都是Ant风格的通配符,但是它们的作用有所不同:
** 代表任意数量的目录,例如 com.example.** 表示匹配 com.example 包及其所有子包下的类或资源文件。
.. 代表任意字符的数量,常用于 SQL 查询中的 LIKE 操作,例如 name LIKE '%example%' 可以使用 name like '%example..%' 替代。
在类名的部分,类名部分整体用号代替,表示类名任意
(.., int):表示方法参数包含任意数量任意类型的参数,且最后一个参数为 int 类型。
在类名的部分,可以使用* 号代替类名的一部分*
例如:Service匹配所有名称以Service结尾的类或接口
在方法名部分,可以使用号表示方法名任意
在方法名部分,可以使用号代替方法名的一部分
例如:*Operation匹配所有方法名以Operation结尾的方法
在方法参数列表部分,使用(..)表示参数列表任意
在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
例如:execution(public int ..Service.* (.., int)) 正确*
*例如:execution( * int ..Service.*(.., int)) 错误
3.4.6、重用切入点表达式
①声明
视频中路径是execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))
@Pointcut("execution(* com.atguigu.aop.annotation.*.*(..))") public void pointCut(){}
②在同一个切面中使用
- 切入点表达式定位的是哪一个方法
@Before("pointCut()") public void beforeMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args); }
③在不同切面中使用
@Before("com.atguigu.aop.CommonPointCut.pointCut()") public void beforeMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args); }
3.4.7、获取通知的相关信息
①获取连接点信息
获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参
- JoinPoint 就是帮助我们获取这个切入点的信息,切入点表达式定位的是哪一个方法,这个定位的就是哪一个方法的信息
@Before("execution(public int com.atguigu.aop.annotation.CalculatorImpl.*(..))") public void beforeMethod(JoinPoint joinPoint){ //获取连接点的签名信息 String methodName = joinPoint.getSignature().getName(); //获取目标方法到的实参信息 String args = Arrays.toString(joinPoint.getArgs()); System.out.println("LoggerAspect,方法:"+signature.getName()+",参数:"+ Arrays.toString(args)); }
②获取目标方法的返回值
@AfterReturning中的属性returning,用来将通知方法的某个形参,接收目标方法的返回值
- 返回通知,在目标对象方法返回值之后执行(有异常就不可能有返回值)
- returning = "result"就是设置接收目标对象方法返回值的一个参数的参数名
@AfterReturning(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", returning = "result") public void afterReturningMethod(JoinPoint joinPoint, Object result){ String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result); }
③获取目标方法的异常
@AfterThrowing中的属性throwing,用来将通知方法的某个形参,接收目标方法的异常
@AfterThrowing(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", throwing = "ex") public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){ String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex); }
3.4.8、环绕通知
@Around("execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))") public Object aroundMethod(ProceedingJoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); Object result = null; try { System.out.println("环绕通知-->目标对象方法执行之前"); //目标方法的执行,目标方法的返回值一定要返回给外界调用者 result = joinPoint.proceed(); System.out.println("环绕通知-->目标对象方法返回值之后"); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("环绕通知-->目标对象方法出现异常时"); } finally { System.out.println("环绕通知-->目标对象方法执行完毕"); } return result; }
LoggerAspect.java
package com.atguigu.spring.aop.annotation; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.util.Arrays; /** * Date:2023/1/1 * Author:ybc * Description: * 1、在切面中,需要通过指定的注解将方法标识为通知方法 * @Before:前置通知,在目标对象方法执行之前执行 * @After:后置通知,在目标对象方法的finally字句中执行(即异常之前) * @AfterReturning:返回通知,在目标对象方法返回值之后执行(有异常就不可能有返回值) * @AfterThrowing:异常通知,在目标对象方法的catch字句中执行 * * * 2、切入点表达式:设置在标识通知的注解的value属性中 * execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int, int) * execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..) * 第一个*表示任意的访问修饰符和返回值类型 * 第二个*表示类中任意的方法 * ..表示任意的参数列表 * 类的地方也可以使用*,表示包下所有的类 * 3、重用切入点表达式 * //@Pointcut声明一个公共的切入点表达式(多次会用到,不然还要重复声明) * @Pointcut("execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))") * public void pointCut(){} * 使用方式:@Before("pointCut()") 把它的value属性当做切入点表达式 * 可以在其它类下面访问不同类的已经设置的切入点,因为包都已经扫描了,当然可以 * * 4、获取连接点的信息 * 在通知方法的参数位置,设置JoinPoint类型的参数,就可以获取连接点所对应方法的信息 * //获取连接点所对应方法的签名信息 * Signature signature = joinPoint.getSignature(); * //获取连接点所对应方法的参数 * Object[] args = joinPoint.getArgs(); * * 5、切面的优先级 * 可以通过@Order注解的value属性设置优先级,默认值Integer的最大值 * @Order注解的value属性值越小,优先级越高(可以建立不同的类来测试) * */ @Component @Aspect //将当前组件标识为切面 public class LoggerAspect { @Pointcut("execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))") public void pointCut(){} //@Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int, int))") //@Before("execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))") @Before("pointCut()") public void beforeAdviceMethod(JoinPoint joinPoint) { //获取连接点所对应方法的签名信息 Signature signature = joinPoint.getSignature(); //获取连接点所对应方法的参数 Object[] args = joinPoint.getArgs(); System.out.println("LoggerAspect,方法:"+signature.getName()+",参数:"+ Arrays.toString(args)); } @After("pointCut()") public void afterAdviceMethod(JoinPoint joinPoint){ //获取连接点所对应方法的签名信息 Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,方法:"+signature.getName()+",执行完毕"); } /** * 在返回通知中若要获取目标对象方法的返回值 * 只需要通过@AfterReturning注解的returning属性 * 就可以将通知方法的某个参数指定为接收目标对象方法的返回值的参数 */ @AfterReturning(value = "pointCut()", returning = "result") public void afterReturningAdviceMethod(JoinPoint joinPoint, Object result){ //获取连接点所对应方法的签名信息 Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,方法:"+signature.getName()+",结果:"+result); } /** * 在异常通知中若要获取目标对象方法的异常 * 只需要通过AfterThrowing注解的throwing属性 * 就可以将通知方法的某个参数指定为接收目标对象方法出现的异常的参数 */ @AfterThrowing(value = "pointCut()", throwing = "ex") public void afterThrowingAdviceMethod(JoinPoint joinPoint, Throwable ex){//Exception也可 //获取连接点所对应方法的签名信息 Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,方法:"+signature.getName()+",异常:"+ex); } @Around("pointCut()") //环绕通知的方法的返回值一定要和目标对象方法的返回值一致,把前面四个整合的效果 public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint){ Object result = null; try { System.out.println("环绕通知-->前置通知"); //就是为了表示目标对象方法的执行 在执行前后可以加上额外的操作 //相当于动态代理中result=method.invoke(target,args) 目标对象返回值是什么,Object就是什么 result = joinPoint.proceed(); System.out.println("环绕通知-->返回通知"); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("环绕通知-->异常通知"); } finally { System.out.println("环绕通知-->后置通知"); } return result; } }
切面优先级测试切面优先级测试(设置多个切面的情况下),取决于Order设置,不设置优先级默认是整数的最大值
以后要么对一个地方进行额外的通知,要么用环绕通知
3.4.9、切面的优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
使用@Order注解可以控制切面的优先级:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低
3.5,基于XML的AOP(了解)
主要是注解为主
3.5.1、准备工作
参考基于注解的AOP环境
3.5.2、实现
< aspectj-autoproxy>这个开启基于注解的AOP就不用带了
aop:aspect 将组件设置为切面,aop:avdisor设置当前的通知的,aop:pointcut设置切入点表达式
为什么 能直接引用?
因为当前的切面用注解+扫描的方式将其配置到了IOC容器中,所以ref引用的id默认就是类名的小驼峰
<context:component-scan base-package="com.atguigu.aop.xml"></context:component-scan> <aop:config> <!--配置切面类--> <aop:aspect ref="loggerAspect"> <!--设置一个公共的切入点表达式--> <aop:pointcut id="pointCut" expression="execution(*com.atguigu.aop.xml.CalculatorImpl.*(..))"/> <aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before> <aop:after method="afterMethod" pointcut-ref="pointCut"></aop:after> <aop:after-returning method="afterReturningMethod" returning="result"pointcut-ref="pointCut"></aop:after-returning> <aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointCut"></aop:after-throwing> <aop:around method="aroundMethod" pointcut-ref="pointCut"></aop:around> </aop:aspect> <aop:aspect ref="validateAspect" order="1"> <aop:before method="validateBeforeMethod" pointcut-ref="pointCut"> </aop:before> </aop:aspect> </aop:config>
4、声明式事务
4.1、JdbcTemplate
事物是数据库中的内容,现在需要在执行SQL时测试事务的功能的;
spring对JBDC进行了封装,封装之后有个框架springjdbc,它有个类叫JDBCTemplate,有各种CRUD方法
4.1.1、简介
Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作
4.1.2、准备工作
①加入依赖
spring-orm对象关系映射(Object Relational Mapping)
spring-tx:带tx的就是操作事务的
spring-test:这个是spring整合junit的一个依赖(可以让测试类在spring的测试环境中执行,这样就不用每一次都获取IOC容器了,可以直接依赖注入获取IOC容器中的bean并使用)
<dependencies> <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.1</version> </dependency> <!-- Spring 持久化层支持jar包 --> <!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包 --> <!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>5.3.1</version> </dependency> <!-- Spring 测试相关 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.1</version> </dependency> <!-- junit测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> </dependency> <!-- 数据源 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.31</version> </dependency> </dependencies>
②创建jdbc.properties
jdbc.user=root jdbc.password=atguigu jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC jdbc.driver=com.mysql.cj.jdbc.Driver
③配置Spring的配置文件
这是第三方jar包,在源文件中不可能用注解+扫描的方式,我们加bean标签
在web工程下必须设置classpath,在java工程下没有web资源写jdbc.properties也可(最好直接设置classpath)
<!-- 导入外部属性文件 --> <context:property-placeholder location="classpath:jdbc.properties" /> <!-- jdbcTemplate需要配置数据源,然后property引用 --> <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="${jdbc.url}"/> <property name="driverClassName" value="${jdbc.driver}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置 JdbcTemplate,不设置id也可以,通过类型获取 --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 装配数据源 --> <property name="dataSource" ref="druidDataSource"/>
优质全套Spring全套教程三https://developer.aliyun.com/article/1491456