⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7245942451105562685 「半夏之沫」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

隔壁组的云计算零零后女同事,后文简称 云女士 ,非说 Go 的 Gin 框架比 Springboot 更加的开箱即用,我心想在 Java 里面 Springboot 已经打遍天下无敌手,这份底蕴岂是 Gin 能比。

但是云女士突出一个执拗,非我要 PK 一把, PK 内容就是她使用 Gin,而我使用 Springboot 快速搭建一个简单的 Crud 工程,最后让其他同事来评判哪个更开箱即用。我毫不犹豫就答应了,作为搭建 Springboot 学习工程的资深 Crud 选手,咱这份底气还是有的。

云女士选择使用 Gin + Gorm 来搭建,而我原本想选择 Springboot + MyBatis,后面转念一想,这 MyBatis 要写 XML 文件,指不定就因为这个被云女士嘲笑了,所以我把 MyBatis 替换为了 MyBatis-Plus,这就足够的简洁了吧。

正文

准备事项

既然是 Crud 工程,自然要准备好操作的表,我和云女士通过如下语句在各自的数据库中创建好了如下两张表。

CREATE TABLE people (
id INT(11) PRIMARY KEY AUTO_INCREMENT,
p_name VARCHAR(255) NOT NULL,
p_age INT(11) NOT NULL
)

CREATE TABLE book (
id INT(11) PRIMARY KEY AUTO_INCREMENT,
b_name VARCHAR(255) NOT NULL,
b_price FLOAT NOT NULL
)

Gin快速搭建Crud工程

云女士的工程结构如下所示。

Gin目录结构

云女士的 go.mod 文件内容如下所示。

module gobase

go 1.17

require (
github.com/gin-gonic/gin v1.6.0
github.com/jinzhu/gorm v1.9.16
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cast v1.5.1
)

云女士定义了两个结构体作为模型( Model ),book.go 文件内容如下所示。

package model

const (
BookTableName = "book"
)

type Book struct {
ID int64 `gorm:"column:id"`
BookName string `gorm:"column:b_name"`
BookPrice float64 `gorm:"column:b_price"`
}

func (b *Book) TableName() string {
return BookTableName
}

people.go 文件内容如下所示。

package model

const (
PeopleTableName = "people"
)

type People struct {
ID int64 `gorm:"column:id"`
PeopleName string `gorm:"column:p_name"`
PeopleAge int64 `gorm:"column:p_age"`
}

func (p *People) TableName() string {
return PeopleTableName
}

云女士补充道,TableName()方法是为模型指定对应的表名。

云女士为 book 表和 people 表分别定义了 Dao 接口,dao.go 文件内容如下所示。

package dao

import "gobase/model"

type BookDao interface {
AddBook(book *model.Book) error
UpdateBook(book *model.Book) error
DeleteBook(book *model.Book) error
ListBookById(id uint) (*model.Book, error)
}

type PeopleDao interface {
AddPeople(book *model.People) error
UpdatePeople(book *model.People) error
DeletePeople(book *model.People) error
ListPeopleById(id uint) (*model.People, error)
}

BookDao 接口对应的实现在book_dao_impl.go文件中,实现如下。

package dao

import (
"github.com/jinzhu/gorm"
"gobase/model"
)

type BookDaoImpl struct {
DB *gorm.DB
}

func (b *BookDaoImpl) AddBook(book *model.Book) error {
if createResult := b.DB.Create(book); createResult.Error != nil {
return createResult.Error
}
return nil
}

func (b *BookDaoImpl) UpdateBook(book *model.Book) error {
if saveResult := b.DB.Save(book); saveResult.Error != nil {
return saveResult.Error
}
return nil
}

func (b *BookDaoImpl) DeleteBook(book *model.Book) error {
if deleteResult := b.DB.Delete(book); deleteResult.Error != nil {
return deleteResult.Error
}
return nil
}

func (b *BookDaoImpl) ListBookById(id uint) (*model.Book, error) {
var book model.Book
if listResult := b.DB.Where("id = ?", id).First(&book); listResult.Error != nil {
return nil, listResult.Error
}
return &book, nil
}

PeopleDao 接口对应的实现在people_dao_impl.go文件中,实现如下。

package dao

import (
"github.com/jinzhu/gorm"
"gobase/model"
)

