Designing a Unified API Response and Global Exception Handling in Spring Boot

This article walks through creating a standardized JSON response structure with a Result enum and wrapper class, demonstrates how to return it from controllers, implements a @ControllerAdvice‑based global exception handler with custom exceptions, and configures Logback for comprehensive logging in Spring Boot applications.

Architect
Architect
Architect
Designing a Unified API Response and Global Exception Handling in Spring Boot

Unified Result Structure

Because most front‑end and back‑end communication uses JSON, the author proposes a uniform response format consisting of five fields: success (boolean indicating whether the request succeeded), code (numeric status code), message (human‑readable description), data (payload, usually a Map<String, Object>), and an optional extra identifier.

@Getter
public enum ResultCodeEnum {
    SUCCESS(true,20000,"成功"),
    UNKNOWN_ERROR(false,20001,"未知错误"),
    PARAM_ERROR(false,20002,"参数错误");

    private Boolean success;
    private Integer code;
    private String message;

    ResultCodeEnum(boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
}

Unified Result Wrapper

The wrapper class R hides its constructor, provides static factory methods ( ok(), error(), setResult()) that populate the fields from ResultCodeEnum, and offers chainable setters so callers can fluently add data, change the message, or adjust the code.

@Data
public class R {
    private Boolean success;
    private Integer code;
    private String message;
    private Map<String, Object> data = new HashMap<>();

    // private constructor
    private R() {}

    public static R ok() {
        R r = new R();
        r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
        r.setCode(ResultCodeEnum.SUCCESS.getCode());
        r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
        return r;
    }

    public static R error() {
        R r = new R();
        r.setSuccess(ResultCodeEnum.UNKNOWN_ERROR.getSuccess());
        r.setCode(ResultCodeEnum.UNKNOWN_ERROR.getCode());
        r.setMessage(ResultCodeEnum.UNKNOWN_ERROR.getMessage());
        return r;
    }

    public static R setResult(ResultCodeEnum result) {
        R r = new R();
        r.setSuccess(result.getSuccess());
        r.setCode(result.getCode());
        r.setMessage(result.getMessage());
        return r;
    }

    // chainable methods
    public R data(Map<String,Object> map) { this.setData(map); return this; }
    public R data(String key, Object value) { this.data.put(key, value); return this; }
    public R message(String message) { this.setMessage(message); return this; }
    public R code(Integer code) { this.setCode(code); return this; }
    public R success(Boolean success) { this.setSuccess(success); return this; }
}

Controller Usage

In a REST controller the unified result is returned directly, for example when listing users:

@RestController
@RequestMapping("/api/v1/users")
public class TeacherAdminController {
    @Autowired
    private UserService userService;

    @GetMapping
    public R list() {
        List<Teacher> list = teacherService.list(null);
        return R.ok().data("itms", list).message("用户列表");
    }
}

The resulting JSON looks like:

{
  "success": true,
  "code": 20000,
  "message": "查询用户列表",
  "data": {
    "itms": [
      {"id":"1","username":"admin","role":"ADMIN","deleted":false,"gmtCreate":"2019-12-26T15:32:29","gmtModified":"2019-12-26T15:41:40"},
      {"id":"2","username":"zhangsan","role":"USER","deleted":false,"gmtCreate":"2019-12-26T15:32:29","gmtModified":"2019-12-26T15:41:40"}
    ]
  }
}

Global Exception Handling

The author introduces a @ControllerAdvice class that centralises exception handling. Specific exceptions are caught with @ExceptionHandler, and a custom CMSException carries its own code and message.

@Data
public class CMSException extends RuntimeException {
    private Integer code;
    public CMSException(Integer code, String message) {
        super(message);
        this.code = code;
    }
    public CMSException(ResultCodeEnum resultCodeEnum) {
        super(resultCodeEnum.getMessage());
        this.code = resultCodeEnum.getCode();
    }
    @Override
    public String toString() {
        return "CMSException{" + "code=" + code + ", message=" + this.getMessage() + '}';
    }
}
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public R error(Exception e) {
        e.printStackTrace();
        return R.error();
    }

    @ExceptionHandler(NullPointerException.class)
    @ResponseBody
    public R error(NullPointerException e) {
        e.printStackTrace();
        return R.setResult(ResultCodeEnum.NULL_POINT);
    }

    @ExceptionHandler(HttpClientErrorException.class)
    @ResponseBody
    public R error(IndexOutOfBoundsException e) {
        e.printStackTrace();
        return R.setResult(ResultCodeEnum.HTTP_CLIENT_ERROR);
    }

    @ExceptionHandler(CMSException.class)
    @ResponseBody
    public R error(CMSException e) {
        e.printStackTrace();
        return R.error().message(e.getMessage()).code(e.getCode());
    }
}

Logback Configuration

For production‑grade logging the article supplies a Logback XML configuration that defines separate appenders for console output and for files at DEBUG, INFO, WARN, and ERROR levels. Profiles dev and pro switch between console‑centric and file‑centric logging, and variables such as ${log.path} make the log directory configurable.

<configuration scan="true" scanPeriod="10 seconds">
    <property name="log.path" value="D:/Documents/logs/edu"/>
    <!-- console appender (dev) -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    <!-- file appenders for each level (debug, info, warn, error) -->
    ... (omitted for brevity) ...
    <springProfile name="dev">
        <root level="info">
            <appender-ref ref="CONSOLE"/>
            <appender-ref ref="DEBUG_FILE"/>
            <appender-ref ref="INFO_FILE"/>
            <appender-ref ref="WARN_FILE"/>
            <appender-ref ref="ERROR_FILE"/>
        </root>
    </springProfile>
    <springProfile name="pro">
        <root level="info">
            <appender-ref ref="ERROR_FILE"/>
            <appender-ref ref="WARN_FILE"/>
        </root>
    </springProfile>
</configuration>

Logging Exception Details

To capture stack traces in the log files, a utility class ExceptionUtil converts an exception into a string, and the global handler is updated to log that string instead of printing to the console.

@Slf4j
public class ExceptionUtil {
    /** Print exception stack trace as a string */
    public static String getMessage(Exception e) {
        String swStr = null;
        try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
            e.printStackTrace(pw);
            pw.flush();
            sw.flush();
            swStr = sw.toString();
        } catch (IOException ex) {
            ex.printStackTrace();
            log.error(ex.getMessage());
        }
        return swStr;
    }
}
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public R error(Exception e) {
        // log the full stack trace
        log.error(ExceptionUtil.getMessage(e));
        return R.error();
    }
    // other handlers unchanged
}

Finally, the author notes that the active Spring profile ( spring.profiles.active) determines which logging configuration is used, and that generated log files can be inspected in the custom directory after the application starts.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaException HandlingSpring Bootapi-designlogbackUnified response
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.