面试官:谈谈你对IOC和AOP的理解及AOP四种实现方式[通俗易懂]

面试官:谈谈你对IOC和AOP的理解及AOP四种实现方式[通俗易懂]一、AOP介绍面向切片编程(AOP—AspectOrientedProgramming)可以说是对OOP(面向对象编程)的补充和完善,面向对象就是将事物的特性和行为抽象为一个对象,如people类有身高、体重、年龄等属性,也有吃饭、睡觉等行为。把这些特性和行为封装成一个类,然后可以统一调用。面向切片也可以举个例子,比如people类有自己的属性和行为,但是有小一部分人生病要去医院看病,看病这个业务逻辑就不属于哪一个类,因为people泛指所有人,所有人不会都看病。AOP就是把医院看病这一个业务逻辑功能

大家好,又见面了,我是你们的朋友全栈君。

一、IOC与AOP介绍

IOC
控制反转(IOC)是一种设计思想,就是将原本在程序中需要手动创建对象,现在交由Spring管理创建。举个例子,原本我们要在A类中调用B类的方法,就要直接在A中new出B类对象,然后调用B类中的方法,虽然能实现效果,不过存在一个问题,更改需求会对源代码进行修改,这是大忌。现在创建B对象就交给了Spring,在Spring中,B类对象被看成Bean对象(Spring中类就是Bean),这个Bean对象由spring容器进行创建和管理,当我们在配置文件中配置<Bean>下的<property>子元素(类的属性)时,Spring就会自动执行在A中B对象的setter方法(前提要有),这样的话A对象获取B对象中的方法,由主动new,变成被动等Spring创建。主动变被动,就可以理解成控制反转,通俗讲就是“你别动,我(Spring)来做就好”,主动变被动,这样就大大降低了耦合,Spring中全都用这种思想,即依赖类不由程序员实例化,而是通过Spring容器帮我们new指定实例并且将实例注入到需要该对象的类中,Spring通过DI(依赖注入)实现IOC(控制反转)。

可以看到后面讲aop时,applicationContext.xml(Spring配置)中就体现这种思想,比如applicationContext.xml注册三个bean,也就是Spring创建了这三个类。id可以理解成变量名,class就是实现类,第一个bean可以等价于UserService userService = new UserServiceImpl(); 同理,第二个等价于Log log = new Log();
在这里插入图片描述
以前Person类中定义age属性,并有getAge/setAge,然后创建并赋值 Person person = new Person(); person.setAge(16);现在换成Spring的话就是<bean id="person" class="com.yx.entity.Person"><property name="age" value="16"></property></bean>,这两个是一一对应的,id相当于变量名person,属性property会自动执行对应的setAge方法。Spring负责创建,这就是以前需主动创建,现在由Spring创建与管理,大大降低耦合,体现控制反转思想(主动变被动)。

AOP
面向切片编程(AOP—Aspect Oriented Programming)可以说是对OOP(面向对象编程)的补充和完善,面向对象就是将事物的特性和行为抽象为一个对象,如people类有身高、体重、年龄等属性,也有吃饭、睡觉等行为。把这些特性和行为封装成一个类,然后可以统一调用。面向切片也可以举个例子,比如people类有自己的属性和行为,但是有小一部分人生病要去医院看病,看病这个业务逻辑就不属于哪一个类,因为people泛指所有人,所有人不会都看病。AOP就是把医院看病这一个业务逻辑功能抽取出来,然后动态把这个功能切入到需要的方法(或行为)中,需要的才切入,这样便于减少系统的重复代码,降低模块间的耦合度。常用到AOP的就是安全校验、日志操作、事务操作等,给你先定义好,然后在想用的地方用,这样不会影响已经在服务器运行的项目,然后又能注入新功能,灵活。我们开发dao->service->controller是纵向的,这个AOP就是横向切入,如横向切入一个日志Log,打印执行过程。
在这里插入图片描述
Spring AOP就是基于动态代理实现的, 分为两种代理,jdk动态代理(基于接口)和cglib代理(基于类的)。如果目标对象实现了接口,就用jdk动态代理,如果未实现接口就用cglib动态代理。虽然动态代理可以解决耦合问题,但比较抽象,复杂,属于底层实现代理模式,我们这里直接用AOP,AOP做了很多封装,只要调用API即可,简化开发,但是AOP底层原理还是需要了解。

注意:动态代理底层利用了反射机制,反射包下Proxy类,如果想了解底层原理的,推荐这个视频 动态代理详解

如果是面试,可以按上面文字答(图片上面的部分),比如面试官让你谈谈对IOC和AOP理解。

二、实现AOP的四种方式

首先,先要建一个maven项目,下面是pom.xml依赖和项目结构
在这里插入图片描述
Spring AOP支持五种类型的通知(先有个印象):

