COLA + Dubbo 3 + JPA 服务接口,校验异常处理

COLA + Dubbo 3 + JPA 服务接口,校验异常处理

Demo

技术要点

Dubbo protostuff 序列化,JPA 审计/自定义主键/Json字段处理
Service validation 校验, 可乐异常,BCrypt加盐,QueryDSL/blazebit 整合
Hutool BeanUtil 拷贝/DesensitizedUtil 脱敏/RegexPool 正则池/IdUtil 雪花id

领域分层

根据可乐整洁架构分为,app/client/common/domain/infra

代码展示

app

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final String DEFAULT_ERR_CODE = "BIZ_ERROR";

    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public Response methodNotAllowed() {
        return SingleResponse.buildFailure(HttpStatus.METHOD_NOT_ALLOWED.value() + "", HttpStatus.METHOD_NOT_ALLOWED.getReasonPhrase());
    }

    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(NoHandlerFoundException.class)
    public Response notFound() {
        return SingleResponse.buildFailure(HttpStatus.NOT_FOUND.value() + "", HttpStatus.NOT_FOUND.getReasonPhrase());
    }

    @ExceptionHandler(HttpMessageNotReadableException.class)
    public Response httpMessageNotReadableException(HttpMessageNotReadableException exception) {
        String stackTraceAsString = Throwables.getStackTraceAsString(exception);
        log.error("InvalidFormatException: " + stackTraceAsString);
        return SingleResponse.buildFailure(DEFAULT_ERR_CODE, exception.getMessage());
    }

    @ExceptionHandler(MissingServletRequestParameterException.class)
    public Response missingServletRequestParameterException(MissingServletRequestParameterException exception) {
        String stackTraceAsString = Throwables.getStackTraceAsString(exception);
        log.error("InvalidFormatException: " + stackTraceAsString);
        return SingleResponse.buildFailure(DEFAULT_ERR_CODE, exception.getMessage());
    }

    @ExceptionHandler(BizException.class)
    public Response bizException(BizException e) {
        log.error("业务异常:", e);
        return SingleResponse.buildFailure(e.getErrCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Response exception(Exception e) {
        log.error("总异常:", e);
        return SingleResponse.buildFailure(DEFAULT_ERR_CODE, e.getMessage());
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public Response validationException(ConstraintViolationException e) {
        log.warn("Service 校验异常:", e);
        String errMessage = e.getConstraintViolations().stream().findFirst().map(ConstraintViolation::getMessage).get();
        return SingleResponse.buildFailure(DEFAULT_ERR_CODE, errMessage);
    }

    @ExceptionHandler(BindException.class)
    public Response bindException(BindException e) {
        log.warn("Controller 校验异常:", e);
        String errMessage = e.getBindingResult().getAllErrors().stream().findFirst().map(DefaultMessageSourceResolvable::getDefaultMessage).get();
        return SingleResponse.buildFailure(DEFAULT_ERR_CODE, errMessage);
    }
}
@Slf4j
@RestController
public class AccountController {

    @Setter(onMethod_ = {@DubboReference(timeout = 300000, loadbalance = LoadbalanceRules.RANDOM)})
    private AccountService accountService;

    @PostMapping("/addAccount")
    public Response addAccount(@RequestBody AccountAddCmd accountAddCmd) {
        Account account = accountService.addAccount(accountAddCmd);
        return SingleResponse.of(account);
    }

    @PostMapping("/removeAccountById")
    public Response removeAccountById(@RequestParam Long id) {
        accountService.removeAccountById(id);
        return SingleResponse.buildSuccess();
    }

    @PostMapping("/accountLogin")
    public Response accountLogin(@RequestBody AccountLoginQry accountLoginQry) {
        Account account = accountService.accountLogin(accountLoginQry);
        account.setMobile(DesensitizedUtil.mobilePhone(account.getMobile()));
        return SingleResponse.of(account);
    }

    @GetMapping("/accountList")
    public Response accountList() {
        List<Map<Expression<?>, ?>> maps = accountService.accountList();
        return SingleResponse.of(maps);
    }
}
server.port: 80
spring.application.name: demo-app

---
spring.jackson:
  date-format: 'yyyy-MM-dd HH:mm:ss'
  time-zone: 'Asia/Shanghai'
  default-property-inclusion: NON_NULL
  serialization:
    fail-on-empty-beans: false

---
dubbo.application.name: ${spring.application.name}
dubbo.consumer.check: false
dubbo.registry.address: nacos://console.nacos.io:8848

---
spring.cloud.nacos.discovery.server-addr: console.nacos.io:8848
spring.cloud.nacos.discovery.username: nacos
spring.cloud.nacos.discovery.password: nacos

---
knife4j:
  enable: true
  openapi:
    title: Demo 展示
    description: "`我是测试`,**你知道吗**"
    email: uid13@qq.com
    concat: Jazz
    url: https://docs.lab.com
    version: v4.0
    license: Apache 2.0
    license-url: https://lab.com/
    terms-of-service-url: https://lab.com/
    group:
      test1:
        group-name: 分组名称
        api-rule: package
        api-rule-resources:
          - com.lab
dependencies {
    implementation(project(":demo-client"))

    implementation("com.querydsl:querydsl-core")
    implementation 'org.springframework.boot:spring-boot-starter-web'

    implementation "org.apache.dubbo:dubbo-spring-boot-starter:${dubboVersion}"
    implementation "org.apache.dubbo:dubbo-rpc-triple:${dubboVersion}"
    implementation "org.apache.dubbo:dubbo-registry-nacos:${dubboVersion}"
    implementation 'org.apache.dubbo:dubbo-serialization-protostuff:2.7.22'

    implementation "com.alibaba.nacos:nacos-client:${nacosVersion}"
    implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery'
    implementation "org.springframework.cloud:spring-cloud-starter-loadbalancer"

    implementation "com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel"
    implementation 'com.alibaba.csp:sentinel-datasource-nacos'

    implementation 'com.github.xiaoymin:knife4j-openapi2-spring-boot-starter:4.1.0'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

client

@EqualsAndHashCode(callSuper = false)
@Data
@FieldNameConstants
public class AccountAddCmd extends Command {
    private Long id;

    @NotBlank(message = "用户名不能为空")
    @Size(min = 4, max = 23, message = "用户名长度为 4-23位")
    @Pattern(regexp = RegexPool.WORD, message = "用户名格式不对")
    private String userName;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 12, message = "密码为 6到12位")
    private String password;

    @Pattern(regexp = RegexPool.MOBILE, message = "手机号格式不对")
    private String mobile;

    @PositiveOrZero(message = "金额必须大于等于0")
    private Long money;

    private BasicInfo info;
}
@Data
@NoArgsConstructor
public class AccountLoginQry extends Query {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 4, max = 23, message = "用户名长度为 4-23位")
    @Pattern(regexp = RegexPool.WORD, message = "用户名格式不对")
    private String userName;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 12, message = "密码为 6到12位")
    private String password;

    // TODO 验证码
}
@Validated
public interface AccountService {
    List<Map<Expression<?>, ?>> accountList();

