真实案例结合下-对于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));
}

为什么空字符串类型的数组需要进行转换?

  1. 返回空数组([]):
  • 含义:通常表示没有相关的数据项。例如,如果adverts字段表示广告列表,空数组表示没有广告可显示。
  • 优势:这种方法在语义上通常更清晰,表示该字段(如广告)当前没有任何项。
  • 使用场景:当确实没有数据时,或者当请求的数据项为空时使用。
  1. 返回包含空字符串的数组([""]):
  • 含义:这通常表示数组中有一个数据项,但该项是空的。这可能在某些情况下引起混淆,因为它暗示了一个数据项的存在,但该项实际上是空的。
  • 优势:在某些特殊情况下,如果前端需要区分“无数据”和“数据为空”的情况,这种方法可能有其用途。
  • 使用场景:这种情况较少见,仅在特定的业务逻辑或前端处理逻辑中才有必要。

通常情况下,推荐使用空数组([],因为它在大多数情况下提供了更清晰、更直观的语义。

另一种角度:一般来说,返回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();
    }		
    ...
}

处理方法:

  1. 赋初值
  2. 定位: x.y.z.lalaal() 如果出现了空指针的话, 需要依次打印出对象(java14以后得到改进给JVM添加一个-XX:+ShowCodeDetailsInExceptionMessages参数启用

对于空指针处理后带来的问题

一些情况下, 由于返回了空数组, 或者空字符串客户端调用不会出现影响, 但是也导致了一定的问题

java 后端实现 切片传输 应答功能_python

先简单介绍一下这个问题:现在有一个service层封装调用了其余的三个service,以完成批量更新操作;其中期待service2中返回满足条件的所有对象, 但是中间由于某些问题(见注释)导致service2中返回了一个空列表(本应该是有数据的哈);但是,并不会报空指针异常;此时中间步骤跳过, 直接进入service3的更新步骤, 由于没有数据, service3 0步迭代完成,并告诉controller层已经执行完成。

导致了 返回值 :success 但是实际上没有完成数据修改

某些问题包括:

  1. service2层需要进行权限验证,本来期望在controller层发现权限不足的异常并捕获, 但是封装service 的接口中没有进行相应的权限操作, 导致了service2 中的数据无法查询到,且service2 中并没有抛出适合的异常,但是做了相应的功能,全权把异常交给了controller层;(导致了无显示的查询异常)(请注意: 此时service2 中的逻辑相对黑盒(代码复杂,重构或者不复用开发成本会提高))
  2. 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层中并没有进行添加, 导致其内部代码无法正常使用

解决方案:

  1. 添加注解(完全照抄照搬原来的service框架)
  2. 分解service2代码模块, 找到适合自己的部分, 并进行小范围改造(庖丁解牛)
  3. 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)