• spring 入门


    spring

    spring是开源的容器框架。spring容器中配置着程序中所需用到的各对象(Bean),当我们需要去使用某对象时,不用去new,可直接从容器中取。

    spring IOC

    之前要想使用某对象,得去new。使用spring后,不用自己new了。当需要使用某对象时,可直接去容器中取。之所以叫控制反转,可能是因为创建对象的控制权从我手里转到了spring手里的缘故。

    spring 相关依赖

    pom.xml

    <dependencies>
    	
    	<dependency>
    		<groupId>org.springframeworkgroupId>
    		<artifactId>spring-beansartifactId>
    		<version>5.3.13version>
    	dependency>
    	<dependency>
    		<groupId>org.springframeworkgroupId>
    		<artifactId>spring-coreartifactId>
    		<version>5.3.13version>
    	dependency>
    	<dependency>
    		<groupId>org.springframeworkgroupId>
    		<artifactId>spring-contextartifactId>
    		<version>5.3.13version>
    	dependency>
    
    	
    	<dependency>
    		<groupId>org.springframeworkgroupId>
    		<artifactId>spring-aopartifactId>
    		<version>5.3.13version>
    	dependency>
    	
    	<dependency>
    		<groupId>org.aspectjgroupId>
    		<artifactId>aspectjweaverartifactId>
    		<version>1.8.13version>
    	dependency>
    
    	
    	<dependency>
    		<groupId>cglibgroupId>
    		<artifactId>cglibartifactId>
    		<version>2.2.2version>
    	dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    spring容器的顶层接口是BeanFactory。他的子接口是ApplicationContext

    ApplicationContext接口有两个实现类:

    • AnnotationConfigApplicationConext(基于注解
    • ClassPathXmlApplicationContext(基于配置文件的)。

    基于配置文件方式

    如果用的容器是ClassPathXmlApplicationContext,就需要配置下面的配置文件

    applicationContext.xml

    <?xml version="1.0" encoding="utf-8">
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/MXLSchema-instance"
    	xmlns:context="http://www.springframework.org/schema/context"		
    	https://www.springframework.org/schema/context/spring-context.xsd	
    ">
    
    	<bean id="student" class="com.cnm.Student">bean>
    beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Test.java

    public class Test {
    	public static void main(String[] args) {
    		// 1.创建容器
    		ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("applicationContext.xml");
    		// 2.从容器中获取对象
    		Student s = application.getBean(Student.class);// 通过类型去获取。
    		// Student s = (Student)application.getBean("student");// 通过bean的ID去获取。返回Object类型,所以可以强转。
    		// Student s = (Student)application.getBean("student", Student.class);// 通过ID去获取但又不想强转时可以用这个。
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    配置bean的几种方式

    第一种方式:使用无参构造方法配置对象(Student类中必须含有无参构造方法

    <bean id="student" class="com.cnm.Student">bean>
    
    • 1

    第二种方式:有参数的构造方法来配置

    <bean id="person" class="com.cnm.Student">
    	
    	<constructor-arg name="name" value="tom">constructor-arg>
    bean>
    
    • 1
    • 2
    • 3
    • 4

    第三种方式:通过静态工厂去配置。即委托一个工厂,让这个工厂来生产对象。

    创建工厂类:
    StudentStaticFactory.java

    public class StudentStaticFactory {
    	public static Student create() {
    		Student student = new Student();
    		student.setName = "asd";
    		return student;
    	}
    
    	public static Student create2(String name) {
    		Student student = new Student();
    		student.setName = name;
    		return student;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    
    <bean id="student" class="com.cnm.StudentStaticFactory" factory-method="create" />
    
    • 1
    • 2
    
    <bean id="student" class="com.cnm.StudentStaticFactory" factory-method="create2">
    	<constructor-arg name="name" value="tom">constructor-arg>
    bean>
    
    • 1
    • 2
    • 3
    • 4

    这种方式的目的是,先创建对象(new)后,在返回前,中间做一些特殊处理,等处理完了再返回。

    第四种方式:实例工厂配置

    新建工厂类:
    StudentInstanceFactory.java

    public class StudentInstanceFactory {
    	public Student create() {
    		Student student = new Student();
    		student.setName("asd");
    		return student;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    
    <bean id="studentFactory" class="com.cnm.StudentInstanceFactory" />
    
    
    <bean id="student" factory-bean="studentFactory" factory-method="create" />
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第五种方式:FactoryBean 配置

    该接口是spring给我们提供的。具体来说,我们创建一个类,去实现该接口,然后再把该实现类配置到容器中。
    spring和其他框架整合时用的较多。比如spring整合mybatis时,mybatis官方就提供了一个SqlSessionFactoryBean这个类,然后他实现了FactoryBean接口,然后在getObject方法中进行了一些处理,最终返回一个mybatis需要的对象。

    StudentFactoryBean.java

    public class StudentFactoryBean implements FactoryBean<Student> {
    	@Override
    	public Student getObject() throws Exception {// 不带泛型的话,返回值就是Object
    		Student s = new Student();
    		s.setName("asd");
    		return s;
    	}
    	@Override
    	public Class<?> getObjectType() {
    		return Student.class;// 返回对象的类型:泛型是啥,就返回啥即可。 
    	}
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    
    <bean id="student" class="com.asd.StudentFactoryBean">bean>
    
    • 1
    • 2
    • 3
    给bean对象进行属性注入

    通过构造方法给属性赋值:

    <bean id="student" class="com.cnm.StudentStaticFactory" factory-method="create2">
    	<constructor-arg name="name" value="tom">constructor-arg>
    bean>
    
    • 1
    • 2
    • 3

    通过set方法属性赋值:

    <bean id="student" class ="com.cnm.Student">
    	
    	<property name="name" value="asd"/>
    
    	
    	<property name="array">
    		<list>
    			<value>avalue>
    			<value>bvalue>
    		list>
    	property>
    
    	
    
    	
    	<property name="array">
    		<map>
    			<entry key="a" value="aa"/>
    			<entry key="b" value="bb"/>
    		map>
    	property>
    bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    基于xml进行bean的自动装配

    比如Student类里面有变量是private Address address。这时候,如何解决这种依赖关系呢?

    
    <bean id="address" class ="com.cnm.Address"/>
    
    <bean id="student" class ="com.cnm.Student" autowire="byType">bean>
    
    • 1
    • 2
    • 3
    • 4

    如果不告诉spring他们之间的依赖关系的话(即没进行自动装配),这样的话spring是不知道Student依赖Address的,所以默认不进行自动装配。

    自动装配就是Student依赖了Address,如果给Student开自动装配的话,他就会在容器中去找有没有Address,若有就直接给配上去了。

    autowire是开启自动装配,autowire="byType"是根据类型来自动注入,即实例化Student时,去找Address类型有没有。

    根据类型注入有个缺点是容器里Address类型的只能有一个,如果有两个bean,不同ID,都是指向Address的话,此时注入就会失败。

    还有autowire="byName"是根据名称(bean的ID)去找(属性名和配置文件中的id一致)。

    至于根据属性名称去找时,实际上他不是特别准确的,spring在找的时候,实际是根据get set方法去找,就是把get和set方法的前缀去掉(即剩下的Address)后去找的。所以如果把set get方法的名字改掉的话,就算属性名称和容器的id一样,结果却是找不到了。

    还有一种类型是autowire="constructor",这是根据构造方法去找,类似于byType,但此时Student类里要有参构造函数,参数可以是Address address

    基于xml + 注解配置bean

    基于xml配置有个问题,比如项目里面有N多个类,这些都配置到bean的话,配置文件可能会变得很复杂。

    基于注解配置Bean,比如在Student类里:

    @Component
    public class Student {
    	...
    }
    
    • 1
    • 2
    • 3
    • 4

    值得注意的是,现在这里用的是@Component注解,但一些注解是被这个@Component注解修饰的,比如@Service@Controller@Repository等注解上面都有@Component注解修饰,所以@Service等注解与@Component效果都一样。这些主要是为了web开发时,讲究分层架构,比如@Controller用在controller层上,@Service用在service层上,@Repository注解用于持久层里。

    当然,使用@Component等注解时,也可以指定名称,比如@Component("student1"),那就得用student1来从容器取类了。

    然后要让配置文件去扫描该注解,只要扫描到,就会把Student创建对象加入到容器中。前提是,要在配置文件里开启spring的注解扫描

    <?xml version="1.0" encoding="utf-8">
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/MXLSchema-instance"
    	xmlns:context="http://www.springframework.org/schema/context"		
    	https://www.springframework.org/schema/context/spring-context.xsd	
    ">
    
    	
    	<context:component-scan base-package="com.cnm"/>
    beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    基于xml + 注解进行bean的自动装配

    Student类依赖于Address类,在两个类上面都加了@Component注解,都干入了容器中,但却没有实现自动装配。

    自动装配用另外注解来实现。比如:

    @Component
    public class Student {
    	@Autowired
    	private Address address;
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    还可以使用@Resource注解,比如:

    @Resource
    private Address address;
    
    • 1
    • 2

    spring在进行自动装配的时候,他自己也会去容器中找对应的bean,比如需要自动装配Address时,他就会自己去容器中找有无Address这个bean。他找的时候,其实是有两种策略的,一种是根据bean的类型去找,即去找Address类型的bean,还有一种是根据名称(即ID)去找,即去找名称是否有叫addressbean

    @Resource有两个属性,一个是name,一个是type。比如你可以指定name去找,@Resource(name="address")

    还可以指定type,如@Resource(type=Address.class),按类型去找时,如果容器中有多个Address类型的话,此时它就会报错。

    当然,也可以既指定type也指定name,如@Resource(type=Address.class, name="address"),假设此时容器里有N个Address类型的bean,那么如果有idaddress的话,就能找到了。(两个都写的话,就是既按类型,也要按名称匹配

    那么默认的情况即一个都不配,直接使用@Resource的话,他会先按名称去找,如果按名称找不到了,再去按类型去找(按类型去找时,如果这个类型有多个bean的话依然会报错)。

    @Autowired则是会按类型去匹配,如果匹配到,就注入,否则就报错,如果配到多个,也会报错。(大部分情况都使用这个,如果需要指定名字或类型之类的,就可以使用@Resource

    另外,如果用注解方式给成员变量赋值的话(目的就是让他有初始值)可以如下:

    @Value("asd")
    private Strint name;
    
    • 1
    • 2

    跟下面的一样效果:

    <bean id="student" class ="com.cnm.Student">
    	
    	<property name="name" value="asd"/>
    bean>
    
    • 1
    • 2
    • 3
    • 4
    spring bean的作用域

    比如配置文件中配置了该bean

    <bean id="student" class ="com.cnm.Student"/>
    
    • 1

    然后在测试类中:

    ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("applicationContext.xml");
    Student s1 = (Student)application.getBean("student");
    Student s2 = (Student)application.getBean("student");
    
    • 1
    • 2
    • 3

    可以发现输出两个对象地址一样,即s1s2是一个对象。所以作用域不配的话,默认是singleton,如下:

    <bean id="student" class ="com.cnm.Student" scope="singleton"/>
    
    • 1

    这时他的作用域是在整个spring容器里,他是单例bean,在整个应用中不管在哪里得到都是同一个对象。

    第二种作用域是prototype,即:

    <bean id="student" class ="com.cnm.Student" scope="prototype"/>
    
    • 1

    这样的话,打印s1s2的地址是不一样的。即这时候容器中的Student对象是不唯一的。每次拿都会创建新的对象给你,所以作用域只限于你当使用的那一次。当从容器中取时,使用完了后,容器就会干掉他(多例bean)。

    还有两种是requestsession,这是和web应用相关的,这里省略。

    也可以基于注解来配置bean的作用域:

    @Component
    @Scope("prototype") //不加此注解时默认是单例。加了后设置为prototype就是多例了。
    public class Student {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    spring中bean的初始化(销毁)方法《干预bean的创建/销毁过程

    先在Student类中随便添加一个方法:

    public void init() {
    	// 这里可以写相关逻辑
    	// 另外,初始化方法是在自动装配完成后(依赖注入之后)执行的,比如Address配置bean后在这里输出就能输出address的值。
    }
    
    • 1
    • 2
    • 3
    • 4

    然后在容器的bean标签添加init-method="init"

    <bean id="student" class ="com.cnm.Student" init-method="init"/>
    
    • 1

    即在容器创建该bean时去执行初始化方法。

    还有销毁的方法,即bean对象销毁时执行,比如在Student加一个方法:

    public void destory() {
    	// 释放资源的逻辑
    }
    
    • 1
    • 2
    • 3
    <bean id="student" class ="com.cnm.Student" init-method="init" destory-method="destory"/>
    
    • 1

    bean销毁时执行的方法一般主要用来释放/关闭外部资源

    也可以通过注解方式,比如:

    @PostConstruct
    public void init() {
    
    }
    
    @PreDestory
    public void destory() {
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    代理模式

    静态代理

    代理模式是指通过代理类来访问原始类(被代理的类)。
    假设有UserService类,以及里面有addUser()方法:

    UserService.java

    public class UserService {
    	public void addUser() {
    		System.out.println("asd");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    假设有个客户端,想要去调用addUser方法,而且还想给addUser方法进行增强,但又不想动UserService的代码,这时,可以通过代理类来访问这个UserService类。

    建立一个客户端Test4:

    // 在正常情况下,可能是像下面这样调addUser方法:
    
    public class Test4 {
    	public static void main(String[] args) {
    		UserService us = new UserService();
    		us.addUser();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果想增强addUser方法的功能,比如在方法执行之前和执行之后输出一句话,正常情况下有可能会去直接改addUser方法,但现在是不想改UserService代码,所以可以去创建代理类

    UserServiceProxy.java

    // UserService的代理类
    public class UserServiceProxy {
    	private UserService userService;// 因为要通过此代理类去调用被代理类的方法
    
    	// 可以通过构造方法把原始类传进来
    	public UserServiceProxy(UserService userService) {
    		this.userService = userService;
    	}
    	
    	// 他也有这个方法,要和被代理类一致。对原始类的功能进行增强。
    	public void addUser() {
    		// 执行前输出一句
    		System.out.println("执行前");
    		// 调用原始对象的方法
    		userService.addUser();
    		// 执行后输出一句
    		System.out.println("执行后");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    // 使用代理类去调用addUser方法
    public class Test4 {
    	public static void main(String[] args) {
    		UserService userService = new UserService();
    		// 创建代理类对象,把被代理类传进去
    		UserServiceProxy us = new UserServiceProxy(userService);
    		us.addUser();// 通过代理类去执行原始类的方法
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    由于UserService的代理类是我们亲自创建的,所以这叫静态代理类

    静态代理的缺点是,假如每增加个功能类,就得加个代理类,如果多的话,会比较麻烦。即静态代理的代理类也不能复用(不同功能类不能复用一个代理类)。

    【补充】
    由于被代理类里面的相关方法在代理类中也会有,所以可以把这个方法单独抽出来。

    IUserService.java

    public interface IUserService {
    	public void addUser();
    }
    
    
    • 1
    • 2
    • 3
    • 4

    然后让UserService类去实现接口:

    public class UserService implements IUserService{
    	...
    }
    
    • 1
    • 2
    • 3

    代理类也要去实现这个接口:

    // UserService的代理类
    public class UserServiceProxy implements IUserService{
    	...
    }
    
    • 1
    • 2
    • 3
    • 4

    一般原始类目标类实现同一个接口,保证原始类的方法在代理类里头都有。

    动态代理之Proxy实现

    动态代理不需要手动去创建代理类,他可以在程序运行期间动态的创建代理类。jdk里提供了一种动态代理的实现方式,即Proxy类(要求是原始类必须要实现接口):

    Test.java

    public class Test {
    	public static void main(String[] args) {
    		// 动态代理(用Proxy方式去实现时,原始类必须实现接口,所以上面补充的内容适用于这里)。
    		// 由于是代理,也需要原始类对象。加final是因为匿名内部类要用到这个变量。
    		final IUserService userService = new UserService();
    		// 参数1:类加载器;参数2:原始类实现的接口;参数3:接口(接口是JDK提供的专门用来动态代理的接口InvocationHandler)的实现类,invoke方法就是实际的代理逻辑
    		// 得到代理类的对象us(返回的代理对象也是接口,所以上面被代理类需要实现接口),这里是强转成IUserService
    		IUserService us = (IUserService)Proxy.newProxyInstance(	userService.getClass().getClassLoader(),
    									userService.getClass().getInterfaces(),
    									new InvocationHandler() {
    										// method是具体的执行方法,即原来要调用的方法(即这里的addUser);args是方法的参数
    										public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    											// 具体的代理逻辑
    												
    											// 执行前输出一句
    											System.out.println("执行前");
    			
    											//用反射方式执行方法,参数1:那个对象执行 参数2:哪个方法
    											Object invoke = method.invoke(userService, args);
    											
    											// 执行后输出一句
    											System.out.println("执行后");
    											return invoke;// 原始类的方法有返回值的话就给他返回
    										}
    									}
    								     );
    		// 通过代理类去执行方法,invoke就会被执行
    		us.addUser();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    这种动态代理的局限是原始类必须实现一个接口,当然也可以实现多个。

    动态代理之cglib实现

    cglib是第三方提供的工具,所以在pom.xml引入(如果不导入,如果已经导入完spring包,也可以使用spring提供的)。

    <dependency>
    	<groupId>cglibgroupId>
    	<artifactId>cglibartifactId>
    	<version>2.2.2version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    public class Test {
    	public static void main(String[] args) {
    		// 通过cglib实现动态代理
    		// 1,创建原始类对象
    		final UserService userService = new UserService();
    		// 2,使用cglib得到代理对象(这里选net.sf.cglib.prixy包就行。而spring倒进来后org.springframework.cglib.proxy是spring自带的包)
    		// 参数1 Class type 即类型;参数2 Callback callback 回调函数,Callback是接口,世纪使用的是其实现类
    		// MethodInterceptor是接口,他继承了Callback
    		UserService us = (UserService)Enhancer.create(userService.getClass(),new MethodInterceptor() {
    			public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    				// 具体的代理逻辑
    				// 执行前输出一句
    				System.out.println("执行前");
    			
    				//用反射方式执行方法,参数1:userService 要执行哪个对象的方法 参数2:objects方法接受的参数数组
    				Object invoke = method.invoke(userService, objects);
    											
    				// 执行后输出一句
    				System.out.println("执行后");
    
    				return invoke;// 有的话就返回
    			}
    		});
    		// 执行代理类的方法
    		us.addUser();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    可以看到cglib是不要求原始类要实现接口,但他要求原始类不能是被final修饰的(因此可以猜测cglib的底层实现是用子类继承了原始类的方式来作为代理类)。

    目前测试的方法addUser既没有返回值,也没有参数,可以新建一个类试一下:
    StudentService.java

    public class StudentService {
    	public String addStudent(String name) {
    		System.out.println("执行方法");
    		return name;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在测试类中:

    ...
    final StudentService studentService = new StudentService();
    StudentService us = (StudentService)Enhancer.create(studentService.getClass(),new MethodInterceptor() {
    	public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    		System.out.println("执行前");
    			
    		//用反射方式执行方法
    		Object invoke = method.invoke(studentService, objects);
    											
    		// 执行后输出一句
    		System.out.println("执行后");
    
    		return invoke;// 有的话就返回
    	}
    });
    
    String str = us.addUser("asd");
    
    ...
    
    输出:
    /*
    执行前
    执行方法
    执行后
    asd
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    spring为了简化动态代理的操作,提供了aop功能,帮我们简化了动态代理的开发。

    spring aop

    aop的概念

    面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术,就是把程序中重复的功能抽取出来,例如性能检测、日志记录、权限控制这些公共逻辑,通过切面编程的方式把功能添加到原有的业务代码中,达到不修改原有代码对功能进行增强的目的。

    aop技术的常用实现方式

    AspectJ
    编译期织入增强逻辑,即编译好的class文件中已经包含了增强逻辑。所以是静态实现。
    Spring aop
    在程序运行期间使用动态代理来增强代码功能。

    aop优势

    不修改源码对功能进行增强,减少重复代码,可维护性强。

    aop相关概念
    • pointcut:切入点,用来描述spring aop要对哪些方法进行拦截
    • advice(通知):拦截到方法后要做的事情,就是通知。通知分为5类,前置通知(比如addUser方法之前执行的)、后置返回通知(比如addUser有返回值的话,返回了以后执行)、异常通知最终通知环绕通知
    • 切面:切入点通知的结合。
    aop的主要功能

    通过配置的方式,简化代理开发,达到不修改业务代码增强功能的目的。

    spring aop的使用步骤

    (1)导入spring aop相关依赖

    <dependency>
    	<groupId>org.springframeworkgroupId>
    	<artifactId>spring-aopartifactId>
    	<version>5.3.13version>
    dependency>
    
    <dependency>
    	<groupId>org.aspectjgroupId>
    	<artifactId>aspectjweaverartifactId>
    	<version>1.8.13version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    (2)制作通知类

    UserServiceAdvice.java

    public class UserServiceAdvice {
    
    	// 在目标方法执行之前会被执行
    	public void beforeAdvice() {
    		System.out.println("前置通知");
    	}
    
    	// 在目标方法成功返回之后会被执行
    	public void afterReturnAdvice() {
    		System.out.println("后置返回通知");
    	}
    
    	// 在目标方法发生异常时会被执行
    	public void exceptionAdive() {
    		System.out.println("异常通知");
    	}
    
    	// 在目标方法结束后执行,发生异常也会执行(类似于try catch中的finally)
    	public void afterAdvice() {
    		System.out.println("最终通知");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    (3)在配置文件中配置aop

    applicationContext.xml

    <?xml version="1.0" encoding="utf-8">
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/MXLSchema-instance"
    	xmlns:context="http://www.springframework.org/schema/context"	
    	xmlns:aop="http://www.springframework.org/schema/aop" 	
    	https://www.springframework.org/schema/aop/spring-aop.xsd		
    ">
    
    	
    	<bean id="userService" class="com.cnm.UserService">bean>	
    
    	
    	
    	<bean id="userServiceAdivce" class="com.cnm.UserServiceAdvice">bean>
    	
    	<aop:config>
    		
    		<aop:aspect ref="userServiceAdvice">
    			
    			
    			<aop:pointcut id="pt1" expression="execution(public void com.cnm.UserService.addUser())"/>
    			
    			<aop:before method="beforeAdvice" pointcut-ref="pt1"/>
    			
    			<aop:after-returning method="afterReturnAdvice" pointcut-ref="pt1"/>
    			
    			<aop:after-throwing method="exceptionAdvice" pointcut-ref="pt1"/>
    			
    			<aop:after method="afterAdvice" pointcut-ref="pt1"/>
    		aop:aspect>
    	aop:config>
    beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    这里需要注意的是启动后可能会爆出“没有UserService 这个bean”的错误,这根aop配置是相关的,如果在配置文件里把aop相关配置去掉的话就就不会报错。

    那为什么aop的配置加上后却找不到UserService 这个bean呢?其实spring的aop有两种实现模式,一种是通过cglib实现,另一种是通过
    jdk的proxy实现,如果待增强的类(UserService)实现了某个接口,spring就会去选择使用proxy实现动态代理,而放到容器的bean(UserService)其实是被增强后的bean,所以被增强后代理对象就会变成IUserService类型,跟之前proxy动态代理例子(动态代理是用接口来接收的)相似的情况。

    而下面测试类是通过userService去拿对象的,所以找不到。

    (4)测试类

    UserService.java

    public class UserService implements IUserService {
    	public void addUser() {
    		System.out.println("asd");
    		// int a = 1/0;手动造异常 
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Test.java

    public class Test {
    	public static void main(String[] args) {
    		ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("applicationContext.xml");
    		// 由于UserService实现了IUserService接口
    		IUserService s = (IUserService)application.getBean(IUserService.class);
    		s.addUser();
    		
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    运行结果:
    (没异常时)
    前置通知
    asd
    后置返回通知
    最终通知
    (有异常时)
    前置通知
    asd
    异常通知
    最终通知
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    环绕通知

    UserServiceAdvice.java

    ...
    //自己手动控制方法的执行时机
    public void arroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
    	// spring会把ProceedingJoinPoint注入进来
    	System.out.println("环绕通知");
    }
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ApplicationContext.xml

    ...
    
    
    <aop:arround method="arroundAdvice" pointcut-ref="pt1"/>
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果这样配置后,运行的话,会发现虽然输出"环绕通知",但不输出addUser里的“asd”了,这说明addUser这个业务方法竟然没被执行。

    这是因为环绕通知需要你手动控制方法的执行时间,如果改成如下:

    public void arroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
    	System.out.println("环绕通知");
    	// 执行原有逻辑。(就是所有东西由我们自己来决定,自由度更高一些)
    	proceedingJoinPoint.proceed();
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样的话就能两个都能输出了。可以看出来环绕通知是更灵活的处理方式来干预原有方法的执行逻辑,比如可以如下用:

    public void arroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
    	try {
    		System.put.println("环绕前置通知");
    		Object proceed = proceedingJoinPoint.proceed();
    		System.put.println("后置返回通知");
    	}catch (Throwable e) {
    		e.printStackTrace();
    		System.put.println("环绕异常通知");
    	}finally {
    		System.put.println("环绕最终通知");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    一般来说,建议环绕通知和上面四个通知不要同时配,要么单独配4个,要么单独配环绕。

    基于注解+xml配置spring aop

    (1)配置目标类的bean

    applicationContext.xml

    ...
    
    <context:component-scan base-package="com.cnm"/>
    
    <aop:aspectj-autoproxy />
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    UserService.java

    @Component // 当然要在配置文件先开启spring的注解扫描
    public class UserService implements IUserService{
    	public void addUser() {
    		...	
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    (2)把通知类配置到容器里

    UserServiceAdvice.java

    @Component // 当然要在配置文件先开启spring的注解扫描
    @Aspect // 指定这个类是切面(与...对应)
    public class UserServiceAdvice {
    	// 定义切入点(与aop:pointcut 对应)
    	// 切入点表达式:权限修饰符 返回值类型 包名.类名.方法名(参数列表)
    	@Pointcut("execution(public void com.cnm.IUserService.addUser())") 
    	public void pt1() {}// 方法名随便写的
    
    	@Before("pt1()")
    	public void beforeAdvice() {
    		print("前置通知");
    	}
    	
    	@AfterReturning("pt1()")
    	public void afterReturnAdvice() {}
    
    	@AfterThrowing("pt1()")
    	public void exceptionAdvice() {}
    
    	@After("pt1()")
    	public void afterAdvice() {}
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    但是像上面那样@Pointcut("execution(public void com.cnm.IUserService.addUser())") 这么写的话,只能增强IUserServiceaddUser方法,而其他的方法无法增强。这样的话,假如其他的类的某方法要增强,又得写@Pointcut("execution(public void com.cnm.IUserService.addUser())")了,这样是很麻烦的。

    可以用通配符 * 。比如@Pointcut("execution(public * com.cnm.*.*(..))")(..)是既支持有参数的,也支持无参数的。所以配置时要注意好哪个部分用通配符来代替,哪个部分写具体的。如果写@Pointcut("execution(public * *..*.*(..))"),包名部分改为*..的话,说明既匹配当前包和他的子包。

    在aop通知中获取方法的参数和返回值

    在前置通知beforeAdvice()中获取addUser()方法的参数和返回值。比如:

    IUserService.java

    public interface IUserService{
    	public String addUser(String username);
    }
    
    • 1
    • 2
    • 3

    UserService.java

    @Component
    public class UserService implements IUserService {
    	public String addUser(String username) {
    		System.out.println(username);
    		return "asd";
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    UserServiceAdvice.java

    @Component 
    @Aspect 
    public class UserServiceAdvice {
    
    	@Pointcut("execution(public void com.cnm.IUserService.addUser())") 
    	public void pt1() {}// 方法名随便写的
    
    	@Before("pt1()")
    	public void beforeAdvice(JoinPoint point) {// 加JoinPoint point 让spring注入进来
    		// 方法的全路径(比如输出:String com.cnm.IUserService.addUser(String))
    		Signature signature = point.getSignature();
    		// 通过point来获取方法的参数,其他通知也一样。
    		Object[] args = point.getArgs();
    		// 输出参数列表
    		if (agrs != null) {
    			for (Object arg: args) {
    				// 输出arg就是参数值
    			}
    		}
    		print("前置通知");
    	}
    
    	// 只能在后置返回通知里能获取addUser方法的返回值(最终通知也获取不到,因为如果发生异常就获取不到了)
    	@AfterReturning("pt1()", returning="returnValue")// 基于注解去配置时,@AfterReturning里有个参数是returning,比如这里设置值为"returnValue"
    	public void afterReturnAdvice(JoinPoint point, String returnValue) {// 这里加一个参数,名称就叫"returnValue"。
    		// 返回值就是returnValue
    	}
    
    	// 环绕通知
    	@Around("pt1()")// 当然注解配置环绕通知时,最好把其他的普通通知的注解配置的都干掉
    	public void arroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
    		try {
    			System.put.println("环绕前置通知");
    
    			// 获取addUser的参数
    			Object[] args = proceedingJoinPoint.getArgs();
    			// 执行原有逻辑
    			Object proceed = proceedingJoinPoint.proceed();// proceed是addUser的返回值
    			System.put.println("后置返回通知");
    		}catch (Throwable e) {
    			e.printStackTrace();
    			System.put.println("环绕异常通知");
    		}finally {
    			System.put.println("环绕最终通知");
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    Test.java

    public class Test {
    	public static void main(String[] args) {
    		ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("applicationContext.xml");
    		// 由于UserService实现了IUserService接口
    		IUserService s = (IUserService)application.getBean(IUserService.class);
    		s.addUser("cnm");
    		
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果是用配置文件方式去获取方法的参数和返回值,获取参数的方法与上面一致,但获取返回值的方式与上面有区别。

    比如注解配置时,可以通过@AfterReturning注解returning参数来获取,而配置文件配置的话,比如如下:

    <aop:config>
    	<aop:aspect ref="userServiceAdvice">
    		<aop:pointcut id="pt1" expression="execution(public void com.cnm.UserService.addUser())"/>
    		
    		<aop:after-returning method="afterReturnAdvice" pointcut-ref="pt1" returning="returnValue"/>
    	aop:aspect>
    aop:config>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    然后跟上面一致,可以通过returnValue参数拿到返回值。

    全注解配置spring容器

    上面的无论是纯xml还是xml + 注解的方式,都避免不了使用配置文件。现在都流行0配置文件,那么,如何完全干掉配置文件呢?

    目前的配置文件如下:
    applicationContext.xml

    ...
    
    <context:component-scan base-package="com.cnm"/>
    
    <aop:aspectj-autoproxy />
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    也就是因为这两行代码所以离不开配置文件。所以解决这两行就行。

    spring提供了两种配置模式,其顶层接口是BeanFactory,他的子接口是ApplicationContext,他有两个实现类分别是AnnotationConfigApplicationConextClassPathXmlApplicationContext,上面一直用的是ClassPathXmlApplicationContext,而AnnotationConfigApplicationConext就是基于注解的。

    所以创建基于AnnotationConfigApplicationConext类的容器,就能实现全注解配置

    配置类去取代配置文件

    AppConfig.java类名随便取

    @Configuration // 告诉spring这是一个配置类
    @ComponentScan("com.cnm") // 指定包扫描的路径
    @EnableAspectJAutoProxy // 开启aop
    public class AppConfig {
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Test.java

    public class Test {
    	public static void main(String[] args) {
    		AnnotationConfigApplicationConext application = new AnnotationConfigApplicationConext(AppConfig.class);
    		// 从容器获取对象
    		IUserService s = (IUserService)application.getBean(IUserService.class);
    		s.addUser("cnm");
    		
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    【以上内容均来自网络】

  • 相关阅读:
    Android webView JS 之间的交互
    HyperMesh网格划分简要流程小试
    Kubernetes 集群中流量暴露的几种方案
    栈实现深度优先搜索
    [Spring笔记] Spring-22-注解开发依赖注入
    电脑使用技巧分享  电脑小白们快收藏
    Linux 中 .tar 和 tar.gz 的区别
    出海季收官,速来 Get 全球化发展实操手册
    cocos creater 鸿蒙 音频卡死 播放失败 不回调
    5G消息发展的前景与挑战
  • 原文地址:https://blog.csdn.net/PurineKing/article/details/127453697