导读

在目前接触过的项目中大多数的项目都会涉及到: crud相关的操作, 哪如何优雅的编写crud操作呢?
带着这个问题,我们发现项目中大量的操作多是 创建实体 、删除实例、 修改实体、 查询单个实体、 分页查询多个实体, 我们有没有好的方式解决呢?
下面我给出crud编写的四种方式 循序渐进 ,并分析其优势劣势,希望有一种能适合你,如果你有其他方式可以留言讨论,在此权当抛砖引玉。

以下内容基于Spring Boot 、Spring MVC、 Spring Data JPA 如果你使用的也是相同的技术栈可以继续往下阅读,如果不是可以当作参考。

crud编写的四种方式

1 裸写crud

最简单最粗暴也是使用最多的一种方式,在写的多了之后可以用生成工具生成。

import lombok.AllArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

/**
 * @author yangrd
 * @date 2019/3/4
 **/
@AllArgsConstructor

@RestController
@RequestMapping("/api/banner")
public class BannerController {

    private BannerRepository repository;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Banner save(Banner banner) {
        return repository.save(banner);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        repository.deleteById(id);
    }

    @PutMapping("/{id}")
    public void update(@PathVariable("id") Banner db, @RequestBody Banner banner) {
        BeanUtils.copyProperties(banner, db);
        repository.save(db);
    }

    @GetMapping
    public Page<Banner> findAll(Pageable pageable) {
        return repository.findAll(pageable);
    }

    @GetMapping("/{id}")
    public Banner finOne(@PathVariable("id") Banner banner) {
        return banner;
    }
}

优势:能控制到代码的每一行并非所有的增删改查都需要

劣势:在业务简单的情况下会编写大量的类似代码 这个时候我们可以用泛型与继承解决 引出 AbstractCrudController

2 extend BaseCrudController

使用抽象的能力,通过抽象类对相同的代码进行封装,减少子类继续编写重复的代码。

import com.st.cms.common.spring.jpa.AbstractEntity;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

/**
 * @author yangrd
 * @date 2019/3/1
 **/
public abstract class BaseCrudController<T extends AbstractEntity, D extends JpaRepository<T, String>> {

    @Autowired
    protected D repository;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public T save(@RequestBody T t) {
        return repository.save(t);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable("id") String id) {
        repository.deleteById(id);
    }

    @PutMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void update(@PathVariable("id") T dbData, @RequestBody T t) {
        BeanUtils.copyProperties(t, dbData,"id");
        repository.saveAndFlush(dbData);
    }


    @GetMapping("/{id}")
    public T get(@PathVariable("id") T t) {
        return t;
    }

}

-

import com.st.cms.common.spring.mvc.BaseCrudController;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author yangrd
 * @date 2019/3/4
 **/
@AllArgsConstructor

@RestController
@RequestMapping("/api/banner")
public class BannerController extends BaseCrudController<Banner,BannerRepository> {

}

优势:在简单的crud操作中通过泛型与继承减少编写大量增删改查的方法

劣势:在findAll方法中入参数不好控制,通过HttpServletRequest可以解决这个问题 但有会引入大量的获取值的方法 因此BaseCrudController中不提供 findAll 方法 由用户编写

3 spring data rest

引入spring-boot-starter-data-rest,crud操作可以直接http调用 ,感兴趣的可以翻看 官方文档

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-rest</artifactId>
    </dependency>

优势:spring 家的东西 可以很好的与spring boot 整合 只需引入一个依赖即可

劣势:和之前业务中返回的数据格式内容不同 (此处也是好处 更统一规范 ,如果一开始前后端约定好数据格式就没有什么太大的问题)

4 ControllerHelper

重点来了哈哈 直接上代码

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.*;

import javax.persistence.EntityNotFoundException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

import static org.springframework.beans.BeanUtils.getPropertyDescriptor;
import static org.springframework.beans.BeanUtils.getPropertyDescriptors;

/**
 * @author yangrd
 * @date 2019/3/1
 **/
@Slf4j

@RestController
@RequestMapping("/api")
public class ControllerHelper implements ApplicationContextAware {

    private MappingManager mappingManager;

