CRUD最佳实践

bean转换

现在的Web项目,大多都是基于MVC三层架构来设计的,主要分为Repository(数据访问层)、Service(业务逻辑层)、Controller(接口层)。
其中Repository和Entity负责数据访问层、Service和BO负责业务逻辑层、Controller和VO负责接口层。
层级之间数据的传递,就会涉及到数据模型的转换。

1. Controller


/**
 * @Author: zhangyu
 * @Description: 用户相关接口
 * @Date: in 2019/8/27 22:34
 */
@RestController
@RequestMapping("/sys/user")
public class SysUserController {

    @Autowired
    private SysUserService userService;
    
    @RequestMapping("/save")
    public JsonData saveUser(UserParam userParam){
        userService.save(userParam);
        return JsonData.success();
    }

}

2. Service

/**
 * @Author: zhangyu
 * @Description: 用户Service层
 * @Date: in 2019/8/27 21:45
 */
@Service
public class UserService {

	@Autowired
	private UserRepository userRepository;

    /**
     * 新增用户
     */
	public void save(UserParam userParam) {
		// 将user参数类转为实体类
		User user = User.convert(userParam);
		// ... 
		userRepository.save(user);
	}
}

/**
 * 用户参数类
 */
public class UserParam {

    private String username;
    private String telephone;
    private String email;
    private Integer status;
	
	// 参数类转换实体类
	public static User convert(UserParam userParam) {
		User user = new User();
		// BeanUtils.copyProperties()是一个浅拷贝方法,只需要把两个对象的属性名称和类型设置成一样的就可以实现属性复制。
		BeanUtils.copyProperties(userParam, user);
		return user;
	}	
	// Getter and Setter
}

3. Repository

public interface UserRepository {
	void save(User user);
}

抽象接口定义

如果像这样的转换操作有很多,那就应该定义一个接口,让bean转换过程规范化。

1. 定义接口

public interface Convert<T, R> {
	R convert(T t);
}

2. 参数类实现转换接口

public class UserParam {
    private String username;
    private String telephone;
    private String email;
    private Integer status;
	
	// 用内部类实现Convert接口
	private static class UserConvert implements Convert<UserParam, User> {
		public User convert(UserParam userParam) {
			User user = new User();
			BeanUtils.copyProperties(userParam, user);
			return user;
		}
	}
	// Getter and Setter
}

3. 使用

@Service
public class UserService {

	@Autowired
	private UserRepository userRepository;

    /**
     * 新增用户
     */
	public void save(UserParam userParam) {
		User user = User.UserConvert.convert(userParam);
		// ... 
		userRepository.save(user);
	}
}

lombok

lombok可以帮我们简化Getter和Setter方法。

去掉Getter/Setter

@Getter
@Setter
public class UserParam {

    private String username;
    private String telephone;
    private String email;
    private Integer status;
    
}

@Data是一个组合注解,等价于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode

链式风格

@Builder
public class UserParam {

    private String username;
    private String telephone;
    private String email;
    private Integer status;
    
	public static void main(String[] args) {
		UserParam userParam = UserParam.builder()
										.username("zhangsan")
										.telephone("1234567891")
										.email("zhangsan@qq.com")
										.status(1)
										.build();
	}
}

bean验证

为什么需要验证?
尽管我们对外提供的接口通常只会有前端来调用,而且前端也会对参数做校验,但如果前端的验证逻辑有问题,或者一些恶意用户通过一些特殊的手段直接把数据传入到后端的接口中,就可能产生脏数据!

jsr 303

hibernate的jsr 303提供了很好的实现,示例:

/**
 * @Author: zhangyu
 * @Description: 用户参数类
 * @Date: in 2019/8/27 21:39
 */
@Getter
@Setter
@ToString
@Builder
public class UserParam {
	@NotBlank(message = "用户名不能为空")
    @Length(min = 1, max = 20, message = "用户名长度需要在1-20以内")
    private String username;