    Account addAccount(@Valid AccountAddCmd accountAddCmd) throws BizException;

    boolean removeAccountById(@NotNull(message = "id不能为空") @Positive(message = "id必须为正数") Long id) throws BizException;

    Account accountLogin(@Valid AccountLoginQry accountLoginQry) throws BizException;
}
dependencies {
    api(project(":demo-domain"))
    api(project(":demo-common"))
}

common

public class MyGenerator implements IdentifierGenerator {
    public static final String MY_GENERATOR = "com.lab.common.domain.MyGenerator";

    @Override
    public Serializable generate(SharedSessionContractImplementor session, Object obj) throws HibernateException {
        return IdUtil.getSnowflakeNextId();
    }
}
@Data
public class DateSearchQry extends Query {
    @ApiModelProperty("创建日期-开始")
    private LocalDateTime startCreateTime;

    @ApiModelProperty("创建日期-结束")
    private LocalDateTime endCreateTime;
}
dependencies {
    implementation 'ch.qos.logback:logback-classic'
    implementation 'org.springframework.data:spring-data-commons'
    implementation 'jakarta.persistence:jakarta.persistence-api'
    implementation 'org.hibernate:hibernate-core'
}

domain

@Entity
@Table(name = "tbl_account",
        uniqueConstraints = {
                @UniqueConstraint(columnNames = {Account.Fields.mobile}, name = "uk_mobile"),
                @UniqueConstraint(columnNames = {Account.Fields.userName}, name = "uk_user_name"),
        },
        indexes = {
                @Index(columnList = Account.Fields.deleted, name = "idx_deleted"),
        }
)
@DynamicUpdate
@DynamicInsert
@Data
@NoArgsConstructor
@TypeDef(name = "json", typeClass = JsonType.class)
@FieldNameConstants
@Accessors(chain = true)
@EntityListeners(AuditingEntityListener.class)
public class Account /*implements Serializable*/ extends BaseTimePojo {
    @Id
    @GeneratedValue(generator = IdentifierGenerator.GENERATOR_NAME)
    @GenericGenerator(name = IdentifierGenerator.GENERATOR_NAME, strategy = MyGenerator.MY_GENERATOR)
    private Long id;
    private String userName;
    private String password;
    private String mobile;
    private Long money;

