Skip to content

AOP & IOC

先理解 AOP 是啥

AOP 全称 Aspect-oriented Programming,中文叫面向切面编程。

你可以这样理解:

你写了很多 Controller 接口,比如登录、查询用户、下单等等。现在老板说:"每个接口都要记录一下谁访问了、花了多长时间、传了什么参数。"

笨办法是每个接口里都写一遍日志代码,又累又乱。

AOP 的思路是:我不改你的业务代码,而是在旁边"切"一刀,统一帮你加上日志功能。

就像你上班打卡,不是你每做一件事都自己记时间,而是门口有个打卡机统一帮你记。

几个术语,用大白话解释

术语 大白话
横切关注点 那些跟核心业务没直接关系、但每个方法都要做的事,比如日志、权限、事务
切面(Aspect) 把上面这些事封装成一个类,用 @Aspect 标记
通知(Advice) 具体什么时候干什么,比如"方法执行前记录开始时间"
连接点(JoinPoint) 可以被切的地方,比如某个方法被调用时
切点(Pointcut) 指定哪些方法要被切,比如"controller 包下所有 public 方法"

通知有 5 种: - @Before:方法执行前 - @After:方法执行后(不管成功失败) - @AfterReturning:方法成功返回后 - @AfterThrowing:方法抛异常后 - @Around:把整个方法包起来,前后都能插手(最常用)

切入点表达式的语法格式规范如下所示:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?
                name-pattern(param-pattern)
                throws-pattern?)
  • modifiers-pattern? 为访问权限修饰符
  • ret-type-pattern 为返回类型,通常用 * 来表示任意返回类型
  • declaring-type-pattern? 为包名
  • name-pattern 为方法名,可以使用 * 来表示所有,或者 set* 来表示所有以 set 开头的类名
  • param-pattern) 为参数类型,多个参数可以用 , 隔开,各个参与也可以使用 * 来表示所有类型的参数,还可以使用 (..) 表示零个或者任意参数
  • throws-pattern? 为异常类型
  • ? 表示前面的为可选项

举个例子:

@Pointcut("execution(public * com.codingmore.controller.*.*(..))")

表示 com.codingmore.controller 包下的所有 public 方法都要应用切面的通知。

实操 AOP 记录接口访问日志

它做了一个"接口访问日志切面",步骤是: 第一步:在 pom.xml 里加 AOP 依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

第二步:写一个 WebLog 类,用来装日志信息,比如: - 操作描述 - 谁操作的 - 开始时间 - 花了多少时间 - 请求路径 - 方法名 - IP - 请求参数 - 返回结果

/**
 * Controller层的日志封装类
 * Created by macro on 2018/4/26.
 */
public class WebLog {
    private String description;
    private String username;
    private Long startTime;
    private Integer spendTime;
    private String basePath;
    private String uri;
    private String url;
    private String method;
    private String ip;
    private Object parameter;
    private Object result;
    //省略了getter,setter方法
}

第三步:写切面类 WebLogAspect:

@Aspect
@Component
public class WebLogAspect {
    @Pointcut("execution(public * com.codingmore.controller.*.*(..))")
    public void webLog() {}
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();   // 记录开始时间
        Object result = joinPoint.proceed();            // 执行原来的方法
        long endTime = System.currentTimeMillis();      // 记录结束时间
        // ... 把各种信息塞进 WebLog,然后打印日志
        return result;                                  // 返回原方法的结果
    }
}
/**
 * 统一日志处理切面--完整代码示例
 */
@Aspect
@Component
@Order(1)
public class WebLogAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);

    @Pointcut("execution(public * com.codingmore.controller.*.*(..))")
    public void webLog() {
    }

    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
    }

    @AfterReturning(value = "webLog()", returning = "ret")
    public void doAfterReturning(Object ret) throws Throwable {
    }

    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        //获取当前请求对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        //记录请求信息(通过Logstash传入Elasticsearch)
        WebLog webLog = new WebLog();
        Object result = joinPoint.proceed();
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method.isAnnotationPresent(ApiOperation.class)) {
            ApiOperation log = method.getAnnotation(ApiOperation.class);
            webLog.setDescription(log.value());
        }
        long endTime = System.currentTimeMillis();
        String urlStr = request.getRequestURL().toString();
        webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath()));
        webLog.setIp(request.getRemoteUser());
        Map<String,Object> logMap = new HashMap<>();
        logMap.put("spendTime",webLog.getSpendTime());
        logMap.put("description",webLog.getDescription());
        LOGGER.info("{}", JSONUtil.parse(webLog));
        return result;
    }
}

核心就一句:joinPoint.proceed() 是"执行你原来那个接口方法",在它前后加代码,就能记录时间和日志。

第四步:启动项目,访问接口,控制台就会自动打印日志。

一句话总结

AOP 就是让你不改业务代码,就能给一批方法统一加上日志、权限、事务等功能。文章就是用一个"记录接口访问日志"的例子,把这个过程完整走了一遍。


IoC 到底是啥

IoC = Inversion of Control,中文叫控制反转。