前置通知[Before advice]:在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常。 
正常返回通知[After returning advice]:在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行。 
异常返回通知[After throwing advice]:在连接点抛出异常后执行。 
返回通知[After advice]:在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。 
环绕通知[Around advice]:环绕通知围绕在连接点前后,比如一个方法调用的前后。
<dependencies>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.6</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.3.6</version>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

方式1:基于经典代理来实现

首先我们要先写一个Sleep接口

public interface Sleep { 
   
    void sleep(); //睡觉
}

Baby类,Baby要睡觉,但是在冬天宝宝睡觉前要脱掉几件厚重的衣服,起床后穿上衣服,那么就可以用AOP切入

public class Baby implements Sleep{ 
   
    @Override
    public void sleep() { 
   
        System.out.println("一岁小宝宝睡觉了。。。");
    }
}

BabyHelper类,帮宝宝脱衣服的人,含前置通知和正常返回通知

import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class BabyHelper implements MethodBeforeAdvice, AfterReturningAdvice { 
   

    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable { 
   
        System.out.println("睡觉前帮宝宝脱去衣服!!!");
    }

    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable { 
   
        System.out.println("起床后帮宝宝穿衣服!!!");
    }
}

有上面三个后,再定义最核心的配置文件,配置切面,application.xml

切面定义:通知、连接点、切入点共同组成了切面:时间、地点和要发生的“故事”,相当于数学里的点连成面,这里就是切点->切面。

如果切面理解还是不深,没关系,后面还有三种实现方式会一直重复切面=通知+连接点+切入点

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 定义被代理者 -->
<bean id="baby" class="com.yx.bean.Baby"></bean>
<!-- 定义通知内容,也就是切入点执行前后需要做的事情 -->
<bean id="sleepHelper" class="com.yx.bean.BabyHelper"></bean>
<!-- 定义切入点位置 sleep方法前后-->
<bean id="sleepPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="pattern" value=".*sleep"></property>
</bean>
<!-- 使切入点与通知相关联,完成切面配置-->
<bean id="sleepHelperAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<!--通知,前置通知before,返回通知afterReturning-->
<!--连接点可以认为方法调用就是一个连接点,通知就是把控切入哪个位置,切点就是具体要做的行为 通知可以告诉这些行为在哪个位置做,连接点把这些行为连接起来-->
<property name="advice" ref="sleepHelper"></property>
<!--切点-->
<property name="pointcut" ref="sleepPointcut"></property>
</bean>
<!-- 设置代理 -->
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 代理的对象,有睡觉能力 -->
<property name="target" ref="baby"></property>
<!-- 代理接口,睡觉接口 -->
<property name="proxyInterfaces" value="com.yx.bean.Sleep"></property>
<!-- 使用切面 即向代理对象切入-->
<property name="interceptorNames" value="sleepHelperAdvisor"></property>
</bean>
</beans>

然后我们来运行测试test.java

public class test { 

//通过AOP代理的方式执行Baby的sleep()方法,会在执行前、执行后切入,实现了AOP的效果
@Test
public void aoptest() { 

@SuppressWarnings("resource")
ApplicationContext appCtx = new FileSystemXmlApplicationContext("application.xml");
Sleep baby = (Sleep) appCtx.getBean("proxy");//获取代理对象,application.xml定义了id="proxy"的bean对象
baby.sleep();
}
}

如下,就成功在sleep()前后切入两个行为
在这里插入图片描述

上面我们就应该对AOP具体实现有了初步理解,这是最经典的,也是最接近原理的配置,就是先配置切点、通知,然后才能组成切面,再使用配置好的切面向代理对象切入,通知把控切入位置,前面还是后面,切点就是要切入的具体内容

下面我们再写一个类似案例,作为巩固

方式2:使用Spring AOP的API接口

然后再新建一个Maven项目,pom文件还是用上面一样,创建service包,并在其中写一个业务UserService,用户可以进行增删改查,我们一样先给出整体结构
在这里插入图片描述

在这里插入图片描述
UserSerivce接口

public interface UserService { 

void add();
void del();
void update();
void select();
}

并写一个实现类UserServiceImpl

public class UserServiceImpl implements UserService { 

@Override
public void add() { 

System.out.println("增加了一个用户");
}
@Override
public void del() { 

System.out.println("删除了一个用户");
}
@Override
public void update() { 

System.out.println("更新了一个用户");
}
@Override
public void select() { 

System.out.println("查询了一个用户");
}
}

再写一个Log包,里面写日志,就是需要新增的功能,实际开发中常常也需要切入安全校验,日志操作和事务操作,写一个前置一个后置
在这里插入图片描述

BeforeLog.java

import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
public class BeforeLog implements MethodBeforeAdvice { 

//Method:要执行目标对象的方法
//args:参数
//target:目标对象
@Override
public void before(Method method, Object[] args, Object target) throws Throwable { 

System.out.println(target.getClass().getName()+"->方法:"+method.getName()+"被执行");
}
}