type PeopleDaoImpl struct {
DB *gorm.DB
}

func (b *PeopleDaoImpl) AddPeople(people *model.People) error {
if createResult := b.DB.Create(people); createResult.Error != nil {
return createResult.Error
}
return nil
}

func (b *PeopleDaoImpl) UpdatePeople(people *model.People) error {
if saveResult := b.DB.Save(people); saveResult.Error != nil {
return saveResult.Error
}
return nil
}

func (b *PeopleDaoImpl) DeletePeople(people *model.People) error {
if deleteResult := b.DB.Delete(people); deleteResult.Error != nil {
return deleteResult.Error
}
return nil
}

func (b *PeopleDaoImpl) ListPeopleById(id uint) (*model.People, error) {
var people model.People
if listResult := b.DB.Where("id = ?", id).First(&people); listResult.Error != nil {
return nil, listResult.Error
}
return &people, nil
}

要操作数据库,肯定需要数据库连接,云女士将数据库连接的管理实现在了mysql_connection_pool.go文件中,内容如下所示。

package mysql

import (
"fmt"
"github.com/jinzhu/gorm"
"gobase/dao"
"log"
"time"
)

const (
UserName = "root"
PassWord = "root"
Host = "192.168.101.8"
Port = 3306
Database = "gotest"
MaxLifetime = 60 * time.Second
MaxIdletime = 30 * time.Second
MaxOpenconns = 6
MaxIdleconns = 2
Dialect = "mysql"
)

type DataSouce struct {
db *gorm.DB
}

func NewDataSource() *DataSouce {
var db *gorm.DB

dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Asia%%2FShanghai",
UserName, PassWord, Host, Port, Database)
db, err := gorm.Open(Dialect, dsn)
if err != nil {
log.Fatal(err.Error())
}

db.DB().SetConnMaxLifetime(MaxLifetime)
db.DB().SetConnMaxIdleTime(MaxIdletime)
db.DB().SetMaxOpenConns(MaxOpenconns)
db.DB().SetMaxOpenConns(MaxIdleconns)

return &DataSouce{
db: db,
}
}

// BookDao 操作book表
func (d *DataSouce) BookDao() dao.BookDao {
return &dao.BookDaoImpl{
DB: d.db,
}
}

// PeopleDao 操作people表
func (d *DataSouce) PeopleDao() dao.PeopleDao {
return &dao.PeopleDaoImpl{
DB: d.db,
}
}

云女士将路由写在了webservice.go文件中,内容如下。

package adapter

import (
"github.com/gin-gonic/gin"
"gobase/mysql"
)

func Init() error {
dataSouce := mysql.NewDataSource()

bookController := NewBookController(dataSouce)
propleController := NewPropleController(dataSouce)

engine := gin.Default()
routerGroupBook := engine.Group("/book")
routerGroupBook.POST("/add", bookController.AddBook)
routerGroupBook.POST("/update", bookController.UpdateBook)
routerGroupBook.POST("/delete", bookController.DeleteBook)
routerGroupBook.POST("/list", bookController.ListBookById)

routerGroupPeople := engine.Group("/people")
routerGroupPeople.POST("/add", propleController.AddPeople)
routerGroupPeople.POST("/update", propleController.UpdatePeople)
routerGroupPeople.POST("/delete", propleController.DeletePeople)
routerGroupPeople.POST("/list", propleController.ListPeopleById)

return engine.Run()
}

其实除了绑定路由,云女士还在Init()函数中进行了简单的服务注入,也就是创建数据库连接池,然后将数据库连接池给到对应的 web 服务。

云女士将操作 book 表对应的 web 服务写在了book_controller.go文件中,其实现如下所示。

package adapter

import (
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/spf13/cast"
"gobase/model"
"gobase/mysql"
"net/http"
)

type BookController struct {
dataSource *mysql.DataSouce
}

func NewBookController(dataSource *mysql.DataSouce) BookController {
return BookController{
dataSource: dataSource,
}
}

func (b *BookController) AddBook(ctx *gin.Context) {
var book model.Book
if err := ctx.ShouldBind(&book); err != nil {
logrus.Error("读取Book信息失败")
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
bookDao := b.dataSource.BookDao()
err := bookDao.AddBook(&book)
if err != nil {
logrus.Error("添加Book失败", err)
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": "success",
})
}