    @NotBlank(message = "电话不能为空")
    @Length(min = 5, max = 11, message = "请检查电话长度是否正确")
    private String telephone;

    @NotBlank(message = "邮箱不能为空")
    @Length(min = 5, max = 50, message = "邮箱长度需要在5-50以内")
    private String email;

    @NotNull(message = "用户状态不能为空")
    @Min(message = "用户状态不合法",value = 0)
    @Max(message = "用户状态不合法", value = 2)
    private Integer status;
}

验证:

/**
 * @Author: zhangyu
 * @Description: 用户Service层
 * @Date: in 2019/8/27 21:45
 */
@Service
public class UserService {

	@Autowired
	private UserRepository userRepository;

    /**
     * 新增用户
     */
	public void save(UserParam userParam) {
		// 使用封装好的工具类校验bean
		BeanValidator.validate(userParam);
		User user = User.convert(userParam);
		// ... 
		userRepository.save(user);
	}
}

/**
 * @Author: zhangyu
 * @Description: 用来校验bean是否符合要求
 * @Date: in 2019/8/25 14:55
 */
public class BeanValidator {
	// 校验工厂
	private static final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
	
	/**
     * 校验单个类,当参数不符合要求时会抛出ParamException
     */
	public static <T> void validate(T t, Class... groups) throws ParamException {
		Preconditions.checkNotNull(t);
		Validator validator = validatorFactory.getValidator();
		Set<ConstraintViolation> validateResult = validator.validate(t, groups);
		if (CollectionUtils.isNotEmpty(validateResult)) {
			LinkedHashMap<String, String> errors = Maps.newLinkedHashMap();
			Iterator iterator = validateResult.iterator();
			// 封装错误信息
			while (iterator.hasNext()) {
				ConstraintViolation validation = iterator.next();
				errors.put(validation.getPropertyPath().toString(), validation.getMessage());
			}
			throw new ParamException(errors.toString());
		}
	}

	/**
     * 校验多个类,当参数不符合要求时会抛出ParamException
     */
     public static void validateList(Collection<?> collection) throw new ParamException {
		Preconditions.checkNotNull(collection);
		Iterator<?> iterator = collection.iterator();
		while (iterator.hasNext()) {
			validate(iterator.next());
		}
	 }

}

Google Guava

guava是谷歌提供的工具类框架。

集合

// 创建集合
List<String> list = Lists.newArrayList();
Set<String> set = Sets.newHashSet();
Map<String, String> map = Maps.newHashMap();

// 创建不可变集合
ImmutableList<String> iList = ImmutableList.of("1", "2", "3");
ImmutableSet<String> iSet = ImmutableSet.of("s1", "s2");
ImmutableMap<String, String> iMap = ImmutableMap.of("k1", "v1", "k2", "v2");

// 其它黑科技集合
// ArrayListMultimap:如果put相同的key,会把value变为list。类似 => Map<Integer, List<Integer>>
Multimap<Integer, List<Integer>> listMap = ArrayListMultimap.create();
listMap.put(1,1);
listMap.put(1,2);
List<Integer> list = (List<Integer>) listMap.get(1);	// [1,2]


字符串

// 1. 拆分字符串
String str = "1-2-3-4-5";
String[] strs = Splitter.on("-").split(str);

基本类型的工具类

guava提供了Bytes、Shorts、Floats、Doubles、Longs、Ints、Chars、Booleans这些基本类型的工具类

boolean exist = Ints.contains(int[] array, int target);	// 判断int数组中是否存在某个int值
int max = Ints.max(int... array);	// 获取int数组中最大的int值
int min = Ints.min(int... array);	// 获取int数组中最小的int值
int[] array = Ints.concat(int[]... arrays);	// 将多个int值转为int数组
List<Integer> list = Ints.asList(int... backingArray); // 将多个int值转为int集合

// 其余的工具类中的方法就不一一列举了...

状态枚举设计

	@Getter
	public enum CommonStatus {
		VALID(1, 有效),
		INVALID(0, 无效);
		
		private int code;
		private String desc;
		CommonStatus(int code, String desc) {
			this.code = code;
			this.desc = desc;
		}
	}

