文章目录
异常处理
SpringMVC提供了三种异常处理器:ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、DefaultHandlerExceptionResolver。
ExceptionHandlerExceptionResolver
传统方式处理异常
在DAO层和Service层中将异常抛出,在Controller层中进行try-catch处理,如下所示:
@GetMapping("/testException")
@ResponseBody
public Map<String,Object> testException(){
System.out.println("收到请求");
HashMap<String, Object> map = new HashMap<>();
try {
//模拟异常
int num=1/0;
map.put("success",true);
map.put("msg","请求接收成功");
} catch (Exception e) {
map.put("success",false);
map.put("msg",e.getMessage());
}
return map;
}
这样在处理器的每个方法这种都会存在try-catch代码块,会造成代码的冗余。
使用SpringMVC提供的ExceptionHandlerExceptionResolver异常处理器处理异常,如下所示:
@GetMapping("/testException")
public String testException(){
System.out.println("收到请求");
int num=1/0;
return "index";
}
//ExceptionHandlerExceptionResolver异常处理器会处理被@ExceptionHandler标注的方法所属Controller的异常
//被@ExceptionHandler标注的方法会捕捉处理器中出现的异常,如果捕捉到则执行该方法中的处理逻辑,相当于catch。
@ExceptionHandler(RuntimeException.class)
public void runtimeException(RuntimeException e){
System.out.println("异常:"+e.getMessage());
}
将错误信息返回给前端
我们需要根据请求类型返回不同的信息给前端。如果是非Ajax请求则返回错误页面提示错误消息;如果是Ajax请求,则返回JSON字符串的错误信息。我们需要在处理异常的方法中进行判断。
首先需要明确Ajax请求与非Ajax请求的区别,Ajax请求中会存在请求头"X-Requested-With: XMLHttpRequest",如下图所示
可以以此作为判断依据,代码如下所示:
@ExceptionHandler(RuntimeException.class)
public ModelAndView runtimeException(RuntimeException e, HttpServletRequest request, HttpServletResponse response) throws IOException {
//如果是Ajax请求,返回JSON字符串,否则返回ModelAndView视图对象。
String req = request.getHeader("X-Requested-With");
if (req!=null&&"XMLHttpRequest".equals(req)){
System.out.println("ajax请求");
HashMap<String, Object> map = new HashMap<>();
map.put("success",false);
map.put("msg","出现异常:"+e.getMessage());
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(map));
}else {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("error",e.getMessage());
modelAndView.setViewName("error");
return modelAndView;
}
return null;
}
error页面只显示异常信息
<h2>${error}</h2>
全局异常处理
定义一个类并使用@ControllerAdvice注解标注使其能够处理全局异常,方法中处理异常的代码无变化,该类中方法捕捉的异常也交由ExceptionHandlerExceptionResolver异常处理器处理。
import com.alibaba.fastjson.JSON;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
@ControllerAdvice
public class MySpringMVCException {
@ExceptionHandler(RuntimeException.class)
public ModelAndView runtimeException(RuntimeException e, HttpServletRequest request, HttpServletResponse response) throws IOException {
String req = request.getHeader("X-Requested-With");
if (req!=null&&"XMLHttpRequest".equals(req)){
System.out.println("ajax请求");
HashMap<String, Object> map = new HashMap<>();
map.put("success",false);
map.put("msg","出现异常:"+e.getMessage());
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(map));
}else {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("error",e.getMessage());
modelAndView.setViewName("error");
return modelAndView;
}
return null;
}
}
对于所有控制器如果没有定义异常处理方法,全局异常处理器则会捕捉并处理该异常,如果配置定义了异常处理方法,那么以处理器中的异常处理方法为准。
在实际开发中,往往异常处理方法只有一个,但是在该方法中判断异常类型并进行对应的处理。
ResponseStatusExceptionResolver
如果Controller中出现的异常没有被ExceptionHandlerExceptionResolver异常处理器捕获或者没有使用@ExceptionHandler和@ControllerAdvice注解进行异常处理,那么会交给ResponseStatusExceptionResolver异常处理器处理。
//定义一个类MyResponseStateException继承需要异常类RuntimeException并使用@ResponseStatus注解标注
//MyResponseStateException类就会捕获RuntimeException,在code属性中定义异常状态码,在reason属性中定义异常出现的原因
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR,reason = "服务器开小差了")
public class MyResponseStateException extends RuntimeException {
}
控制器中的方法
@GetMapping("/testEx")
public void testEx(){
System.out.println("testEx收到请求");
//在方法中抛出自定义类的异常,以此模拟出现异常的情况
throw new MyResponseStateException();
}
运行程序,在地址栏输入"/testEx",结果如图:
DefaultHandlerExceptionResolver
如果出现的异常以上两个异常处理器都没有进行处理,那么异常会交给DefaultHandlerExceptionResolver异常处理器处理。
在springMVC.xml配置文件中,配置出现异常映射的界面
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<!--key为异常类完整路径,值为一个可以被视图解析器支持的页面名称,如果出现错误,则跳转到error.jsp页面中-->
<!--key一般为Exception,以捕获所有没有被处理的异常并返回友好页面,提高用户体验-->
<prop key="java.lang.Exception">error</prop>
</props>
</property>
<!--将错误消息的key放在request域中-->
<property name="exceptionAttribute" value="error"/>
</bean>
控制器中的方法
@GetMapping("/testEx2")
public void testEx2(){
System.out.println("testEx收到请求");
int num=1/0;
}
error页面只显示异常信息
<h2>${error}</h2>
日志记录
我们通常使用Log4j来进行日志记录的管理,包括对日志输出的目的地,输出的信息级别和输出的格式等。
其实我们的System.out.println()
语句是最简单的日志记录,Log4j的优点是它在禁止一些特定的信息输出的同时不妨碍其它信息的输出的能力。这个能力源自于日志命名空间。
首先需要导入依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
配置log4j.properties
配置文件
#USE THIS SETTING FOR OUTPUT MYBATIS`s SQL ON THE CONSOLE
log4j.rootLogger=debug, Console,E
#Console
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
#log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n
log4j.appender.Console.layout.ConversionPattern=[ %l ] %d{yyyy/MM/dd HH:mm:ss} %-5p [%c{1}] - %l - %m%n
### 保存异常信息到单独文件 ###
log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
log4j.appender.E.File = error.log
log4j.appender.E.Append = true
log4j.appender.E.Threshold = ERROR
log4j.appender.E.layout = org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
LogAspect
类
import org.apache.log4j.Logger;
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.springframework.stereotype.Component;
@Aspect
@Component
public class LogAspect {
//定义Logger对象作为成员变量
private static Logger logger=Logger.getLogger(LogAspect.class);
@Pointcut("execution(* com.young.service.*.*(..))")
public void logPointcut(){}
@Around("logPointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
String cname=proceedingJoinPoint.getTarget().getClass().getName();
String mname=proceedingJoinPoint.getSignature().getName();
long stime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
long etime = System.currentTimeMillis();
String msg=String.format("%s类%s方法执行时间为%s毫秒",cname,mname,(etime-stime));
//使用Logger对象记录日志信息
logger.debug(msg);
//System.out.println(msg);
return result;
}
}
配置文件解释
日志级别
Log4j的级别有:0FF、FATAL、ERROR、WARN、INFO、DEBUG、ALL。Log4j建议只使用四个级别,优先级从高到低分别是ERROR、WARN、INFO、DEBUG。
通过定义的级别,可以控制到应用程序中相应级别的日志信息。
规则:
1.只要大于等于指定的控制级别,就可以输出。
2.如果有多个logger,都可以匹配输出,则每个logger都产生输出,其中根logger匹配所有的输出;而级别控制来源于路径最详细的logger。
输出源
Log4j允许日志请求被输出到多个输出源,一个输出源被称做一个Appender,一个logger可以设置多个的Appender。
常见Appender
org.apache.log4j.ConsoleAppender - 控制台
org.apache.log4j.FileAppender - 文件
org.apache.log4j.Dai 1yRoll ingFileAppender - 每天产生一个日志文件
org.apache.log4j.RollingFileAppender - 文件大小到达指定尺寸的时候产生一个新的文件
org.apache.log4j.WriterAppender - 将日志信息以流格式发送到任意指定的地方
org.apache.log4j.jdbc.JDBCAppender - 把日志用JDBC记录到数据库中
布局
布局就是指输出信息的格式,在Log4j中称作Layout。
常见Layout
org.apache.log4j.HTMLLayout - 以HTML表格形式布局
org.apache.log4j.PatternLayout - 可以灵活地指定布局模式
org.apache.log4j.SimpleLayout - 包含日志信息的级别和信息字符串
org.apache.log4j.TTCCLayout - 包含日志产生的时间、线程、类别等等信息
常用的PatternLayout
%m - 输出代码中指定的消息
%p - 输出优先级,即DEBUG, INFO,WARN,ERROR,FATAL
%r - 输出自应用启动到输出该log信息耗费的毫秒数
%c - 输出所属的类目,通常就是所在类的全名
%t - 输出产生该日志事件的线程名
%n - 输出一个回车换行符,Windows 平台为“\r\n”,Unix平台为"\n”
%d - 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy-MM-dd HH:mm:ss}
%l - 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例: Testlog4.main(TestLog4.java:10)
拦截器
我们使用SpringAOP来记录Service和Dao以及异常处理类的运行时间,而对于Controller无法使用切面类来记录运行时间。因为SpringContext是父容器,SpringMVC是子容器,父容器无法访问子容器。所以SpringMVC中提供了拦截器,拦截器只会拦截发送到Controller的请求,可以在方法执行前,方法执行后,视图解析器解析完毕等事件中被执行,所以可以使用拦截器来记录Controller运行时间。
自定义拦截器MyInterceptor
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyInterceptor implements HandlerInterceptor {
//在业务处理器被调用之前调用
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("==preHandle==");
long stime = System.currentTimeMillis();
request.setAttribute("stime",stime);
//如果返回false就不会执行Controller中的方法,所以需要返回true
return true;
}
//在方法执行之后视图返回值渲染之前被调用
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("==postHandle==");
}
视图渲染完成之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("==afterCompletion==");
long etime = System.currentTimeMillis();
long stime = Long.parseLong(request.getAttribute("stime").toString());
long time=etime-stime;
String path = request.getRequestURI();
System.out.println(path+"请求耗时-->"+time);
}
}
在springMVC.xml
中配置拦截器
<mvc:interceptors>
<mvc:interceptor>
<!--拦截所有的Controller方法 让MyInterceptor处理-->
<mvc:mapping path="/**"/>
<bean id="myInterceptor" class="com.young.interceptor.MyInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
运行结果
==preHandle==
[ com.alibaba.druid.pool.DruidDataSource.init(DruidDataSource.java:1003) ] 2020/06/10 10:42:51 INFO [DruidDataSource] - com.alibaba.druid.pool.DruidDataSource.init(DruidDataSource.java:1003) - {dataSource-1} inited
[ com.young.aspect.LogAspect.around(LogAspect.java:28) ] 2020/06/10 10:42:51 DEBUG [LogAspect] - com.young.aspect.LogAspect.around(LogAspect.java:28) - com.young.service.UserService类queryAll方法执行时间为444毫秒
==postHandle==
==afterCompletion==
/user/queryAll请求耗时-->663