真实案例结合下-对于java异常处理的-空指针的思考-AOP切面
最近在公司接到一个需求,做一个接口 返回位置碎片换传输对象;涉及到从数据库中查东西然后做处理返回;
主要针对两个实践的角度来谈
先简单描述一下工作流以及遇到的issues:
- 为了尽快提供对外接口, 在原有服务中进行调用+业务代码塞到了controller层
- 后面将代码调整到了 serviceImpl层
关于异常处理
业务层对于异常处理的一般直接抛出,然后再controller层会被统一异常处理捕获
业务实现层抛出的样例:
if (Result == null) {
//抛出自定义异常
throw new ParamsInvalidException("数据异常,请联系管理员");
}
全局配置-异常处理器:
注意:这里的controllerAdvice是一个controller级别的切面环绕,其标注的类可以用来封装一个切面所有属性的,包括切入点和需要织入的切面逻辑
@ControllerAdvice(basePackages = {"xxx---YYY"})
public class X {
private static final Logger logger = Logger.getLogger(X.class);
private final Logger a = Logger.getLogger(getClass());
/**
* 判断错误是否是已定义的已知错误,不是则由未知错误代替,同时记录在log中
* @param b
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Y c(Exception b){
logger.error("[系统异常] {}",b);
a.error("[系统异常] {}",b);
return Y.d(EnumMsg.SERVER_ERROR.getCode(), "系统异常");
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public Y e(MethodArgumentNotValidException f) {
BindingResult g = f.getBindingResult();
String h = "";
for (FieldError i : g.getFieldErrors()) {
h += i.getDefaultMessage() + ", ";
}
logger.info("[入参校验异常]"+h);
return Y.d(HttpStatus.BAD_REQUEST.value(), h);
}
/**
* 处理H异常
* @return
*/
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Y l(BusinessException m) {
logger.info("[N] {}", m);
Integer n = m.getErrCode();
return Y.d(n, m.getMessage());
}
@ExceptionHandler(value = HException.class)
@ResponseBody
public Y j(ParamsInvalidException k) {
logger.info("[H] {}", k);
return Y.d(HttpStatus.BAD_REQUEST.value(), k.getMessage());
}
/**
* 处理N异常
* @return
*/
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Y l(BusinessException m) {
logger.info("[N] {}", m);
Integer n = m.getErrCode();
return Y.d(n, m.getMessage());
}
/**
* 处理M异常
* @return
*/
@ExceptionHandler(TagFilterException.class)
@ResponseBody
public Y o(TagFilterException p) {
logger.info("[M] {}", p);
Integer q = 901;
return Y.d(q, p.getMessage());
}
}
全局配置-自定义异常处理类:
package com.data.api.exception;
public class NException extends RuntimeException{
/**
* 自定义参数异常, 可以在controller层被aop捕获
*/
private static final long serialVersionUID = 9990669847913759761L;
public NException(String messgae) {
super(messgae);
}
}
package com.data.api.exception;
public class MException extends RuntimeException{
/**
* 自定义参数异常, 可以在controller层被aop捕获
*/
private static final long serialVersionUID = 8780669847913559761L;
public MException(String messgae) {
super(messgae);
}
}
package com.data.api.exception;
public class HException extends RuntimeException{
/**
* 自定义参数异常, 可以在controller层被aop捕获
*/
private static final long serialVersionUID = 8780669847913559761L;
private Integer errCode;//错误码
public H(Integer code,String messgae) {
super(messgae);
this.errCode = code;
}
public Integer getErrCode() {
return errCode;
}
}
对于空值的处理
使用空字符串
""
而不是默认的null
可避免很多NullPointerException
,编写业务逻辑时,用空字符串""
表示未填写比null
安全得多。返回空字符串
""
、空数组而不是null
对于空数组的返回
{
"status": 200,
"msg": "success",
"data": {
"adverts": [],
}
}
对于 “adverts”: [] 我返回了 “adverts”: [“”],被要求修改为特定格式,做以下空值返回
//对于空字符串的处理
if ("".equals(split[0])){
response.setModules(Collections.emptyList());
} else {
response.setModules(Arrays.asList(split));
}
为什么空字符串类型的数组需要进行转换?
- 返回空数组(
[]
):
- 含义:通常表示没有相关的数据项。例如,如果
adverts
字段表示广告列表,空数组表示没有广告可显示。 - 优势:这种方法在语义上通常更清晰,表示该字段(如广告)当前没有任何项。
- 使用场景:当确实没有数据时,或者当请求的数据项为空时使用。
- 返回包含空字符串的数组(
[""]
):
- 含义:这通常表示数组中有一个数据项,但该项是空的。这可能在某些情况下引起混淆,因为它暗示了一个数据项的存在,但该项实际上是空的。
- 优势:在某些特殊情况下,如果前端需要区分“无数据”和“数据为空”的情况,这种方法可能有其用途。
- 使用场景:这种情况较少见,仅在特定的业务逻辑或前端处理逻辑中才有必要。
通常情况下,推荐使用空数组([]
),因为它在大多数情况下提供了更清晰、更直观的语义。
另一种角度:一般来说,返回0前端判空比较麻烦,前端判空需要包括以下字段: undefine + null +“”+0.00等情况;最好用的其实是返回空字符串,前端方便处理
对于空指针的处理
大多数场景下要求我们避免 nullPointerException
指针这个概念实际上源自C语言,Java语言中并无指针。我们定义的变量实际上是引用,Null Pointer更确切地说是Null Reference,不过两者区别不大。
如果遇到NullPointerException
,我们应该如何处理?首先,必须明确,NullPointerException
是一种代码逻辑错误,遇到NullPointerException
,遵循原则是早暴露,早修复,严禁使用catch
来隐藏这种编码错误:
// 错误示例: 捕获NullPointerException
try {
transferMoney(from, to, amount);
} catch (NullPointerException e) {
}
特殊情况下,调用方需要得到一个null
比如返回null
表示文件不存在,那么考虑返回Optional<T>
:
public Optional<String> readFromFile(String file) {
if (!fileExist(file)) {
return Optional.empty();
}
...
}
处理方法:
- 赋初值
- 定位: x.y.z.lalaal() 如果出现了空指针的话, 需要依次打印出对象(java14以后得到改进给JVM添加一个
-XX:+ShowCodeDetailsInExceptionMessages
参数启用
对于空指针处理后带来的问题
一些情况下, 由于返回了空数组, 或者空字符串客户端调用不会出现影响, 但是也导致了一定的问题
先简单介绍一下这个问题:现在有一个service层封装调用了其余的三个service,以完成批量更新操作;其中期待service2中返回满足条件的所有对象, 但是中间由于某些问题(见注释)导致service2中返回了一个空列表(本应该是有数据的哈);但是,并不会报空指针异常;此时中间步骤跳过, 直接进入service3的更新步骤, 由于没有数据, service3 0步迭代完成,并告诉controller层已经执行完成。
导致了 返回值 :success 但是实际上没有完成数据修改
某些问题包括:
- service2层需要进行权限验证,本来期望在controller层发现权限不足的异常并捕获, 但是封装service 的接口中没有进行相应的权限操作, 导致了service2 中的数据无法查询到,且service2 中并没有抛出适合的异常,但是做了相应的功能,全权把异常交给了controller层;(导致了无显示的查询异常)(请注意: 此时service2 中的逻辑相对黑盒(代码复杂,重构或者不复用开发成本会提高))
- service2 中调用了page 分页插件, 且在controller层中进行了相关注解的引用如下:
注解引入:
@PostMapping(value = "/Page") @X public Y getZ(@RequestBody @Validated A B) { C<D> E = F.G(B); return Y.H(E); }
自定义注解:
/** * Created by boots */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) public @interface X { String description() default ""; }
此时@X 注解在封装的service层中并没有进行添加, 导致其内部代码无法正常使用
解决方案:
- 添加注解(完全照抄照搬原来的service框架)
- 分解service2代码模块, 找到适合自己的部分, 并进行小范围改造(庖丁解牛)
- service之间添加空值判断和异常抛出
修改后 实际业务代码
controller层
/**
* xxxx-一键更新
*/
@PutMapping(value = "/X")
public Y<Boolean> Z(@RequestBody A B) {
boolean C = D.E(B);
if (B.F() == 2) {
this.G(B.H());
}
return Y.ok(C);
}
service层
public boolean A(B C) {
D E = new D();
int F = C.G();
E.H(C.I());
//查询所有
List<J> K = L.M(E);
//判空
if (K == null || K.isEmpty()){
throw new N("无法查找");
}
K.forEach(O -> {
J P = new J();
P.Q(O.R());
P.S(F);
L.T(P);
});
return true;
}
java商业代码学习用的魔法值替换脚本 (python )
import re
def replace_identifiers(java_code):
class_method_pattern = r'\b[A-Z][a-zA-Z0-9_]*\b' # 匹配类名和方法名
generic_pattern = r'<[ ?A-Z][a-zA-Z0-9_, ?]*>' # 匹配泛型
comment_pattern = r'(//.*?$|/\*.*?\*/)' # 匹配注释
string_literal_pattern = r'".*?"' # 匹配字符串字面量
placeholder = 'X'
current_char = ord(placeholder)
def replacer(match):
nonlocal current_char
# 避免替换注释和字符串字面量
if match.group(0).startswith(("//", "/*", "*/", '"')):
return match.group(0)
replacement = chr(current_char)
current_char += 1
return replacement
# 首先替换泛型参数
java_code = re.sub(generic_pattern, replacer, java_code)
# 然后替换类和方法名
java_code = re.sub(class_method_pattern, replacer, java_code)
# 最后,过滤注释和字符串字面量
java_code = re.sub(comment_pattern, replacer, java_code)
java_code = re.sub(string_literal_pattern, replacer, java_code)
return java_code
# 读取源代码
with open('source.java', 'r') as file:
java_code = file.read()
# 替换标识符
modified_code = replace_identifiers(java_code)
# 将修改后的代码写入新文件
with open('modified_source.java', 'w') as file:
file.write(modified_code)