Java 后端详解(三):全局异常处理与 JPA 数据库映射

72 阅读14分钟

上篇:Java 后端详解(二):注解、参数绑定、评论与用户认证
入门:项目介绍-Spring-Boot-Vue博客系统
本篇以 GlobalExceptionHandlerJwtAuthenticationEntryPointentity 包为例,讲清:异常如何统一返回给前端未登录 401 为什么不走全局异常处理器Java 类如何映射成数据库表


一、本篇要解决的问题

问题涉及文件
Service 里 throw new RuntimeException("用户名已存在") 之后发生了什么?GlobalExceptionHandler.java
@Valid 校验失败时,谁把 400 错误返回给前端?GlobalExceptionHandler.java
未登录访问 /api/users/me 时,谁返回 401?会进 GlobalExceptionHandler 吗?SecurityConfig.java + JwtAuthenticationEntryPoint.java
@Entity@Column@GeneratedValue 分别是什么意思?entity/Article.java
Java 字段怎么变成 MySQL 的 CREATE TABLEapplication.yml + JPA

二、GlobalExceptionHandler 全貌

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleRuntimeException(RuntimeException e) {
        return ApiResponse.error(400, e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining("; "));
        return ApiResponse.error(400, message);
    }
}

它是整个后端的统一异常出口:任何地方抛出的特定异常,都会在这里被捕获,转成统一的 JSON 格式返回。


三、三个核心注解

1. @RestControllerAdvice — 全局拦截器

加在类上,表示:这个类专门处理 Controller 调用链中抛出的异常(包括 Service 里 throw 的)。

Controller 调用 ServiceService 抛出异常
        ↓
异常向上冒泡,Spring 按类型匹配 @ExceptionHandler
        ↓
返回统一 JSONApiResponse

注意@RestControllerAdvice 管不到 Spring Security 过滤器链里的「未登录」场景。未带 token 或 token 无效时,请求在进 Controller 之前就会被 JwtAuthenticationEntryPoint 拦截并直接写 401 JSON,详见下文 链路 C

和普通 @ControllerAdvice 的区别:@RestControllerAdvice = @ControllerAdvice + @ResponseBody,返回值直接写成 JSON,不用再额外标注。

2. @ExceptionHandler(异常类型.class) — 指定处理哪种异常

@ExceptionHandler(RuntimeException.class)
public ApiResponse<Void> handleRuntimeException(RuntimeException e) { ... }

含义:当任何地方抛出 RuntimeException(及其子类)时,执行这个方法

本项目 Service 里的业务错误都用 RuntimeException

位置抛出的异常
AuthService.registerthrow new RuntimeException("用户名已存在")
AuthService.loginthrow new RuntimeException("用户名或密码错误")
CommentService.createthrow new RuntimeException("文章不存在")
ArticleService.deletethrow new RuntimeException("无权操作该文章")
UserServicethrow new RuntimeException("未登录")

3. @ResponseStatus(HttpStatus.BAD_REQUEST) — 设置 HTTP 状态码

@ResponseStatus(HttpStatus.BAD_REQUEST)  // HTTP 400

告诉 Spring:这个方法返回时,HTTP 响应状态码设为 400 Bad Request

最终前端收到的响应:

{
  "code": 400,
  "message": "用户名已存在",
  "data": null
}

四、三条异常处理链路