    @PostMapping("/{repository}")
    public ResponseEntity create(@PathVariable String repository, @RequestBody String reqJSON) {
        return mappingManager.getJpaRepository(repository).map(repo -> {
            Object object = mappingManager.getEntityObj(repository);
            Object req = JSON.parseObject(reqJSON, object.getClass());
            BeanUtils.copyProperties(req, object);
            return ResponseEntity.status(HttpStatus.CREATED).body(repo.saveAndFlush(object));
        }).
                orElseGet(() -> ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{repository}/{id}")
    public ResponseEntity delete(@PathVariable String repository, @PathVariable Long id) {
        return mappingManager.getJpaRepository(repository).map(repo -> {
            repo.deleteById(id);
            return ResponseEntity.noContent().build();
        }).
                orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PutMapping("/{repository}/{id}")
    public ResponseEntity update(@PathVariable String repository, @PathVariable Long id, @RequestBody String reqJSON) {
        return mappingManager.getJpaRepository(repository).map(repo -> {
            repo.findById(id).ifPresent(db -> {
                Object req = JSON.parseObject(reqJSON, db.getClass());
                BeanUtils.copyProperties(req, db);
                repo.saveAndFlush(db);
            });
            return ResponseEntity.noContent().build();
        }).
                orElseGet(() -> ResponseEntity.notFound().build());
    }


    @GetMapping("/{repository}/{id}")
    public ResponseEntity get(@PathVariable String repository, @PathVariable Long id) {
        return mappingManager.getJpaRepository(repository).map(repo -> ResponseEntity.ok(repo.findById(id))).
                orElseGet(() -> ResponseEntity.notFound().build());
    }

    @GetMapping("/{repository}")
    public ResponseEntity get(@PathVariable String repository, Pageable pageable) {
        return mappingManager.getJpaRepository(repository).map(repo -> ResponseEntity.ok(repo.findAll(pageable))).
                orElseGet(() -> ResponseEntity.notFound().build());
    }

    class MappingManager {
        private Map<String, JpaRepository> entity4Repositories = new HashMap<>();
        private Map<String, Class> entity4Class = new HashMap<>();

        MappingManager(ApplicationContext applicationContext) {
            Map<String, JpaRepository> repositoryBeans = applicationContext.getBeansOfType(JpaRepository.class);
            repositoryBeans.forEach((repositoryName, repositoryBean) -> {
                Class entityClass = MappingSupport.getEntityClass(repositoryBean);
                String entityClassName = MappingSupport.getEntityName(entityClass);
                entity4Repositories.put(entityClassName, repositoryBean);
                entity4Class.put(entityClassName, entityClass);
            });
        }

        public Optional<JpaRepository> getJpaRepository(String repository) {
            return Optional.ofNullable(entity4Repositories.get(repository));
        }

        public Object getEntityObj(String repository) {
            return Optional.ofNullable(entity4Class.get(repository)).map(clazz -> {
                try {
                    return clazz.newInstance();
                } catch (InstantiationException | IllegalAccessException e) {
                    e.printStackTrace();
                }
                return null;
            }).orElseThrow(EntityNotFoundException::new);
        }
    }

    static class MappingSupport {
        static Class getEntityClass(JpaRepository jpaRepository) {
            Type[] jpaInterfacesTypes = jpaRepository.getClass().getGenericInterfaces();
            Type[] type = ((ParameterizedType) ((Class) jpaInterfacesTypes[0]).getGenericInterfaces()[0]).getActualTypeArguments();
            return (Class) type[0];
        }

        static String getEntityName(Class clazz) {
            String simpleName = clazz.getSimpleName();
            return simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
        }
    }

    /**
     * @author yangrd
     * @date 2018/8/30
     * @see org.springframework.beans.BeanUtils#copyProperties(Object, Object, Class, String...)
     **/
    static class BeanUtils {

        public static <T> T map(Object source, Class<T> targetClass) {
            T targetObj = null;
            try {
                targetObj = targetClass.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
            copyProperties(source, targetObj);
            return targetObj;
        }

        /**
         * 只拷贝不为null的属性
         *
         * @param source the source bean
         * @param target the target bean
         * @throws BeansException if the copying failed
         */
        public static void copyProperties(Object source, Object target) throws BeansException {
            copyProperties(source, target, null, (String[]) null);
        }

        /**
         * 只拷贝不为null的属性
         *
         * @param source           the source bean
         * @param target           the target bean
         * @param editable         the class (or interface) to restrict property setting to
         * @param ignoreProperties array of property names to ignore
         * @throws BeansException if the copying failed
         * @see BeanWrapper
         */
        private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties)
                throws BeansException {

            Assert.notNull(source, "Source must not be null");
            Assert.notNull(target, "Target must not be null");

            Class<?> actualEditable = target.getClass();
            if (editable != null) {
                if (!editable.isInstance(target)) {
                    throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");
                }
                actualEditable = editable;
            }
            PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
            List<String> ignoreList = (ignoreProperties != null) ? Arrays.asList(ignoreProperties) : null;

            for (PropertyDescriptor targetPd : targetPds) {
                Method writeMethod = targetPd.getWriteMethod();
                if (writeMethod != null && (ignoreProperties == null || (!ignoreList.contains(targetPd.getName())))) {
                    PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                    if (sourcePd != null) {
                        Method readMethod = sourcePd.getReadMethod();
                        if (readMethod != null &&
                                ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                            try {
                                if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                    readMethod.setAccessible(true);
                                }
                                Object value = readMethod.invoke(source);
                                if (value != null) {
                                    //只拷贝不为null的属性 by zhao
                                    if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                        writeMethod.setAccessible(true);
                                    }
                                    writeMethod.invoke(target, value);
                                }
                            } catch (Throwable ex) {
                                throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                            }
                        }
                    }
                }
            }
        }
    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Assert.notNull(applicationContext, "");
        this.mappingManager = new MappingManager(applicationContext);
    }
}

-
优势:对spring data rest弱势的补充可以在不改变 之前习惯的数据格式的情况下狠方便的前移, 最重要的是相比于 Abstract 方法可以被覆盖 如 findAll 如果你使用 如 api/user/{id} spring mvc会优先匹配它 而不是 api/{repository}/{id} ,后期可以根据自身业务需要打成jar包放在私服上面方便其他项目使用