通用返回值

在做接口设计时,我们通常会和前端约定好返回的数据格式。所以我们通常会在每个接口中都创建一个通用返回值的对象,这样与业务无关的操作应该考虑将其设计成通用的。

1. 定义注解

/**
 * @Author: zhangyu
 * @Description: 忽略封装统一的响应结果
 * @Date: in 2020/4/21 16:36
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreResponse {
}

2. 通用返回对象

/**
 * @Author: zhangyu
 * @Description: 统一的响应类
 * @Date: in 2020/4/21 16:28
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CommonResponse<T> implements Serializable {

    private Integer code;
    private String message;
    private T data;

    public CommonResponse(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

}

3. 使用ResponseBodyAdvice封装返回值

/**
 * @Author: zhangyu
 * @Description: 封装统一的响应结果
 * @Date: in 2020/4/21 16:30
 */
@RestControllerAdvice
public class CommonResponseAdvice implements ResponseBodyAdvice<Object> {

    /**
     * 判断目标类/方法是否包含{@link IgnoreResponse}注解
     */
    @Override
    @SuppressWarnings("all")
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        if (methodParameter.getDeclaringClass().isAnnotationPresent(IgnoreResponse.class)) {
            return false;
        }
        if (methodParameter.getMethod().isAnnotationPresent(IgnoreResponse.class)) {
            return false;
        }
        return true;
    }

    /**
     * 封装响应结果
     */
    @Override
    @SuppressWarnings("all")
    public Object beforeBodyWrite(Object o,
                                  MethodParameter methodParameter,
                                  MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> aClass,
                                  ServerHttpRequest serverHttpRequest,
                                  ServerHttpResponse serverHttpResponse) {
        CommonResponse<Object> response = new CommonResponse<>(0, "");
        if (null == o) {
            return response;
        } else if (o instanceof CommonResponse) {
            return (CommonResponse<Object>) o;
        } else {
            response.setData(o);
        }
        return response;
    }
}

4.使用

@RestController
public class TestController {

	@GetMapping(value = "/")
	public String index() {
		return "hello";	// 这里等价于 new CommonResponse(0, "", "index");
	}
	
	@IgnoreResponse	// 不封装成通用返回值
	@GetMapping(value = "/")
	public String index2() {
		return "hello"; 
	}
	
}

自动注入审计字段

在做业务系统数据库设计的时候,我们通常都会创建一些相关的审计字段,比如:create_at(创建时间),create_by(创建人),update_at(更新时间),update_by(更新人)。
每次都获取当前的操作人,将数据设置到实体类中,然后写到数据库里。这种无关业务的操作最好设计成通用的。

1. 定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {FIELD, METHOD})
public @interface CreateAt {}

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {FIELD, METHOD})
public @interface CreateBy {}

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {FIELD, METHOD})
public @interface UpdateAt {}

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {FIELD, METHOD})
public @interface UpdateBy {}

2. AOP拦截

定义aop拦截insert和update方法,对参数对象中包含@CreateAt、@CreateBy、@UpdateAt、@UpdateBy注解的属性进行赋值

/**
 * @Author: zhangyu
 * @Description: 拦截insert*和update*方法,注入审计字段
 * @Date: in 2019/9/2 18:57
 */
@Aspect
@Component
public class AuditFieldInjectionAOP  {
    @Pointcut("execution(* com.rainy.permission.repository..*.*Repository.insert*(..))")
    private void insertPoint() {
    }

    @Pointcut("execution(* com.rainy.permission.repository..*.*Repository.update*(..))")
    private void updatePoing() {
    }

    @Around("insertPoint()")
    public Object insertAround(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();
        for (Object arg : args) {
            AuditFieldAction.createAuditing(arg);
        }
        return pjp.proceed();
    }

    @Around("updatePoing()")
    public Object updateAround(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();
        for (Object arg : args) {
            AuditFieldAction.updateAuditing(arg);
        }
        return pjp.proceed();
    }
}