    @Column(name = Fields.info, nullable = false, columnDefinition = "json COMMENT '信息'")
    @Type(type = "json")
    private BasicInfo info;

    @Column(nullable = false, columnDefinition = "bit(1) default 0 comment '逻辑删除'")
    private boolean deleted;
}
@Data
@MappedSuperclass
public class BaseTimePojo {
    @CreatedDate
    public LocalDateTime createTime;

    @LastModifiedDate
    public LocalDateTime updateTime;
}
@Data
@NoArgsConstructor
@FieldNameConstants
public class BasicInfo {
    private String province;
    private String city;
    private String address;
}
dependencies {
    implementation(project(":demo-common"))

    api 'com.querydsl:querydsl-jpa'

    annotationProcessor(
            "com.querydsl:querydsl-apt:5.0.0:jpa",
            "jakarta.persistence:jakarta.persistence-api",
    )
    implementation 'org.hibernate:hibernate-core'
    implementation 'io.hypersistence:hypersistence-utils-hibernate-55:3.3.2'
    implementation 'org.springframework.data:spring-data-jpa'
}

sourceSets.main.java.srcDirs += ['src/main/generated']

compileJava {
    options.annotationProcessorGeneratedSourcesDirectory(file('src/main/generated/'))
}

clean {
    delete '/src/main/generated'
}

infra

@EnableJpaAuditing
@EnableTransactionManagement
@Configuration
public class QueryDSLConfig {
    @Bean
    public CriteriaBuilderFactory createCriteriaBuilderFactory(EntityManagerFactory entityManagerFactory) {
        return Criteria.getDefault().createCriteriaBuilderFactory(entityManagerFactory);
    }

    @Bean
    @Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public BlazeJPAQuery<Map<Expression<?>, ?>> blazeJPAQuery(EntityManager entityManager, CriteriaBuilderFactory createCriteriaBuilderFactory) {
        return new BlazeJPAQuery<>(entityManager, createCriteriaBuilderFactory);
    }
}
@Repository
public interface AccountRepository extends JpaRepository<Account, Long>, QuerydslPredicateExecutor<Account> {
    Account getAccountByUserName(String userName);
}
@Slf4j
@DubboService(timeout = 300000, loadbalance = LoadbalanceRules.RANDOM)
@RequiredArgsConstructor
public class AccountServiceImpl implements AccountService {

    private final AccountRepository accountRepository;

    public List<Map<Expression<?>, ?>> accountList() {
        QAccount account = QAccount.account;

        BooleanBuilder where = new BooleanBuilder();

        BlazeJPAQuery<Map<Expression<?>, ?>> blazeJPAQuery = DomainFactory.create(BlazeJPAQuery.class);
        blazeJPAQuery = blazeJPAQuery
                .select(Projections.map(account))
                .from(account)
                .where(where)
                .orderBy(account.id.desc());

        return blazeJPAQuery.fetch();
    }