AfterLog.java

import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
public class AfterLog implements AfterReturningAdvice { 

@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { 

System.out.println("执行了"+method.getName()+",返回结果为:"+returnValue);
}
}

resources包进行相关配置,包括注册bean,配置切面
在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean,将所有的实体类先注册-->
<bean id="userService" class="com.yx.service.UserServiceImpl"></bean>
<bean id="beforeLog" class="com.yx.log.BeforeLog"></bean>
<bean id="afterLog" class="com.yx.log.AfterLog"></bean>
<!--配置aop切面-->
<aop:config>
<!--切入点 expression为表达式,参数为切入的位置,UserServiceImpl.*表示类中所有方法都切入,*(..)两个点表示不指定参数(任意)-->
<aop:pointcut id="pointcut" expression="execution(* com.yx.service.UserServiceImpl.*(..))"/>
<!--配置类切入到哪里:如beforeLog类切入到expression表达式指的位置,即UserServiceImpl类下的所有方法-->
<aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>
</beans>

用Spring AOP的API接口进行切入的好处就是,他会自动帮你创建和管理类,不用亲自和以前一样new对象,这样可以实现解耦,因为你只要设置目标类路径即可,然后无论类怎么变都无需进行改动,上面就可以把beforeLog和afterLog切入到UserServiceImpl所有方法中。

写一个测试类进行测试

import com.yx.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyTest { 

public static void main(String[] args) { 

//加载配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//通过目标的bean id,获得代理对象
UserService proxy = (UserService)context.getBean("userService");//获取注册的bean对象,实例对象变成bean对象,就是代理对象
proxy.add();
}
}

在这里插入图片描述
这样比第一个基于经典代理实现方式更简洁,只要两步
1、先注册实体类,实例对象就变为bean对象
2、配置aop切面,这里只要在<aop:config></aop:config>中配置切点和通知即可,切点配合expression为表达式指定具体哪些切入方法

再回顾一下这两步,是不是精简许多

	<!--1、注册bean,将实体类注册-->
<bean id="userService" class="com.yx.service.UserServiceImpl"></bean>
<bean id="beforeLog" class="com.yx.log.BeforeLog"></bean>
<bean id="afterLog" class="com.yx.log.AfterLog"></bean>
<!--2、配置AOP切面-->
<aop:config>
<!--切入点 expression为表达式,参数为切入的位置,UserServiceImpl.*表示类中所有方法都切入,*(..)两个点表示不指定参数(任意)-->
<aop:pointcut id="pointcut" expression="execution(* com.yx.service.UserServiceImpl.*(..))"/>
<!--配置类切入到哪里:如beforeLog类切入到expression表达式指的位置,即UserServiceImpl类下的所有方法-->
<aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>

方式3:自定义类来实现AOP

在创建一个Diy包,在其中自定义一个类diyPointCut,类中定义要切入的方法
在这里插入图片描述

public class diyPointCut { 

public void before(){ 

System.out.println("=========方法执行前==========");
}
public void after(){ 

System.out.println("=========方法执行后==========");
}
}

applicationContext1.xml配置文件,和方式1类似,但稍微有不同,不同就是自定义注册

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<bean id="userService" class="com.yx.service.UserServiceImpl"></bean>
<bean id="diy" class="com.yx.Diy.diyPointCut"></bean>
<aop:config>
<!--自定义切面-->
<aop:aspect ref="diy">
<!--切入点-->
<aop:pointcut id="point" expression="execution(* com.yx.service.UserServiceImpl.*(..))"/>
<!--通知(指定diyPointCut类中的哪个method,aop:before就表示前置)-->
<aop:before method="before" pointcut-ref="point"></aop:before>
<aop:after method="after" pointcut-ref="point"></aop:after>
</aop:aspect>
</aop:config>
</beans>

MyTest改成加载applicationContext1.xml 并运行,结果如下
在这里插入图片描述
自定义的好处:可以直接在一个自定义类中定义切入的多个方法,并且不用实现对应接口,比如之前的用API接口的实现方式,还要去写用了什么通知,前置通知还是后置通知,但这里不用了,如下图implements MethodBeforeAdviceimplements AfterReturningAdvice这里自定义就不用去实现了,直接用aop:before或者aop:after就可以表示用了什么通知,只要在自定义类中定义需要切入的方法即可,如此处diyPointCut自定义类就定义了before和after这两个普通方法
在这里插入图片描述

上面这样就更简便了,能不能更简便一点,能!!!注解大法来了,我们开发中常用的什么@AutoWired,@Controller注解,都是编译自动帮你做了注册bean等操作,虽然开发可以这么做,但是我们最好还是要知道原理,不能仅仅做个调接口的程序员。