链路 A:业务异常(RuntimeException

以注册时用户名重复为例:

POST /api/auth/register  { "username": "summer", ... }
        ↓
AuthController → AuthService.register()
        ↓
userRepository.existsByUsername("summer") == truethrow new RuntimeException("用户名已存在")
        ↓
异常向上冒泡(Service → Controller,不会正常 return)
        ↓
Spring 扫描 @RestControllerAdvice,按异常类型匹配 @ExceptionHandler
        ↓
调用 handleRuntimeException(e)   ← 不是 throw 直接调用的
        ↓
return ApiResponse.error(400, "用户名已存在")
        ↓
HTTP 400 + JSON

对应代码:

// AuthService.java
if (userRepository.existsByUsername(request.getUsername())) {
    throw new RuntimeException("用户名已存在");
}

// GlobalExceptionHandler.java
@ExceptionHandler(RuntimeException.class)
public ApiResponse<Void> handleRuntimeException(RuntimeException e) {
    return ApiResponse.error(400, e.getMessage());  // e.getMessage() = "用户名已存在"
}

e.getMessage() 就是 throw 时括号里的字符串。

重要:throw 并不是直接调用 handleRuntimeException

很多人看到 throw new RuntimeException("用户名已存在") 就会以为代码「自动跳」到了 handleRuntimeException其实不是

误解实际情况
throw 会调用 handleRuntimeExceptionService 里只负责抛异常,不会、也不能写 handleRuntimeException(...)
方法名要对上才会处理Spring 看的是异常类型RuntimeException.class),不是方法名
Handler 和 Service 有直接引用关系两者互不 import,完全由 Spring 在运行时串联

完整机制如下:

1. AuthService 里执行 throw new RuntimeException("用户名已存在")
2. 当前方法中断,异常沿调用栈向上抛:Service → Controller
3. Controller 没有 try-catch,异常继续向上
4. Spring MVC 拦截到这个未处理异常
5. 发现存在 @RestControllerAdvice 标注的 GlobalExceptionHandler
6. 在其中查找 @ExceptionHandler,匹配异常类型:
   - 抛出的是 RuntimeException → 匹配 handleRuntimeException
   - 若是 MethodArgumentNotValidException → 匹配更具体的 handleValidationException
7. Spring 调用匹配到的方法,把异常对象 e 传进去
8. 方法 return ApiResponse.error(400, e.getMessage()) → 写成 HTTP 响应

对应代码分工:

// AuthService — 只管「出错了」,不管怎么返回 JSON
throw new RuntimeException("用户名已存在");

// GlobalExceptionHandler — 只管「怎么响应」,不用知道是谁抛的
@ExceptionHandler(RuntimeException.class)
public ApiResponse<Void> handleRuntimeException(RuntimeException e) {
    return ApiResponse.error(400, e.getMessage());
}

一句话throw 负责中断业务;@RestControllerAdvice + @ExceptionHandler 负责按异常类型接住并转成统一 JSON。这是 Spring 的全局异常处理机制,不是 Java 语言本身的「自动跳转」。

若同时存在多个 @ExceptionHandler,Spring 会优先匹配最具体的子类。例如 MethodArgumentNotValidException 也是 RuntimeException 的子类,但校验失败会走 handleValidationException,而不是 handleRuntimeException

上篇讲过 @Valid 触发 DTO 字段校验。校验失败时,Spring 不会进 Service,而是直接抛 MethodArgumentNotValidException

POST /api/auth/register  { "username": "", "password": "123" }
        ↓
@Valid 检查 RegisterRequest
        ↓
username 违反 @NotBlank → 校验失败
        ↓
抛出 MethodArgumentNotValidException(不会进入 AuthService)
        ↓
GlobalExceptionHandler.handleValidationException()
        ↓
收集所有字段错误信息,用 "; " 拼接
        ↓
HTTP 400 + JSON

处理代码:

String message = e.getBindingResult().getFieldErrors().stream()
        .map(FieldError::getDefaultMessage)   // 取 @NotBlank(message="...") 里的 message
        .collect(Collectors.joining("; "));   // 多个错误用分号连接
return ApiResponse.error(400, message);

示例响应:

{
  "code": 400,
  "message": "用户名不能为空; 昵称不能为空",
  "data": null
}

