赞
踩
优点
缺点
知名企业案例
优点
缺点
Hibernate Validator 是 SpringBoot 内置的校验框架,只要集成了 SpringBoot 就自动集成了它,我们可以通过在对象上面使用它提供的注解来完成参数校验。
常用注解:
有时候框架提供的校验注解并不能满足我们的需要,此时我们就需要自定义校验注解。比如还是上面的添加品牌,此时有个参数 showStatus,我们希望它只能是 0 或者 1,不能是其他数字,此时可以使用自定义注解来实现该功能。
@GetMapping(path = "/list")
ListAccountResponse listAccounts(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestParam int offset, @RequestParam @Min(0) int limit);
// GetOrCreate is for internal use by other APIs to match a user based on their phonenumber or email.
@PostMapping(path= "/get_or_create")
GenericAccountResponse getOrCreateAccount(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid GetOrCreateRequest request);
@GetMapping(path = "/get")
GenericAccountResponse getAccount(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestParam @NotBlank String userId);
@PutMapping(path = "/update")
GenericAccountResponse updateAccount(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid AccountDto newAccount);
@GetMapping(path = "/get_account_by_phonenumber")
GenericAccountResponse getAccountByPhonenumber(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestParam @PhoneNumber String phoneNumber);
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class AccountDto { @NotBlank private String id; private String name; @Email(message = "Invalid email") private String email; private boolean confirmedAndActive; @NotNull private Instant memberSince; private boolean support; @PhoneNumber private String phoneNumber; @NotEmpty private String photoUrl; }
@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface PhoneNumber {
String message() default "Invalid phone number";
Class[] groups() default {};
Class[] payload() default {};
}
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
@Override
public boolean isValid(String phoneField, ConstraintValidatorContext context) {
if (phoneField == null) return true; // can be null
return phoneField != null && phoneField.matches("[0-9]+")
&& (phoneField.length() > 8) && (phoneField.length() < 14);
}
}
使用全局异常处理来处理校验逻辑的思路很简单,首先我们需要通过 @ControllerAdvice 注解定义一个全局异常的处理类,然后自定义一个校验异常,当我们在 Controller 中校验失败时,直接抛出该异常,这样就可以达到校验失败返回错误信息的目的了
使用到的注解:
@RestControllerAdvice public class GlobalExceptionTranslator { static final ILogger logger = SLoggerFactory.getLogger(GlobalExceptionTranslator.class); @ExceptionHandler(MissingServletRequestParameterException.class) public BaseResponse handleError(MissingServletRequestParameterException e) { logger.warn("Missing Request Parameter", e); String message = String.format("Missing Request Parameter: %s", e.getParameterName()); return BaseResponse .builder() .code(ResultCode.PARAM_MISS) .message(message) .build(); } @ExceptionHandler(MethodArgumentTypeMismatchException.class) public BaseResponse handleError(MethodArgumentTypeMismatchException e) { logger.warn("Method Argument Type Mismatch", e); String message = String.format("Method Argument Type Mismatch: %s", e.getName()); return BaseResponse .builder() .code(ResultCode.PARAM_TYPE_ERROR) .message(message) .build(); } @ExceptionHandler(MethodArgumentNotValidException.class) public BaseResponse handleError(MethodArgumentNotValidException e) { logger.warn("Method Argument Not Valid", e); BindingResult result = e.getBindingResult(); FieldError error = result.getFieldError(); String message = String.format("%s:%s", error.getField(), error.getDefaultMessage()); return BaseResponse .builder() .code(ResultCode.PARAM_VALID_ERROR) .message(message) .build(); } @ExceptionHandler(BindException.class) public BaseResponse handleError(BindException e) { logger.warn("Bind Exception", e); FieldError error = e.getFieldError(); String message = String.format("%s:%s", error.getField(), error.getDefaultMessage()); return BaseResponse .builder() .code(ResultCode.PARAM_BIND_ERROR) .message(message) .build(); } @ExceptionHandler(ConstraintViolationException.class) public BaseResponse handleError(ConstraintViolationException e) { logger.warn("Constraint Violation", e); Set<ConstraintViolation<?>> violations = e.getConstraintViolations(); ConstraintViolation<?> violation = violations.iterator().next(); String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName(); String message = String.format("%s:%s", path, violation.getMessage()); return BaseResponse .builder() .code(ResultCode.PARAM_VALID_ERROR) .message(message) .build(); } @ExceptionHandler(NoHandlerFoundException.class) public BaseResponse handleError(NoHandlerFoundException e) { logger.error("404 Not Found", e); return BaseResponse .builder() .code(ResultCode.NOT_FOUND) .message(e.getMessage()) .build(); } @ExceptionHandler(HttpMessageNotReadableException.class) public BaseResponse handleError(HttpMessageNotReadableException e) { logger.error("Message Not Readable", e); return BaseResponse .builder() .code(ResultCode.MSG_NOT_READABLE) .message(e.getMessage()) .build(); } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public BaseResponse handleError(HttpRequestMethodNotSupportedException e) { logger.error("Request Method Not Supported", e); return BaseResponse .builder() .code(ResultCode.METHOD_NOT_SUPPORTED) .message(e.getMessage()) .build(); } @ExceptionHandler(HttpMediaTypeNotSupportedException.class) public BaseResponse handleError(HttpMediaTypeNotSupportedException e) { logger.error("Media Type Not Supported", e); return BaseResponse .builder() .code(ResultCode.MEDIA_TYPE_NOT_SUPPORTED) .message(e.getMessage()) .build(); } @ExceptionHandler(ServiceException.class) public BaseResponse handleError(ServiceException e) { logger.error("Service Exception", e); return BaseResponse .builder() .code(e.getResultCode()) .message(e.getMessage()) .build(); } @ExceptionHandler(PermissionDeniedException.class) public BaseResponse handleError(PermissionDeniedException e) { logger.error("Permission Denied", e); return BaseResponse .builder() .code(e.getResultCode()) .message(e.getMessage()) .build(); } @ExceptionHandler(Throwable.class) public BaseResponse handleError(Throwable e) { logger.error("Internal Server Error", e); return BaseResponse .builder() .code(ResultCode.INTERNAL_SERVER_ERROR) .message(e.getMessage()) .build(); } }
@ExceptionHandler(ServiceException.class) public BaseResponse handleError(ServiceException e) { logger.error("Service Exception", e); return BaseResponse .builder() .code(e.getResultCode()) .message(e.getMessage()) .build(); } @ExceptionHandler(Throwable.class) public BaseResponse handleError(Throwable e) { logger.error("Internal Server Error", e); return BaseResponse .builder() .code(ResultCode.INTERNAL_SERVER_ERROR) .message(e.getMessage()) .build(); }
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BaseResponse {
private String message;
@Builder.Default
private ResultCode code = ResultCode.SUCCESS;
public boolean isSuccess() {
return code == ResultCode.SUCCESS;
}
}
@Controller @SuppressWarnings(value = "Duplicates") public class GlobalErrorController implements ErrorController { static final ILogger logger = SLoggerFactory.getLogger(GlobalErrorController.class); @Autowired ErrorPageFactory errorPageFactory; @Autowired SentryClient sentryClient; @Autowired StaffjoyProps staffjoyProps; @Autowired EnvConfig envConfig; @Override public String getErrorPath() { return "/error"; } @RequestMapping("/error") public String handleError(HttpServletRequest request, Model model) { Object statusCode = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); Object exception = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); ErrorPage errorPage = null; if (statusCode != null && (Integer)statusCode == HttpStatus.NOT_FOUND.value()) { errorPage = errorPageFactory.buildNotFoundPage(); } else { errorPage = errorPageFactory.buildInternalServerErrorPage(); } if (exception != null) { if (envConfig.isDebug()) { // no sentry aop in debug mode logger.error("Global error handling", exception); } else { sentryClient.sendException((Exception)exception); UUID uuid = sentryClient.getContext().getLastEventId(); errorPage.setSentryErrorId(uuid.toString()); errorPage.setSentryPublicDsn(staffjoyProps.getSentryDsn()); logger.warn("Reported error to sentry", "id", uuid.toString(), "error", exception); } } model.addAttribute(Constant.ATTRIBUTE_NAME_PAGE, errorPage); return "error"; } }
DTO:
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class AccountDto { @NotBlank private String id; private String name; @Email(message = "Invalid email") private String email; private boolean confirmedAndActive; @NotNull private Instant memberSince; private boolean support; @PhoneNumber private String phoneNumber; @NotEmpty private String photoUrl; }
DMO:
@Data @NoArgsConstructor @AllArgsConstructor @Builder @Entity public class Account { @Id @GenericGenerator(name = "system-uuid", strategy = "uuid") @GeneratedValue(generator = "system-uuid") private String id; private String name; private String email; private boolean confirmedAndActive; private Instant memberSince; private boolean support; private String phoneNumber; private String photoUrl; }
DTO 和 DMO 互转:
public AccountDto update(AccountDto newAccountDto) { Account newAccount = this.convertToModel(newAccountDto); Account existingAccount = accountRepo.findAccountById(newAccount.getId()); if (existingAccount == null) { throw new ServiceException(ResultCode.NOT_FOUND, String.format("User with id %s not found", newAccount.getId())); } entityManager.detach(existingAccount); if (!serviceHelper.isAlmostSameInstant(newAccount.getMemberSince(), existingAccount.getMemberSince())) { throw new ServiceException(ResultCode.REQ_REJECT, "You cannot modify the member_since date"); } if (StringUtils.hasText(newAccount.getEmail()) && !newAccount.getEmail().equals(existingAccount.getEmail())) { Account foundAccount = accountRepo.findAccountByEmail(newAccount.getEmail()); if (foundAccount != null) { throw new ServiceException(ResultCode.REQ_REJECT, "A user with that email already exists. Try a password reset"); } } if (StringUtils.hasText(newAccount.getPhoneNumber()) && !newAccount.getPhoneNumber().equals(existingAccount.getPhoneNumber())) { Account foundAccount = accountRepo.findAccountByPhoneNumber(newAccount.getPhoneNumber()); if (foundAccount != null) { throw new ServiceException(ResultCode.REQ_REJECT, "A user with that phonenumber already exists. Try a password reset"); } } if (AuthConstant.AUTHORIZATION_AUTHENTICATED_USER.equals(AuthContext.getAuthz())) { if (!existingAccount.isConfirmedAndActive() && newAccount.isConfirmedAndActive()) { throw new ServiceException(ResultCode.REQ_REJECT, "You cannot activate this account"); } if (existingAccount.isSupport() != newAccount.isSupport()) { throw new ServiceException(ResultCode.REQ_REJECT, "You cannot change the support parameter"); } if (!existingAccount.getPhotoUrl().equals(newAccount.getPhotoUrl())) { throw new ServiceException(ResultCode.REQ_REJECT, "You cannot change the photo through this endpoint (see docs)"); } // User can request email change - not do it :-) if (!existingAccount.getEmail().equals(newAccount.getEmail())) { this.requestEmailChange(newAccount.getId(), newAccount.getEmail()); // revert newAccount.setEmail(existingAccount.getEmail()); } } newAccount.setPhotoUrl(Helper.generateGravatarUrl(newAccount.getEmail())); try { accountRepo.save(newAccount); } catch (Exception ex) { String errMsg = "Could not update the user account"; serviceHelper.handleException(logger, ex, errMsg); throw new ServiceException(errMsg, ex); } serviceHelper.syncUserAsync(newAccount.getId()); LogEntry auditLog = LogEntry.builder() .authorization(AuthContext.getAuthz()) .currentUserId(AuthContext.getUserId()) .targetType("account") .targetId(newAccount.getId()) .originalContents(existingAccount.toString()) .updatedContents(newAccount.toString()) .build(); logger.info("updated account", auditLog); // If account is being activated, or if phone number is changed by current user - send text if (newAccount.isConfirmedAndActive() && StringUtils.hasText(newAccount.getPhoneNumber()) && !newAccount.getPhoneNumber().equals(existingAccount.getPhoneNumber())) { serviceHelper.sendSmsGreeting(newAccount.getId()); } this.trackEventWithAuthCheck("account_updated"); AccountDto accountDto = this.convertToDto(newAccount); return accountDto; } public void updatePassword(String userId, String password) { String pwHash = passwordEncoder.encode(password); int affected = accountSecretRepo.updatePasswordHashById(pwHash, userId); if (affected != 1) { throw new ServiceException(ResultCode.NOT_FOUND, "user with specified id not found"); } LogEntry auditLog = LogEntry.builder() .authorization(AuthContext.getAuthz()) .currentUserId(AuthContext.getUserId()) .targetType("account") .targetId(userId) .build(); logger.info("updated password", auditLog); this.trackEventWithAuthCheck("password_updated"); } private AccountDto convertToDto(Account account) { return modelMapper.map(account, AccountDto.class); } private Account convertToModel(AccountDto accountDto) { return modelMapper.map(accountDto, Account.class); }
Account Client:
@FeignClient(name = AccountConstant.SERVICE_NAME, path = "/v1/account", url = "${staffjoy.account-service-endpoint}") // TODO Client side validation can be enabled as needed // @Validated public interface AccountClient { @PostMapping(path = "/create") GenericAccountResponse createAccount(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid CreateAccountRequest request); @PostMapping(path = "/track_event") BaseResponse trackEvent(@RequestBody @Valid TrackEventRequest request); @PostMapping(path = "/sync_user") BaseResponse syncUser(@RequestBody @Valid SyncUserRequest request); @GetMapping(path = "/list") ListAccountResponse listAccounts(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestParam int offset, @RequestParam @Min(0) int limit); // GetOrCreate is for internal use by other APIs to match a user based on their phonenumber or email. @PostMapping(path= "/get_or_create") GenericAccountResponse getOrCreateAccount(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid GetOrCreateRequest request); @GetMapping(path = "/get") GenericAccountResponse getAccount(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestParam @NotBlank String userId); @PutMapping(path = "/update") GenericAccountResponse updateAccount(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid AccountDto newAccount); @GetMapping(path = "/get_account_by_phonenumber") GenericAccountResponse getAccountByPhonenumber(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestParam @PhoneNumber String phoneNumber); @PutMapping(path = "/update_password") BaseResponse updatePassword(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid UpdatePasswordRequest request); @PostMapping(path = "/verify_password") GenericAccountResponse verifyPassword(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid VerifyPasswordRequest request); // RequestPasswordReset sends an email to a user with a password reset link @PostMapping(path = "/request_password_reset") BaseResponse requestPasswordReset(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid PasswordResetRequest request); @PostMapping(path = "/request_email_change") BaseResponse requestEmailChange(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid EmailChangeRequest request); // ChangeEmail sets an account to active and updates its email. It is // used after a user clicks a confirmation link in their email. @PostMapping(path = "/change_email") BaseResponse changeEmail(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid EmailConfirmation request); }
继承关系:
客户端调用范例:
GenericAccountResponse genericAccountResponse = null;
try {
genericAccountResponse = this.accountClient.getAccount(AuthConstant.AUTHORIZATION_WHOAMI_SERVICE, userId);
} catch (Exception ex) {
String errMsg = "unable to get account";
handleErrorAndThrowException(ex, errMsg);
}
if (!genericAccountResponse.isSuccess()) {
handleErrorAndThrowException(genericAccountResponse.getMessage());
}
AccountDto account = genericAccountResponse.getAccount();
封装消息+捎带:
环境定义:
public class EnvConstant {
public static final String ENV_DEV = "dev";
public static final String ENV_TEST = "test";
public static final String ENV_UAT = "uat"; // similar to staging
public static final String ENV_PROD = "prod";
}
环境配置:
// environment related configuration @Data @Builder public class EnvConfig { private String name; private boolean debug; private String externalApex; private String internalApex; private String scheme; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) private static Map<String, EnvConfig> map; static { map = new HashMap<String, EnvConfig>(); EnvConfig envConfig = EnvConfig.builder().name(EnvConstant.ENV_DEV) .debug(true) .externalApex("staffjoy-v2.local") .internalApex(EnvConstant.ENV_DEV) .scheme("http") .build(); map.put(EnvConstant.ENV_DEV, envConfig); envConfig = EnvConfig.builder().name(EnvConstant.ENV_TEST) .debug(true) .externalApex("staffjoy-v2.local") .internalApex(EnvConstant.ENV_DEV) .scheme("http") .build(); map.put(EnvConstant.ENV_TEST, envConfig); // for aliyun k8s demo, enable debug and use http and staffjoy-uat.local // in real world, disable debug and use http and staffjoy-uat.xyz in UAT environment envConfig = EnvConfig.builder().name(EnvConstant.ENV_UAT) .debug(true) .externalApex("dusan-uat.local") .internalApex(EnvConstant.ENV_UAT) .scheme("http") .build(); map.put(EnvConstant.ENV_UAT, envConfig); // envConfig = EnvConfig.builder().name(EnvConstant.ENV_UAT) // .debug(false) // .externalApex("staffjoy-uat.xyz") // .internalApex(EnvConstant.ENV_UAT) // .scheme("https") // .build(); // map.put(EnvConstant.ENV_UAT, envConfig); envConfig = EnvConfig.builder().name(EnvConstant.ENV_PROD) .debug(false) .externalApex("dunsan.com") .internalApex(EnvConstant.ENV_PROD) .scheme("https") .build(); map.put(EnvConstant.ENV_PROD, envConfig); } public static EnvConfig getEnvConfg(String env) { EnvConfig envConfig = map.get(env); if (envConfig == null) { envConfig = map.get(EnvConstant.ENV_DEV); } return envConfig; } }
开发测试环境禁用 Sentry 异常日志:
@Aspect @Slf4j public class SentryClientAspect { @Autowired EnvConfig envConfig; @Around("execution(* io.sentry.SentryClient.send*(..))") public void around(ProceedingJoinPoint joinPoint) throws Throwable { // no sentry logging in debug mode if (envConfig.isDebug()) { log.debug("no sentry logging in debug mode"); return; } joinPoint.proceed(); } }
Sentry 是统一的异常管理平台,支持异常事件的收集、展示、告警等功能。
ThreadPoolTaskExecutor:
AsyncExecutor 配置:
Configuration @EnableAsync @Import(value = {StaffjoyRestConfig.class}) @SuppressWarnings(value = "Duplicates") public class AppConfig { public static final String ASYNC_EXECUTOR_NAME = "asyncExecutor"; @Bean(name=ASYNC_EXECUTOR_NAME) public Executor asyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setTaskDecorator(new ContextCopyingDecorator()); executor.setCorePoolSize(3); executor.setMaxPoolSize(5); executor.setQueueCapacity(100); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setThreadNamePrefix("AsyncThread-"); executor.initialize(); return executor; } }
Async 标注:
@Async(AppConfig.ASYNC_EXECUTOR_NAME) public void trackEventAsync(String userId, String eventName) { if (envConfig.isDebug()) { logger.debug("intercom disabled in dev & test environment"); return; } Event event = new Event() .setUserID(userId) .setEventName("v2_" + eventName) .setCreatedAt(Instant.now().toEpochMilli()); try { Event.create(event); } catch (Exception ex) { String errMsg = "fail to create event on Intercom"; handleException(logger, ex, errMsg); throw new ServiceException(errMsg, ex); } logger.debug("updated intercom"); }
线程上下文拷贝:
// https://stackoverflow.com/questions/23732089/how-to-enable-request-scope-in-async-task-executor
public class ContextCopyingDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
RequestAttributes context = RequestContextHolder.currentRequestAttributes();
return () -> {
try {
RequestContextHolder.setRequestAttributes(context);
runnable.run();
} finally {
RequestContextHolder.resetRequestAttributes();
}
};
}
}
@Configuration @EnableAsync @Import(value = {StaffjoyRestConfig.class}) @SuppressWarnings(value = "Duplicates") public class AppConfig { public static final String ASYNC_EXECUTOR_NAME = "asyncExecutor"; @Bean(name=ASYNC_EXECUTOR_NAME) public Executor asyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // for passing in request scope context executor.setTaskDecorator(new ContextCopyingDecorator()); executor.setCorePoolSize(3); executor.setMaxPoolSize(5); executor.setQueueCapacity(100); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setThreadNamePrefix("AsyncThread-"); executor.initialize(); return executor; } }
<!-- Swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() //为当前包下controller生成API文档 .apis(RequestHandlerSelectors.basePackage("com.7d.PmsBrand.controller")) .paths(PathSelectors.any()) .build() .apiInfo(apiEndPointsInfo()) .useDefaultResponseMessages(false); } private ApiInfo apiEndPointsInfo() { return new ApiInfoBuilder().title("PmsBrand REST API") .description("7d Account REST API") .contact(new Contact("7d", "https://zuozewei.blog.csdn.net", "zuozewei@hotmail.com")) .license("The MIT License") .licenseUrl("https://opensource.org/licenses/MIT") .version("V2") .build(); } }
给 Controller 添加 Swagger 注解:
/** * 品牌管理Controller */ @Api(tags = "PmsBrandController", description = "商品品牌管理") @Controller @RequestMapping("/brand") public class PmsBrandController { @Autowired private PmsBrandService brandService; private static final Logger LOGGER = LoggerFactory.getLogger(PmsBrandController.class); @ApiOperation("获取所有品牌列表") @RequestMapping(value = "listAll", method = RequestMethod.GET) @ResponseBody public CommonResult<List<PmsBrand>> getBrandList() { return CommonResult.success(brandService.listAllBrand()); } @ApiOperation("添加品牌") @RequestMapping(value = "/create", method = RequestMethod.POST) @ResponseBody public CommonResult createBrand(@RequestBody PmsBrand pmsBrand) { CommonResult commonResult; int count = brandService.createBrand(pmsBrand); if (count == 1) { commonResult = CommonResult.success(pmsBrand); LOGGER.debug("createBrand success:{}", pmsBrand); } else { commonResult = CommonResult.failed("操作失败"); LOGGER.debug("createBrand failed:{}", pmsBrand); } return commonResult; } @ApiOperation("更新指定id品牌信息") @RequestMapping(value = "/update/{id}", method = RequestMethod.POST) @ResponseBody public CommonResult updateBrand(@PathVariable("id") Long id, @RequestBody PmsBrand pmsBrandDto, BindingResult result) { CommonResult commonResult; int count = brandService.updateBrand(id, pmsBrandDto); if (count == 1) { commonResult = CommonResult.success(pmsBrandDto); LOGGER.debug("updateBrand success:{}", pmsBrandDto); } else { commonResult = CommonResult.failed("操作失败"); LOGGER.debug("updateBrand failed:{}", pmsBrandDto); } return commonResult; } @ApiOperation("删除指定id的品牌") @RequestMapping(value = "/delete/{id}", method = RequestMethod.GET) @ResponseBody public CommonResult deleteBrand(@PathVariable("id") Long id) { int count = brandService.deleteBrand(id); if (count == 1) { LOGGER.debug("deleteBrand success :id={}", id); return CommonResult.success(null); } else { LOGGER.debug("deleteBrand failed :id={}", id); return CommonResult.failed("操作失败"); } } @ApiOperation("分页查询品牌列表") @RequestMapping(value = "/list", method = RequestMethod.GET) @ResponseBody public CommonResult<CommonPage<PmsBrand>> listBrand(@RequestParam(value = "pageNum", defaultValue = "1") @ApiParam("页码") Integer pageNum, @RequestParam(value = "pageSize", defaultValue = "3") @ApiParam("每页数量") Integer pageSize) { List<PmsBrand> brandList = brandService.listBrand(pageNum, pageSize); return CommonResult.success(CommonPage.restPage(brandList)); } @ApiOperation("获取指定id的品牌详情") @RequestMapping(value = "/{id}", method = RequestMethod.GET) @ResponseBody public CommonResult<PmsBrand> brand(@PathVariable("id") Long id) { return CommonResult.success(brandService.getBrand(id)); } }
修改 MyBatis Generator 注释的生成规则:
CommentGenerator为MyBatis Generator的自定义注释生成器,修改addFieldComment方法使其生成Swagger的@ApiModelProperty注解来取代原来的方法注释,添加addJavaFileComment方法,使其能在import中导入@ApiModelProperty,否则需要手动导入该类,在需要生成大量实体类时,是一件非常麻烦的事。
/** * 自定义注释生成器 */ public class CommentGenerator extends DefaultCommentGenerator { private boolean addRemarkComments = false; private static final String EXAMPLE_SUFFIX="Example"; private static final String API_MODEL_PROPERTY_FULL_CLASS_NAME="io.swagger.annotations.ApiModelProperty"; /** * 设置用户配置的参数 */ @Override public void addConfigurationProperties(Properties properties) { super.addConfigurationProperties(properties); this.addRemarkComments = StringUtility.isTrue(properties.getProperty("addRemarkComments")); } /** * 给字段添加注释 */ @Override public void addFieldComment(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) { String remarks = introspectedColumn.getRemarks(); //根据参数和备注信息判断是否添加备注信息 if(addRemarkComments&&StringUtility.stringHasValue(remarks)){ // addFieldJavaDoc(field, remarks); //数据库中特殊字符需要转义 if(remarks.contains("\"")){ remarks = remarks.replace("\"","'"); } //给model的字段添加swagger注解 field.addJavaDocLine("@ApiModelProperty(value = \""+remarks+"\")"); } } /** * 给model的字段添加注释 */ private void addFieldJavaDoc(Field field, String remarks) { //文档注释开始 field.addJavaDocLine("/**"); //获取数据库字段的备注信息 String[] remarkLines = remarks.split(System.getProperty("line.separator")); for(String remarkLine:remarkLines){ field.addJavaDocLine(" * "+remarkLine); } addJavadocTag(field, false); field.addJavaDocLine(" */"); } @Override public void addJavaFileComment(CompilationUnit compilationUnit) { super.addJavaFileComment(compilationUnit); //只在model中添加swagger注解类的导入 if(!compilationUnit.isJavaInterface()&&!compilationUnit.getType().getFullyQualifiedName().contains(EXAMPLE_SUFFIX)){ compilationUnit.addImportedType(new FullyQualifiedJavaType(API_MODEL_PROPERTY_FULL_CLASS_NAME)); } } }
运行代码生成器重新生成 mbg 包中的代码:
运行com.7d.mall.tiny.mbg.Generator 的 main方法,重新生成 mbg 中的代码,可以看到 PmsBrand 类中已经自动根据数据库注释添加了@ApiModelProperty注解
CORS全称Cross-Origin Resource Sharing,意为跨域资源共享。当一个资源去访问另一个不同域名或者同域名不同端口的资源时,就会发出跨域请求。如果此时另一个资源不允许其进行跨域资源访问,那么访问的那个资源就会遇到跨域问题。
覆盖默认的CorsFilter
添加GlobalCorsConfig配置文件来允许跨域访问。
设置 SpringSecurity 允许 OPTIONS 请求访问
在SecurityConfig类的configure(HttpSecurity httpSecurity) 方法中添加如下代码。
.antMatchers(HttpMethod.OPTIONS)//跨域请求会先进行一次options请求
.permitAll()
AOP 通过在 controller 层建一个切面来实现接口访问的统一日志记录。
添加日志信息封装类 WebLog
用于封装需要记录的日志信息,包括操作的描述、时间、消耗时间、url、请求参数和返回结果等信息。
/** * Controller层的日志封装类 */ public class WebLog { /** * 操作描述 */ private String description; /** * 操作用户 */ private String username; /** * 操作时间 */ private Long startTime; /** * 消耗时间 */ private Integer spendTime; /** * 根路径 */ private String basePath; /** * URI */ private String uri; /** * URL */ private String url; /** * 请求类型 */ private String method; /** * IP地址 */ private String ip; /** * 请求参数 */ private Object parameter; /** * 请求返回的结果 */ private Object result; //省略了getter,setter方法 }
添加切面类 WebLogAspect:
定义了一个日志切面,在环绕通知中获取日志需要的信息,并应用到controller层中所有的public方法中去。
/** * 统一日志处理切面 */ @Aspect @Component @Order(1) public class WebLogAspect { private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class); @Pointcut("execution(public * com.dunsan.mall.tiny.controller.*.*(..))") public void webLog() { } @Before("webLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { } @AfterReturning(value = "webLog()", returning = "ret") public void doAfterReturning(Object ret) throws Throwable { } @Around("webLog()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); //获取当前请求对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); //记录请求信息 WebLog webLog = new WebLog(); Object result = joinPoint.proceed(); Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); if (method.isAnnotationPresent(ApiOperation.class)) { ApiOperation apiOperation = method.getAnnotation(ApiOperation.class); webLog.setDescription(apiOperation.value()); } long endTime = System.currentTimeMillis(); String urlStr = request.getRequestURL().toString(); webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath())); webLog.setIp(request.getRemoteUser()); webLog.setMethod(request.getMethod()); webLog.setParameter(getParameter(method, joinPoint.getArgs())); webLog.setResult(result); webLog.setSpendTime((int) (endTime - startTime)); webLog.setStartTime(startTime); webLog.setUri(request.getRequestURI()); webLog.setUrl(request.getRequestURL().toString()); LOGGER.info("{}", JSONUtil.parse(webLog)); return result; } /** * 根据方法和传入的参数获取请求参数 */ private Object getParameter(Method method, Object[] args) { List<Object> argList = new ArrayList<>(); Parameter[] parameters = method.getParameters(); for (int i = 0; i < parameters.length; i++) { //将RequestBody注解修饰的参数作为请求参数 RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class); if (requestBody != null) { argList.add(args[i]); } //将RequestParam注解修饰的参数作为请求参数 RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class); if (requestParam != null) { Map<String, Object> map = new HashMap<>(); String key = parameters[i].getName(); if (!StringUtils.isEmpty(requestParam.value())) { key = requestParam.value(); } map.put(key, args[i]); argList.add(map); } } if (argList.size() == 0) { return null; } else if (argList.size() == 1) { return argList.get(0); } else { return argList; } } }
服务配置文件处理方式:
对于各个项目分环境部署,最麻烦的就是配置文件的问题,不同的环境需要加载不同的配置,好在 Spring Boot 框架加载配置是非常方便的,我们可以针对不同的环境分别配置不同的配置文件,这里有两个地方要注意一下:
镜像可以分为基础镜像和应用镜像:
基础镜像要求体积尽量小,方便拉取,同时安装一些必要的软件,方便后期进入容器内排查问题,我们需要准备好服务运行的底层系统镜像,比如 Centos、Ubuntu 等常见 Linux 操作系统,然后基于该系统镜像,构建服务运行需要的环境镜像,比如一些常见组合:Centos + Jdk、Centos + Jdk + Tomcat、Centos + nginx 等,由于不同的服务运行依赖的环境版本不一定一致,所以还需要制作不同版本的环境镜像,例如如下基础镜像版本。
这样,就可以标识该基础镜像的系统版本及软件版本,方便后边选择对应的基础镜像来构建应用镜像
有了上边的基础镜像后,就很容易构建出对应的应用镜像了,例如一个简单的应用镜像 Dockerfile 如下:
FROM registry.docker.com/baseimg/centos-jdk:7.5_1.8
COPY app-name.jar /opt/project/app.jar
EXPOSE 8080
ENTRYPOINT ["/java", "-jar", "/opt/project/app.jar"]
当然,这里我建议使用另一种方式来启动服务,将启动命令放在统一 shell 启动脚本执行,例如如下Dockerfile 示例:
FROM registry.docker.com/baseimg/centos-jdk:7.5_1.8
COPY app-name.jar /opt/project/app.jar
COPY entrypoint.sh /opt/project/entrypoint.sh
EXPOSE 8080
ENTRYPOINT ["/bin/sh", "/opt/project/entrypoint.sh"]
将服务启动命令配置到 entrypoint.sh,这样我们可以扩展做很多事情,比如启动服务前做一些初始化操作等,还可以向容器传递参数到脚本执行一些特殊操作,而且这里变成脚本来启动,这样后续构建镜像基本不需要改 Dockerfile 了。
#!/bin/bash
# do other things here
java -jar $JAVA_OPTS /opt/project/app.jar $1 > /dev/null 2>&1
上边示例中,我们就注入 $JAVA_OPTS 环境变量,来优化 JVM 参数,还可以传递一个变量,这个变量大家应该就猜到了,就是服务启动加载哪个配置文件参数,例如:–spring.profiles.active=prod
MyBatis Generator
是 MyBatis 的代码生成器,支持为 MyBatis 的所有版本生成代码。非常容易及快速生成 Mybatis 的Java POJO文件及数据库 Mapping 文件。
SpringBoot 它使用“习惯优于配置”(项目中存在大量的配置,此外还内置一个习惯性的配置,让你无须手动进行配置)的理念让 Java 项目快速运行起来。使用 SpringBoot 很容易创建一个独立运行(运行 Jar ,内嵌 Servlet 容器)、准生产级别的基于 Spring 的框架项目,使用 SpringBoot 你可以不用或者只需要很少的 Spring 配置。
用白话来理解,就是 SpringBoot 其实不是什么新框架,它默认配置了很多框架的使用方式,就像 Maven 整合了所有的 Jar 包,SpringBoot 整合了几乎所有的框架。
官网:https://spring.io/projects/spring-boot
LogBack 是 Log4j 的改良版本,比 Log4j 拥有更多的特性,同时也带来很大性能提升,同时天然支持SLF4J。
LogBack 官方建议配合 Slf4j 使用,这样可以灵活地替换底层日志框架。
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
Druid 是一个关系型数据库连接池,它是阿里巴巴的一个开源项目。Druid 支持所有 JDBC 兼容数据库,包括了Oracle、MySQL、PostgreSQL、SQL Server、H2等。
Druid 在监控、可扩展性、稳定性和性能方面具有明显的优势。通过 Druid 提供的监控功能,可以实时观察数据库连接池和SQL查询的工作情况。使用 Druid 连接池在一定程度上可以提高数据访问效率。
p6spy 是一个开源项目,通常使用它来跟踪数据库操作,查看程序运行过程中执行的sql语句。
官网:https://github.com/p6spy/p6spy
dynamic-datasource-spring-boot-starter 是一个基于 springboot 的快速集成多数据源的启动器。
其支持 Jdk 1.7+, SpringBoot 1.4.x、1.5.x、 2.0.x。
官网:https://github.com/baomidou/dynamic-datasource-spring-boot-starter
MyBatis PageHelper 实现了通用的分页查询,其支持的数据有,mysql、Oracle、DB2、PostgreSQL等主流的数据库。
github: https://github.com/pagehelper/Mybatis-PageHelper
PageHelper.startPage(pageNum, pageSize);
//之后进行查询操作将自动进行分页
List<PmsBrand> brandList = brandMapper.selectByExample(new PmsBrandExample());
//通过构造PageInfo对象获取分页信息,如当前页码,总页数,总条数
PageInfo<PmsBrand> pageInfo = new PageInfo<PmsBrand>(list);
Swagger是一款Restful 接口的文档在线自动生成、功能测试框架。一个规范和完整的框架,用于生成、描述、调用和可视化Restful 风格的Web服务,加上Swagger-UI,可以有很好的呈现。
Lombok 项目是一个 Java 库,它会自动插入您的编辑器和构建工具中,从而使您的Java更加生动有趣。
永远不要再写另一个 getter 或 equals 方法,带有一个注释的您的类有一个功能全面的生成器,自动化您的日志记录变量等等。
Hutool 是一个小而全的Java工具类库,它帮助我们简化每一行代码,避免重复造轮子。如果你有需要用到某些工具类的时候,不妨在 Hutool 里面找找。
Maven 作为一个构建工具,不仅能帮我们自动化构建,还能够抽象构建过程,提供构建任务实现;它跨平台,对外提供了一致的操作接口,这一切足以使它成为优秀的、流行的构建工具。
Maven 不仅是构建工具,还是一个依赖管理工具和项目管理工具,它提供了中央仓库,能帮助我们自动下载构件。
MySQL是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。
MySQL是一种关系数据库管理系统,关系数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。
MySQL所使用的 SQL 语言是用于访问数据库的最常用标准化语言。MySQL 软件采用了双授权政策,分为社区版和商业版,由于其体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,一般中小型网站的开发都选择 MySQL 作为网站数据库。
----- 摘抄自百度百科
格式:[type: description] #[相关任务编号]
fix:修复登录正确提示不准确缺陷 #demo-1243
add:添加登录拦截校验功能 #demo-1240
update:删除登陆弹出框提示 #demo-1241
test:增加控制接口测试用例 #demo-1242
关联任务单/缺陷单编号,例如:“demo-124”;
《java开发手册》v1.7.0 嵩山版:
参考资料:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。