控制反转就是把创建和管理 bean 的过程转移给了第三方。而这个第三方,就是 Spring IoC Container,对于 IoC 来说,最重要的就是容器。

容器负责创建、配置和管理 bean,也就是它管理着 bean 的生命,控制着 bean 的依赖注入。

通俗点讲,因为项目中每次创建对象是很麻烦的,所以我们使用 Spring IoC 容器来管理这些对象,需要的时候你就直接用,不用管它是怎么来的、什么时候要销毁,只管用就好了。

举个例子,就好像父母没时间管孩子,就把小朋友交给托管所,就安心的去上班而不用管孩子了。托儿所,就是第三方容器,负责管理小朋友的吃喝玩乐;父母,相当于程序员,只管接送孩子,不用管他们吃喝。

等下,bean 又是什么?

Bean 其实就是包装了的 Object,无论是控制反转还是依赖注入,它们的主语都是 object,而 bean 就是由第三方包装好了的 object(想一下别人送礼物给你的时候都是要包装一下的,自己造的就免了)。

Bean 是 Spring 的主角,有种说法叫 Spring 就是面向 bean 的编程(Bean Oriented Programming, BOP)。

一句话解释:

以前是你自己 new 对象,现在把"创建和管理对象(Bean)"这件事交给 Spring 容器去做,你需要的时候直接拿就行。

打个比方: - 以前:你想喝咖啡,自己买豆子、磨粉、煮水、冲泡,全程自己搞。 - 现在:你直接去咖啡店,说"来杯美式",店员(Spring 容器)给你做好,你只管喝。

咖啡店就是那个"第三方容器",你不再控制咖啡怎么做,控制权交出去了,这就是"反转"。

为什么要这么做(解藕)

想象一个项目里有 A、B、C、D 四个对象,互相 new 来 new 去:

A → new B
B → new C
C → new D

一旦 C 改了构造方法,B 就得跟着改;B 改了,A 也得改。牵一发动全身,这就是耦合太紧。

用了 Spring 容器之后:

A ─┐
B ─┼──→ Spring 容器(统一管理)
C ─┤
D ─┘

每个对象都只跟容器打交道,彼此不直接依赖。谁需要什么,容器负责"注入"进去。这就是解藕。

几个关键名词

名词 大白话
Bean 被 Spring 容器管理的对象。其实就是包装过的 Object
IoC 容器 管理所有 Bean 的地方,负责创建、配置、销毁
BeanFactory 最基础的容器,可以理解成一个 HashMap:key 是 bean 名字,value 是 bean 对象
ApplicationContext 高级容器,是 BeanFactory 的子类,功能更多,实际开发常用
依赖注入(DI) 容器把对象需要的资源"送进去",而不是对象自己去拿
IoC 和 DI 的关系:
- IoC 是思想(控制权交给容器)
- DI 是实现方式(具体怎么交?靠注入)
- 它俩其实是同一件事的不同角度描述

不用 IoC vs 用 IoC

不用 IoC(传统写法):

Rectangle rect = new Rectangle();  // 自己 new
rect.setLength(2);
rect.setWidth(3);

对象是你自己创建的,控制权在你手里。

用 IoC:

ApplicationContext context = new ClassPathXmlApplicationContext("service.xml");
Rectangle rect = context.getBean("rectangle", Rectangle.class);

你没有 new,对象是 Spring 容器根据配置文件帮你创建好、注入好属性,你直接拿。

配置文件长啥样

Spring 需要一个 XML 配置文件告诉它:要创建哪些对象、属性怎么赋值。

<bean id="rectangle" class="com.example.Rectangle">
    <property name="length" value="2"/>
    <property name="width" value="3"/>
</bean>
  • id:这个 bean 的唯一名字(身份证)
  • class:类的全限定名(从包名到类名)
  • property:给属性赋值,name 对应 setter 方法名

注意:必须要有 setter 方法,因为 Spring IoC 默认是通过 setter 注入的。

两个常见疑问

1. 容器里的对象是单例吗? 是的,默认是单例。你 getBean 两次拿到的是同一个对象(== 返回 true)。想改成每次新建,配置里加 scope="prototype"。

2. 对象什么时候创建? 默认在容器启动时就全部创建好了(singleton 情况下)。所以即使你不 getBean,构造函数也会执行。

手动导 jar 包 vs Maven

通过Spring Framework 八大模块: - 用 IoC 功能,至少需要 4 个 jar 包:spring-beans、spring-core、spring-context、spring-expression,另外还要 commons-logging。 - 手动导太麻烦,实际开发都用 Maven,在 pom.xml 里写依赖,自动下载。

一句话总结

IoC 就是把"创建和管理对象"的权力从程序员手里交给 Spring 容器,从而实现解藕。你不再 new 对象,而是从容器里"拿"现成的。DI(依赖注入)是实现 IoC 的具体手段。


对比:

IoC AOP
解决什么 对象怎么创建、怎么管理 给一批方法统一加功能
核心 容器管 Bean 切面管通知
关系 Spring 的根基 建立在 IoC 之上