方式4:基于注解实现

新增一个注解类,@Aspect表示切面,@Before表示前置通知,@After表示后置通知,通知结合execution(* com.yx.service.UserServiceImpl.*(..))表示对类中哪些方法有通知,这里指的是UserServiceImpl类中的全部方法
在这里插入图片描述

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class AnnotationPointCut { 

@Before("execution(* com.yx.service.UserServiceImpl.*(..))")
public void before(){ 

System.out.println("==========方法执行前(注解)==========");
}
@After("execution(* com.yx.service.UserServiceImpl.*(..))")
public void after(){ 

System.out.println("==========方法执行后(注解)==========");
}
}

本质还是扫描到注解以后,就会转成类似方式3的配置文件,@Before(切点)
applicationContext2.xml只需要注册bean,开启注解即可

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<bean id="userService" class="com.yx.service.UserServiceImpl"></bean>
<bean id="annotationPointCut" class="com.yx.Diy.AnnotationPointCut"></bean>
<!--开启注解支持,会自动扫描注解-->
<aop:aspectj-autoproxy/>
</beans>

MyTest修改加载的配置文件,改成applicationContext2.xml
在这里插入图片描述
在这里插入图片描述

至此四种方式都讲解完毕,我们常常用最后一种,因为基于注解来写,真的太简单了!!!

下面是整体总结构
在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/150704.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • file_handle_windows句柄

    file_handle_windows句柄1.概述在实际工作中会经常遇到一些bug,有些就需要用到文件句柄,文件描述符等概念,比如报错:toomanyopenfiles,如果你对相关知识一无所知,那么debug起来将会异常痛苦。在linux操作系统中,文件句柄(包括Socket句柄)、打开文件、文件指针、文件描述符的概念比较绕,而且windows的文件句柄又与此有何关联和区别?这一系列的问题是我们不得不面对的。博主通过翻…

    2022年10月10日
  • Linux下rpm安装lrzsz

    Linux下rpm安装lrzsz下载yum-yinstallyum-utilsyumdownloader–resolve–destdir=/root/soft/lrzszlrzsz[root@instance-1apocjsh~]#yumdownloader–resolve–destdir=/root/soft/lszrzlrzszLastmetadataexpirationcheck:0:02:48agoonSat19Mar202211:22:41PMCST.lrzs

  • 做3D游戏建模师有年龄限制吗?

    做3D游戏建模师有年龄限制吗?3D游戏建模师是一门非常火热的行业,现在很多游戏厂商都在高薪招聘3D游戏建模师。因此很多人在想要学习次世代建模技术的同时,又担心企做这个会不会有年龄的限制。其实大可不必担心,因为只要本身具有学习能力,愿意接受新知识,愿意与时俱进的提高自己,那么年龄就不是你的问题。其实年龄大的人从事3D游戏建模行业也是有一定优势的,他们生活阅历丰富,对于如何把握人物建模和场景建模方面的细节会有自己独到的理解。同时又具备一定缺陷,在学习方面会有一定的障碍,因为不管是记忆力还是对新知识的接受能力都会有所下降,所以一定要选择一

  • 快速制作机房3D效果图教程「建议收藏」

    快速制作机房3D效果图教程「建议收藏」作者:广州麦景科技有限公司林鲁刚 原文接随着信息网络技术的不断发展,大量数据中心的建设,机房监控软件已经成为了机房管理者重要的管理工具,机房监控软件也从无到有,从2D到3D,从静态到三维动态的改进。不多说,直接上图↓以前是这样的现在是这样的或者这样的(麦景数据中心可视化管理平台)现在教大家如何画好一张机房效果图,所用软件有↓前期准备资料

  • Linux下默认RPM安装路径「建议收藏」

    Linux下默认RPM安装路径「建议收藏」针对RPM包一般情况下命令放在/usr/bin或/usr/sbin下库在/usr/lib下数据文件在/usr/share/下命名:rpm -pql [rpm文件名],来查看一个rpm包里有哪些文件,即安装的路径如果是从源码安装的话  可以选择安装路径:用这条命今看参数./configure –help./configure –prefix=/

  • 基于LM331的电压-频率转换电路详细介绍[通俗易懂]

    基于LM331的电压-频率转换电路详细介绍[通俗易懂]目录1.LM331简介2.引脚分布与功能3.LM331的功能框图4.V/F转换的工作原理5.LM331的V/F转换电路1.LM331简介LM331是由美国NS公司(已被TI公司收购)生产的高精度频率-电压转换芯片,可以用于AD转换、频率-电压转换、电压-频率转换和转速测量等。当用作频率-电压转换时输出频率与输入电压成正比例关系,线性失真最大为0.01%。动态范围广,最大可达100dB;温度稳定性高,温度系数为±50ppm/℃;工作范围广(1Hz-100kHz);外.

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号