Day25:统一处理异常、AOP编程、统一记录日志

表现层在最外面,异常在这层处理。

SpringBoot处理异常的简单实现

把error文件夹放在templates文件夹下,html命名为状态吗:

image

修改404.html和500.html为模版(注意图片路径修改为动态)

image

更细粒度的处理异常的方式

  • @ControllerAdvice

    • 用于修饰类,表示该类是Controller的全局配置类。
    • 在此类中,可以对Controller进行如下三种全局配置:

    异常处理方案、绑定数据方案、绑定参数方案。

  • @ExceptionHandler

    • 用于修饰方法,该方法会在Controller出现异常后被调用,用于处理捕获到的异常。
  • @ModelAttribute

    • 用于修饰方法,该方法会在Controller方法执行前被调用,用于为Model对象绑定参数。
  • @DataBinder

    • 用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器。

示例

  1. 在HomeController中添加如下方法:
@RequestMapping(path = "/error", method = RequestMethod.GET)
public String getErrorPage() {
    return "/error/500";
}
  1. 在controller包下新加一个包叫Advice,new一个class ExceptionAdvice
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {
    private static final Logger logger = getLogger(ExceptionAdvice.class);
    @ExceptionHandler({Exception.class})
    public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
        logger.error("服务器发生异常:" + e.getMessage());
        for(StackTraceElement element : e.getStackTrace()) {
            logger.error(element.toString());
        }
        String xRequestedWith = request.getHeader("x-requested-with");
        if("XMLHttpRequest".equals(xRequestedWith)) {
            response.setContentType("application/plain;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(CommunityUtil.getJsonString(1, "服务器异常"));
        } else {
            response.sendRedirect(request.getContextPath() + "/error");
        }

    }
}
  • annotations = Controller.class是@ControllerAdvice注解的一个属性,它用于指定这个advice应该应用于哪些类。在您的代码中,annotations = Controller.class表示这个advice应该应用于所有带有@Controller注解的类。
  • 。getLogger(ExceptionAdvice.class)是一个静态方法,它返回一个与指定类(在这里是ExceptionAdvice类)关联的Logger实例。(.class不要漏)
  • 异步请求的request.getHeader(“x-requested-with”) 是XMLHttpRequest
  • getWriter()是HttpServletResponse接口中的一个方法,它返回一个PrintWriter对象,可以用来向客户端发送字符文本。这个PrintWriter对象会自动使用响应的字符编码(通过setCharacterEncoding方法设置),所以你可以直接写入字符串,而不需要转换为字节。

AOP编程

  • Aspect Oriented Programing, 即面向方面(切面)编程。
  • AOP是一种编程思想,是对OOP的补充, 可以进一步提高编程的效率。

image

(相当于一刀切开,在每一层都用到)

image

  • Target:在AOP编程中,目标(Target)是指被一个或多个切面(Aspect)所通知(advise)的对象。也就是说,目标是包含业务逻辑的类,这些类的某些方法将被切面中的通知所增强。
  • Joinpoint:连接点(Joinpoint)是指在程序执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。在Spring AOP中,一个连接点总是表示一个方法的执行。
  • Aspect:切面(Aspect)是一个模块,它包含一些通知和切入点。通知(Advice)是切面在特定的连接点(Joinpoint)上执行的代码,切入点(Pointcut)则定义了这些通知应该在何处执行。切面的作用是将通用的功能从业务逻辑中分离出来,以实现代码的重用和解耦。
  • Weaving:织入(Weaving)是将切面插入到目标对象以创建一个被通知的对象的过程。这个过程可以在编译时(如AspectJ的编译器),加载时或运行时完成。Spring AOP默认在运行时进行织入。
  • Pointcut:切入点(Pointcut)是指在哪些Joinpoint(连接点)上应用Advice(通知)。它是一个表达式,用于匹配方法执行的点。例如,你可以定义一个切入点来匹配所有在某个特定类中的方法,或者所有的setter方法,等等。在Spring AOP中,切入点表达式通常使用AspectJ的切入点表达式语言。
  • Advice:通知(Advice)是切面(Aspect)在特定的连接点(Joinpoint)上执行的代码。它是切面的主要内容,定义了切面要完成的工作。在Spring AOP中,通知可以是以下五种类型之一:前置通知(Before),后置通知(After),返回通知(After-returning),异常通知(After-throwing)和环绕通知(Around)。前置通知在连接点之前执行,后置通知在连接点之后执行,返回通知在连接点正常返回后执行,异常通知在连接点抛出异常后执行,环绕通知可以在连接点前后都执行。

