IOC,全程Inversion of Control(控制反转)
通过控制反转(创建对象的权限交给框架,所以叫反转)创建的对象被称为Spring Bean
,这个Bean和用new创建出来的对象是没有任何区别的。
官方解释:Spring 通过 IoC 容器来管理所有 Java 对象的实例化和初始化,控制对象与对象之间的依赖关系。
举个例子:有一杯水,杯子相当于IOC容器,杯子里面的水相当于Bean对象,这个杯子从开始装水,到水被喝完结束这过程,就相当于容器控制对象的生命周期(从对象被创建,到最后被销毁的过程)
控制反转是一种思想而非技术。
控制反转是为了降低程序耦合度,提高程序扩展力。
控制反转,反转的是什么?
控制反转这种思想如何实现呢?
DI(Dependency Injection):依赖注入,依赖注入实现了控制反转的思想。
依赖注入:
依赖注入常见的实现方式包括两种:
所以结论是:IOC 就是一种控制反转的思想, 而 DI 是对IoC的一种具体实现。
Bean管理说的是:Bean对象的创建,以及Bean对象中属性的赋值(或者叫做Bean对象之间关系的维护)。
Spring 的 IoC 容器就是 IoC思想的一个落地的产品实现。IoC容器中管理的组件也叫做 bean。在创建 bean 之前,首先需要创建IoC 容器。Spring 提供了IoC 容器的两种实现方式:
①BeanFactory
这是 IoC 容器的基本实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用。
②ApplicationContext
BeanFactory 的子接口,提供了更多高级特性。面向 Spring 的使用者,几乎所有场合都使用 ApplicationContext 而不是底层的 BeanFactory。
③ApplicationContext接口 的主要实现类
类型名 | 简介 |
---|---|
ClassPathXmlApplicationContext | 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象 |
FileSystemXmlApplicationContext | 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象 |
ConfigurableApplicationContext | ApplicationContext 的子接口,包含一些扩展方法 refresh() 和 close() ,让 ApplicationContext 具有启动、关闭和刷新上下文的能力。 |
WebApplicationContext | 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。 |
一般项目里很少用这种XML管理Bean的,这里就不演示了,想看的可以转战
尚硅谷Spring6
何为注解?注解是代码中的一种特殊标记
用注解创建Spring Bean的过程
(没有用SpringBoot,默认只用Spring,如果用SpringBoot就可以用@ComponentScan等等来指定扫描路径)
Spring 默认不使用注解装配 Bean,因此我们需要在 Spring 的 XML 配置中,通过 context:component-scan 元素开启 Spring Beans的自动扫描功能。开启此功能后,Spring 会自动从扫描指定的包(base-package 属性设置)及其子包下的所有类,如果类上使用了 @Component 注解,就将该类装配到容器中。
看一下XML配置文件
com.atguigu.spring6
这包下的文件就会被扫描,只要扫描到 @Component 注解,就将该类装配到容器中
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.atguigu.spring6">context:component-scan>
beans>
注意:在使用 context:component-scan 元素开启自动扫描功能前,首先需要在 XML 配置的一级标签 中添加 context 相关的约束。
情况一:最基本的扫描方式
和上面的例子一样,当前路径下全扫
<context:component-scan base-package="com.atguigu.spring6">
context:component-scan>
情况二:指定要排除的组件
可以排除某些不想被扫描的类
<context:component-scan base-package="com.atguigu.spring6">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
context:component-scan>
情况三:仅扫描指定组件
<context:component-scan base-package="com.atguigu" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
context:component-scan>
Spring 提供了以下多个注解,这些注解可以直接标注在 Java 类上,将它们定义成 Spring Bean。
功能其实都一样,比如说我的controller类叫XXXcontroller,并不是非要用@Controller来标记,用@Service等其他注解也一样可以完成目标,这里用不同名字的注解是为了更好的区分类功能。
注解 | 说明 |
---|---|
@Component | 该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。 |
@Repository | 该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Service | 该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Controller | 该注解通常作用在控制层(如SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
单独使用@Autowired注解,默认根据类型装配。【默认是byType】
看下Autowired源码
package org.springframework.beans.factory.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}
源码中有两处需要注意:
第一处:该注解可以标注在哪里?
@Target的元注解就标记了注解的位置
ElementType.CONSTRUCTOR
构造方法上ElementType.METHOD
方法上ElementType.PARAMETER
形参上ElementType.FIELD
属性上ElementType.ANNOTATION_TYPE
注解上第二处:该注解有一个required属性,默认值是true,表示在注入的时候要求被注入的Bean必须是存在的,如果不存在则报错。如果required属性设置为false,表示注入的Bean存在或者不存在都没关系,存在的话就注入,不存在的话,也不报错。
这几个场景都是说明注解可以用的地方
创建UserDao接口(假装有数据库)
PS:Dao层和那个Mapper层是一个东西
package com.atguigu.spring6.dao;
public interface UserDao {
public void print();
}
创建UserDaoImpl实现
package com.atguigu.spring6.dao.impl;
import com.atguigu.spring6.dao.UserDao;
import org.springframework.stereotype.Repository;
@Repository
public class UserDaoImpl implements UserDao {
@Override
public void print() {
System.out.println("Dao/Mapper层执行结束");
}
}
创建UserService接口
package com.atguigu.spring6.service;
public interface UserService {
public void out();
}
创建UserServiceImpl实现类
package com.atguigu.spring6.service.impl;
import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void out() {
userDao.print();
System.out.println("Service层执行结束");
}
}
创建UserController类
package com.atguigu.spring6.controller;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
@Autowired
private UserService userService;
public void out() {
userService.out();
System.out.println("Controller层执行结束。");
}
}
测试一
package com.atguigu.spring6.bean;
import com.atguigu.spring6.controller.UserController;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class UserTest {
private Logger logger = LoggerFactory.getLogger(UserTest.class);
@Test
public void testAnnotation(){
//从配置文件中读取Bean配置信息
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
//获取Bean
UserController userController = context.getBean("userController", UserController.class);
//调用方法
userController.out();
logger.info("执行成功");
}
}
测试结果:
以上构造方法和setter方法都没有提供,经过测试,仍然可以注入成功。
修改UserServiceImpl类
package com.atguigu.spring6.service.impl;
import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void out() {
userDao.print();
System.out.println("Service层执行结束");
}
}
修改UserController类
package com.atguigu.spring6.controller;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
private UserService userService;
//这种就相当于把整个Service注入了,那么userService就可以直接调用
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void out() {
userService.out();
System.out.println("Controller层执行结束。");
}
}
测试:成功调用
修改UserServiceImpl类
package com.atguigu.spring6.service.impl;
import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
@Autowired
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void out() {
userDao.print();
System.out.println("Service层执行结束");
}
}
修改UserController类
package com.atguigu.spring6.controller;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
private UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
public void out() {
userService.out();
System.out.println("Controller层执行结束。");
}
}
测试:成功调用
修改UserServiceImpl类
package com.atguigu.spring6.service.impl;
import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
//直接把注解加入到参数上面,就可以自动注入,后面就可以调用了
public UserServiceImpl(@Autowired UserDao userDao) {
this.userDao = userDao;
}
@Override
public void out() {
userDao.print();
System.out.println("Service层执行结束");
}
}
修改UserController类
package com.atguigu.spring6.controller;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
private UserService userService;
public UserController(@Autowired UserService userService) {
this.userService = userService;
}
public void out() {
userService.out();
System.out.println("Controller层执行结束。");
}
}
测试:成功调用
修改UserServiceImpl类
package com.atguigu.spring6.service.impl;
import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
// @Autowired 这个注解在只有一个有参构造是可以省略!
private UserDao userDao;
//唯一的一个有参构造,但凡多一个都注入不了
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void out() {
userDao.print();
System.out.println("Service层执行结束");
}
}
测试通过
当有参数的构造方法只有一个时,@Autowired注解可以省略。
说明:有多个构造方法时呢?大家可以测试(再添加一个无参构造函数),测试报错
@Autowired注解是默认byType进行注入的
@Qualifier注解是默认根据名称进行注入的
添加dao层实现
package com.atguigu.spring6.dao.impl;
import com.atguigu.spring6.dao.UserDao;
import org.springframework.stereotype.Repository;
@Repository
public class UserDaoRedisImpl implements UserDao {
@Override
public void print() {
System.out.println("Redis Dao层执行结束");
}
}
此时UserDao 就会有两个实现类(之前还创建了一个UserDao的实现类,加上这个就两个)。因为是根据类型注入的,装配的过程就会出现两个对应UserDao的对象。
测试:测试异常
错误信息中说:不能装配,UserDao这个Bean的数量等于2
怎么解决这个问题呢?当然要byName,根据名称进行装配了。
修改UserServiceImpl类
用Qualifier指定bean(实现类)的名字,默认首字母小写
package com.atguigu.spring6.service.impl;
import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
//先类型,类型注入不了就按指定名字
@Autowired
@Qualifier("userDaoImpl") // 指定bean(实现类)的名字,默认首字母小写
private UserDao userDao;
@Override
public void out() {
userDao.print();
System.out.println("Service层执行结束");
}
}
总结
@Resource注解也可以完成属性注入。那它和@Autowired注解有什么区别?
我们知道Resource会根据名字进行注入,那么这个名字在制定了名字时,也就是@Resource(value="名称")
时,就会按照这个指定的名字注入。
如果没有指定这个名字,就会按照被注入类的属性名称(不是类里的属性,是哪个myUserDao的名称)来注入。
不指定时:
**指定名称时:**按照已经设定好的属性来注入
修改UserDaoImpl类
package com.atguigu.spring6.dao.impl;
import com.atguigu.spring6.dao.UserDao;
import org.springframework.stereotype.Repository;
@Repository("myUserDao")
public class UserDaoImpl implements UserDao {
@Override
public void print() {
System.out.println("Dao层执行结束");
}
}
修改UserServiceImpl类
package com.atguigu.spring6.service.impl;
import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Resource(name = "myUserDao")
private UserDao myUserDao;
@Override
public void out() {
myUserDao.print();
System.out.println("Service层执行结束");
}
}
测试通过,这种是正常的根据设定好的名称去注入
修改UserDaoImpl类,我这里定义好了名字,但是注入的位置没有标记
package com.atguigu.spring6.dao.impl;
import com.atguigu.spring6.dao.UserDao;
import org.springframework.stereotype.Repository;
@Repository("myUserDao")
public class UserDaoImpl implements UserDao {
@Override
public void print() {
System.out.println("Dao层执行结束");
}
}
修改UserServiceImpl类
package com.atguigu.spring6.service.impl;
import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserDao myUserDao;
@Override
public void out() {
myUserDao.print();
System.out.println("Service层执行结束");
}
}
测试通过
当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名。
修改UserServiceImpl类,userDao1属性名不存在
package com.atguigu.spring6.service.impl;
import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserDao userDao1;
@Override
public void out() {
userDao1.print();
System.out.println("Service层执行结束");
}
}
测试异常
根据异常信息得知:显然当通过name找不到的时候,自然会启动byType进行注入,以上的错误是因为UserDao接口下有两个实现类导致的。所以根据类型注入就会报错。
@Resource的set注入可以自行测试
总结:
@Resource注解:默认byName(指注解上的那个value名字)注入,没有指定name时把属性名当做name,根据name找不到时,才会byType注入。byType注入时,某种类型的Bean只能有一个。
我们都知道,Spring框架的IOC是基于Java反射机制实现的,下面我们先回顾一下java反射。
新建一个Car类用于反射测试
public class Car {
private String name;
private int age;
private String color;
...一大堆setter getter constructor 这里不赘述了
private void run() {
System.out.println("私有方法-run.....");
}
}
要测试的内容:
public class TestCar {
//获取Class对象的多种方式
@Test
public void test01() throws Exception {
// 1 类名.class
Class class1=Car.class;
// 2 对象.getClass()
Car car = new Car();
Class class2 = car.getClass();
// 3 Class.forName("类的全路径")
//因为有可能路径不对获取不到,所以要捕获或者抛出异常
Class class3 = Class.forName("com.cc.reflect.Car");
//把刚刚获取的类实例化
Car o=(Car) class3.getDeclaredConstructor().newInstance();
//输出类的信息
System.out.println(class1.toString());
System.out.println(class2.toString());
System.out.println(class3.toString());
}
}
这里有个条件,把Car的其中一个构造方法改成私有的,原本是公开的
package com.cc.reflect;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Constructor;
public class TestCar {
//获取构造方法
@Test
public void test02() throws Exception {
//获取class文件
//这里的car有三个属性,并且已经写好了构造、setter、getter
Class clazz = Car.class;
//获取所有构造方法(public的),放在数组里(这个数组因为是遍历公有方法的,所以这个数组只能发现并存放一个共有的构造)
Constructor[] constructors=clazz.getConstructors();
for (Constructor c: constructors) {
System.out.println("构造方法名称"+c.getName()+ "参数个数"+c.getParameterCount());
}
//获取所有构造方法(public和private的),放在数组里(这个数组因为是遍历公有+私有方法的,所以这个数组发现并存放所有的构造(无论是共有还是私有))
//PS:Declared声称的
Constructor[] constructors1=clazz.getDeclaredConstructors();
for (Constructor c: constructors1) {
System.out.println("全部构造方法名称"+c.getName()+ "参数个数"+c.getParameterCount());
}
}
}
@Test
public void test03() throws Exception {
//获取class文件
Class clazz = Car.class;
//指定有参数构造创建对象
//1 构造public
Constructor c1 = clazz.getConstructor(String.class, int.class, String.class);
Car car1 = (Car)c1.newInstance("夏利", 10, "红色");
System.out.println(car1.toString());
}
@Test
public void test03() throws Exception {
//获取class文件
Class clazz = Car.class;
//2 构造private
Constructor c2 = clazz.getDeclaredConstructor(String.class, int.class, String.class);
//默认是访问不了私有方法的,把这个选项打开才能访问到,实例化的方法和之前一样
c2.setAccessible(true);
Car car2 = (Car)c2.newInstance("捷达", 20, "蓝色");
System.out.println(car2);
}
主要是先获取field属性数组,遍历,匹配,设置可进入,set值。
@Test
public void test04() throws Exception {
//获取class文件
Class clazz = Car.class;
//通过无参构造创建car对象
Car car = (Car)clazz.getDeclaredConstructor().newInstance();
//获取属性,属性集合是个数组
Field[] fields=clazz.getDeclaredFields();
//遍历这个数组获取属性,操作属性
for (Field field : fields) {
//当匹配到name属性时,进入操作
if (field.getName().equals("name")){
//将私有属性允许操作
field.setAccessible(true);
//设置值(car对象的name属性设置为 五菱宏光)
field.set(car,"五菱宏光");
}
}
System.out.println(car.toString());
}
@Test
public void test05(){
//创建对象
Car car = new Car("奔驰",10,"黑色");
Class clazz = car.getClass();
//1.获取类内部的public方法
//和上面的属性大同小异,也是通过用数组接收类内部的所有方法,组成一个Method数组
Method[] methods=clazz.getMethods();
for (Method m:methods){
System.out.println("所有public方法的名字"+m.getName());
}
}
@Test
public void test05() throws Exception {
//创建对象
Car car = new Car("奔驰",10,"黑色");
Class clazz = car.getClass();
//1.获取类内部的public方法
//和上面的属性大同小异,也是通过用数组接收类内部的所有方法,组成一个Method数组
Method[] methods=clazz.getMethods();
for (Method m:methods){
//System.out.println("所有public方法的名字"+m.getName());
if (m.getName().equals("test1")){
//遍历到名字为test1的方法时执行
m.invoke(car);
}
}
}
@Test
public void test06() throws Exception {
//创建对象
Car car = new Car("奔驰",10,"黑色");
Class clazz = car.getClass();
//1.获取类内部的public方法
//也是通过用数组接收类内部的所有方法,组成一个Method数组,通过getDeclaredMethods获取
Method[] methods=clazz.getDeclaredMethods();
for (Method m:methods){
System.out.println("所有public+private方法的名字"+m.getName());
}
}
输出结果:输出所有私有和公有方法
@Test
public void test06() throws Exception {
//创建对象
Car car = new Car("奔驰",10,"黑色");
Class clazz = car.getClass();
//1.获取类内部的public方法
//也是通过用数组接收类内部的所有方法,组成一个Method数组,通过getDeclaredMethods获取
Method[] methods=clazz.getDeclaredMethods();
for (Method m:methods){
if (m.getName().equals("test2")){
//遍历到名字为test2的私有方法时执行
m.setAccessible(true);
m.invoke(car);
}
}
}
先空着,占个位,以后再补
声明计算器接口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);
}
创建对应实现类
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
.....很多接口方法实现
}
此时来了一个需求,我要在输出的时候打日志log
如果一行一行代码写,就会变成这样
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;
}
......省略很多相似的实现方法,但是都是带了很多log
}
打个比方我要是有一天需要改每一个日志的输出方法,那么就会非常难搞,每一个都得这么改,过于复杂。
①现有代码缺陷**
针对带日志功能的实现类,我们发现有如下缺陷:
②解决思路
解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
③困难
解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。
①介绍
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
使用代理后:
相当于把目标对象用代理对象包裹起来
②生活中的代理
③相关术语
创建静态代理类(代理类是另外的类,不是在原有类上操作的):
这也就是相当于解耦了,唯一的区别是不用动原来的核心代码,但是本质上相当于copy出来了一份,在copy的基础上进行修改增强。
public class CalculatorStaticProxy implements Calculator {
// 将被代理的目标对象声明为成员变量
private Calculator target;
public CalculatorStaticProxy(Calculator 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 ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxy(){
/**
* newProxyInstance():创建一个代理实例
* 其中有三个参数:
* 1、classLoader:加载动态生成的代理类的类加载器
* 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
* 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
*/
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/**
* 几个参数的含义
* proxy:代理对象
* method:代理对象需要实现的方法,即其中需要重写的方法
* args:method所对应方法的参数
*/
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) {
e.printStackTrace();
System.out.println("[动态代理][日志] "+method.getName()+",异常:"+e.getMessage());
} finally {
System.out.println("[动态代理][日志] "+method.getName()+",方法执行完毕");
}
return result;
}
};
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
}
}
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
分散在每个各个模块中解决同一样的问题,如用户验证、日志管理、事务处理、数据缓存都属于横切关注点。
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
增强,通俗说,就是你想要增强的功能,比如 安全,事务,日志等。
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
封装通知方法的类。
被代理的目标对象。
向目标对象应用通知之后创建的代理对象。
这也是一个纯逻辑概念,不是语法定义的。
把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。通俗说,就是spring允许你使用通知的地方
定位连接点的方式。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
Spring 的 AOP 技术可以通过切入点定位到特定的连接点。通俗说,要实际去增强的方法
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。
本质上:动态代理的底层是静态代理,在配置好动态代理类之后,系统按照你的配置要求,对目标类生成静态代理类,执行的时候就去自动执行增强过的动态代理类了
引入AOP的依赖
创建一个计算器类的接口以及对应实现类
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;
}
....省略其他代码
}
首先要在xml配置文件中开启组件扫描
在Spring的配置文件中配置:
创建一个bean.xml文件,并写入相关配置
<context:component-scan base-package="com.cc.annotationAop">context:component-scan>
<aop:aspectj-autoproxy />
看一下切入点表达式的构成
切入点表达式:
execution(访问修饰符 增强方法返回类型 方法所在类全类名.方法名(方法参数))
用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
在包名的部分,使用“*…”表示包名任意、包的层次深度任意
在类名的部分,类名部分整体用*号代替,表示类名任意
在类名的部分,可以使用*号代替类名的一部分
在方法名部分,可以使用*号表示方法名任意
在方法名部分,可以使用*号代替方法名的一部分
在方法参数列表部分,使用(…)表示参数列表任意
在方法参数列表部分,使用(int,…)表示参数列表以一个int类型的参数开头
在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
以前置通知和后置通知为例,对方法进行增强。
// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
public class LogAspect {
// 设置切入点和通知类型
// 切入点表达式:execution(访问修饰符 增强方法返回类型 方法所在类全类名.方法名(方法参数))
// 通知类型:
// 前置@Before(value = "切入点表达式配置切入点"),这里增强了CalculatorImpl的add方法的任意参数
@Before(value = "execution(public int com.cc.annotationAop.CalculatorImpl.add(..))")
public void beforeMethod(JoinPoint joinPoint){
//可以根据JoinPoint获取对象信息(方法名,参数等等)
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
// 后置@After(),这里增强了CalculatorImpl的任意方法的任意参数
@After("execution(* com.cc.annotationAop.CalculatorImpl.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->后置通知,方法名:"+methodName);
}
}
测试一下:
注意:AOP的情况下,手动创建对象是没办法增强的
public class testAop {
@Test
public void testAdd(){
//xml方式获取对象(注意,对象必须由框架创建,不能手动new,手动new就不执行AOP了)
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
Calculator calculator = ac.getBean( Calculator.class);
calculator.add(1, 2);
}
}
测试通过,可以发现,按照预想结果输出,JoinPoint信息也能输出
以上就是两种通知类型,还有许多其他的通知形式
和@After()差不太多,主要是多了一个方法返回值
如果和@After()同时存在,那么先执行@AfterReturning()的增强内容,再执行@After()的增强内容,优先级略高
必须方法正常执行结束以后,有返回值,才会触发AfterReturning
如果方法都被异常中断了,没有返回值,那么返回值结果也就没有意义了,所以触发的条件就是方法正常结束
// 返回@AfterReturning() 在被代理的目标方法成功结束后执行,可以获取到目标方法的执行结果
// 注意,returning的返回值是增强方法结果的返回值,对应的属性名字要和传入的参数名字保持一致
//此处如果多个属性的话,value就要标注了
@AfterReturning(value = "execution(* com.cc.annotationAop.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.cc.annotationAop.CalculatorImpl.*(..))",throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint,Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:" + methodName + ",结果:" + ex);
}
(删掉刚刚手动异常的内容)
环绕通知就是在之前增强的基础上,再包一层,其他增强也会执行
注意,这里如果想执行方法,就不能用JoinPoint了,JoinPoint只能获取方法信息,无法促使方法执行,这里改用ProceedingJoinPoint对象,才可以执行方法
环绕通知也有和AfterReturning一样的返回值,可以操作返回值
// 环绕@Around()
// 在方法的执行前后都会执行,只有execution执行点一个参数
// 注意,这里如果想执行方法,就不能用JoinPoint了,JoinPoint只能获取方法信息,无法促使方法执行
// 这里改用ProceedingJoinPoint对象,可以执行方法
// 环绕通知也可以有AfterReturning的返回值,可以操作返回值
@Around("execution(* com.cc.annotationAop.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint){
Object result = null;
try {
System.out.println("环绕通知-->目标对象方法执行之前");
//目标对象(连接点)方法的执行
result = proceedingJoinPoint.proceed();
System.out.println("环绕通知-->目标对象方法返回值之后");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知-->目标对象方法出现异常时");
} finally {
System.out.println("环绕通知-->目标对象方法执行完毕");
}
return result;
}
各种通知的执行顺序:
- Spring版本5.3.x以前:
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
- Spring版本5.3.x以后:
- 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
表达式可以复用,要不然每个都写一遍太麻烦,而且不好维护
注意:如果不是在同一个切面(切面类)使用的话,比如:A切面类里定义好的重用表达式,在B切面类使用,就要在路径前加上包名来区分
在重用表达式定义的类里面使用,就直接用方法名即可
如果不在重用表达式定义的类里面使用,需要包名+类名+方法名
①重用切入点表达式声明
@Pointcut("execution(* com.cc.annotationAop.CalculatorImpl.add(..))")
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("引用全路径.类名.表达式定义()")
@Before("com.cc.annotationAop.LogAspect.pointCut()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序
使用@Order注解可以控制切面的优先级:
前期准备参考注解的AOP
具体实现的形式在bean.xml里面进行
<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:config>
现在项目上用的不多,就不详细展开了