哎,闲来无事,今天做一个spring boot+jpa的完整教程(从空项目到可正常使用运行)。先说下我做的这个是个cloud 的子项目。不过除了依赖,配置文件和注释。剩下的和单独的项目没啥区别。我也尽量不把cloud的配置拿出来误导大家。我用的是eclipse。框架结构是spring boot +jpa+mysql
(因为我这个是一个子项目,功能上单独看可能有些单薄而且不太容易理解。所以大家不用太纠结功能,主要是层次结构和思路之类的。)然后这里说一下开发的大体思路:
首先一个项目的开发要从需求开始:
根据需求的有什么功能,要实现什么
——>然后我们画出流程图(或者思维导向图,这个我没做过,但是看群里有人提到过)
——>流程图出完了,大概就可以设计数据库或者模型了(领域模型我个人感觉是比较新颖的一种方式,总听说,但是我没实际上用过。大概的区别是我们传统的是数据库到实体的一个关系。而领域模型是实体到数据库的一个因果关系。)
——>最后再根据需求,流程,数据库设计接口(无论是手写还是swagger之类的,但是我总觉得接口文档应该先于代码的编写。我们要在敲代码之前就对需要写什么代码有所计划)
——>最后才是根据接口文档来实现接口,最终完成代码并前后端联调。
然后我们照着这个思路一步步走下去。
第一步:创建spring boot项目:
这个没有啥可说的。然后说个题外话,现在是2019.7.16日。然后eclipse直接生成的boot是版本最高的2.1.6版本的。pom文件会报错。原因是maven版本和boot版本不兼容问题。建议大家改成2.1.4以下。因为我是基于老项目搭建的子项目所以boot版本1.5.17。比较低,我其实个人不是很喜欢。
第二步:导包(在创建boot项目的时候什么都没依赖):
这里还要说一下,我实现已经计划好了要用到的技术。所以这里需要导的依赖也很明确。大概讲一下吧:首先boot启动(这个是boot项目自带的),然后web依赖必不可少,然后test依赖进行单元测试的,我反正是导入了。剩下的因为我用到了jpa所以导入data-jpa。因为我数据库是mysql,所以导入mysql驱动。到这就ok了,所必备的依赖就这么多。但是如果你的项目中用到了别的就需要导入相关的依赖。
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
runtime
大概说一下常用的(个人总结):比如我觉得json和对象相互转化的依赖,
net.sf.json-lib
json-lib
2.4
jdk15
com.fasterxml.jackson.core
jackson-databind
还有如果用到了redis,那么redis依赖,如果用到了oss,oss的依赖等等,在这我就不一一列举了。
第三步:建项目结构(我这里的是我个人的做法,没有标准答案的,如果你跟我做的不同别盲目跟风但也别喷):
背景是我这个是前后端分离的项目。所以我最终需要提供的也就是接口。然后遵循mvc原则,虽然没有”v“视图了,但是mc还是要分开,而且咱们java中约定俗成的细化,service层也必不可少。所以我项目大概分了五个包:
这里简单的讲一下(我再重申一次:
因为我的是微服务,所以功能单一,没有再细分!如果是完整项目,可能这个controller中也要分好几个controller的!同样service也是这个道理!甚至有的业务复杂的还要再封装一层的。所以千万别盲目跟风~~~)
controller:只有一个类,是此项目所有接口的汇总。
entity:这里是所有数据库映射到项目中的实体对象。(我这里是用插件直接生成的。jpa直接生成实体的方法我以前写过。不会的可以戳进去自己照着做。如果你愿意手敲我也没意见。jpa根据表生成实体)。
jpa:这个命名其实没有一定的准则,反正我个人喜欢这么叫。你愿意叫dao或者model也都ok。这个类算是jpa中的持久层吧。如果正式一点的解释:Repository是把一个数据存储区的数据给封装成对象的集合并提供了对这些集合的操作。
service:中有一个接口service,和一个实现了接口的实现类serviceImpl。
tool:这个包是我自己封装的一些常用的工具。比如雪花算法,雪花id的生成,前端返回值的对象封装,一些对时间日期格式转化的封装之类的。反正这个是要随着项目一点一点扩充的。在文章最后我会把我的tool贴出来供大家参考。
大概每一层的用意已经说完了,接下来贴我项目的完整目录:
因为我这里只用到了两个表,所以只有两个entity和repository。然后实体是我直接插件生成然后复制粘贴进来的。repository是我自己创建继承jpaRepository接口的(这里其实可以再自定义接口,但是我觉得我这里没啥必要,所以直接继承jpaRepository了)。我大概复制粘贴给大家看下:
entity:(其实get/set方法啥的不用看~~主要是看看注释之类的吧。)
package org.ourtowns.xadvt.entity;
import java.io.Serializable;
import javax.persistence.*;
/**
* The persistent class for the MSG_XADVT_CONTENT database table.
*
*/
@Entity
@Table(name="MSG_XADVT_CONTENT")
@NamedQuery(name="MsgXadvtContent.findAll", query="SELECT m FROM MsgXadvtContent m")
public class MsgXadvtContent implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name="ID")
private String id;
@Lob
@Column(name="CONTENT")
private String content;
public MsgXadvtContent() {
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public String getContent() {
return this.content;
}
public void setContent(String content) {
this.content = content;
}
}
这里有一个小知识:那就是非映射字段@Transient注释:就是在数据库里没有这个字段,但是我们为实体加上这个属性。比如说我有个订单表,订单里有多个商品。这种一般都采用主从表的形式(我们一般不直接设置表的外键关联)。然后实体中要想表达这种关联关系,一般都是在订单表中加一个非映射属性List
写法就是定义好属性再生成get/set方法就ok了:
@Transient
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
jpa层(这里除了继承JpaRepository也没啥了。我这就是空类,然后增删改查的方法也要随着业务和代码的写入一点点补充):
package org.ourtowns.xadvt.jpa;
import org.ourtowns.xadvt.entity.MsgXadvtContent;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MsgXadvtContentRepository extends JpaRepository
{ }
这里还有个数据库配置别忘记了。反正咱们这个项目也没用redis等中间件。所以只配置个数据库就ok了。附上我的配置(我现在觉得截图更容易理解~~哈哈,而且手敲一次没坏处~~马赛克是咱们这个项目用不到的。以防看着混乱):
第四步,接口代码的实现:
因为我之前说过开发撸代码的前提就是接口文档已经设计好了,所以我们照着之前设计的文档开始写接口吧。(如果你看到现在并没有设计好接口文档我建议你这个时候把项目先放着,然后塌心下来先把接口文档设计好。我个人比较习惯手写接口文档,然后我把设计的文档截图大家可以参考下。我就是纯粹记事本手写的,没啥技术含量)。
对了,这里再给大家安利一个技巧,我这个也是在上一家公司约定俗成的东西,可能有更好的实现方式欢迎大家留言或者私信探讨。就是在前后端传参是个对象的时候,因为java的大小写敏感,所以极其特别容易因为一些大小写问题而传参失败。(前端:我传这个参数了啊~你:???我特么没收到啊??哈哈~~)所以我这里习惯性把一个java对象用json串的形式打印出来。这样前端照着传对象也不太容易出错而且好多可以直接知道字段的意思,也省的一个个问了。这就要用到我上面说的json和对象相互转化的依赖了(忘记了的可以去上面找)。
代码中的做法(仅代表个人,不一定是最好的,你要是有别的方法可以略过这段):
package demo.entity;
import net.sf.json.JSONObject;
public class Json {
public static void main(String[] args) {
MsgXadvt msgXadvt = new MsgXadvt();
msgXadvt .setId("用户id");
msgXadvt . setFeild1("字段1");
msgXadvt . setFeild2("字段二");
msgXadvt . setFeild3("如果是string可以写字段是干什么的");
msgXadvt . setFeild4("如果是int可以随意01啥的");
JSONObject o = JSONObject.fromObject(msgXadvt);
//最后以json对象的形式打印出
System.out.println("MsgXadvt:"+o.toString());
}
}
随便建一个类并且用main方法打印出想要的json串贴到接口文档中。下面的就是我完整的第一版接口文档:
因为我们这个是微服务项目,所以我定义的这个服务端口8079,还有就是每个接口的路径。因为最后还要用网关整合,所以这里路径都是最简方式。你们如果是具体的项目这些要根据实际情况和要求自己设计。我这里只不过是供大家参考。接下来要做的就是代码中实现这些接口了。
4.1)返回值的封装:
首先我们要定义好的就是给前端返回值的格式。总不能想传啥传啥,一点格式和规矩都没有。这样前端都不知道接口调用是成功了还是失败了之类的。一般都是用一个封装好了的对象来返回。我这里是自己定义的一个ResultBean。把这个对象的代码贴出:package org.ourtowns.xadvt.tool;
/**
* 用于封装返回结果
* @author 变异者
*
*/
public class ResultBean {
private int status; //状态码:200是正常调用 500是程序出错
private String msg; //返回消息:成功还是失败
private Object data; //返回的对象。有时候我们要返回一些数据就放在这个里,如果不需要返回数据则是null
private Boolean result; //接口调用的结果 true是成功。false是失败。
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Boolean getResult() {
return result;
}
public void setResult(Boolean result) {
this.result = result;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public ResultBean() {
}
public ResultBean(int status, String msg, Object data,Boolean result) {
super();
this.status = status;
this.msg = msg;
this.data = data;
this.result = result;
}
}
解释下好多人可能不太明白status和最后的result有什么不同:区别于status的是这个注重的是结果的成功失败。status注重的是程序的成功失败。打个比方,用户登陆。我要验证用户的账号和密码是否正确。如果这个时候数据库连接崩了,程序运行异常则会返回500和false。告诉前端是我们程序异常所以失败了。但是如果数据库正常连接,然后发现用户的密码错误了,这个时候返回200和false。告诉前端我们程序正常运行了,但是得到的结果是密码错误,所以false。然后只有当程序正常运行,账号密码都正确,才会返回200和true。(但是这个也是我们自己定义的,可能换个公司换个团队是有不一样定义的。我这里只是参考)。
然后将这个返回结果封装成一个工具方法:
/**
* 返回数据格式
* @param status
* @param msg
* @param flag
* @param object
* @return
*/
public static ResultBean result(int status,String msg,Object object,boolean flag) {
return new ResultBean(status, msg, object, flag);
}
4.2)接口的书写:
我个人习惯先在service接口中把所有要实现的接口写出来,再去一个个代码实现。因为这里也不需要复制粘贴,毕竟每个业务都不会一样。所以我直接上截图而不是文本。
接下来就是在它的实现类里实现这些方法的具体逻辑,这里要注意的是实现类要被spring管理哦~~别忘了添加@Service。而且这里肯定要用到jpa层,我建议大家先都注入进来,毕竟早晚都要做的。还是截图表示。
然后我这里先写一个接口的demo,大家可以参考一下格式。其实200,500这些用枚举或者常量都可以。但是我这里就懒了,所以直接这么写。(如果你有更好的办法可以略过这块。)
@Override
public ResultBean addXadvt(MsgXadvt msgXadvt) {
try {
//这里写你的逻辑代码。
return Tools.result(200, "添加广告成功", ”如果有要返回的数据放这里,没有就是null“, true);
} catch (Exception e) {
logger.info("添加广告失败:" + e.getMessage(), e);
return Tools.result(500, "添加广告失败", null, false);
}
}
然后具体代码的实现我就不一步一步的写了。jpa的一些增删改查用法我也不在这里写了。有问题的可以去看我专门写jpa用法的文章。当我们在实现类中把所有的接口都用代码实现。下一步就是写入controller层了。
4.3)controller的编写:
这里有一个个人的小技巧,可以参考,不喜勿喷。就是我的controller也会继承相应的service接口。这样一来方法一键生成,省事又不容易出错。但是在返回的时候调用的确实实现类的代码实现。这有一个小问题,就是书写一定要注意!如果不小心返回了自己的这个方法,相当于无限递归了。不过一测便知,不算太难解决。
下面是我的controller的代码截图,注意点我都标出来了:
嗯~到了这里,其实咱们这个项目基本上就算是完成了。剩下的就是调试的部分了~
第五步,接口调试:
额,大家都做完所有的接口都在controller中配置好以后,可以启动boot的启动类跑一跑了~反正我这里是一次跑起来了~你如果有什么问题根据问题解决吧~~如图所示,在8079启动成功~~
接下来,我这里因为有postman所以直接用postman测试了。亲们如果没有要么测试用例要么直接网址访问都ok。然后关于postman的用法我也有完整的文章教程。想学的可以自己戳链接:postman安装使用教程。
就调用第一个添加的接口作为测试。我这边截个调用成功的图作为收尾吧~~
调用结果:
其实一个完整的项目就是这么简单~~不过我这里还是要说一些自己的理解:
1)一个项目,其实经验多的亲们都会发现,真正敲代码的时候甚至都不到工期的一半。因为需求,思路,设计等等都是基石。然后才开始敲代码。再之后联调,调试。又是一个让人又爱又恨的阶段。这期间你能明显的看到你的成果,功能的实现。但是bug不可避免。甚至有些bug要调试好久最后发现是一个很微小的疏忽而已。心态要放好。
2)没有一步到位的设计。哪怕之前有了流程,接口文档,数据库设计。但是真正到了实践肯定会有所欠缺或忽略。这个是正常现象。只要主方向别错。一些小的改动很正常很正常。不要因为这样觉得你能力不足或者怀疑自己。
3)最忌讳的就是眼光过高!我见过一个人,在开发一款产品。产品没出,没上线。然后整天寻思分库分表。说实话,我没觉得他有多未雨绸缪,只是可笑的杞人忧天而已。曾经一个我很喜欢的架构师说过一句话,任何功能都是先实现再优化!所以如果有小白或者萌新看到这篇文章,希望你们别被各种言语的诱导而想错。你可以设计数据冗余,表不合理。应该一次查询的东西分三次查询。但是你最后项目做出来了,可以慢慢的思考优化。这胜过于你用无限长的时间针对数据库设计无限的优化,可是实际上项目还没有开始编写要强的多。
4)这是一个学习的建议,也是一个心得:学到即得到,知识不会是无用的。
5)最后一点:以上代码都是我个人风格代码。没多优秀或者怎么值得学习。我只是拿出来给经验少,没思路的人一种参考。可能一个人或者一个团队一种风格,比我这样做更好的有的是。然后不喜勿喷。如果你觉得我哪里做的不对或者可以更好愿意的话可以留言或者私信交流交流。
全文手打~~这么不容易的写个文~~如果你觉得用到了~留个言点个赞转个发什么的啊~