AOP的实现

AspectJ(学习代价高)

  • AspectJ是语言级的实现,它扩展了Java语言,定义了AOP语法。
  • AspectJ在编译期织入代码,它有一个专门的编译器,用来生成遵守Java字节码规范的class文件。

Spring AOP(常用性价比最高)

  • Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器。
  • Spring AOP在运行时通过代理的方式织入代码(记住!!!),只支持方法类型的连接点。
  • Spring支持对AspectJ的集成。

Spring AOP

JDK动态代理(Spring默认,必须有接口)

  • Java提供的动态代理技术,可以在运行时创建接口的代理实例。
  • Spring AOP默认采用此种方式,在接口的代理实例中织入代码。

CGLib动态代理

  • 采用底层的字节码技术,在运行时创建子类代理实例。
  • 当目标对象不存在接口时,Spring AOP会采用此种方式,在子类实例中织入代码。

AOP示例

在Controller下创建一个新的包Aspect,创建类AlphaAspect:

@Component
@Aspect
public class AlphaAspect {
    //定义切点
    //service包下的所有方法、所有参数、所有返回值
    @Pointcut("execution(* com.newcoder.community.service.*.*(..))")
    public void pointcut() {

    }
    //定义通知
    //在切点之前执行
    @Before("pointcut()")
    public void before() {
        System.out.println("before");
    }

    @After("pointcut()")
    public void after() {
        System.out.println("after");
    }

    @AfterReturning("pointcut()")
    public void afterReturning() {
        System.out.println("afterReturning");
    }

    @AfterThrowing("pointcut()")
    public void afterThrowing() {
        System.out.println("afterThrowing");
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        System.out.println("before obj");
        Object obj = joinPoint.proceed();
        System.out.println("after obj");
        return obj;
    }
}
  • @Pointcut(“execution(* com.newcoder.community.service..(…))”)注解表示制定生效的路径,这里是service包下的所有方法、所有参数、所有返回值;
  • @Before(“pointcut()”)在函数之前、@After(“pointcut()”)在函数之后、@AfterReturning(“pointcut()”)函数返回之后、@AfterThrowing(“pointcut()”)抛出异常之后;
  • 注意@Around(“pointcut()”)实在执行之间,通过Object obj = joinPoint.proceed();这个就相当于执行业务组件了:

这段代码是一个环绕通知(Around Advice),它是AOP(面向切面编程)中的一种通知类型。环绕通知可以在方法调用前后都执行一些代码,并且可以决定是否执行目标方法。

@Around(“pointcut()”)注解表示这个通知应用于pointcut()定义的切入点。在这个例子中,pointcut()定义的切入点是com.newcoder.community.service包下的所有方法。 around方法的参数ProceedingJoinPoint joinPoint是一个特殊的JoinPoint,它代表了切入点,也就是要被通知的目标方法。

在around方法中,首先执行System.out.println(“before obj”);打印一条消息,然后调用joinPoint.proceed()执行目标方法,并将结果保存在obj变量中。**joinPoint.proceed()方法的调用是必须的,否则目标方法不会被执行。**然后执行System.out.println(“after obj”);打印一条消息,最后返回目标方法的结果。 所以,这个环绕通知在目标方法执行前后都打印了一条消息,并且返回了目标方法的结果。

运行后可以发现输出了很多:

image

统一记录日志

需求:对所有的组件及日志(这是系统需求与业务需求耦合这样不好)

解决:面向AOP切面编程。

创建ServiceLogApect.java:

@Component
@Aspect
public class ServiceLogAspect {
    private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);

    //定义切点
    //service包下的所有方法、所有参数、所有返回值
    @Pointcut("execution(* com.newcoder.community.service.*.*(..))")
    public void pointcut() {

    }

    @Before("pointcut()")
    public void before(JoinPoint joinPoint) {
        //用户[1.2.3.4],在[xxx]时间,访问了[com.newcoder.community.service.xxx()]。
        logger.debug("before");
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String ip = request.getRemoteHost();
        String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
        logger.info(String.format("用户[%s], 在[%s], 访问了[%s]", ip, now, target));

    }
}
  • ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes():得到当前hold的request的属性;
  • String ip = request.getRemoteHost();得到IP
  • String target = joinPoint.getSignature().getDeclaringTypeName() + “.” + joinPoint.getSignature().getName();得到切面切到的JointCut的签名

最后可以看到日志有这些输出:

image

  • 27
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值