概述
这个是外包一个公司的时候,用到了一个不太成熟的框架来写代码,后来发现,框架几乎没有对异常进行处理的代码,所以花了一些时间,来解决这个问题,所以有很多不成熟的东西在里面,个人经验,大神勿喷。
先上成果图,感兴趣的可以看一下,不感兴趣的也可以不用在这浪费时间。
先说一下,以下步骤完成了哪些功能
- 通过在后端抛出一个自定义异常类,可以把异常消息通过json返回给前台通过处理展示给用户查看。
- 抛出的异常消息,支持国际化,可以抛出一个代码,通过I18N文件来对代码进行消息定制,最终解析为配置的真正需要显 示的消息,当然如果该CODE没有找到对应配置,则直接返回该code.如果先麻烦,其实这里异常也支持直接就写报错信息。如throw new RuntimeException("我要报错了!");
- 在对异常进行全局处理的同时,配合Log4j 1.2来对异常消息进行单独文件记录,每日轮循生成,方便查看异常,当然同时这里也有对CATALINA.OUT文件的接管。
- 为什么要用Log4j1.2,因为原框架配置的就是1.2。所以再这个基础上去做的尝试;
返回的异常消息,前台可以通过处理来把信息展示给用户。
这个是每天生成的日志,CATALINA即控制台日志,EXCEPTION即只记录报错的异常消息,发现查找出错原因
报错信息如下
大神绕道之后,下面我就开始装逼了。下面开始详细解释步骤。
- 首先自定义一个异常类,该类继承运行时异常类,不要继承Exception。
package com.sinotrans.hd.dev.pub.exception; import org.springframework.core.NestedRuntimeException; /** * 自定义异常类 * @author DDF 2017年11月28日 * */ public class GlobalCustomizeException extends NestedRuntimeException{ private static final long serialVersionUID = 1L; private String code; private String message; // 可替换消息参数,消息配置中不确定的值用大括号包着数组角标的方式,如{0} 占位,抛出异常的时候使用带params的构造函数赋值,即可替换 private Object[] params; /** * 最基本的直接写代码 * @param code */ public GlobalCustomizeException(String code) { super(code); this.message = code; } /** * 接受一个代码和占位消息 * @param code * @param params */ public GlobalCustomizeException(String code, Object... params) { super(code); this.message = code; this.params = params; } /** * 接受一个异常来转换消息 * @param e * @param params */ public GlobalCustomizeException(Exception e, Object... params) { super(e.getMessage(), e); this.params = params; this.message = e.getMessage(); } /** * 这个其实是最推荐的最标准当然也是最麻烦的写法 * @param enumClass 这个是代码需要定义在一个枚举类里,枚举类实现了该接口,方便可以定义多个枚举代码类 * @param params 如果有占位消息,请使用这个 */ public GlobalCustomizeException(ExceptionEnumInterface enumClass, Object... params) { super(enumClass.get()); this.message = enumClass.get(); this.params = params; } /** * 如上。只不过这个没有需要替换的占位消息 * @param enumClass */ public GlobalCustomizeException(ExceptionEnumInterface enumClass) { super(enumClass.get()); this.message = enumClass.get(); } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public Object[] getParams() { return params; } public void setParams(Object[] params) { this.params = params; } }
- 其实这几个类的顺序个人很纠结应该怎么来放进来更容易理解,现在大家还是见谅吧。定义出了异常类之后,是时候该展示真正的技术了。那么打个比方,我想抛给前台一个异常,如前面所说我要报错了。则按照刚才定义出来的类代码如下
throw new GlobalCustomizeException("我要报错了");
- 先把最基本的流程梳理一遍吧。如何对这个异常类进行处理,使之能够返回到前端呢?这时候需要用到SpirngMVC的一个类HandlerExceptionResolver,本人对理论的东西很难解释清楚,也没有过多去了解。总之,SpringMVC对异常处理有很多种方式,有在方法上,有全局异常监控处理的。所以本文只解释这个类就是一个全局异常的,只要系统中有异常被抛出类,就会被该类所拦截。我们需要重写resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)方法 。该方法返回的是一个ModelAndView。但是我却无法做到让方法不去找视图,没办法最后用的是HttpServletResponse来返回的数据,而且返回的是null,所以没想明白这一块。到这一步其实又有一个纠结点,是应该把成品的代码贴出来还是直接先贴出来满足这部分的代码然后在后面完善,但谁让我懒呢?下面直接贴出来成品代码。这一块因为本身是处理异常的代码自己又有异常,所以写的有点乱,而且个人也想不很清楚这一块最好怎么写。这一步的日志记录和MessageSource先忽略。后面会说到。
/** * */ package com.sinotrans.hd.dev.pub.exception; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Locale; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.shiro.SecurityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; import com.sinotrans.hd.dev.framework.exception.DataException; import com.sinotrans.hd.dev.pub.util.Constants; /** * 用于处理全局异常的一个类,支持国际化,标准请抛出GlobalCustomizeException, * 其参数需要在 实现了com.sinotrans.hd.dev.pub.exception.ExceptionEnumInterface接口的枚举类中定义 , * 对应的消息文件在exception.properties所处Locale中配置 * 附加配合Log4j单独记录异常日志到文件中 * @author DDF 2017年11月28日 * */ @Component public class GlobalExceptionHandler implements HandlerExceptionResolver { @Autowired private MessageSource messageSource; /** 异常日志 */ private static Logger log = LoggerFactory.getLogger("exceptionLog"); /** 资源文件baseName */ // private static final String EXCEPTION_RESOURCE_BASE_NAME = "exception"; @SuppressWarnings("unchecked") @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { // 很显然,这一步是先打印以下当前请求,和请求参数,后面方法结束还会有一个Over,这样方便区分一个报错的方法的范围 // 最初这里也对Request的Body区域进行的读操作,后来发现读出来之后后面的方法就获取不到了, // 需要另寻她发来解决这个问题,超纲。所以这是一个遗憾。 request.setCharacterEncoding(Constants.CHARSET_U8); log.info("RequestMapping [{}] progress start.......", request.getServletPath()); if (Constants.isNotNull(request.getQueryString())) { log.info("QueryString: [{}]", URLDecoder.decode(request.getQueryString(), Constants.CHARSET_U8)); } } catch (UnsupportedEncodingException e1) { log.error(e1.getMessage()); } finally { ModelAndView mv = new ModelAndView(); GlobalCustomizeException exception = null; String codeKey = ""; String codeValue = ""; if (ex instanceof GlobalCustomizeException) { // 对抛出的异常进行定制化消息处理 exception = (GlobalCustomizeException) ex; Locale locale = request.getLocale(); codeKey = exception.getMessage(); codeValue = messageSource.getMessage(codeKey, exception.getParams(), codeKey, locale); // JDK自带的国际化支持写法 /*ResourceBundle resourceBundle = ResourceBundle.getBundle(EXCEPTION_RESOURCE_BASE_NAME, locale); if (resourceBundle != null && resourceBundle.containsKey(codeKey)) { codeValue = resourceBundle.getString(codeKey); if (Constants.isNotNull(exception.getParams())) { MessageFormat mf = new MessageFormat(codeValue, locale); codeValue = mf.format(exception.getParams()); } } else { codeValue = codeKey; }*/ } else if (ex instanceof DataException) { // 兼容原框架抛出的DataException(原框架抛出的异常是这个,但是无任何处理, // 而且是异常的Exception。这里进行了兼容,保证抛出的这个异常也能被返回前台) exception = new GlobalCustomizeException(ex.getMessage()); codeKey = ex.getMessage(); codeValue = ex.getMessage(); } else { // 可对运行时异常在资源文件中进行定制化消息,如果不定义的话,直接返回原错误信息 // 这里说的是比如定义了代码为java.lang.NullPointerException,也可以为空指针定义人性化的错误信息 exception = new GlobalCustomizeException(ex.toString()); Locale locale = request.getLocale(); codeKey = exception.getMessage(); codeValue = messageSource.getMessage(codeKey, exception.getParams(), codeKey, locale); } /** * 配合log4j的日志管理将异常信息输出到文件中 * ------------------------------异常日志start----------------------- */ StringWriter sw = new StringWriter(); PrintWriter errorPw = new PrintWriter(sw); ex.printStackTrace(errorPw); Object []logMessage = null; // 把解析后的消息附加上 if (Constants.isNotNull(codeKey) && !codeKey.equals(codeValue)) { logMessage = new Object[] { ((Map<String, Object>) SecurityUtils.getSubject().getSession(). getAttribute("user")).get("LOGIN_NAME"), codeValue, sw.toString() }; } else { logMessage = new Object[] { ((Map<String, Object>) SecurityUtils.getSubject().getSession(). getAttribute("user")).get("LOGIN_NAME"), sw.toString() }; } // 异常日志记录异常消息 log.error("currUser: {}, {}, message: {}", logMessage); /** ------------------------------异常日志start----------------------- */ log.info("RequestMapping [{}] progress end.......", request.getServletPath()); StringBuilder sbl = new StringBuilder(); // 将错误信息返回前端 response.setStatus(500); response.setContentType("text/html;charset=UTF-8"); sbl.append("{").append(""code": "").append(codeKey).append("", "") .append("message": "").append(codeValue).append(""}"); mv.addObject("message", sbl.toString()); mv.setViewName(""); PrintWriter pw = null; try { pw = response.getWriter(); pw.write(sbl.toString()); } catch (IOException e) { log.error(e.getMessage()); return null; } finally { if (pw != null) { pw.close(); } } } return null; } }
- 其实如果不需要国际化这一块的代码是相当简单的。但是由于上面用到了国际化,其实JDK自带的ResouceBundle和MessageFormat也可以通过资源文件来处理国际化,上面注释部分的代码,需要用到资源文件的BaseName。但现在用到的是Spirng对国际化的一个更加好用的处理,该类为org.springframework.context.support.ResourceBundleMessageSource。该类可以对国际化消息的获取提供更加人性化的处理,如如果获取不到是抛出异常,但是使用默认值,默认值是如何。如上面贴出来的代码使用的就是默认值,而且默认值就是抛出消息的代码
public final String getMessage(String code, Object[] args, String defaultMessage, Locale locale)
public final String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException
- 接上,该类的使用也很简单,只需要在Spring的配置文件中把该类配置成bean即可。通过下面配置还可以看到一个好处,即资源文件可以配置多个。不需要在一个资源文件中把所有的系统消息都写在里面。这样可以吧资源的配置分为系统类、业务类等。关于I18N文件,这里简单做一下解释。I18N即国际化,国际化文件包含一个最基本的文件就叫baseName。如下面所示取名为exception。则一定有一个配置文件为exception.properties。该配置文件作为默认的资源文件,通常该文件还需要配合Locale理解。Locale代表着一个区域,如中国为zh_CN, 美国为en_US。每个请求服务器都能或得到该请求锁代表的时区,参照request.getLocale。资源文件的寻找步骤为,如果当前请求的区域为中国,则系统会自动根据baseName_区域.properties中去寻找资源,则为exception_zn_CN.properties。同理美国为exception_en_US.properties。但如果找不到对应的资源文件,则exception.properties则会在此时生效。
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basenames"> <list> <value>exception</value> </list> </property> </bean>
- 下面开始介绍这个消息枚举类的定义,该部分分为两个小部分
6.1 首先是代码枚举类的接口,该接口在上述异常类中已出现,名为ExceptionEnumInterface,定义接口可以让不同模块的报错信息分别定义在不同的类中,方便查看和管理,而处理的时候只需要对接口类进行处理接口
/**
*
*/
package com.sinotrans.hd.dev.pub.exception;
/**
* @author DDF 2018年2月1日
*
异常枚举定义的接口类
*
*/
public interface ExceptionEnumInterface {
public String get();
}
6.2 下面就是定义枚举类的具体实现了,引用时为GlobalExceptionEnum.UNKNOW_ERROR。实际上对应的值为括号内的字符"UNKNOW_ERROR",如使用标准枚举类抛出异常则代码为
throw new GlobalCustomizeException(GlobalExceptionEnum.UNKNOW_ERROR);
而此时需要的时要在I18N文件中针对UNKNOW_ERROR配置该代码对应的具体消息。
/**
*
*/
package com.sinotrans.hd.dev.pub.exception;
/**
* 定义异常消息的代码,get方法返回实际值,这个值需要在exception.properties、exception_zh_CN.properties、
* exception_en_US中配置,请根据实际情况在对应的Locale资源文件中配置,至少配置exception.properties
* @author DDF 2017年12月1日
*
*/
public enum GlobalExceptionEnum implements ExceptionEnumInterface {
UNKNOW_ERROR("UNKNOW_ERROR");
private String code;
GlobalExceptionEnum (String code) {
this.code = code;
}
@Override
public String get() {
return this.code;
}
}
7. 下面开始定义I18N文件。I18N在上述有过简介,所以I18N文件代表的其实是一组文件。定义如下。
## exception.properties
UNKNOW_ERROR=u672Au77E5u9519u8BEF
## exception_zh_CN.properties
UNKNOW_ERROR=u672Au77E5u9519u8BEF
## exception_en_US.properties
UNKNOW_ERROR=Unknown error
8. 配置Log4j。对配置有疑问的建议去官方去查看以下Log4j1.2的文档,本人英语很渣,但1.2的文档对这Pattern的介绍却十分详细易懂,反而2.x的文档上去一看,一脸懵逼。下面的效果图在最初已经贴出来,配合看全局异常处理类的代码就可以看出来异常是可以很清楚的单独记录在一个文件,以后别人说报错了,自己直接抓过来异常日志,谁操作了什么,报了什么错,哪行代码一目了然。
# 输出到文件,这个是控制台的日志,每天生成一个。
log4j.appender.CATALINA = org.apache.log4j.DailyRollingFileAppender
log4j.appender.CATALINA.File = ${catalina.base}/logs/CATALINA_KPI_HR
log4j.appender.CATALINA.Encoding = UTF-8
log4j.appender.CATALINA.BufferSize = 8K
# Roll-over the log once per day
log4j.appender.CATALINA.DatePattern = '.'yyyy-MM-dd'.log'
log4j.appender.CATALINA.layout = org.apache.log4j.PatternLayout
log4j.appender.CATALINA.layout.ConversionPattern = %d [%t] %-5p %c.%M- %m%n
# 打印到控制台
log4j.appender.CONSOLE = org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Encoding = UTF-8
log4j.appender.CONSOLE.layout = org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern = %d %-5p %c.%M- %m%n
# Log everything. Good for troubleshooting
log4j.logger.org.hibernate = DEBUG
# Log all JDBC parameters
log4j.logger.org.hibernate.type = ALL
# Root logger option
log4j.rootLogger=INFO, CATALINA, CONSOLE
#异常 LOG
log4j.logger.exceptionLog= INFO, EXCEPTION
log4j.appender.EXCEPTION = org.apache.log4j.DailyRollingFileAppender
log4j.appender.EXCEPTION.Encoding = UTF-8
log4j.appender.CATALINA.BufferSize = 5K
log4j.appender.EXCEPTION.DatePattern = '.'yyyy-MM-dd'.log'
log4j.appender.EXCEPTION.File = ${catalina.base}/logs/EXCEPTION_KPI_HR
#log4j.appender.EXCEPTION.File=/opt/apache-tomcat-9.0.0.HR/logs/KPI_HR_ERROR.log
log4j.appender.EXCEPTION.layout = org.apache.log4j.PatternLayout
## 这里对自定义统一异常类的处理支持不是很好,打印的抛出类的信息都是异常父类,而不是真正的逻辑代码。这部分已修改为通过代码支持,这里仅需定义格式即可
##log4j.appender.EXCEPTION.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS}[任务运行] %p [%t] %C.%M(%L) | %m%n
log4j.appender.EXCEPTION.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %p %m%n
9. 对于占位符之类的处理,比如说某某错误,一些消息是固定的,但是在该消息中需要提示与数据本身相关的东西,应该配置一个占位符,在抛出异常的时候把该部分内容传入进行替换。
## exception.properties
REPLACE_UNKNOW_ERROR={0}u672Au77E5u9519u8BEF{1}
## exception_zh_CN.properties
REPLACE_UNKNOW_ERROR={0}u672Au77E5u9519u8BEF{1}
## exception_en_US.properties
REPLACE_UNKNOW_ERROR={0}Unknown error{1}
10 开搞
@ResponseBody
@RequestMapping("/test1")
public void test1() throws Exception {
// 最基本的直接抛出异常,满足这部分功能与国际化的所有代码都可删除
throw new GlobalCustomizeException("未知错误!");
}
@ResponseBody
@RequestMapping("/test2")
public void test2() throws Exception {
// 这部分代码可以使用国际化,但直接传字符类型的代码,不需要在枚举类中定义代码
throw new GlobalCustomizeException("UNKNOW_ERROR");
}
@ResponseBody
@RequestMapping("/test3")
public void test3() throws Exception {
// 通过枚举类来处理消息的国际化映射
throw new GlobalCustomizeException(GlobalExceptionEnum.UNKNOW_ERROR);
}
@ResponseBody
@RequestMapping("/test4")
public void test4() throws Exception {
// 下面是支持占位符的处理,按照顺序一次替换资源文件中的占位符
throw new GlobalCustomizeException("REPLACE_UNKNOW_ERROR", "之前", "之后");
}
@ResponseBody
@RequestMapping("/test5")
public void test5() throws Exception {
// 通过枚举类来处理消息的国际化映射,按照顺序一次替换资源文件中的占位符
throw new GlobalCustomizeException(GlobalExceptionEnum.REPLACE_UNKNOW_ERROR, "之前", "之后");
}
@ResponseBody
@RequestMapping("/test6")
public void test6() throws Exception {
// 也可以把系统内置异常代码定义成代码,然后在资源文件中配置消息
throw new RuntimeException("未知错误");
}
@ResponseBody
@RequestMapping("/test7")
public void test7() throws Exception {
// 兼容其它写的并不规范的自定义异常
throw new DataException("未知错误");
}
11. 总结
肯定会有很多疏漏的地方,望海涵。
另有遗留问题如下
1. 如前面已经说过HandlerExceptionResolver的resolveException方法是返回一个ModelAndView。当然如果是要在特定的页面显示信息,那么setView()方法设置对面的视图名称即可,应该问题不大。可个人感觉如果消息只能在特定的视图能看到,那么这种消息对用户的意义又有多大呢?毕竟这会导致直接离开当前用户操作的视图。
2. log4j的Pattern提供了非常丰富的获取数据的格式,譬如我们最需要的当前的调用栈、异常栈等一层层抛出来的信息,但是使用了这些配置以后,打印的日志尽是底层的异常父类的与业务逻辑无关的代码,让人很是无语。所以本文中是通过代码来获取真正需要的报错的逻辑代码信息
3. 还是log4j的问题,关于记录日志的频率,为每天一次,但是经过使用发现,比如异常类,刚才是的文件名称是定义的基本文件名,按正常情况下第二天会重新生成一个,而昨天的则会附加上日期归为历史,新的日志会记录在新的文件。比如在10号生成了一个这个日志文件,里面又日志产生,但是在11号一天都没有日志产生,那么在12号没有产生日志的前提下再看目录发现并没有产生10号和11号的历史。所以猜测归为历史日志的前提是第二天有日志产生,才能触发这部分的处理。
最后
以上就是感性母鸡为你收集整理的利用SpringMVC+log4j 1.2 实现自定义异常抛出给前台处理和对异常文件的单独记录的全部内容,希望文章能够帮你解决利用SpringMVC+log4j 1.2 实现自定义异常抛出给前台处理和对异常文件的单独记录所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复