java编程最佳实践

导语

笔者一直保持着一定速度的文档更新频率(每半年更新一篇文章),希望在这段学习的时间中,能给读者带来一些启发,本篇文章从”数据库审计字段”,”方法级别数据验证”,””返回值约束”,“业务逻辑中的门面模式”,“业务异常设计”,“枚举状态设计”等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<T, PK extends Serializable, E> {
        long countByExample(E example);
        int deleteByExample(E example);
        int deleteByPrimaryKey(PK id);
        int insert(T record);
        int insertSelective(T record);
        List<T> 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主要有两个方法:

  1. markCreateAuditing(Object) : 标记插入实体的数据值
  2. 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<Field> fieldList = getFields(targetEntity);
        doMarkCreateAuditing(targetEntity, fieldList);
    }
    private static void doMarkCreateAuditing(Object targetEntity, List<Field> 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<Field> 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<Field> getFields(Object targetEntity) {
        List<Field> 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<String> listString();

这样,我们就知道这个接口返回值,如果没有数据,则会返回空集合,而不是空对象(null),可以减少空指针异常的发生,并且这个接口的实现者也需要按照这样的接口定义来写。

这里可以称他为约定编程。

其他方式

不一定必须使用spring提供的方法验证,如果项目中不是spring项目,或者使用了早期不支持这样方法验证的spring呢,我们要怎么办呢?

推荐要使用Guava:

Preconditions.checkNotNull();
Preconditions.checkArgument();

但是这样的话,就要和团队人员进行约束,比如入参必须不能为空,返回值是集合的话不能为空对象等。。

我给大家的建议是,如果有集合返回值的话,使用前需要进行验证:

CollectionUtils.isEmpty(Collection);

然后才进行使用,没有看到明显的方法约束的话,一定要这么做。

相信墨菲定律一定存在吧!

总结

方法级别的验证,可以带来接口使用上的约束,对于调用者和实现者来说,都是一个不错的选择,如果可以,一定要这么做,如果不可以,请约束你自己!

返回值约束

我相信,你尝尝会有这样的迷惑: 返回值我到底能不能为空呢?

有的程序员给了自己一个错误的判定:”可以为空,调用者调用的时候判断一下,如果可以为空,则怎样怎样,不为空,则怎样怎样”

如果你脑海中也有以上的答案,请忘记它,因为你是错误的,起码在jdk8之后是错误的

举例 :
环境:jdk8
理念:异常设计
关键字:Optional

Optional

java.util.Optional是jdk8给我看到的很棒的东西,他教给了我如何去处理空值的问题.

Optional的语义是:可以为空

看代码:

Optional<Plan> 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<Plan> 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<ErrorInfo> errors ;

    public ServiceException(String description,String errorCode,String errorMsg){
        this(description);
        this.addError(errorCode,errorMsg);
    }

    public ServiceException(String description){
        super(description);
        this.errors = Lists.<ErrorInfo>newArrayList();
    }

    public ServiceException addError(String errorCode,String errorMsg){
        this.errors.add(new ErrorInfo(errorCode,errorMsg));
        return this ;
    }

    public List<ErrorInfo> 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<ErrorInfo> 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<ErrorInfo>()
                   .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》中,建议可以用枚举来实现单例,但是我不建议你这么做,他的语义是很不清晰的。

真正的枚举用法,我理解可以分为以下几种:

  1. 状态模式/策略模式
  2. 业务属性状态(如上)
  3. 固定的常量属性(比如 月份这样固定的枚举值)

希望大家按照这三种用法去使用。这样的设计会给你带来很多好处。

总结

枚举的使用场景给大家介绍完了,希望你再创新之前,可以先按照这样的思想去思考。等你真正创新,想到了新的枚举用法,请给我留言。(哈哈)

关于作者

坚持原创技术分享,您的支持将鼓励我继续创作!