func (b *BookController) UpdateBook(ctx *gin.Context) {
var book model.Book
if err := ctx.ShouldBind(&book); err != nil {
logrus.Error("读取Book信息失败")
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
bookDao := b.dataSource.BookDao()
err := bookDao.UpdateBook(&book)
if err != nil {
logrus.Error("更新Book失败", err)
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": "success",
})
}

func (b *BookController) DeleteBook(ctx *gin.Context) {
var book model.Book
if err := ctx.ShouldBind(&book); err != nil {
logrus.Error("读取Book信息失败")
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
bookDao := b.dataSource.BookDao()
err := bookDao.DeleteBook(&book)
if err != nil {
logrus.Error("删除Book失败", err)
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": "success",
})
}

func (b *BookController) ListBookById(ctx *gin.Context) {
id := cast.ToUint(ctx.Query("id"))
bookDao := b.dataSource.BookDao()
book, err := bookDao.ListBookById(id)
if err != nil {
logrus.Error("查询Book失败", err)
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
ctx.JSON(http.StatusOK, book)
}

云女士将操作 people 表对应的 web 服务写在了people_controller.go文件中,其实现如下所示。

package adapter

import (
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/spf13/cast"
"gobase/model"
"gobase/mysql"
"net/http"
)

type PeopleController struct {
dataSource *mysql.DataSouce
}

func NewPropleController(dataSource *mysql.DataSouce) PeopleController {
return PeopleController{
dataSource: dataSource,
}
}

func (p *PeopleController) AddPeople(ctx *gin.Context) {
var people model.People
if err := ctx.ShouldBind(&people); err != nil {
logrus.Error("读取People信息失败")
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
peopleDao := p.dataSource.PeopleDao()
err := peopleDao.AddPeople(&people)
if err != nil {
logrus.Error("添加People失败", err)
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": "success",
})
}

func (p *PeopleController) UpdatePeople(ctx *gin.Context) {
var people model.People
if err := ctx.ShouldBind(&people); err != nil {
logrus.Error("读取People信息失败")
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
peopleDao := p.dataSource.PeopleDao()
err := peopleDao.UpdatePeople(&people)
if err != nil {
logrus.Error("更新People失败", err)
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": "success",
})
}

func (p *PeopleController) DeletePeople(ctx *gin.Context) {
var people model.People
if err := ctx.ShouldBind(&people); err != nil {
logrus.Error("读取People信息失败")
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
peopleDao := p.dataSource.PeopleDao()
err := peopleDao.DeletePeople(&people)
if err != nil {
logrus.Error("删除People失败", err)
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": "success",
})
}

func (p *PeopleController) ListPeopleById(ctx *gin.Context) {
id := cast.ToUint(ctx.Query("id"))
peopleDao := p.dataSource.PeopleDao()
people, err := peopleDao.ListPeopleById(id)
if err != nil {
logrus.Error("查询People失败", err)
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "failed",
})
return
}
ctx.JSON(http.StatusOK, people)
}

最后,云女士简单的展示了一下对 book 表和 prople 表的 Crud 操作。

book 表和 people 表的增删改成功时返回内容如下所示。

Gin-增删改成功

book 表和 people 表的查询成功时返回内容如下所示。

Gin-查询图书

Gin-查询人物

Springboot快速搭建Crud工程

云女士基于 Gin 和 Gorm 搭建的 Crud 工程,我看完后内心扑哧一笑:不过如此。

那现在该轮到我表演了。首先给出整个工程结构图如下所示。

Springboot目录结构

POM 文件内容如下所示。

<?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 http://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.4.1</version>
</parent>

<groupId>com.lee.javabase</groupId>
<artifactId>javabase</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

</project>

同样,定义 book 表和 people 表对应的实体类 Book 和 People,如下所示。

@Getter
@Setter
public class Book {
@TableField("id")
private int id;

@TableField("b_name")
private String bookName;
@TableField("b_price")
private float bookPrice;

}

@Getter
@Setter
public class People {

@TableField("id")
private int id;

@TableField("p_name")
private String peopleName;
@TableField("p_age")
private int peopleAge;

}

然后定义定义接口,如下所示。