链路 C:未登录访问(AuthenticationEntryPoint

访问需要登录的接口(如 GET /api/users/me),但没带 token 或 token 无效时:

GET /api/users/me
(无 Authorization 头,或 Bearer token 已过期)
        ↓
JwtAuthenticationFilter 尝试解析 token → 失败或为空
        ↓
SecurityContext 中没有有效认证信息
        ↓
authorizeHttpRequests 检查:/api/users/** 需要 authenticated()
        ↓
判定为「未认证」→ 请求不会进入 UserController
        ↓
Spring Security 调用 JwtAuthenticationEntryPoint.commence()
        ↓
直接往 HttpServletResponse 写 JSON(不是 throw 异常)
        ↓
HTTP 401 + JSON

SecurityConfig 中的配置:

.exceptionHandling(ex -> ex.authenticationEntryPoint(jwtAuthenticationEntryPoint))
写法含义
AuthenticationEntryPointSpring Security 定义的接口(契约)
JwtAuthenticationEntryPoint项目里的实现类implements AuthenticationEntryPoint
.authenticationEntryPoint(jwtAuthenticationEntryPoint)配置方法:把实现类注册给 Security,未登录时调用它的 commence()

实现代码:

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        objectMapper.writeValue(response.getOutputStream(),
                ApiResponse.error(401, "未登录或登录已过期"));
    }
}

前端收到的响应:

{
  "code": 401,
  "message": "未登录或登录已过期",
  "data": null
}

为什么不会进 GlobalExceptionHandler

HTTP 请求
    ↓
Spring Security 过滤器链(JwtAuthenticationFilter 等)  ← 链路 C 在这里结束
    ↓
DispatcherServlet
    ↓
Controller / Service                                      ← 链路 AB 在这里处理
    ↓
GlobalExceptionHandler
组件作用范围
JwtAuthenticationEntryPointSecurity 过滤器链,请求还没进 Controller
GlobalExceptionHandlerSpring MVC Controller 层,请求已通过 Security 之后

commence()直接写 response,不是 throw 异常,所以 @RestControllerAdvice 完全拦截不到。

和「已登录但没权限」的区别

情况处理组件典型状态码
未登录(没 token / token 无效)AuthenticationEntryPoint401
已登录但权限不够AccessDeniedHandler(本项目未单独配置)403

authenticationEntryPoint 只管第一种:「你是谁?」还没搞清楚

相关配置:STATELESS 会话策略

.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

表示 Spring Security 不使用 HttpSession 保存登录状态。本项目用 JWT,每次请求由 JwtAuthenticationFilter 解析 token 写入 SecurityContext,请求结束后上下文清理,下次请求需再次携带 token。这与 AuthenticationEntryPoint 配合,构成完整的无状态 API 认证体系。

三条链路对比

业务异常参数校验失败未登录访问
异常类型RuntimeExceptionMethodArgumentNotValidException无(Security 直接写 response)
触发位置Service 里 throwController 入参 @ValidSecurity 过滤器链
是否进入 Controller是(抛异常前可能已执行部分逻辑)
message 来源e.getMessage()DTO 注解里的 message = "..."固定文案 "未登录或登录已过期"
处理方法handleRuntimeExceptionhandleValidationExceptionJwtAuthenticationEntryPoint.commence()
HTTP 状态码400400401
是否经过 GlobalExceptionHandler

五、ApiResponse:统一响应格式

异常处理和正常接口共用同一个响应结构:

public class ApiResponse<T> {
    private int code;       // 业务状态码
    private String message;
    private T data;

    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "success", data);
    }

    public static <T> ApiResponse<T> error(int code, String message) {
        return new ApiResponse<>(code, message, null);
    }
}
场景codemessagedata
成功200"success" 或自定义实际数据
业务错误400"用户名已存在"null
校验失败400"评论内容不能为空"null
未登录401"未登录或登录已过期"null

前端只需判断 code === 200 即成功,否则读 message 提示用户。


六、为什么用 RuntimeException 而不是自定义异常?

当前项目是简化写法:所有业务错误统一抛 RuntimeException,靠 message 字符串区分。

更规范的做法是自定义异常:

public class BusinessException extends RuntimeException {
    private final int code;
    // ...
}

对本项目而言,RuntimeException + 全局处理器已经足够。要点是:不要在 Controller 里 try-catch,让异常自然抛到 GlobalExceptionHandler


七、JPA 实体:Java 类 ↔ 数据库表

整体关系

@Entity 类(Java)          MySQL 表
─────────────────         ─────────────
Article.java        →     article 表
User.java           →     users 表
Comment.java        →     comment 表

JPA(Java Persistence API)负责:把 Java 对象映射成数据库表,把字段读写转换成 SQL

配置文件 application.yml

spring:
  jpa:
    hibernate:
      ddl-auto: update    # 启动时自动建表/更新表结构
    show-sql: true        # 控制台打印 SQL

ddl-auto: update 表示:实体类改了,启动时自动 ALTER TABLE,开发阶段很方便(生产环境建议改为 validate 或用手动迁移)。


八、实体类上的注解

Article 为例:

image.png

@Data
@Entity
@Table(name = "article")
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String title;

    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;

    @Column(length = 500)
    private String summary;

    @Column(nullable = false, length = 50)
    private String author;

    private Long userId;

    @CreationTimestamp
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @UpdateTimestamp
    @Column(nullable = false)
    private LocalDateTime updatedAt;
}

类级别注解

注解作用示例
@Entity标记这是数据库实体,JPA 会管理它必须有,否则不映射
@Table(name = "article")指定表名不写则默认用类名 Article
@DataLombok:自动生成 getter/setter不是 JPA 注解

User 表名用 users 而不是 user,因为 user 在 MySQL 中是保留字:

@Table(name = "users")
public class User { ... }

九、@Id@GeneratedValue:主键怎么来

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
注解含义
@Id这是主键字段
@GeneratedValue主键值自动生成,不用自己设
strategy = GenerationType.IDENTITY策略:用数据库自增(MySQL 的 AUTO_INCREMENT

strategy 是什么意思?

strategy@GeneratedValue参数名,意思是「用哪种策略生成主键」。

@GeneratedValue(strategy = GenerationType.IDENTITY)   // 数据库自增
@GeneratedValue(strategy = GenerationType.SEQUENCE)     // 数据库序列(PostgreSQL)
@GeneratedValue(strategy = GenerationType.AUTO)        // 让 JPA 自动选择

本项目用 MySQL,选 IDENTITY 最合适。

插入时的流程

Article article = new Article();
article.setTitle("我的第一篇文章");
// 此时 article.getId() == null,不用管 id

articleRepository.save(article);
// 数据库执行:INSERT INTO article (title, ...) VALUES (...)
// 数据库自动生成 id = 1
// JPA 把 id 回填:article.getId() == 1

等价 SQL:

CREATE TABLE article (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,  -- @Id + @GeneratedValue(IDENTITY)
    ...
);

几种主键策略对比

策略谁生成 id典型数据库
IDENTITY数据库插入时自增MySQL、SQL Server
SEQUENCE数据库序列 nextvalPostgreSQL、Oracle
AUTOJPA 按数据库自动选通用
TABLE单独一张表维护 id较少用

十、@Column:列的类型与约束

@Column 描述普通字段对应数据库列的规则(主键一般用 @Id + @GeneratedValue,不必再加 @Column)。

@Column(nullable = false, length = 200)
private String title;
属性含义对应 SQL
nullable = false不允许为空NOT NULL
length = 200字符串最大长度VARCHAR(200)
unique = true值必须唯一UNIQUE
columnDefinition = "TEXT"自定义列类型TEXT
updatable = false插入后不允许更新更新 SQL 不包含该列

项目中的实际用法

// User.java — 登录名唯一、最长 50
@Column(nullable = false, unique = true, length = 50)
private String username;

// Article.java — 正文用大文本类型
@Column(nullable = false, columnDefinition = "TEXT")
private String content;

// Article.java — 创建时间:必填、不可更新
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;

不写 @Column

private Long userId;   // 无 @Column

JPA 使用默认规则:字段名 userId → 列名 user_id(驼峰转下划线),类型按 Java 类型推断。

@Column@GeneratedValue 的区别

@Column@GeneratedValue
管什么列的类型、长度、是否必填主键 id 怎么自动生成
用在哪普通字段(也可用于主键,但少见)@Id 主键
类比 SQLVARCHAR(200) NOT NULLAUTO_INCREMENT

十一、时间字段:@CreationTimestamp@UpdateTimestamp

@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;

@UpdateTimestamp
@Column(nullable = false)
private LocalDateTime updatedAt;
注解作用
@CreationTimestamp插入时自动填入当前时间
@UpdateTimestamp每次更新时自动刷新为当前时间

代码里不用手动 setCreatedAt(new Date()),保存时 Hibernate 自动处理。

updatable = false 保证 createdAt 创建后不会被改掉。


十二、三张表对照

users 表(User.java

@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false, unique = true, length = 50)
    private String username;
    @Column(nullable = false)
    private String password;      // BCrypt 密文
    @Column(nullable = false, length = 50)
    private String nickname;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

article 表(Article.java

字段类型说明
idLong 自增主键
titleVARCHAR(200) NOT NULL标题
contentTEXT NOT NULL正文
summaryVARCHAR(500)摘要,可空
authorVARCHAR(50) NOT NULL作者昵称(冗余展示)
userIdBIGINT作者用户 ID
createdAt / updatedAtDATETIME NOT NULL创建/更新时间

comment 表(Comment.java

@Entity
@Table(name = "comment")
public class Comment {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private Long articleId;       // 属于哪篇文章
    private Long parentId;        // 父评论 ID,顶级评论为 null
    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;
    @Column(nullable = false, length = 50)
    private String author;
    private LocalDateTime createdAt;
}

十三、Entity 与 DTO 为什么要分开?

上篇讲过 CommentRequestComment 实体的区别,这里从数据库角度补充:

Entity(实体)DTO(请求/响应对象)
包路径com.blog.entitycom.blog.dto
注解@Entity @Column 等 JPA 注解@NotBlank @Size 等校验注解
用途映射数据库表接收/返回 API 数据
id有(数据库自增)一般没有
passwordUserUserResponse 没有(安全)

原则:数据库结构归 Entity,API 入参归 DTO,不要混用。


十四、Repository:怎么操作数据库

public interface ArticleRepository extends JpaRepository<Article, Long> {
}

JpaRepository<Article, Long> 中:

  • Article:操作的实体类型
  • Long:主键类型(对应 @Id private Long id

继承后自动拥有常用方法,无需写 SQL:

方法作用
save(article)新增或更新
findById(id)按主键查询
findAll()查全部
deleteById(id)按主键删除
existsById(id)判断是否存在

UserRepository 还支持按方法名自动生成查询:

Optional<User> findByUsername(String username);   // SELECT * FROM users WHERE username = ?
boolean existsByUsername(String username);        // SELECT COUNT(*) > 0 ...

CommentRepository

List<Comment> findByArticleIdOrderByCreatedAtAsc(Long articleId);
// SELECT * FROM comment WHERE article_id = ? ORDER BY created_at ASC

方法名即查询:findBy + 字段名 + OrderBy + 排序字段 + Asc/Desc


十五、一次完整的「发表评论」数据流

把本篇和上篇串起来:

1. POST /api/articles/5/comments
   Body: { "content": "不错" }
   Header: Authorization: Bearer <token>

2. JwtAuthenticationFilter 解析 token → 当前用户写入 SecurityContext
   → 若无 token 或 token 无效,且该接口需登录
   → JwtAuthenticationEntryPoint 返回 401(不进 CommentController)

3. Controller: @PathVariable articleId=5, @Valid @RequestBody CommentRequest
   → 若 content 为空 → MethodArgumentNotValidException → GlobalExceptionHandler → 400

4. CommentService.create(5, request)
   → articleRepository.existsById(5) == false
   → throw new RuntimeException("文章不存在")
   → GlobalExceptionHandler → 400 "文章不存在"

5. 校验通过:
   Comment comment = new Comment();
   comment.setArticleId(5);
   comment.setContent("不错");
   comment.setAuthor(当前用户昵称);
   commentRepository.save(comment);
   → INSERT INTO comment (...) VALUES (...)
   → id 由数据库 AUTO_INCREMENT 生成
   → createdAt 由 @CreationTimestamp 自动填入

6. 返回 ApiResponse.success("评论成功", comment)

十六、常见问题

Q1:throw new RuntimeExceptionreturn ApiResponse.error 有什么区别?

  • throw:在 Service 里表示「出错了,中断执行」,由 GlobalExceptionHandler 统一处理
  • return ApiResponse.error:在 Handler 里构造最终 JSON

Service 层只负责 throw,不要自己写 return error

Q1.1:为什么 throw new RuntimeException 能触发 handleRuntimeException

不是 throw 直接调用了那个方法,而是 Spring 在异常向上冒泡时,根据 @ExceptionHandler(RuntimeException.class) 按类型匹配后自动调用。详见上文 「重要:throw 并不是直接调用 handleRuntimeException 一节。

Q2:@Valid 校验失败会走 handleRuntimeException 吗?

不会。@Valid 在 Controller 入参绑定阶段就失败了,直接走 handleValidationException

Q2.1:未登录访问会进 GlobalExceptionHandler 吗?

不会。 未登录时 Spring Security 在过滤器链阶段就拦截了,调用 JwtAuthenticationEntryPoint.commence() 直接写 401 JSON,请求不会到达 Controller,因此 @RestControllerAdvice 不会介入。详见上文 链路 C

Q2.2:AuthenticationEntryPoint 接口和 .authenticationEntryPoint(...) 配置方法是同一个东西吗?

不是,但相关:

  • AuthenticationEntryPoint:Spring Security 定义的接口
  • JwtAuthenticationEntryPoint:实现该接口的具体类
  • .authenticationEntryPoint(jwtAuthenticationEntryPoint)HttpSecurity配置方法,把实现类注册进去

配置方法的参数类型就是 AuthenticationEntryPoint 接口,传入的是你的实现实例。

Q3:strategy = GenerationType.IDENTITY@Column 能写在同一个字段上吗?

主键字段一般写:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

不需要再加 @Column@GeneratedValue 管 id 怎么生成,@Column 管列约束,职责不同。

Q4:为什么 saveid 从 null 变成有值?

IDENTITY 策略下,数据库执行 INSERT 后生成自增 id,JPA 会把这个值回填到 Java 对象的 id 字段。

Q5:生产环境 ddl-auto: update 安全吗?

开发阶段方便,但生产环境建议改为 validate(只校验不修改)或使用 Flyway/Liquibase 做版本化迁移,避免实体改动意外改表结构。


十七、本篇小结

主题关键知识点
全局异常@RestControllerAdvice + @ExceptionHandler + @ResponseStatus
业务错误Service throw new RuntimeException("消息") → Handler 转 ApiResponse.error
参数校验@Valid 失败 → MethodArgumentNotValidException → 拼接字段 message
未登录Security 过滤器链 → AuthenticationEntryPoint → 401 JSON,不进 GlobalExceptionHandler
无状态认证SessionCreationPolicy.STATELESS + JWT,不用 HttpSession
实体映射@Entity @Table 类对应表
主键@Id + @GeneratedValue(strategy = IDENTITY) = 数据库自增
列约束@Column(nullable, length, unique, columnDefinition)
时间@CreationTimestamp / @UpdateTimestamp 自动维护
数据访问JpaRepository 继承即得 CRUD,方法名可自动生成查询