本篇文章从”数据库审计字段”,”方法级别数据验证”,””返回值约束”,“业务逻辑中的门面模式”,“业务异常设计”,“枚举状态设计”等6个方面作为出发点,讲解在真正项目开发中,java编程的最佳实践。本文的所有代码和思想都是笔者自己的实际经验和见解,希望对读者有所帮助。
数据库审计字段
在做业务系统数据库设计的时候,我相信你总会创建一些相关的审计字段,比如:创建人,创建时间,更新人,更新时间。
每次重复的在会话中获取创建人(更新人)和创建时间(更新时间),然后从controller层传入到service层,在进行entity赋值,然后进行插入数据库(reposity层)。
这种对业务无关的操作,最好可以做成通用的,那么,如何设计一个通用的审计日志插入呢?
举例:
框架:spring boot + mybatis
数据库:mysql
辅助工具:lombok
定义注解
注解在实体中的字段,都会自动赋值到实体字段中:@CreateAt/@CreateBy/@UpdateAt/@UpdateBy
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {FIELD, METHOD, ANNOTATION_TYPE})
public @interface CreateAt {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {FIELD, METHOD, ANNOTATION_TYPE})
public @interface CreateBy {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {FIELD, METHOD, ANNOTATION_TYPE})
public @interface UpdateAt {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {FIELD, METHOD, ANNOTATION_TYPE})
public @interface UpdateBy {
}
AOP 拦截
使用mybatis生成数据库代码,然后进行抽象,其他的生成类进行集成:
public interface MybatisBaseRepository {
long countByExample(E example);
int deleteByExample(E example);
int deleteByPrimaryKey(PK id);
int insert(T record);
int insertSelective(T record);
List selectByExample(E example);
T selectByPrimaryKey(PK id);
int updateByExampleSelective(@Param("record") T record, @Param("example") E example);
int updateByExample(@Param("record") T record, @Param("example") E example);
int updateByPrimaryKeySelective(T record);
int updateByPrimaryKey(T record);
}
aop需要拦截insert、update然后对其中的泛型实体 T 进行赋值(这里边的T有可能包含以上注解,对这些注解的字段进行赋值)
AOP代码如下:
@Slf4j
@Aspect
@Component
public class MybatisAuditAOPDefault {
@Pointcut("execution(* com.tasly.chm.repository..*.*Repository.insert*(..))")
private void insertCutMethod() {
}
@Pointcut("execution(* com.tasly.chm.repository..*.*Repository.update*(..))")
private void updateCutMethod() {
}
@Around("insertCutMethod()")
public Object doInsertAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (Object arg : args) {
AuditingAction.markCreateAuditing(arg);
}
return pjp.proceed();
}
@Around("updateCutMethod()")
public Object doupdateAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (Object arg : args) {
AuditingAction.markUpdateAuditing(arg);
}
return pjp.proceed();
}
}
审计处理类-AuditingAction
AuditingAction主要有两个方法:
markCreateAuditing(Object) : 标记插入实体的数据值
markUpdateAuditing(Object) : 标记更新实体的数据值
AuditingAction:
@UtilityClass
public class AuditingAction {
public void markCreateAuditing(Object targetEntity) throws IllegalAccessException {
boolean isList = List.class.isAssignableFrom(targetEntity.getClass());
if(isList){
List list = (List) targetEntity;
if(CollectionUtils.isEmpty(list)){
return;
}
for(Object target : list){
doMarkCreateAudditingSingle(target);
}
return ;
}
doMarkCreateAudditingSingle(targetEntity);
}
private static void doMarkCreateAudditingSingle(Object targetEntity) throws IllegalAccessException {
List fieldList = getFields(targetEntity);
doMarkCreateAuditing(targetEntity, fieldList);
}
private static void doMarkCreateAuditing(Object targetEntity, List fieldList) throws IllegalAccessException {
Date currentDate = new Date();
for (Field field : fieldList) {
if (AnnotationUtils.getAnnotation(field, CreateBy.class) != null) {
ReflectionUtils.makeAccessible(field);ReflectionUtils.setField(field,targetEntity,getAuditingProvider().auditingUser());
}
if (AnnotationUtils.getAnnotation(field, CreateAt.class) != null) {
ReflectionUtils.makeAccessible(field);ReflectionUtils.setField(field,targetEntity,currentDate);
}
}
}
public void markUpdateAuditing(Object targetEntity) throws IllegalAccessException {
doMarkUpdateAuditingSingle(targetEntity);
}
private static void doMarkUpdateAuditingSingle(Object targetEntity) throws IllegalAccessException {
List fieldList = getFields(targetEntity);
Date currentDate = new Date();
for (Field field : fieldList) {
if (AnnotationUtils.getAnnotation(field, UpdateBy.class) != null) {
ReflectionUtils.makeAccessible(field); ReflectionUtils.setField(field,targetEntity,getAuditingProvider().auditingUser());
}
if (AnnotationUtils.getAnnotation(field, UpdateAt.class) != null) {
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field,targetEntity,currentDate);
}
}
}
private static List getFields(Object targetEntity) {
List fieldList = new ArrayList<>();
Class tempClass = targetEntity.getClass();
while (tempClass != null) {//当父类为null的时候说明到达了最上层的父类(Object类).
fieldList.addAll(Arrays.asList(tempClass.getDeclaredFields()));
tempClass = tempClass.getSuperclass(); //得到父类,然后赋给自己
}
return fieldList;
}
private AuditingProvider getAuditingProvider() {
return SpringContexts.getBeansByClass(AuditingProvider.class)
.values()
.stream()
.filter(auditingProvider -> !SystemMetaObject.forObject(auditingProvider).hasGetter("h"))//不是MapperProxy的代理类
.findFirst()
.orElseThrow(NotFoundAuditingProvider::new);
}
}
审计人提供者 AuditingProvider
public interface AuditingProvider {
String auditingUser();
}
需要实现提供器类,负责提供审计人的操作,默认实现如:
public class MybatisAuditingProvider implements AuditingProvider {
@Override
public String auditingUser() {
return String.valueOf(SecurityUser.get().getUserId());
}
}
总结
自动插入审计字段信息的设计已经设计好了。、 当然,在此基础上,我还完成了AutoConfig的注解类@EnableMybatisAudit,你也可以脑洞大开的试一下。
方法级别数据验证
无论写什么样的代码,提供出去的api接口,一定要很健壮。
先不考虑业务逻辑的前提下,我们应该很明确的指出定义的接口的出参和入参的要求
举例:
框架: spring boot
验证:jsr 303规范 (hibernate实现)
方式:spring 提供的方法级别验证
开启方法级别验证方式
直接使用hibernate实现的国际化就可以,不要重复造轮子
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor(@Autowired LocalValidatorFactoryBean localValidatorFactoryBean) {
MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
methodValidationPostProcessor.setValidator(localValidatorFactoryBean);
return methodValidationPostProcessor;
}
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver slr = new CookieLocaleResolver();
slr.setDefaultLocale(Locale.CHINA);
slr.setCookieMaxAge(3600);
return slr;
}
@Bean
public LocalValidatorFactoryBean validator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setProviderClass(org.hibernate.validator.HibernateValidator.class);
validator.setValidationMessageSource(getMessageSource());
return validator;
}
private ResourceBundleMessageSource getMessageSource(){
ResourceBundleMessageSource rbms = new ResourceBundleMessageSource();
rbms.setDefaultEncoding(StandardCharsets.UTF_8.toString());
rbms.setUseCodeAsDefaultMessage(false);
rbms.setCacheSeconds(60);
rbms.setBasenames("classpath:org/hibernate/validator/ValidationMessages");
return rbms;
}
使用方式
我们要在业务接口中定义出参和入参的要求,这些注解我相信你很熟悉了,对 ,没错,是jsr 303规范验证:
public interface PlanService {
@NotNull
CreatedPlanBO addPlanCompleted(@NotNull @Valid CreatedPlanBO createdPlanBO);
@NotNull
Plan addPlan(@NotNull @Valid PlanBO planBO);
Plan addOneYearToPlan(@NotNull Integer id) throws PlanNotFoundException;
void deleteOneYearToPlan(@NotNull Integer id)
throws PlanNotFoundException,PlanAtLeastOneYearException;
void deletePlan(@NotNull Integer id) throws PlanNotFoundException;
}
要在实现类中一定要标识注解 @Validated, 否则不生效的,看一下实现:
@Validated
@Service
public class PlanServiceImpl implements PlanService {
//省略代码实现
}
这样我们使用其他人写的service时,直接看到接口定义就可以使用了,很方便。
尤其这样的接口定义 :
@NotNull
List listString();
这样,我们就知道这个接口返回值,如果没有数据,则会返回空集合,而不是空对象(null),可以减少空指针异常的发生,并且这个接口的实现者也需要按照这样的接口定义来写。
这里可以称他为约定编程。
其他方式
不一定必须使用spring提供的方法验证,如果项目中不是spring项目,或者使用了早期不支持这样方法验证的spring呢,我们要怎么办呢?
推荐要使用Guava:
Preconditions.checkNotNull();
Preconditions.checkArgument();
但是这样的话,就要和团队人员进行约束,比如入参必须不能为空,返回值是集合的话不能为空对象等。。
我给大家的建议是,如果有集合返回值的话,使用前需要进行验证:
CollectionUtils.isEmpty(Collection);
然后才进行使用,没有看到明显的方法约束的话,一定要这么做。
相信墨菲定律一定存在吧!
总结
方法级别的验证,可以带来接口使用上的约束,对于调用者和实现者来说,都是一个不错的选择,如果可以,一定要这么做,如果不可以,请约束你自己!
返回值约束
我相信,你尝尝会有这样的迷惑: 返回值我到底能不能为空呢?
有的程序员给了自己一个错误的判定:”可以为空,调用者调用的时候判断一下,如果可以为空,则怎样怎样,不为空,则怎样怎样”
如果你脑海中也有以上的答案,请忘记它,因为你是错误的,起码在jdk8之后是错误的
举例 :
环境:jdk8
理念:异常设计
关键字:Optional
Optional
java.util.Optional是jdk8给我看到的很棒的东西,他教给了我如何去处理空值的问题.
Optional的语义是:可以为空
看代码:
Optional get(id);
这段代码,很漂亮,它告诉我们,通过id获取Plan,这个Plan是有可能为空的,那么可以理解为:”设计接口的作者,希望你可以通过Plan是否存在来控制你的业务逻辑”
怎么样,是不是很棒
在看一个接口:
Plan get(id);
这个接口是很迷惑的,你只知道通过id可以获取Plan,但是其他的你却不知道。
这样的接口很糟糕,因为调用者害怕调用的Plan是空值,这样,他们就不得不做一些没必要的验证了。
有没有什么更好的方式来告诉调用者,接口一定不能为空的?
异常声明
异常是个好东西,比如上边的接口,我们设计成:
Plan get(id) throws PlanNotFoundException;
这样调用者就很清晰的知道,如果我通过id获取Plan的时候,是有可能因为找不到抛出异常的,这样的设计,更起到警戒的作用,调用者会很清晰的知道get(id)方法必须有返回值。
方法验证
还有一种方式写法,已经介绍过了,不在赘述了。
@Notnull
Plan get(id);
总结
三种接口定义方法,由你来选择:
Optional get(id);
Plan get(id) throws PlanNotFoundException;
@Notnull
Plan get(id);
希望这样的总结,可以给你带来启示
业务逻辑中的门面模式
我们经常碰到这样的场景,自己模块的service通常要调用其他模块的service使用,当然,这种情况下,一般都会直接调用,然后完成自己的service.
我想说,如果你是这么想的,恭喜你,后期你会遇到无穷无尽的service嵌套service,并且埋点做起来很难。如果你的service很慢,你会检查是你的service慢,还是调用其他人的service慢。这样会给你早晨很大的麻烦.
那如何去做呢,我给大家一个建议
使用门面模式进行封装。
创建外部模块
在自己的模块创建 external 包,用来包装你调用的其他service
写法
public interface PlanFamingFacade {
void checkFarmingTaskItem(@NotNull Plan plan,@NotNull Integer FarmingTaskItemId);
}
他是一个面向FamingService服务类的一个转化层,我相信,好处应该是不言而喻的吧
你可以做aop,做埋点,检测各种其他模块调用的效率,而且,其他模块变化的时候,你可以快速做出相应,比如做自己模块的cache.
而且,你可以将调用模块返回的对象,转成你自己模块的对象(BO 对象),这样更加灵活。
总结
门面的模式,其实在微服务设计中也是常常存在的,比如调用不同服务时的熔断机制。
如果可以,一定要使用门面模式,使用后的不久,你会来感谢我的!
业务异常设计
我之前详细讲过关于异常的理解,如果没看过,可以去看一下: 如何优雅的设计java异常
这次要讨论的是业务模块中,如何实际的使用和设计异常。以及异常在代码中的写法。
举例:
工具: lombok
总异常码设计
按照模块来划分大的异常码
public class ErrorCodeBase {
public static final long HERBS = 20000;
public static final long PROCESSING = 30000L;
public static final long PLAN = 40000L;
public static final long SEED = 50000L;
public static final long SOLAR_TERM = 50000L;
public static final long BLOCK = 70000L;
public static final long PRODUCTION = 80000L;
public static final long COMPANY = 90000L;
}
通用异常设计
义务异常需要定为为RuntimeException,这样写的好处是不需要让调用者通过异常来控制业务逻辑
public abstract class ServiceException extends RuntimeException{
private List errors ;
public ServiceException(String description,String errorCode,String errorMsg){
this(description);
this.addError(errorCode,errorMsg);
}
public ServiceException(String description){
super(description);
this.errors = Lists.newArrayList();
}
public ServiceException addError(String errorCode,String errorMsg){
this.errors.add(new ErrorInfo(errorCode,errorMsg));
return this ;
}
public List getErrors() {
return errors;
}
protected String errorCode(long base,long index){
return String.valueOf(base + index);
}
}
其中ErrorInfo比较简单:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ErrorInfo {
private String code ;
private String message ;
}
主要是为了Controller中异常信息的转化, 可以将异常信息合理的传给前端,进行判断和显示
子模块异常设计
子模块通过ServiceException来定义各个子模块的异常.
异常码设计,通过偏移量来进行累加,这样统一管理,避免异常码重复
class ErrorCodes {
public static final String PLAN_NOT_FOUND = String.valueOf(ErrorCodeBase.PLAN + 1L);
public static final String YEAR_OUT_OF_BUNDS = String.valueOf(ErrorCodeBase.PLAN + 2L);
public static final String PLAN_TASK_NOT_FOUND = String.valueOf(ErrorCodeBase.PLAN + 3L);
public static final String PLAN_TASK_SUPPLY_NOT_FOUND = String.valueOf(ErrorCodeBase.PLAN + 4L);
public static final String PLAN_AT_LEAST_ONE_YEAR = String.valueOf(ErrorCodeBase.PLAN + 5L);
public static final String PLAN_HERBS_NOT_FOUND_EXCEPTION = String.valueOf(ErrorCodeBase.PLAN + 6L);
public static final String PLAN_NOT_FOUND_FARMING_TASK_ITEM_EXCEPTION = String.valueOf(ErrorCodeBase.PLAN + 7L);
public static final String PLAN_SOLAR_TERM_NOT_FOUND_EXCEPTION = String.valueOf(ErrorCodeBase.PLAN + 8L);
public static final String PLAN_COMPANY_ROLE_NOT_FOUND_EXCEPTION = String.valueOf(ErrorCodeBase.PLAN + 9L);
}
class PlanException extends ServiceException {
public PlanException(String errorCode, String errorMsg) {
super("plan_error", errorCode, errorMsg);
}
}
最终的子模块的某异常类,如下:
public class PlanNotFoundException extends PlanException {
public PlanNotFoundException( String errorMsg) {
super(PLAN_NOT_FOUND, errorMsg);
}
public PlanNotFoundException() {
super(PLAN_NOT_FOUND, "种植计划未找到");
}
}
业务异常使用
在业务层接口定义中,你可以暴露出可能出现的异常,然后由调用者选择是否需要处理你的异常(因为是非受检异常),比如:
@NotNull
PlanTask addEmtpyPlanTask(@NotNull Integer planId, @NotNull String year)
throws PlanNotFoundException, YearIsOutOfBundsException;
当然,对于中小型项目的异常处理,做法比较简单,可以在Controller层做一个通用的异常处理类,然后直接将ServiceException中的ErrorInfo直接转成前端可读的异常信息,做法比较简单,我就不在赘述了,请看代码:
@ExceptionHandler(ServiceException.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public ErrorEntity handlerServiceException(ServiceException e){
log.warn("ServiceException:"+e.getMessage(),e);
if( log.isDebugEnabled() ){
log.debug("--ServiceException:"+e.getMessage());
for(ErrorInfo errorInfo : e.getErrors()){
log.debug("----code:{},message:{}",errorInfo.getCode(),errorInfo.getMessage());
}
}
return new ErrorEntity()
.setStatus(HttpStatus.FORBIDDEN.value())
.setDescription(e.getMessage())
.setErrors(e.getErrors());
}
当然,除了上述方式以外,如果调用者需要进行异常的转化,也可以直接try..catch,然后转化成自己的异常,或者做一些其他业务逻辑(但是不建议通过异常来控制业务流程)
异常设计最小化
我之前还设计过一种比较简化的异常方式,可以在小型项目中进行异常处理(一定记住,不能因为项目小,就不进行异常处理,它可以很简单,但不能没有):
public enum Exceptions {
//权限角色
NOT_FIND_ROLE(1000001L,"找不到相关角色!"),
//学生模块异常
STUDENT_NO_TEXIST(2000001L,"学生不存在"),
STUDENT_IS_TEXIST(2000002L,"学生存在"),
STUDENT_IMPORT_IS_TEXIST(2000003L,"导入的数据学生存在");
Long errCode;
String errMsg;
Exceptions(Long errCode, String errMsg){
this.errCode = errCode;
this.errMsg = errMsg;
}
public ServiceException exception(){
return new ServiceException(this.errCode,this.errMsg);
}
public ServiceException exception(Object errData){
return new ServiceException(this.errCode,errData,this.errMsg);
}
public ServiceException exception(Object errData,Throwable e){
return new ServiceException(this.errCode,this.errMsg,errData,e);
}
}
这个异常,在使用起来非常方便:
throw Exceptions.NOT_FIND_ROLE.exception();
这样的简约风格,可以给你带来很好的效果
因为这样的设计是对异常码和异常信息编程,而不是对异常类型进行编程。
所以缺点就是,在service接口定义中,不能指定异常类型。
一种简单风格的举例
“如果找不到,需要抛出异常”,这是一个很常见的设计方式,接口定义如下:
void deletePlan(@NotNull Integer id) throws PlanNotFoundException;
实现起来,建议配合jdk8一起使用:
Plan plan = Optional.ofNullable(planRepository.selectByPrimaryKey(id))
.orElseThrow(PlanNotFoundException::new);
因为不用一直if..else的判断,所以,你的代码会很干净。
总结
两种异常风格的设计,和如何使用都在这里了,建议这么去设计你的异常。
无论异常处理怎么复杂,都是一个异常链调用的过程。非常简单!
枚举状态设计
你常常会在义务代码中见到这样的需求,这条消息的状态可能是已删除,或者这条订单的状态是已发货,这时候,则会用到枚举值,我给大家的建议是,枚举值一定要设计标志位,这样,可以将标志位存在数据库中,减少存储大小,也可以在代码中确保标志位不会改变。
设计枚举
public enum NeedGpsEnum {
NEED(0, "需要"),
NO_NEED(1, "不需要");
NeedGpsEnum(int value, String type) {
this.value = value;
this.type = type;
}
private int value;
private String type;
public int getValue() {
return value;
}
}
这样的设计,保证了NEED 和NO_NEED的标志位(0/1)是不会因为枚举位置的改变而变动的,而且,从代码可读性角度,我们可以知道NEED和NO_NEED是什么意思(需要/不需要)。
这样的可读性和易用性都比较强,建议这么写!
枚举应用场景
《Effective java》中,建议可以用枚举来实现单例,但是我不建议你这么做,他的语义是很不清晰的。
真正的枚举用法,我理解可以分为以下几种:
状态模式/策略模式
业务属性状态(如上)
固定的常量属性(比如 月份这样固定的枚举值)
希望大家按照这三种用法去使用。这样的设计会给你带来很多好处。
总结
枚举的使用场景给大家介绍完了,希望你再创新之前,可以先按照这样的思想去思考。等你真正创新,想到了新的枚举用法,请给我留言。
原文链接:http://lrwinx.github.io/2017/10/13/java%E7%BC%96%E7%A8%8B%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/