@Mapper
public interface BookMapper extends BaseMapper<Book> {
}

@Mapper
public interface PeopleMapper extends BaseMapper<People> {
}

最后是对应的 Controller 实现, BookController 实现如下。

@Slf4j
@RestController
@RequestMapping("/book")
public class BookController {

@Autowired
private BookMapper bookMapper;

@PostMapping("/add")
public ResponseEntity<String> addBook(@RequestBody Book book) {
try {
bookMapper.insert(book);
return new ResponseEntity<>("添加图书成功", HttpStatus.OK);
} catch (Exception e) {
log.error("添加图书失败", e);
return new ResponseEntity<>("添加图书失败", HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@PostMapping("/update")
public ResponseEntity<String> updateBook(@RequestBody Book book) {
try {
bookMapper.updateById(book);
return new ResponseEntity<>("更新图书成功", HttpStatus.OK);
} catch (Exception e) {
log.error("更新图书失败", e);
return new ResponseEntity<>("更新图书失败", HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@PostMapping("/delete")
public ResponseEntity<String> deleteBook(@RequestParam("id") int id) {
try {
bookMapper.deleteById(id);
return new ResponseEntity<>("删除图书成功", HttpStatus.OK);
} catch (Exception e) {
log.error("删除图书失败", e);
return new ResponseEntity<>("删除图书失败", HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@PostMapping("/list")
public ResponseEntity<Book> listBook(@RequestParam("id") int id) {
try {
Book book = bookMapper.selectById(id);
return new ResponseEntity<>(book, HttpStatus.OK);
} catch (Exception e) {
log.error("查询图书失败", e);
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

PeopleController 实现如下所示。

@Slf4j
@RestController
@RequestMapping("/people")
public class PeopleController {

@Autowired
private PeopleMapper peopleMapper;

@PostMapping("/add")
public ResponseEntity<String> addPeople(@RequestBody People people) {
try {
peopleMapper.insert(people);
return new ResponseEntity<>("添加人物成功", HttpStatus.OK);
} catch (Exception e) {
log.error("添加人物失败", e);
return new ResponseEntity<>("添加人物失败", HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@PostMapping("/update")
public ResponseEntity<String> updatePeople(@RequestBody People people) {
try {
peopleMapper.updateById(people);
return new ResponseEntity<>("更新人物成功", HttpStatus.OK);
} catch (Exception e) {
log.error("更新人物失败", e);
return new ResponseEntity<>("更新人物失败", HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@PostMapping("/delete")
public ResponseEntity<String> deletePeople(@RequestParam("id") int id) {
try {
peopleMapper.deleteById(id);
return new ResponseEntity<>("删除人物成功", HttpStatus.OK);
} catch (Exception e) {
log.error("删除人物失败", e);
return new ResponseEntity<>("删除人物失败", HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@PostMapping("/list")
public ResponseEntity<People> listPeople(@RequestParam("id") int id) {
try {
People people = peopleMapper.selectById(id);
return new ResponseEntity<>(people, HttpStatus.OK);
} catch (Exception e) {
log.error("查询人物失败", e);
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

启动应用程序, book 表的 Crud 操作结果如下所示。

prople 表的 Crud 操作结果如下所示。

总结

我宣布,Springboot 就是快速搭建 Crud 工程的神。

其实,在基于 Gin 和 Gorm 搭建 Crud 工程时,云女士还是写得复杂了一点,但是我有幸看过她们云平台的项目的代码,云女士写得也没毛病,虽然是个简化版,但也是严格遵从她们项目的代码结构来实现的。

说回 Springboot,毫无疑问,无论是天然自带 Tomcat 或 Jetty ,还是和三方框架整合的各种 Starter 包,Springboot 都将开箱即用做到了极致,但是转念又一想,其实 Springboot 和 Gin 严格来说做比较没啥意义,就像 Java 和 Go 的比较一样,我觉得也没啥意义,各自的优势区间不一样,并且各自也都在相关的领域叱咤风云。

各位看官,你们觉得呢。

文章目录
  1. 1. 前言
  2. 2. 正文
    1. 2.1. 准备事项
    2. 2.2. Gin快速搭建Crud工程
    3. 2.3. Springboot快速搭建Crud工程
  3. 3. 总结