【加精】Spring全家桶系列–SpringBoot之AOP详解

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 【加精】Spring全家桶系列–SpringBoot之AOP详解

  • //本文作者:cuifuan
  • //本文将收录到菜单栏:《Spring全家桶》专栏中

面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。
OOP中模块化的关键单元是类,而在AOP中,模块化单元是方面。

准备工作

首先,使用AOP要在build.gradle中加入依赖

  • 1
    2
    
    //引入AOP依赖
    compile "org.springframework.boot:spring-boot-starter-aop:${springBootVersion}"
  • 然后在application.yml中加入

    123
    spring:  aop:    proxy-target-class: true

    spring:
    aop:
    proxy-target-class: true

    1.@Pointcut 切入点

    定义一个切点。
    例如我们要在一个方法加上切入点,根据方法的返回的对象,方法名,修饰词来写成一个表达式或者是具体的名字

    我们现在来定义一个切点

    12345678910111213141516171819202122
    package com.example.aop; import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component; /** * 类定义为切面类 */@Aspect@Componentpublic class AopTestController {    private static final Logger logger = LoggerFactory.getLogger(AopTestController.class);    /**     * 定义一个切点     */    @Pointcut(value = "execution(public String test (..))")    public void cutOffPoint() {    }}

    package com.example.aop;

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;

    /**

    • 类定义为切面类

    /
    @Aspect
    @Component
    public class AopTestController {
    private static final Logger logger = LoggerFactory.getLogger(AopTestController.class);
    /
    *
    * 定义一个切点
    */
    @Pointcut(value = “execution(public String test (..))”)
    public void cutOffPoint() {
    }
    }

    这里的切点定义的方法是

    12345
    @GetMapping("hello")    public String test(){        logger.info("欢迎关注Java知音");        return "i love java";    }

    @GetMapping(“hello”)
    public String test(){
    logger.info(“欢迎关注Java知音”);
    return “i love java”;
    }

    如果你想写个切入点在所有返回对象为Area的方法,如下

    @Pointcut(“execution(public com.example.entity.Area (..))”)

    等很多写法,也可以直接作用在某些包下

    注意:private修饰的无法拦截

    2.@Before前置通知

    在切入点开始处切入内容

    在之前的AopTestController类中加入对test方法的前置通知

    1234
    @Before("cutOffPoint()")    public void beforeTest(){        logger.info("我在test方法之前执行");    }

    @Before(“cutOffPoint()”)
    public void beforeTest(){
    logger.info(“我在test方法之前执行”);
    }

    这里@Before里的值就是切入点所注解的方法名

    Spring全家桶系列--SpringBoot之AOP详解

    在方法左侧出现的图标跟过去以后就是所要通知的方法 这里就是配置正确了,我们来浏览器调用一下方法

    Spring全家桶系列--SpringBoot之AOP详解

    联想一下,这样的效果可以用在哪里,想像如果要扩展一些代码,在不需要动源代码的基础之上就可以进行拓展,美滋滋

    3.@After 后置通知

    和前置通知相反,在切入点之后执行

    1234
    @After("cutOffPoint()")    public void doAfter(){        logger.info("我是在test之后执行的");    }

    @After(“cutOffPoint()”)
    public void doAfter(){
    logger.info(“我是在test之后执行的”);
    }

    控制台执行结果

    Spring全家桶系列--SpringBoot之AOP详解

    这里定义一个通知需要重启启动类,而修改通知方法的内容是可以热部署的

    4.@Around环绕通知

    和前两个写法不同,实现的效果包含了前置和后置通知。

    当使用环绕通知时,proceed方法必须调用,否则拦截到的方法就不会再执行了

    环绕通知=前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的

    123456789101112131415
    ThreadLocalLong startTime = new ThreadLocal();    @Around("cutOffPoint()")    public Object doAround(ProceedingJoinPoint pjp){        startTime.set(System.currentTimeMillis());        logger.info("我是环绕通知执行");        Object obj;        try{            obj = pjp.proceed();            logger.info("执行返回值 : " + obj);            logger.info(pjp.getSignature().getName()+"方法执行耗时: " + (System.currentTimeMillis() - startTime.get()));        } catch (Throwable throwable) {            obj=throwable.toString();        }        return obj;    }

    ThreadLocalLong startTime = new ThreadLocal();
    @Around(“cutOffPoint()”)
    public Object doAround(ProceedingJoinPoint pjp){
    startTime.set(System.currentTimeMillis());
    logger.info(“我是环绕通知执行”);
    Object obj;
    try{
    obj = pjp.proceed();
    logger.info(“执行返回值 : “ + obj);
    logger.info(pjp.getSignature().getName()+”方法执行耗时: “ + (System.currentTimeMillis() - startTime.get()));
    } catch (Throwable throwable) {
    obj=throwable.toString();
    }
    return obj;
    }

    执行结果:

    Spring全家桶系列--SpringBoot之AOP详解

    1.环绕通知可以项目做全局异常处理

    2.日志记录

    3.用来做数据全局缓存

    4.全局的事物处理 等

    5.@AfterReturning

    切入点返回结果之后执行,也就是都前置后置环绕都执行完了,这个就执行了

    123456789
    /**     * 执行完请求可以做的     * @param result     * @throws Throwable     */    @AfterReturning(returning = "result", pointcut = "cutOffPoint()")    public void doAfterReturning(Object result) throws Throwable {        logger.info("大家好,我是@AfterReturning,他们都秀完了,该我上场了");    }

    /**
    * 执行完请求可以做的
    * @param result
    * @throws Throwable
    */
    @AfterReturning(returning = “result”, pointcut = “cutOffPoint()”)
    public void doAfterReturning(Object result) throws Throwable {
    logger.info(“大家好,我是@AfterReturning,他们都秀完了,该我上场了”);
    }

    执行结果

    Spring全家桶系列--SpringBoot之AOP详解

    应用场景可以用来在订单支付完成之后就行二次的结果验证,重要参数的二次校验,防止在方法执行中的时候参数被修改等等

    6.@AfterThrowing

    这个是在切入执行报错的时候执行

    1234567
    // 声明错误e时指定的抛错类型法必会抛出指定类型的异常    // 此处将e的类型声明为Throwable,对抛出的异常不加限制    @AfterThrowing(throwing = "e",pointcut = "cutOffPoint()")    public void doAfterReturning(Throwable e) {        logger.info("大家好,我是@AfterThrowing,他们犯的错误,我来背锅");        logger.info("错误信息"+e.getMessage());    }

    // 声明错误e时指定的抛错类型法必会抛出指定类型的异常
    // 此处将e的类型声明为Throwable,对抛出的异常不加限制
    @AfterThrowing(throwing = “e”,pointcut = “cutOffPoint()”)
    public void doAfterReturning(Throwable e) {
    logger.info(“大家好,我是@AfterThrowing,他们犯的错误,我来背锅”);
    logger.info(“错误信息”+e.getMessage());
    }

    在其他切入内容中随意整个错误出来,制造一个环境。

    下面是@AfterThrowing的执行结果

    Spring全家桶系列--SpringBoot之AOP详解

    7.AOP用在全局异常处理

    定义切入点拦截ResultBean或者PageResultBean

    12345678
    @Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))")    public void handlerPageResultBeanMethod() {    }      @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))")    public void handlerResultBeanMethod() {    }

    @Pointcut(value = “execution(public com.example.beans.PageResultBean *(..)))”)
    public void handlerPageResultBeanMethod() {
    }

    
    @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))")
    public void handlerResultBeanMethod() {
    }
    

    下面是AopController.java

    123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
    package com.example.aop; import com.example.beans.PageResultBean;import com.example.beans.ResultBean;import com.example.entity.UnloginException;import com.example.exception.CheckException;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component; /** * 使用@Aspect注解将此类定义为切面类 * 根据晓风轻著的ControllerAOP所修改 * 晓风轻大佬(很大的佬哥了):https://xwjie.github.io/ */@Aspect@Componentpublic class AopController {     private static final Logger logger = LoggerFactory.getLogger(AopController.class);     ThreadLocalResultBean resultBeanThreadLocal = new ThreadLocal();    ThreadLocalPageResultBean? pageResultBeanThreadLocal = new ThreadLocal();    ThreadLocalLong start = new ThreadLocal();     /**     * 定义一个切点     */    @Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))")    public void handlerPageResultBeanMethod() {    }      @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))")    public void handlerResultBeanMethod() {    }     @Around("handlerPageResultBeanMethod()")    public Object handlerPageResultBeanMethod(ProceedingJoinPoint pjp) {        start.set(System.currentTimeMillis());        try {            pageResultBeanThreadLocal.set((PageResultBean?)pjp.proceed());            logger.info(pjp.getSignature() + " 方法执行耗时:" + (System.currentTimeMillis() - start.get()));        } catch (Throwable e) {            ResultBean? resultBean = handlerException(pjp , e);            pageResultBeanThreadLocal.set(new PageResultBean().setMsg(resultBean.getMsg()).setCode(resultBean.getCode()));        }        return pageResultBeanThreadLocal.get();    }     @Around("handlerResultBeanMethod()")    public Object handlerResultBeanMethod(ProceedingJoinPoint pjp) {        start.set(System.currentTimeMillis());        try {            resultBeanThreadLocal.set((ResultBean?)pjp.proceed());            logger.info(pjp.getSignature() + " 方法执行耗时:" + (System.currentTimeMillis() - start.get()));        } catch (Throwable e) {            resultBeanThreadLocal.set(handlerException(pjp , e));        }        return resultBeanThreadLocal.get();    }    /**     * 封装异常信息,注意区分已知异常(自己抛出的)和未知异常     */    private ResultBean? handlerException(ProceedingJoinPoint pjp, Throwable e) {         ResultBean? result = new PageResultBean();        logger.error(pjp.getSignature() + " error ", e);         // 已知异常        if (e instanceof CheckException) {            result.setMsg(e.getLocalizedMessage());            result.setCode(ResultBean.FAIL);        } else if (e instanceof UnloginException) {            result.setMsg("Unlogin");            result.setCode(ResultBean.NO_LOGIN);        } else {            result.setMsg(e.toString());            result.setCode(ResultBean.FAIL);        }        return result;    }}

    package com.example.aop;

    import com.example.beans.PageResultBean;
    import com.example.beans.ResultBean;
    import com.example.entity.UnloginException;
    import com.example.exception.CheckException;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;

    /**

    • 使用@Aspect注解将此类定义为切面类
    • 根据晓风轻著的ControllerAOP所修改
    • 晓风轻大佬(很大的佬哥了):https://xwjie.github.io/

    */
    @Aspect
    @Component
    public class AopController {

    
    private static final Logger logger = LoggerFactory.getLogger(AopController.class);
    
    ThreadLocalResultBean resultBeanThreadLocal = new ThreadLocal();
    ThreadLocalPageResultBean? pageResultBeanThreadLocal = new ThreadLocal();
    ThreadLocalLong start = new ThreadLocal();
    
    /**
     * 定义一个切点
     */
    @Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))")
    public void handlerPageResultBeanMethod() {
    }
    
    
    @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))")
    public void handlerResultBeanMethod() {
    }
    
    @Around("handlerPageResultBeanMethod()")
    public Object handlerPageResultBeanMethod(ProceedingJoinPoint pjp) {
        start.set(System.currentTimeMillis());
        try {
            pageResultBeanThreadLocal.set((PageResultBean?)pjp.proceed());
            logger.info(pjp.getSignature() + " 方法执行耗时:" + (System.currentTimeMillis() - start.get()));
        } catch (Throwable e) {
            ResultBean? resultBean = handlerException(pjp , e);
            pageResultBeanThreadLocal.set(new PageResultBean().setMsg(resultBean.getMsg()).setCode(resultBean.getCode()));
        }
        return pageResultBeanThreadLocal.get();
    }
    
    @Around("handlerResultBeanMethod()")
    public Object handlerResultBeanMethod(ProceedingJoinPoint pjp) {
        start.set(System.currentTimeMillis());
        try {
            resultBeanThreadLocal.set((ResultBean?)pjp.proceed());
            logger.info(pjp.getSignature() + " 方法执行耗时:" + (System.currentTimeMillis() - start.get()));
        } catch (Throwable e) {
            resultBeanThreadLocal.set(handlerException(pjp , e));
        }
        return resultBeanThreadLocal.get();
    }
    /**
     * 封装异常信息,注意区分已知异常(自己抛出的)和未知异常
     */
    private ResultBean? handlerException(ProceedingJoinPoint pjp, Throwable e) {
    
        ResultBean? result = new PageResultBean();
        logger.error(pjp.getSignature() + " error ", e);
    
        // 已知异常
        if (e instanceof CheckException) {
            result.setMsg(e.getLocalizedMessage());
            result.setCode(ResultBean.FAIL);
        } else if (e instanceof UnloginException) {
            result.setMsg("Unlogin");
            result.setCode(ResultBean.NO_LOGIN);
        } else {
            result.setMsg(e.toString());
            result.setCode(ResultBean.FAIL);
        }
        return result;
    }
    

    }

    用上面的环绕通知可以对所有返回ResultBean或者PageResultBean的方法进行切入,这样子就不用在业务层去捕捉错误了,只需要去打印自己的info日志。

    看下面一段代码

    1234567891011121314
    @Transactional    @Override    public int insertSelective(Area record) {        record.setAddress("test");        record.setPostalcode(88888);        record.setType(3);        int i=0;        try {            i = areaMapper.insertSelective(record);        }catch (Exception e){            logger.error("AreaServiceImpl insertSelective error:"+e.getMessage());        }        return i;    }

    @Transactional
    @Override
    public int insertSelective(Area record) {
    record.setAddress(“test”);
    record.setPostalcode(88888);
    record.setType(3);
    int i=0;
    try {
    i = areaMapper.insertSelective(record);
    }catch (Exception e){
    logger.error(“AreaServiceImpl insertSelective error:”+e.getMessage());
    }
    return i;
    }

    假如上面的插入操作失败出错了? 你认为会回滚吗?

    答案是:不会。

    为什么?

    因为你把错误捕捉了,事物没检测到异常就不会回滚。

    那么怎么才能回滚呢?

    在catch里加throw new RuntimeException().

    可是那么多业务方法每个设计修改的操作都加,代码繁琐,怎么进行处理呢?

    在这里用到上面的AOP切入处理,错误不用管,直接抛,抛到控制层进行处理,这样的话,接口调用的时候,出错了,接口不会什么都不返回,而是会返回给你错误代码,以及错误信息,便于开发人员查错。

    8.以上用的是log4j2的日志处理

    先移除springboot自带的log日志处理

    在build.gradle中增加

    1234567891011
    configurations {    providedRuntime    // 去除SpringBoot自带的日志    all*.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'}ext {    springBootVersion = '2.0.1.RELEASE'}dependencies {    compile "org.springframework.boot:spring-boot-starter-log4j2:${springBootVersion}"}

    configurations {
    providedRuntime
    // 去除SpringBoot自带的日志
    all*.exclude group: ‘org.springframework.boot’, module: ‘spring-boot-starter-logging’
    }
    ext {
    springBootVersion = ‘2.0.1.RELEASE’
    }
    dependencies {
    compile “org.springframework.boot:spring-boot-starter-log4j2:${springBootVersion}”
    }

    然后在application.yml中增加

    123456
    logging:  level:    com:      example:        dao: debug  config: classpath:log4j2-spring.xml

    logging:
    level:
    com:
    example:
    dao: debug
    config: classpath:log4j2-spring.xml

    log4j2-spring.xml

    12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
    ?xml version="1.0" encoding="UTF-8"?!--日志级别以及优先级排序: OFF  FATAL  ERROR  WARN  INFO  DEBUG  TRACE  ALL --!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出--!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数--configuration status="INFO" monitorInterval="30"    !--先定义所有的appender--    appenders        !--这个输出控制台的配置--        console name="Console" target="SYSTEM_OUT"            !--输出日志的格式--            PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n}"/        /console         !--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用--        File name="Test" fileName="logs/test.log" append="false"            PatternLayout pattern="%highlight{[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n}"/        /File         RollingFile name="RollingFileInfo" fileName="logs/log.log" filePattern="logs/info.log.%d{yyyy-MM-dd}"            !-- 只接受level=INFO以上的日志 --            ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/            PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n}"/            Policies                TimeBasedTriggeringPolicy modulate="true" interval="1"/                SizeBasedTriggeringPolicy/            /Policies        /RollingFile         RollingFile name="RollingFileError" fileName="logs/error.log" filePattern="logs/error.log.%d{yyyy-MM-dd}"            !-- 只接受level=WARN以上的日志 --            Filters                ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY" /            /Filters            PatternLayout pattern="%highlight{[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n}"/            Policies                TimeBasedTriggeringPolicy modulate="true" interval="1"/                SizeBasedTriggeringPolicy/            /Policies        /RollingFile     /appenders     !--然后定义logger,只有定义了logger并引入的appender,appender才会生效--    loggers        !--过滤掉spring和mybatis的一些无用的DEBUG信息--        logger name="org.springframework" level="INFO"/logger        logger name="org.mybatis" level="INFO"/logger        root level="all"            appender-ref ref="Console"/            appender-ref ref="Test"/            appender-ref ref="RollingFileInfo"/            appender-ref ref="RollingFileError"/        /root    /loggers/configuration

    ?xml version=”1.0” encoding=”UTF-8”?
    !–日志级别以及优先级排序: OFF FATAL ERROR WARN INFO DEBUG TRACE ALL –
    !–Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出–
    !–monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数–
    configuration status=”INFO” monitorInterval=”30”
    !–先定义所有的appender–
    appenders
    !–这个输出控制台的配置–
    console name=”Console” target=”SYSTEM_OUT”
    !–输出日志的格式–
    PatternLayout pattern=”%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n}”/
    /console

    
        !--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用--
        File name="Test" fileName="logs/test.log" append="false"
            PatternLayout pattern="%highlight{[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n}"/
        /File
    
        RollingFile name="RollingFileInfo" fileName="logs/log.log" filePattern="logs/info.log.%d{yyyy-MM-dd}"
            !-- 只接受level=INFO以上的日志 --
            ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/
            PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n}"/
            Policies
                TimeBasedTriggeringPolicy modulate="true" interval="1"/
                SizeBasedTriggeringPolicy/
            /Policies
        /RollingFile
    
        RollingFile name="RollingFileError" fileName="logs/error.log" filePattern="logs/error.log.%d{yyyy-MM-dd}"
            !-- 只接受level=WARN以上的日志 --
            Filters
                ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY" /
            /Filters
            PatternLayout pattern="%highlight{[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n}"/
            Policies
                TimeBasedTriggeringPolicy modulate="true" interval="1"/
                SizeBasedTriggeringPolicy/
            /Policies
        /RollingFile
    
    /appenders
    
    !--然后定义logger,只有定义了logger并引入的appender,appender才会生效--
    loggers
        !--过滤掉spring和mybatis的一些无用的DEBUG信息--
        logger name="org.springframework" level="INFO"/logger
        logger name="org.mybatis" level="INFO"/logger
        root level="all"
            appender-ref ref="Console"/
            appender-ref ref="Test"/
            appender-ref ref="RollingFileInfo"/
            appender-ref ref="RollingFileError"/
        /root
    /loggers
    

    /configuration

    之后在你要打印日志的类中增加

    1234567
    private static final Logger logger = LoggerFactory.getLogger(你的类名.class);     public static void main(String[] args) {        logger.error("error级别日志");        logger.warn("warning级别日志");        logger.info("info级别日志");    }

    private static final Logger logger = LoggerFactory.getLogger(你的类名.class);

    
    public static void main(String[] args) {
        logger.error("error级别日志");
        logger.warn("warning级别日志");
        logger.info("info级别日志");
    }
    

    有了日志后就很方便了,在你的方法接收对象时打印下,然后执行了逻辑之后打印下, 出错之后很明确了,就会很少去Debug的,养成多打日志的好习惯,多打印一点info级别的日志,用来在开发环境使用,在上线的时候把打印的最低级别设置为warning,这样你的info级别日志也不会影响到项目的重要Bug的打印

    写这个博客的时候我也在同时跑着这个项目,有时候会出现一些错误,例如jar包版本,业务层引用无效,AOP设置不生效等等,也同时在排查解决,如果你遇到了同样的错误,可以去我的GitHub联系我,如小弟有时间或许也能帮到你,谢谢
    Github地址:https://github.com/cuifuan

    ** **

    点击图片加入Spring交流群

    ↓↓↓

    看完本文有收获?请转发分享给更多人

    Spring全家桶系列--SpringBoot之AOP详解

     

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> 【加精】Spring全家桶系列–SpringBoot之AOP详解


     上一篇
    Springboot webSocket(二) Springboot webSocket(二)
    本文将通过搭建一个一对一聊天服务器,深入学习更多的socket知识。 1、发送消息在前面我们写了一个自动回复的小例子,用到了 @MessageMapping("/hello")和 @SendTo("/top
    下一篇 
    springboot缓存开发实战 springboot缓存开发实战
    前言:缓存在开发中是一个必不可少的优化点,近期在公司的项目重构中,关于缓存优化了很多点,比如在加载一些数据比较多的场景中,会大量使用缓存机制提高接口响应速度,简介提升用户体验。关于缓存,很多人对它都是既爱又恨,爱它的是:它能大幅提升响应