讲解在真正项目开发中常见的6个方面,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 {

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/

你可能感兴趣的:(讲解在真正项目开发中常见的6个方面,java编程的解决方法)