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'