3. 处理审计字段

/**
 * @Author: zhangyu
 * @Description: 处理审计字段
 * @Date: in 2019/9/2 19:23
 */
public class AuditFieldAction {
	public static void createAuditing(Object entity) {
		if (entity instanceof List) {
			List list = (List) entity;
			for(Object target : list){
                doCreateAuditing(entity);
            }
            return;
		}
		doCreateAuditing(entity);
	}

	private static void doCreateAuditing(Object entity) {
		List<Field> fields = getFields(entity);
		for (Field field : fields) {
			if (AnnotationUtils.getAnnotation(field, CreateBy.class) != null) {
                ReflectionUtils.makeAccessible(field);
                ReflectionUtils.setField(field,entity,ContextHolder.getCurrentUser().getId());
            }
            if (AnnotationUtils.getAnnotation(field, CreateAt.class) != null) {
                ReflectionUtils.makeAccessible(field);
                ReflectionUtils.setField(field,entity,LocalDateTime.now());
            }
		}
		// 创建时填充updateAt和updateBy
		doUpdateAuditing(entity, fields);
	}
	
	public static void updateAuditing(Object entity) {
		List<Field> fields = getFields(entity);
		doUpdateAuditing(entity, fields);
	}
	private static void doUpdateAuditing(Object entity, List<Field> fields) {
		for (Field field : fields) {
			if (AnnotationUtils.getAnnotation(field, UpdateBy.class) != null) {
                ReflectionUtils.makeAccessible(field);
                ReflectionUtils.setField(field,entity,ContextHolder.getCurrentUser().getId());
            }
            if (AnnotationUtils.getAnnotation(field, UpdateAt.class) != null) {
                ReflectionUtils.makeAccessible(field);
                ReflectionUtils.setField(field,entity,LocalDateTime.now());
            }
		}
	}

	private static void doCreateAuditing(Object entity) {
		List<Field> fields = getFields(entity);
		for (Field field : fields) {
			if (AnnotationUtils.getAnnotation(field, UpdateBy.class) != null) {
                ReflectionUtils.makeAccessible(field);
                ReflectionUtils.setField(field,entity,ContextHolder.getCurrentUser().getId());
            }
            if (AnnotationUtils.getAnnotation(field, UpdateAt.class) != null) {
                ReflectionUtils.makeAccessible(field);
                ReflectionUtils.setField(field,entity,LocalDateTime.now());
            }
		}
	}

	private static List<Field> getFields(Object entity) {
		List<Field> fields = Lists.newArrayList();
		Class clazz = entity.getClass();
        while (clazz != null) {//当父类为null的时候说明到达了最上层的父类(Object类).
            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
            clazz= clazz.getSuperclass(); //得到父类,然后赋给自己
        }
        return fields;
	}
}

4. 过滤器和ThreadLocal保存当前会话信息

/**
 * @Author: zhangyu
 * @Description: 登录过滤器
 * @Date: in 2019/9/1 12:55
 */
@Slf4j
public class LoginFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        User user = (User) request.getSession().getAttribute("user");
        if(user == null){
            String path = "/permission/signin.html";
            response.sendRedirect(path);
            return;
        }
        // 保存会话中的用户和请求信息
        RequestHolder.add(user);
        RequestHolder.add(request);
        filterChain.doFilter(request, response);
        return;
    }

    @Override
    public void destroy() {
    }
}


保存上下文信息

/**
 * @Author: zhangyu
 * @Description: 用来保存当前上下文信息
 * @Date: in 2019/9/1 12:41
 */
public class ContextHolder {

    //存放当前会话中的用户信息
    private static final ThreadLocal<User> userHolder = new ThreadLocal<>();

    //存放当前会话中的HttpServletRequest
    private static final ThreadLocal<HttpServletRequest> requestHolder = new ThreadLocal<>();

    public static void add(User user){
        userHolder.set(user);
    }