    @Override
    public Account addAccount(AccountAddCmd accountAddCmd) {
        if (StringUtils.equalsAnyIgnoreCase(accountAddCmd.getUserName(), "admin", "boss")) {
            throw ExceptionFactory.bizException(accountAddCmd.getUserName() + " 不允许注册");
        }

        Account accountByUserName = accountRepository.getAccountByUserName(accountAddCmd.getUserName());
        Assert.isTrue(accountByUserName == null, "用户名已存在");

        Account account = new Account();
        CopyOptions copyOptions = CopyOptions.create(null, true, AccountAddCmd.Fields.id);
        BeanUtil.copyProperties(accountAddCmd, account, copyOptions);
        account.setPassword(new BCryptPasswordEncoder().encode(accountAddCmd.getPassword()));
        return accountRepository.save(account);
    }

    @Override
    public boolean removeAccountById(Long id) {
        Account account = accountRepository.findById(id).orElseThrow(() -> ExceptionFactory.bizException("用户不存在"));
        account.setDeleted(true);
        accountRepository.save(account);
        return true;
    }

    @Override
    public Account accountLogin(AccountLoginQry accountLoginQry) {
        Account accountByUserName = accountRepository.getAccountByUserName(accountLoginQry.getUserName());
        Assert.notNull(accountByUserName, "用户不存在");
        Assert.isFalse(accountByUserName.isDeleted(), "该用户已经被禁用");

        boolean matches = new BCryptPasswordEncoder().matches(accountLoginQry.getPassword(), accountByUserName.getPassword());
        Assert.isTrue(matches, "用户名密码不匹配");

        return accountByUserName;
    }
}
@EnableDubbo
@EntityScan("com.lab.demo")
@EnableJpaRepositories(basePackages = {"com.lab.demo"})
@SpringBootApplication(scanBasePackages = {"com.alibaba.cola", "com.lab"})
@Controller
public class DemoInfraApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoInfraApplication.class, args);
    }

    @GetMapping("")
    public String index() {
        return "redirect:/h2";
    }
}
server.port: 0
spring.application.name: demo-infra

---
spring.config.import:
  - classpath:/jdbc-h2.yml
  - classpath:/dubbo-service.yml

---
spring.datasource:
  hikari:
    connection-timeout: 10000
    validation-timeout: 3000
    idle-timeout: 60000
    login-timeout: 5
    max-lifetime: 60000
    maximum-pool-size: 10
    minimum-idle: 5
    #       read-only: false
    auto-commit: false

---
spring.jpa:
  database-platform: H2
  show-sql: true
  open-in-view: true
  generate-ddl: false
  hibernate:
    ddl-auto: update
    naming.physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
  properties.hibernate:
    dialect: org.hibernate.dialect.H2Dialect
    format_sql: false
    use_sql_comments: true
    connection.autocommit: true
spring:
  datasource:
    url: jdbc:h2:mem:test
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true
      settings:
        web-allow-others: true
      path: /h2
dubbo.application:
  name: ${spring.application.name}
  qos-enable: true
  metadata-type: remote

dubbo.protocol:
  name: dubbo
  serialization: protostuff
  port: -1

dubbo.registry:
  register-mode: instance
  address: nacos://console.nacos.io:8848
  use-as-metadata-center: true
  use-as-config-center: true
