持久層和控制器的封裝
在平時開發中,會有很多單表或者多表的增刪改查功能,仔細看會發現,持久層和控制器的代碼基本差不多。如果我們把持久層和控制器增刪改查這種重複性高的代碼全部提取出來,做一個小封裝,這樣在開發中是不是就可以減少很多代碼量了。從而提高開發效率。
這裏案例持久層使用的是Spring-Data-Jpa
新建一個SpringBoot項目,選擇需要使用到的依賴。
項目中所有的依賴pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mcy</groupId>
<artifactId>springboot-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在application.properties文件中配置數據庫的鏈接。
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdemo?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.use-new-id-generator-mappings=false
spring.servlet.multipart.max-request-size=2048MB
spring.servlet.multipart.max-file-size=2048MB
server.port=8080
在寫代碼之前,我們先來看一下項目的目錄結構。
代碼實現
對持久層的封裝,我們首先需要確定的就是找到對應的實體類,之後才能對其進行操作,可以通過反射機制找到當前的實體類。
新建一個反射工具類GenericsUtils,這個類很重要,可以幫助我們找到對應的實體類。
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
public class GenericsUtils {
/**
* 通過反射,獲得定義Class時聲明的父類的範型參數的類型. 如public BookManager extends
* GenricManager<Book>
*
* @param clazz The class to introspect
* @return the first generic declaration, or <code>Object.class</code> if cannot be determined
*/
public static Class getSuperClassGenricType(Class clazz) {
return getSuperClassGenricType(clazz, 0);
}
/**
* 通過反射,獲得定義Class時聲明的父類的範型參數的類型. 如public BookManager extends GenricManager<Book>
*
* @param clazz clazz The class to introspect
* @param index the Index of the generic ddeclaration,start from 0.
*/
public static Class getSuperClassGenricType(Class clazz, int index)
throws IndexOutOfBoundsException {
Type genType = clazz.getGenericSuperclass();
if (!(genType instanceof ParameterizedType)) {
return Object.class;
}
Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
if (index >= params.length || index < 0) {
return Object.class;
}
if (!(params[index] instanceof Class)) {
return Object.class;
}
return (Class) params[index];
}
}
新建CommonRepository接口,爲公共的Reposit接口,該類繼承JpaRepository,JpaSpecificationExecutor兩個接口,這兩個接口中包含了基本的查詢。之後在新建Repository就可以直接繼承這個公共的CommonRepository接口就可以了,有兩個泛型接口,實體類名稱和id的類型,T表示實體類名稱,ID表示id的類型。代碼如下。
@NoRepositoryBean
public interface CommonRepository<T,ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
}
新建CommonService類,爲公共的Server類,包含了一下基本查詢的方法,之後的Server類,只需要繼承這個類,就會包含CommonService類中的所有查詢方法,不用在每個類都去寫一遍,如果有需要更改的話,可以重寫繼承類中的方法。和Repository接口一樣,它也有有兩個泛型接口,實體類名稱和id的類型,T表示實體類名稱,ID表示id的類型。代碼如下。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
/**
* 公共Service
* @param <T>
* @param <ID>
*/
public class CommonService<T,ID> {
@Autowired(required = false)
private CommonRepository<T, ID> baseDAO;
//查詢所有
public List<T> findAll(){
return baseDAO.findAll();
}
//根據id查詢
public T findById(ID id) {
Optional<T> optional = baseDAO.findById(id);
if(optional.isPresent()) {
return optional.get();
}
return null;
}
//保存方法
@Transactional
public void save(T entity) {
baseDAO.save(entity);
}
//根據實體類刪除
public void delete(T entity) {
baseDAO.delete(entity);
}
//根據id查詢
@Transactional
public void deleteById(ID id) {
baseDAO.deleteById(id);
}
//排序查詢所有
public List<T> findAll(Sort sort){
return baseDAO.findAll(sort);
}
//動態條件查詢
public List<T> findAll(Specification<T> spec){
return baseDAO.findAll(spec);
}
//分頁查詢
public Page<T> findAll(Pageable pageable){
return baseDAO.findAll(pageable);
}
//動態條件分頁查詢
public Page<T> findAll(Specification<T> spec, Pageable pageable){
return baseDAO.findAll(spec,pageable);
}
//動態條件排序查詢
public List<T> findAll(Specification<T> spec, Sort sort){
return baseDAO.findAll(spec,sort);
}
}
最後就是控制器的封裝了,控制器需要先通過反射機制的工具類GenericsUtils,找到對應的實體類。之後封裝一下常用的方法。不同於上面兩個類的是,他有三個泛型接口,除了實體類和ID的類型外,他還有一個BaseForm類,BaseForm爲公共的Form類,該類中就一個id和一個查詢條件。因爲這裏寫數據保存方法的數據接收是很使用的Form接收的,使用Form裏邊的ID是判斷是修改還是新增方法(修改和新增用的一個方法,用id是否爲null區分),BaseForm類代碼如下。
public class BaseForm<ID> {
private ID id;
private String search;
public ID getId() {
return id;
}
public void setId(ID id) {
this.id = id;
}
public String getSearch() {
return search;
}
public void setSearch(String search) {
this.search = search;
}
}
其中search字段是BootstrapTable查詢傳遞參數的屬性名,所以寫在了公共的Form類中,不需要的可以刪除不要。
公共的CommonController控制器代碼如下(主要包了數據顯示,新增修改頁面的跳轉,和數據查詢和刪除的方法)。
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 公共Controller
* @param <T>
* @param <ID>
* @param <Form>
*/
public class CommonController<T, ID, Form extends BaseForm<ID>>{
//通過反射工具類GenericsUtils,獲得到實體類
@SuppressWarnings("unchecked")
private Class<T> clazz = GenericsUtils.getSuperClassGenricType(getClass());
@Autowired
private CommonService<T, ID> baseService;
//數據顯示頁面
@RequestMapping(value="/manage")
public void manage(ModelMap map) {
}
//修改和新增頁面,共用的一個頁面
@RequestMapping(value="/edit")
public void edit(Form form, ModelMap map) throws InstantiationException, IllegalAccessException {
T model=clazz.newInstance();
ID id = form.getId();
if(id!=null) {
model=baseService.findById(id);
}
map.put("model", model);
}
//數據保存方法
@RequestMapping(value="/save")
@ResponseBody
public Object save(Form form) {
try {
T model=clazz.newInstance();
ID id = form.getId();
if(id!=null) {
model=baseService.findById(id);
}
BeanUtils.copyProperties(form, model,"id");
baseService.save(model);
return new AjaxResult("數據保存成功");
} catch (Exception e) {
return new AjaxResult(false,"數據保存失敗");
}
}
//刪除方法
@RequestMapping(value="/delete")
@ResponseBody
public Object delete(ID id) {
try {
baseService.deleteById(id);
return new AjaxResult("數據刪除成功");
} catch (Exception e) {
return new AjaxResult(false,"數據刪除失敗");
}
}
//動態查詢方法
public Specification<T> buildSpec(Form form){
return null;
}
//分頁數據查詢
@RequestMapping(value="/page")
@ResponseBody
public Object page(TablePageable pageParam,Form form) {
PageRequest pageable = pageParam.bulidPageRequest();
Specification<T> spec = buildSpec(form);
Page<T> page=baseService.findAll(spec, pageable);
return AjaxResult.bulidPageResult(page);
}
}
分頁查詢中的TablePageable類,是用於接收Bootstraptable傳遞過來的分頁參數,代碼如下。
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
/**
* 分頁的一個工具類,接收分頁信息
*/
public class TablePageable {
private Integer limit; //分頁
private Integer offset;//首記錄號(從0開始)
private String sort; //排序字段
private String order; //順序,逆序
public Integer getLimit() {
return limit;
}
public void setLimit(Integer limit) {
this.limit = limit;
}
public Integer getOffset() {
return offset;
}
public void setOffset(Integer offset) {
this.offset = offset;
}
public String getSort() {
return sort;
}
public void setSort(String sort) {
this.sort = sort;
}
public String getOrder() {
return order;
}
public void setOrder(String order) {
this.order = order;
}
public PageRequest bulidPageRequest() {
int page=(offset!=null&&limit!=null)?offset/limit:0;
int size=limit!=null?limit:10;
if(sort==null) {
return PageRequest.of(page, size);
}else {
Order order2=new Order(Direction.fromString(order), sort);
Sort sort2= Sort.by(order2);
return PageRequest.of(page,size,sort2 );
}
}
public PageRequest bulidPageable(Sort sort) {
int page=(offset!=null&&limit!=null)?offset/limit:0;
int size=limit!=null?limit:10;
return PageRequest.of(page, size, sort);
}
public Sort bulidSort() {
Order order2=new Order(Direction.fromString(order), sort);
Sort sort2= Sort.by(order2);
return sort2;
}
}
分頁查詢中返回的AjaxResult類中的bulidPageResult方法是用於轉載BootstrapTable表格數據的,根據BootstrapTable接收數據的格式來寫。代碼如下。
import org.springframework.data.domain.Page;
import java.util.HashMap;
/**
* 方法執行成功後,返回的工具類
*/
public class AjaxResult {
private Boolean success;
private String msg; //提示信息
public Boolean getSuccess() {
return success;
}
public void setSuccess(Boolean success) {
this.success = success;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public AjaxResult(String msg) {
super();
this.success=true;
this.msg = msg;
}
public AjaxResult(Boolean success, String msg) {
super();
this.success = success;
this.msg = msg;
}
@SuppressWarnings("rawtypes")
public static HashMap<String, Object> bulidPageResult(Page page) {
HashMap<String, Object> result=new HashMap<>();
result.put("total", page.getTotalElements());
result.put("rows", page.getContent());
return result;
}
}
TablePageable類和AjaxResult類,根據自己開發需求來寫。
封裝文件寫完了,下面就需要來寫一個案例測試一下了。新建一個User實體類,隨便給幾個字段,用於測試。代碼如下。
import javax.persistence.*;
@Entity
@Table(name = "tb_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id; //主鍵
private String username; //姓名,username
private String loginName; //用戶名,
private Integer age; //年齡
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public User() {
}
public User(String username, String loginName, Integer age) {
this.username = username;
this.loginName = loginName;
this.age = age;
}
}
實體類新建完成後,新建對應的Form接收類,繼承BaseForm類,代碼如下。
import com.mcy.springbootdemo.custom.BaseForm;
public class UserForm extends BaseForm<Integer> {
private String username; //姓名,username
private String loginName; //登錄名
private Integer age; //年齡
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
新建對應的UserRepository類,繼承公共的CommonRepository類,泛型傳遞對應的實體類和ID類型,代碼如下。
import com.mcy.springbootdemo.custom.CommonRepository;
import com.mcy.springbootdemo.entity.User;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends CommonRepository<User, Integer> {
}
新建對應的UserService類,繼承公共的CommonService類,泛型也是傳遞對應的實體類和ID類型,代碼如下。
import com.mcy.springbootdemo.custom.CommonService;
import com.mcy.springbootdemo.entity.User;
import org.springframework.stereotype.Service;
@Service
public class UserService extends CommonService<User, Integer> {
}
新建對應的控制器UserController,同樣繼承公共的CommonController類,泛型傳遞對應的實體類,ID類型和對應的Form。如果對應的方法有需要改動的,可以重寫繼承的CommonController類中的方法,比如動態查詢,就需要重寫CommonController類中的buildSpec方法,代碼如下。
import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;
@Controller
public class UserController extends CommonController<User, Integer, UserForm> {
@Autowired
private UserRepository userRepository;
@RequestMapping(value = {"/", "index"})
public String index(){
return "manage";
}
@Override
public Specification<User> buildSpec(UserForm form) {
Specification<User> spec = new Specification<User>() {
private static final long serialVersionUID = 1L;
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//本集合用於封裝查詢條件
List<Predicate> predicates = new ArrayList<>();
if (StringUtils.hasText(form.getSearch())) {
Predicate name = cb.like(root.get("loginName"), "%" + form.getSearch() + "%");
Predicate username = cb.like(root.get("username"), "%" + form.getSearch() + "%");
Predicate or = cb.or(name, username);
predicates.add(or);
}
return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}
};
return spec;
}
}
後臺代碼就這些了,是不是很簡潔,省下了不少代碼。
接下來就是兩個頁面了,一個用於顯示數據,一個用於新增和刪除數據的頁面。數據顯示用BootstrapTable顯示的,可以根據自己需求更改。
數據顯示頁面manage.html,代碼如下。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Spring-Data-Jpa持久層和控制器的封裝</title>
<link rel="stylesheet" href="static/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="static/bootstrap-table/dist/bootstrap-table.css" />
<script src="static/bootstrap-table/dist/jquery.js" ></script>
<script src="static/bootstrap/dist/js/bootstrap.js" ></script>
<script src="static/bootstrap-table/dist/bootstrap-table.js" ></script>
<script src="static/bootstrap-table/dist/locale/bootstrap-table-zh-CN.js" ></script>
</head>
<body>
<!-- 增刪改 -->
<div id="toolbar" class="btn-group">
<button type="button" class="btn btn-default" onclick="btn_add()">
<span class="glyphicon glyphicon-plus"></span>新增
</button>
<button type="button" class="btn btn-default" onclick="btn_edit()">
<span class="glyphicon glyphicon-pencil"></span>修改
</button>
<button type="button" class="btn btn-default" onclick="btn_delete()">
<span class="glyphicon glyphicon-remove"></span>刪除
</button>
</div>
<div style="margin: 20px;">
<table id="user_table"></table>
</div>
<script type="text/javascript">
$("#user_table").bootstrapTable({
url: 'page', //表格數據請求地址
toolbar: '#toolbar', //自定義組件
striped: true, //隔行換色
height: tableHeight(), //設置高度
pagination: true, //顯示錶格的底部工具欄
sidePagination: 'server', //client客戶端分頁,server服務器分頁
pageNumber: 1, //初始的頁數
pageSize: 10, //默認每頁數據
pageList: [10, 15, 50, 100], //設置分頁選擇每頁顯示的條數
search: false, //定義右上方的搜索框,輸入即可以開始搜索
showColumns: true, //選列的下拉菜單
showRefresh: true, //刷新按鈕
showToggle: true, //視圖切換
search: true,
toolbarAlign: 'left', //自定義按鈕位置
clickToSelect: true, //點擊行選中
singleSelect: true, //單選
queryParams: function (param){ //傳遞參數
var params = {};
params['offset'] = param.offset; // 頁碼
params['limit'] = param.limit; // 條數
params['search'] = param.search; // 搜索內容
params['sort'] = param.sort; // 排序字段
params['order'] = param.order; // 排序方式
return params;
},
columns:[{
checkbox: true //多選框
},{
field: 'id', //每列的字段名
title: 'id', //表頭所顯示的名字
halign: 'center', //表頭的對齊方式
valign: 'middle', //表格對齊方式居中
order: 'asc', //默認排序方式
sortable: true, //設置可以排序
align: 'center' //表格數據對齊方式
},{
field: 'username',
title: '姓名',
valign: 'middle',
halign: 'center',
align: 'center'
},{
field: 'loginName',
title: '用戶名',
valign: 'middle',
halign: 'center',
align: 'center'
},{
field: 'age',
title: '年齡',
valign: 'middle',
halign: 'center',
align: 'center'
}]
});
function tableHeight() {
return $(window).height() - 100;
}
//新增
function btn_add(){
var dialog = $('<div class="modal fade" id="myModal" tabindex="-1" aria-labelledby="myModalLabel"></div>');
dialog.load("edit");
$("body").append(dialog);
/*彈出模態框,綁定關閉後的事件*/
dialog.modal().on('hidden.bs.modal', function () {
//刪除
dialog.remove();
$("#user_table").bootstrapTable('refresh');
});
}
//修改
function btn_edit(){
var str = $("#user_table").bootstrapTable('getSelections');
if(str.length != 1){
alert("請選中一行進行編輯");
return ;
}
var dialog = $('<div class="modal fade" id="myModal" tabindex="-1" aria-labelledby="myModalLabel"></div>');
var id = str[0].id;
dialog.load("edit?id="+id);
/*添加到body中*/
$("body").append(dialog);
/*彈出模態框,綁定關閉後的事件*/
dialog.modal().on('hidden.bs.modal', function () {
//刪除模態框
dialog.remove();
$("#user_table").bootstrapTable('refresh');
});
}
/*刪除*/
function btn_delete(){
var str = $("#user_table").bootstrapTable('getSelections');
if(str.length != 1){
alert("請選中一行進行刪除");
}else{
if(confirm("確定刪除選中這一行嗎?")){
var id = str[0].id;
$.post('delete',{id:id},function(){
/* refresh刷新 */
$("#user_table").bootstrapTable('refresh');
alert("刪除成功!");
});
}
}
}
</script>
</body>
</html>
新增和修改頁面edit.html,代碼如下。
<meta charset="UTF-8">
<body>
<form id="myForm" class="form-horizontal" role="form" >
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 class="modal-title" id="myModalLabel">操作</h4>
</div>
<div class="modal-body">
<input type="hidden" name="id" data-th-value="${model.id}">
<div class="form-group">
<label class="col-sm-2 control-label">姓名</label>
<div class="col-sm-9">
<input type="text" name="username" data-th-value="${model.username}" class="form-control" placeholder="姓名">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">用戶名</label>
<div class="col-sm-9">
<input type="text" name="loginName" data-th-value="${model.loginName}" class="form-control" placeholder="用戶名">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">年齡</label>
<div class="col-sm-9">
<input type="text" name="age" data-th-value="${model.age}" class="form-control" placeholder="地址">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" onclick="btnSubmit()" class="btn btn-primary">
<span class="glyphicon glyphicon-floppy-disk" aria-hidden="true"></span>保存
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>關閉
</button>
</div>
</div>
</div>
</form>
<script th:inline="javascript">
function btnSubmit() {
var form= new FormData($("#myForm")[0]);
$.ajax({
url: 'save',
type: 'post',
data: form,
processData: false, //不處理數據
contentType: false, //不設置內容類型
success: function(result){
if(result.success){
$("#myModal").modal("hide");
alert("數據保存成功");
}else{
alert(result.msg);
}
},
error: function(result){
alert("數據保存失敗!");
}
})
}
</script>
</body>
效果如下。
案例代碼下載
下載地址:https://github.com/machaoyin/springboot-demo
如果對你有幫助,點贊關注一下唄^_^,留下你的足跡。