@NotNull,@NotBlank使用及拦截配置

引入依赖

   <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.2.5.Final</version>
        </dependency>

在实体类中加入注解

 对应的参数使用类型

字符串使用@NotBlank

对象型基本类型是 @NotNull,数组是使用NotNull

集合型@NotEmpty

 在controller层加上@Valid开启效验

 开启返回前端的参数

这是一个最基本的普通返回做测试使用,实际项目中肯定是不行的,要有封装返回类,可以将String改成公司的封装返回类,

package com.sdk.controller.controller.qurzy;

import lombok.extern.slf4j.Slf4j;
import org.apache.poi.util.StringUtil;
import org.hibernate.validator.internal.engine.path.PathImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.Servlet;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.Set;

/**
 * 替换掉blade框架RestExceptionTranslator的异常处理,不显示参数名称返回前端
 *
 * 全局异常处理
 * @author WQJ
 */
@Order(Integer.MIN_VALUE)
@Configuration
@ConditionalOnWebApplication(
        type = ConditionalOnWebApplication.Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    public GlobalExceptionHandler() {
        System.out.println("GlobalExceptionHandler init");
    }

    @ExceptionHandler({MissingServletRequestParameterException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleError(MissingServletRequestParameterException e) {
        log.info("GlobalExceptionHandler拦截器");
        String message = String.format("缺少必要的请求参数: %s", e.getParameterName());
        return message;
    }

    @ExceptionHandler({MethodArgumentTypeMismatchException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleError(MethodArgumentTypeMismatchException e) {
        log.info("GlobalExceptionHandler");
        String message = String.format("请求参数格式错误: %s", e.getName());
        return message;
    }

    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleError(MethodArgumentNotValidException e) {
        log.info("GlobalExceptionHandler");
        return this.handleError(e.getBindingResult());
    }

    @ExceptionHandler({BindException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleError(BindException e) {
        log.info("GlobalExceptionHandler");
        return this.handleError(e.getBindingResult());
    }

    private String handleError(BindingResult result) {
        FieldError error = result.getFieldError();
//        String message = String.format("%s:%s", error.getField(), error.getDefaultMessage());
        String message = String.format("%s", error.getDefaultMessage());
        return  message;
    }

    @ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleError(ConstraintViolationException e) {
        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
        ConstraintViolation<?> violation = (ConstraintViolation)violations.iterator().next();
        String path = ((PathImpl)violation.getPropertyPath()).getLeafNode().getName();
        String message = String.format("%s:%s", path, violation.getMessage());
        return  message;
    }

    @ExceptionHandler({NoHandlerFoundException.class})
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String handleError(NoHandlerFoundException e) {
        log.info("GlobalExceptionHandler");
        return e.getMessage();
    }

    @ExceptionHandler({HttpMessageNotReadableException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleError(HttpMessageNotReadableException e) {
        log.info("GlobalExceptionHandler");
        return e.getMessage();
    }

    @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    public String handleError(HttpRequestMethodNotSupportedException e) {
        log.info("GlobalExceptionHandler");
        return e.getMessage();
    }

    @ExceptionHandler({HttpMediaTypeNotSupportedException.class})
    @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
    public String handleError(HttpMediaTypeNotSupportedException e) {
        log.info("GlobalExceptionHandler");
        return  e.getMessage();
    }

    @ExceptionHandler({HttpMediaTypeNotAcceptableException.class})
    @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
    public String handleError(HttpMediaTypeNotAcceptableException e) {
        log.info("GlobalExceptionHandler");

        return e.getMessage();
    }
}

<think>好的,我现在需要处理用户的问题:使用Spring Boot和MyBatis-Plus创建一个能够处理多线程并发请求、防止重复数据的生产级接口。用户还提供了一个JSON示例,我需要根据这个结构来设计接口。 首先,我得理清用户的需求。用户希望一个POST接口,接收给定的JSON格式数据,处理时需要考虑高并发和重复数据的问题。作为生产环境代码,必须考虑线程安全、数据一致性和性能优化。 接下来,分析可能的问题点。多线程环境下,重复数据提交是一个关键点。如何防止重复?通常可以采用数据库唯一索引、分布式锁或者业务层校验。这里需要结合MyBatis-Plus的特性来实现。 首先设计数据表结构。根据JSON结构,主表是展览信息,从表是门票列表。需要将这两个实体映射到数据库表中。主表可能有exhibition_id作为主键,从表的ticket_id为主键,同时需要确保同一card_no在同一个exhibition下唯一,避免重复插入。因此,在从表中设置唯一索引(exhibition_id, card_no)是必要的。 然后是实体类的设计。使用MyBatis-Plus需要定义对应的实体类,主表Exhibition和从表Ticket。注意使用注解如@TableId指定主键,@TableField映射字段名。 接下来是DAO层,即Mapper接口。继承MyBatis-Plus的BaseMapper,获得基本的CRUD操作。对于批量插入门票,可能需要自定义方法,使用SQL的INSERT IGNORE或ON DUPLICATE KEY UPDATE来处理重复数据。 服务层的设计需要考虑事务管理和并发控制。处理主表和从表的数据插入需要放在一个事务中,保证原子性。对于高并发,可以使用分布式锁,比如Redis的Redisson来实现,防止多个线程同时处理同一请求导致数据重复。同时,使用@Transactional注解确保事务的完整性。 控制层接收JSON请求,将其映射到DTO对象。这里需要定义两个DTO:ExhibitionRequestDTO和TicketDTO,结构对应JSON的层次。使用@RequestBody接收POST请求的数据。 在插入门票数据时,需要注意批量操作的效率。MyBatis-Plus的saveBatch方法默认是逐条插入,这在生产环境中效率不高。需要配置批量插入的SQL会话,启用批处理模式,比如在配置文件中设置jdbc-url添加rewriteBatchedStatements=true,并在MyBatisPlusConfig中配置性能分析插件,设置批量操作的参数。 防止重复数据的策略包括数据库唯一索引和业务逻辑校验。唯一索引是最可靠的,即使应用层有漏洞,数据库层也能拦截重复数据。业务层可以在插入前查询是否存在,但高并发下可能失效,因此需要结合分布式锁。例如,在插入前获取一个基于exhibitionId和cardNo的锁,确保同一时间只有一个线程处理相同的数据。 异常处理方面,需要捕获唯一约束冲突的异常,比如DuplicateKeyException,并返回友好的错误信息,而不是让服务器抛出500错误。同时,事务管理要确保在发生异常时回滚,避免脏数据。 测试方面,需要考虑使用JMeter或Postman进行压力测试,模拟多线程请求,验证接口的并发处理能力和重复数据拦截效果。确保在高并发下,系统响应时间和资源消耗在可接受范围内。 最后,代码的健壮性和可维护性需要考虑日志记录、参数校验、全局异常处理等。例如,使用@Valid对DTO进行校验,确保传入参数符合要求,减少非法数据的处理。 可能遇到的问题包括:MyBatis-Plus批量插入的性能问题,需要正确配置;分布式锁的实现需要考虑锁的粒度和超时时间,避免死锁;事务的传播机制和隔离级别需要合理设置,防止脏读或不可重复读。 总结步骤:设计数据库表和唯一索引→创建实体类和DTO→编写Mapper接口和自定义批量插入方法→实现服务层,添加分布式锁和事务→配置MyBatis-Plus批量处理→编写控制器→异常处理和日志→测试验证。</think>我将基于Spring Boot + MyBatis-Plus实现一个生产级的高并发接口,包含以下核心设计: 1. 数据库设计(MySQL示例): ```sql -- 主表 CREATE TABLE exhibition_info ( id BIGINT PRIMARY KEY COMMENT '主键ID', exhibition_id VARCHAR(32) NOT NULL COMMENT '展览ID', exhibition_id_type CHAR(1) NOT NULL COMMENT '展览ID类型', source_from VARCHAR(32) NOT NULL COMMENT '来源', UNIQUE KEY idx_exhibition_id (exhibition_id) ); -- 门票表 CREATE TABLE ticket_info ( ticket_id BIGINT PRIMARY KEY COMMENT '主键ID', exhibition_id VARCHAR(32) NOT NULL COMMENT '展览ID', card_no VARCHAR(64) NOT NULL COMMENT '卡号', face_status CHAR(1) NOT NULL COMMENT '人脸状态', id_number VARCHAR(32) NOT NULL COMMENT '身份证号', name VARCHAR(64) NOT NULL COMMENT '姓名', number_type CHAR(1) NOT NULL COMMENT '号码类型', ticket_status CHAR(1) NOT NULL COMMENT '门票状态', ticket_type_name VARCHAR(32) NOT NULL COMMENT '票种名称', UNIQUE KEY idx_exhibition_card (exhibition_id, card_no) ); ``` 2. 实体类: ```java // ExhibitionInfo.java @Data @TableName("exhibition_info") public class ExhibitionInfo { @TableId(value = "id", type = IdType.ASSIGN_ID) private Long id; private String exhibitionId; private String exhibitionIdType; private String sourceFrom; } // TicketInfo.java @Data @TableName("ticket_info") public class TicketInfo { @TableId(value = "ticket_id", type = IdType.ASSIGN_ID) private Long ticketId; private String exhibitionId; private String cardNo; private String faceStatus; private String idNumber; private String name; private String numberType; private String ticketStatus; private String ticketTypeName; } ``` 3. Mapper接口: ```java public interface TicketMapper extends BaseMapper<TicketInfo> { @Insert("<script>" + "INSERT IGNORE INTO ticket_info " + "(exhibition_id, card_no, face_status, id_number, name, number_type, ticket_status, ticket_type_name) " + "VALUES " + "<foreach collection='list' item='item' separator=','>" + "(#{item.exhibitionId}, #{item.cardNo}, #{item.faceStatus}, #{item.idNumber}, " + "#{item.name}, #{item.numberType}, #{item.ticketStatus}, #{item.ticketTypeName})" + "</foreach>" + "</script>") int batchInsertIgnore(@Param("list") List<TicketInfo> list); } ``` 4. Service层: ```java @Service @RequiredArgsConstructor public class ExhibitionService { private final RedissonClient redissonClient; private final ExhibitionInfoMapper exhibitionInfoMapper; private final TicketMapper ticketMapper; @Transactional(rollbackFor = Exception.class) public void processExhibition(ExhibitionRequestDTO dto) { // 处理主表 ExhibitionInfo exhibition = new ExhibitionInfo(); BeanUtils.copyProperties(dto, exhibition); exhibitionInfoMapper.insertOrUpdate(exhibition); // 分布式锁防止重复提交 RLock lock = redissonClient.getLock("EXHIBITION_LOCK:" + dto.getExhibitionId()); try { if (lock.tryLock(3, 5, TimeUnit.SECONDS)) { // 处理票务数据 List<TicketInfo> tickets = dto.getTicketList().stream().map(t -> { TicketInfo info = new TicketInfo(); BeanUtils.copyProperties(t, info); info.setExhibitionId(dto.getExhibitionId()); return info; }).collect(Collectors.toList()); // 批量插入(忽略重复) ticketMapper.batchInsertIgnore(tickets); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("获取锁失败", e); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } } ``` 5. Controller层: ```java @RestController @RequiredArgsConstructor public class ExhibitionController { private final ExhibitionService exhibitionService; @PostMapping("/api/exhibition") public ResponseEntity<?> createExhibition(@Valid @RequestBody ExhibitionRequestDTO dto) { try { exhibitionService.processExhibition(dto); return ResponseEntity.ok().build(); } catch (DuplicateKeyException e) { return ResponseEntity.status(HttpStatus.CONFLICT) .body(Result.error("数据重复提交")); } } } // DTO类 @Data public class ExhibitionRequestDTO { @NotBlank private String exhibitionId; @NotBlank @Pattern(regexp = "[0-1]") private String exhibitionIdType; @NotBlank private String sourceFrom; @Valid @NotEmpty private List<TicketDTO> ticketList; } @Data public class TicketDTO { @NotBlank private String cardNo; @NotBlank @Pattern(regexp = "[0-1]") private String faceStatus; @NotBlank @Size(min = 18, max = 18) private String idNumber; @NotBlank private String name; @NotBlank @Pattern(regexp = "[0-1]") private String numberType; @NotBlank @Pattern(regexp = "[0-1]") private String ticketStatus; @NotBlank private String ticketTypeName; } ``` 生产级保障措施: 1. 并发控制: - 使用Redisson分布式锁保证同一展览ID的请求串行处理 - 数据库唯一索引防止数据重复(exhibition_id + card_no组合唯一) - 批量插入使用INSERT IGNORE避免重复 2. 性能优化: - MyBatis-Plus开启批量操作模式(配置文件中添加:`mybatis-plus.configuration.default-executor-type=batch`) - MySQL连接池配置HikariCP: ```yaml spring: datasource: hikari: maximum-pool-size: 20 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 ``` 3. 异常处理: - 全局异常处理器捕获DuplicateKeyException - 事务管理保证数据一致性 4. 安全校验: - 使用@Valid进行参数校验 - 身份证号正则校验 - 状态字段枚举值校验 测试建议: ```java // 测试用例 @SpringBootTest class ExhibitionServiceTest { @Autowired private ExhibitionService service; @Test void testConcurrentInsert() throws InterruptedException { int threadCount = 100; ExecutorService executor = Executors.newFixedThreadPool(threadCount); CountDownLatch latch = newCountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { executor.execute(() -> { try { ExhibitionRequestDTO dto = buildTestDTO(); service.processExhibition(dto); } finally { latch.countDown(); } }); } latch.await(); // 验证数据库实际插入数量 } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值