dependencies {
    implementation(project(":demo-client"))

    implementation 'org.springframework.boot:spring-boot-starter'

    implementation "org.apache.dubbo:dubbo-spring-boot-starter:${dubboVersion}"
    implementation "org.apache.dubbo:dubbo-rpc-triple:${dubboVersion}"
    implementation "org.apache.dubbo:dubbo-registry-nacos:${dubboVersion}"
    implementation "org.apache.dubbo:dubbo-serialization-protostuff:2.7.22"

    implementation "com.alibaba.nacos:nacos-client:${nacosVersion}"

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation "com.blazebit:blaze-persistence-integration-querydsl-expressions:${blazebitVersion}"
    implementation "com.blazebit:blaze-persistence-integration-hibernate-5.6:${blazebitVersion}"
    runtimeOnly "com.blazebit:blaze-persistence-core-impl:${blazebitVersion}"
    implementation 'org.springframework.boot:spring-boot-starter-web'

    implementation 'org.yaml:snakeyaml:1.33'
    runtimeOnly 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

demo 父工程 构建配置

buildscript {
    ext {
        sbVersion = '2.6.13'
        scVersion = '2021.0.5'
        scaVersion = '2021.0.5.0'
        dubboVersion = '3.1.10'
        nacosVersion = '2.2.1'
        blazebitVersion = '1.6.8'
        hutoolVersion = '5.8.16'
        colaVersion = '4.3.1'
    }
}

plugins {
    id 'java'
    id 'java-library'
    id 'org.springframework.boot' version "${sbVersion}"
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

allprojects {
    apply plugin: 'java'
    apply plugin: 'java-library'
    apply plugin: 'org.springframework.boot'

    group "com.lab"
    version "1.0-SNAPSHOT"

    sourceCompatibility = 17
    targetCompatibility = 17

    configurations {
        developmentOnly
        runtimeClasspath {
            extendsFrom developmentOnly
        }
        compileOnly {
            extendsFrom annotationProcessor
        }
    }

    tasks.withType(JavaCompile).configureEach {
        options.encoding = 'UTF-8'
    }

    repositories {
        mavenLocal()
        maven { url 'https://maven.aliyun.com/repository/public' }
        maven { url 'https://maven.aliyun.com/repository/spring' }
        mavenCentral()
    }

}

subprojects {
    jar.enabled = true

    apply plugin: 'io.spring.dependency-management'

    dependencies {
        implementation 'com.alibaba.cola:cola-component-dto'
        implementation 'com.alibaba.cola:cola-component-exception'
        implementation 'com.alibaba.cola:cola-component-extension-starter'
        implementation 'com.alibaba.cola:cola-component-domain-starter'
        implementation 'com.alibaba.cola:cola-component-statemachine'

        implementation 'org.springframework.boot:spring-boot-starter-validation'
        implementation 'io.swagger:swagger-annotations'

        implementation 'org.apache.commons:commons-lang3'
        implementation 'commons-beanutils:commons-beanutils'
        implementation 'cn.hutool:hutool-core'
        implementation 'cn.hutool:hutool-json'
        implementation 'cn.hutool:hutool-system'
        implementation 'com.google.guava:guava'
        implementation 'org.springframework.security:spring-security-crypto'

        implementation 'org.apache.skywalking:apm-toolkit-logback-1.x'

        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
        annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

        testCompileOnly 'org.projectlombok:lombok'
        testAnnotationProcessor 'org.projectlombok:lombok'
        testAnnotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    }

    dependencyManagement {
        imports {
            mavenBom("org.springframework.boot:spring-boot-dependencies:${sbVersion}")
            mavenBom("org.springframework.cloud:spring-cloud-dependencies:${scVersion}")
            mavenBom "com.alibaba.cloud:spring-cloud-alibaba-dependencies:${scaVersion}"
        }

        dependencies {
            dependency "com.alibaba.cola:cola-component-dto:${colaVersion}"
            dependency "com.alibaba.cola:cola-component-exception:${colaVersion}"
            dependency "com.alibaba.cola:cola-component-extension-starter:${colaVersion}"
            dependency "com.alibaba.cola:cola-component-domain-starter:${colaVersion}"
            dependency "com.alibaba.cola:cola-component-statemachine:${colaVersion}"

            dependency 'io.swagger:swagger-annotations:1.6.6'
            dependency 'org.apache.commons:commons-lang3:3.12.0'
            dependency 'commons-beanutils:commons-beanutils:1.9.4'

            dependency 'com.google.guava:guava:31.1-jre'
            dependency "cn.hutool:hutool-core:${hutoolVersion}"
            dependency "cn.hutool:hutool-json:${hutoolVersion}"
            dependency "cn.hutool:hutool-system:${hutoolVersion}"

            dependency 'org.apache.skywalking:apm-toolkit-logback-1.x:8.15.0'
        }
    }
}
pluginManagement {
    repositories {
        mavenLocal()
        maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
        maven { url 'https://maven.aliyun.com/repository/spring-plugin' }
        gradlePluginPortal()
    }
}
rootProject.name = 'demo'
include 'demo-infra'
include 'demo-app'
include 'demo-client'
include 'demo-common'
include 'demo-domain'
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值