BBS论坛项目相关-7:统一异常处理和日志记录
统一异常处理
@ControllerAdvice:用于修饰类,表示该类是controller配置类;在此类中,可以对controller进行三种全局配置:异常处理方案,绑定数据方案,绑定参数方案
@ExceptionHandler:用于修饰方法,该方法会在controller出现异常后被调用,用于处理捕获到的异常
@ModelAttribute:用于修饰方法,在controller方法执行前被调用,用于为Model对象绑定参数
@DataBinder:用于修饰方法,在Controller方法执行前被调用,用于绑定参数的转换器
将404.html页面放在templates的error文件夹下,发生404异常时会自动跳转到该404页面
因为所有异常抛出后都会到达表现层,所以只需要在表现层统一处理即可,整体扫描太费时间,可以只扫描带有controller注解的
遍历e.getStackTrace()对象,记录错误信息,记录完错误信息需要给浏览器一个响应,重定向到刚才的错误页面,需要额外处理,因为浏览器访问服务器可能是普通请求,也可能是异步请求,普通请求返回到500.html,对于异步请求,需要返回json字符串,所以需要区分开,可通过request判断,查看请求头。
浏览器访问服务器
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {
private static final Logger logger = LoggerFactory.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");
}
}
}
统一日志记录
拦截器和控制通知都是针对控制层或异常进行处理,但这里统一日志不止是针对控制层或异常处理,也可能对业务组件进行记录。
AOP记录日志:
AOP:
1 编译时织入,需使用特殊的编译器
2 装载时织入,需使用特殊的类装载器
3 运行时织入,需为目标生成代理对象
AspectJ:扩展了java语言,定义了AOP语法,在编译期织入代码,有一个专门的编译器,用来生成java字节码规范的class文件
Spring AOP:使用纯java实现,不需要专门的编译过程,不需要特殊的类加载器,在运行时通过代理的方式织入代码,只支持方法类型的连接点,Spring支持对AspectJ的集成
代理:
JDK动态代理
Java提供动态代理技术,可以在运行时创建接口的代理实例
SpringAOP默认采用此种方式,在接口的代理实例中织入代码
CGLib动态代理:
采用底层的字节码技术,在运行时创建子类代理实例
当目标对象不存在接口时,Spring AOP会采用此种方式
AOP注解
Aspect(切面):通常是一个类,里面可以定义切入点和通知
JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用
Advice(通知):AOP在特定的切入点上执行的增强处理,有before,after,afterReturning,afterThrowing,around
Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式
@Pointcut(“execution(* com.aijava.springcode.service….(…))”)
第一个表示匹配任意的方法返回值,…(两个点)表示零个或多个,上面的第一个…表示service包及其子包,第二个表示所有类,第三个*表示所有方法,第二个…表示方法的任意参数个数
5种注解通知
-@Before 前置通知,在方法执行之前执行
@Before(“execution(public int com.ArithmticCalculator.*(. .))”)
(JoinPoint joinPoint)
-@After 后置通知 (注意在后置通知中不能访问目标方法执行的结果) 无论是否异常
@After(“execution(public int com.ArithmticCalculator.*(. .))”)
(JoinPoint joinPoint)
-@AfterReturning 返回通知,在方法返回结果之后执行
@AfterReturning(value=“execution(public int com.ArithmticCalculator.add(int i,int j))”,
returning=”result”)
(JoinPoint joinPoint,Object result)
-@AfterThrowing 异常通知,在方法抛出异常之后,可以访问到异常对象,且可以指定在出现特定异常时再执行通知
@AfterThrowing(value=“execution(public int com.ArithmticCalculator.add(int i,int j))”,
throwing=”ex”)
(JoinPoint joinPoint,Exception ex)
-@Around 环绕通知,环绕着方法执行
需要携带ProceedingJoinPoint类型的参数
环绕通知类似于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法
//@Component
//@Aspect
public class ServiceLogAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
// 记录格式:用户[1.2.3.4],在[xxx],访问了[com.nowcoder.community.service.xxx()].
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));
}
}
要获得用户IP可通过request对象获取,request对象可通过RequestContextHolder对象获取。