    public static void add(HttpServletRequest request){
        requestHolder.set(request);
    }


    public static User getCurrentUser(){
        return userHolder.get();
    }


    public static HttpServletRequest getCurrentRequest(){
        return requestHolder.get();
    }

    /**
     * 从ThreadLocal中移除SysUser对象和Request对象
     */
    public static void remove(){
        userHolder.remove();
        requestHolder.remove();
    }


}

在拦截器中删除掉上下文信息

/**
 * @Author: zhangyu
 * @Description: 请求监听器
 * @Date: in 2019/8/25 16:32
 */
@Slf4j
public class HttpInterceptor extends HandlerInterceptorAdapter {

    private static final String REQUEST_START_TIME = "requestStartTime";

    //处理请求前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();
        Map params = request.getParameterMap();
        log.info("request start uri:{}, params:{}", uri, JsonMapper.object2String(params));
        long start = System.currentTimeMillis();
        request.setAttribute(REQUEST_START_TIME, start);
        return true;
    }

    //请求正常结束后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    //任何情况下(包括异常情况)都会调用
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        String uri = request.getRequestURI();
        Long start = (Long) request.getAttribute(REQUEST_START_TIME);
        long end = System.currentTimeMillis();
        log.info("request complete uri:{}, cost:{}ms", uri, end - start);
        //请求执行结束后删除ThreadLocal中的信息, 防治内存溢出
        removeThreadLocalInfo();
    }

    /**
     * 移除ThreadLocal中的信息
     */
    public void removeThreadLocalInfo () {
        RequestHolder.remove();
    }
}

日志最佳实践

执行后,而不是执行前。

// 正例
xxxService.operate();
log.info("do something");

// 反例
log.info("do something");
xxxService.operate();

第一个日志可以很清楚的表明之前的操作是成功的,否则看到的就不是日志,而是异常信息了。
第二个日志会让人很困惑,我们没办法得知下面的操作是否成功。

单独的参数和消息。

// 正例
sender.send(message);
log.info("send message, [url: {}, cost: {}]", url, cost);

//反例
sender.send(message);
log.info("send message to {}", url)

WARNING和ERROR。

try {
	userService.register(user);
	log.info("user registered, [user: {}]", user);
} catch (IllegalBeanException e) {
	log.warn("illegal param, [user: {}, message: {}]", user, e.getMessage());
} catch (Exception e) {
	log.error("user register failed, [user: {}, error message: {}]", user, e.getMessage());
}

public class UserService {
	public void register(User user) throws IllegalBeanException {
		// 校验bean属性是否合法,不合法则抛出 {@link IllegalBeanException}
		BeanValidator.check(user);
		// do something...
	}
}

可以预期的异常,这是警告。
意料之外的一场,这是错误。

INFO用于业务,DEBUG用于技术。

public void register(User user) {
	// 保存用户信息
	log.debug("保存用户信息成功。[user: {}]", user);
	// 发送欢迎邮件
	log.debug("发送欢迎邮件成功。[user: {}, email: {}]", user, email);
	log.info("用户注册成功。[user: {}]", user);
}

debug日志可以用来更加详细的描述业务的执行过程。

基于充血模型的CRUD实践

充血模型与贫血模型

贫血模型就是把数据和行为分隔开,会破坏面向对象中的封装特性,是一种面向过程的编程风格。
比如我们平时开发的Web项目,通常都是基于MVC三层架构来设计的,项目分为Repository层、Service层、Controller层。
Entity和Repository负责数据访问层、BO和Service负责业务逻辑层、VO和Controller负责接口层。
但是实际上,Entity、BO和VO通常都只是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑,逻辑主要集中在Service层,然后通过Service来操作BO,这种被称为 贫血模型
充血模型是相反的,是把数据和业务逻辑封装到同一个类中,这种模型可以满足面向对象的封装特性
实际上,充血模型也还是按照MVC三层架构设计,只不过在Service层中使用Domain来代替BO,Domain既包含数据,也包含业务逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值