1.前言
一年多没碰Spring,在学scala的时候突然又接触到了,有点懵逼,故根据PK老师的一份Spring Boot与mysql交互的基础代码来回顾一遍Spring Boot。前前后后找了很多文章,又唤醒了尘封的记忆。
这个项目的代码架构如下所示:
2.pom
Spring Boot的系统构建支持Maven,而pom.xml是Maven的主要配置文件,需要在pom.xml中对Maven项目进行配置。具体说来,最重要的是添加了两个依赖:对Spring data jpa的依赖和mysql的驱动包。其中jpa是当前Java标准ORM(对象关系映射)通用规范,说人话就是能够让我们在不写臃肿且重复的SQL语句的情况下,只用平时操作对象的方法即可完成对数据库的操作。而要想将Java与mysql链接则需要mysql-connector-java驱动包。
<!--添加Data的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--添加MySQL驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
顺便要记一下的是,spring-boot-starter的版本是在一开始的parent中给出的,如下面的代码所示。之后其他与之相关的依赖项都不需要写版本号了,就像上面的spring-boot-starter-data-jpa一样。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
3.resources
resources文件夹是Spring Boot默认存放静态资源文件的位置,我们可以将配置文件放在这里。配置文件主要有两种,一种是以.properties为后缀名的文件,另一种是以.yml为后缀名的文件,两种格式有些差距,但使用起来的功能相差不大。我们在下面新建了application.yml文件作为配置文件。.yml文件写的时候注意次一级前加的是空格不是tab,且冒号后需要空一格再写内容。
server:
port: 7777
context-path: /scala-boot
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/imoocbootscala
jpa:
hibernate:
ddl-auto: update
database: mysql
具体到内容来看,在对web的配置方面,指定端口号用到了server.port,指定初始地址用到了server.context-path。
在对mysql配置来看,配置mysql驱动用到了spring.datasource.driver-class-name。这里提一嘴,在我运行的时候发现这里会报个错,原因在于com.mysql.jdbc.Driver已经被弃用了,变成时代的眼泪了,根据提示改成com.mysql.cj.jdbc.Driver即可。
继续说回来,在spring.datasource下还需配置自己mysql的账号(username)、密码(password)与链接地址(url)。这里有个坑在url这,在使用某些新版本mysql驱动的情况下,说不定会报The server time zone value ' й ʱ ' is unrecognized or represents more than one time zone。这时候只要在url的地址后面加上?serverTimezone=UTC即可,即变成jdbc:mysql://localhost:3306/imoocbootscala?serverTimezone=UTC,这个好像是时区产生的问题。
下面说最后的jpa.hibernate.ddl-auto,jpa在上面刚唠过,hibernate是jpa的具体实现(那jpa就相当于接口了),ddl-auto主要存在四种属性,具体内容画个表了解一下,这个表的内容参考了YoungLee16大佬的博客。另外jpa.database就不说了,大家都懂。
create | 每次运行该程序,没有表格会新建表格,表内有数据会清空 |
create-drop | 每次程序结束的时候会清空表 |
update | 每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新 |
validate | 运行程序会校验数据与数据库的字段类型是否相同,不同会报错 |
4.domain层
domain是领域的意思。在domain层的核心成员叫做domain entity(领域实体),用人话说可以理解为与数据库中表对应起来的JavaBean。JavaBean也不太像人话,用廖雪峰大佬的话说:
如果读写方法符合以下这种命名规范:
// 读方法: public Type getXyz() // 写方法: public void setXyz(Type value)
那么这种class被称为JavaBean。JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。
豁然开朗 ,廖雪峰大佬牛批!另外domain层的代码如下所示。
package com.imooc.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* 数据库元数据
*/
@Entity
@Table
public class MetaDatabase {
@Id
@GeneratedValue
/**数据库ID*/
private Integer id;
/**数据库名称*/
private String name;
/**数据库存放的文件系统地址*/
private String location;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
}
主要的问题出在@Entity、@Table、@Id、@GeneratedValue这四个玩意上。这种@xxxx形式的东西在Java中叫做注解,是Java语言用于工具处理的标注。具体内容可以看这里。
其中@Entity表示该类为一个实体类,相当于指明了这个类为一个实体Bean;@Table指定了@Entity所要映射的数据库表,当实体类与其映射的数据库表名不同名时,需要使用 @Table注解的name属性说明(如果@Table缺省,则类名作为表名),该标注与 @Entity 注解并列使用,置于实体类声明语句之前。一个常规的二连击是这样的:
@Entity
@Table(name = "I_have_a_girlfriend")
public class balabala{
...
}
@Id所注解的关键字就是对应表中的主键,这个很好理解。@GeneratedValue 表示在向表中创建这个对象的时候,就会自动创建主键id,可以理解为是主键生成的策略,@GeneratedValue包含了两个属性,generator和strategy。其中generator负责声明主键生成器的名称,strategy属性具体内容见下表。
AUTO | 主键由程序控制,默认值 | |
IDENTITY | 主键由数据库生成, 采用数据库自增长 | Oracle不支持 |
SEQUENCE | 通过数据库的序列产生主键 | mysql不支持 |
TABLE | 提供特定的数据库产生主键 |
一个常规的二连击是这样的:
@Id
@GeneratedValue(generator = "baby_I_Love_U")
private Integer id;
另外提一嘴@Column,基本用法是标识实体类中属性与数据表中字段的对应关系。比如这个样子:
@Column(name = "role_name")
private String roleName;
5.repository(DAO)层
repository是仓库的意思。它的定义是通过用来访问领域对象的一个类似集合的接口,在领域层与上层之间进行协调。这里的具体解释可以参考这里。简单说来,我们可以将repository看作仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。这样做的好处是将领域模型从客户代码和数据映射层之间解耦出来。让我们来看下repository层的代码:
package com.imooc.repository;
import com.imooc.domain.MetaDatabase;
import org.springframework.data.repository.CrudRepository;
public interface MetaDatabaseRepository extends CrudRepository<MetaDatabase,Integer>{
}
这里我们的接口继承了CrudRepository,我们回溯一下去查CrudRepository接口的源码,可以看到该接口能够实现包括保存、查找、计数、删除等常规的数据库增删改查操作。需要传入的为待操作的数据库与其Id,关于<S extends T>这类泛型知识可以看这里。
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
<S extends T> S save(S var1);
<S extends T> Iterable<S> save(Iterable<S> var1);
T findOne(ID var1);
boolean exists(ID var1);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> var1);
long count();
void delete(ID var1);
void delete(T var1);
void delete(Iterable<? extends T> var1);
void deleteAll();
}
6.service层
service顾名思义是服务的意思。这层主要任务是通过调用repository层来完成相应的业务逻辑处理。主要需要了解的还是三个注解:@Service、@Autowired、@Transactional。(写到这里刚好是情人节0点,头又秃了一点)
package com.imooc.service;
import com.imooc.domain.MetaDatabase;
import com.imooc.repository.MetaDatabaseRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MetaDatabaseService {
@Autowired
private MetaDatabaseRepository metaDatabaseRepository;
@Transactional
public void save(MetaDatabase metaDatabase) {
metaDatabaseRepository.save(metaDatabase);
}
public Iterable<MetaDatabase> query(){
return metaDatabaseRepository.findAll();
}
}
@Service注解可以标记当前类是一个service类,会将当前类自动注入到Spring容器中,不需要再在applicationContext.xml文件定义bean了。这里要理解一下DI(依赖注入)和容器两个概念。有一个很秀的比喻是这么说的:“容器就像后宫,皇帝只需点名。皇帝点到谁,谁就来了”。可以说,Spring 容器是 Spring 框架的核心。容器将创建对象,把它们连接在一起,配置它们,并管理他们的整个生命周期从创建到销毁。至于依赖注入,这里可以引用这篇文章具体的做一下比喻:
在理解依赖注入之前,看如下这个问题在各种社会形态里如何解决:一个人(Java实例,调用者)需要一把斧子(Java实例,被调用者)。
(1)原始社会里,几乎没有社会分工。需要斧子的人(调用者)只能自己去磨一把斧子(被调用者)。对应的情形为:Java程序里的调用者自己创建被调用者。
(2)进入工业社会,工厂出现。斧子不再由普通人完成,而在工厂里被生产出来,此时需要斧子的人(调用者)找到工厂,购买斧子,无须关心斧子的制造过程。对应Java程序的简单工厂的设计模式。
(3)进入“按需分配”社会,需要斧子的人不需要找到工厂,坐在家里发出一个简单指令:需要斧子。斧子就自然出现在他面前。对应Spring的依赖注入。
第一种情况下,Java实例的调用者创建被调用的Java实例,必然要求被调用的Java类出现在调用者的代码里。无法实现二者之间的松耦合。
第二种情况下,调用者无须关心被调用者具体实现过程,只需要找到符合某种标准(接口)的实例,即可使用。此时调用的代码面向接口编程,可以让调用者和被调用者解耦,这也是工厂模式大量使用的原因。但调用者需要自己定位工厂,调用者与特定工厂耦合在一起。
第三种情况下,调用者无须自己定位工厂,程序运行到需要被调用者时,系统自动提供被调用者实例。事实上,调用者和被调用者都处于Spring的管理下,二者之间的依赖关系由Spring提供。
所谓依赖注入,是指程序运行过程中,如果需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部的注入。
至于通过applicationContext.xml引出的Application Context概念也很重要,它是 BeanFactory 的子接口,也被成为 Spring 上下文。具体解释如下:
Application Context 是 spring 中较高级的容器。和 BeanFactory 类似,它可以加载配置文件中定义的 bean,将所有的 bean 集中在一起,当有请求的时候分配 bean。 另外,它增加了企业所需要的功能。
Application Context 包含 BeanFactory 所有的功能,一般情况下,相对于 BeanFactory,ApplicationContext 会更加优秀。当然,BeanFactory 仍可以在轻量级应用中使用,比如移动设备或者基于 applet 的应用程序。
与之有关的概念还有IoC(控制反转),这是一个面向对象编程的设计原则,由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找,而其中最常见的实现方式就是依赖注入了。
这些概念理解后,@Autowired就容易说清了,它被称为自动装配,就是说它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。换句话说,Spring将项目中的实体进行集中管理,然后通过@Autowired注解作为标识,以自动注入的方式来给实体注入属性。
最后一个@Transactional注解,Transactional的翻译叫“事务性的”,它的功能是让service层每个业务方法调用是都打开一个Transaction(事务)。事务的意思是指访问并可能更新数据库中各种数据项中的一个程序执行单元。在service方法添加事务时,若发生unchecked exception,就会回滚。有一点要注意的是@Transactional注解只能写在public方法上,标注在protected和private上时并不会生效。
说完注解再说一下Iterable<MetaDatabase>。Iterable是迭代器接口,实现集合的遍历。
7.controller层
controller是控制层,它的主要用处是接受客户端的请求,然后调用service层业务逻辑,获取到数据,传递数据给视图层(客户端)用于视觉呈现。代码如下:
package com.imooc.controller;
import com.imooc.domain.MetaDatabase;
import com.imooc.service.MetaDatabaseService;
import com.imooc.utils.ResultVO;
import com.imooc.utils.ResultVOUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/meta/database")
public class MetaDatabaseController {
@Autowired
private MetaDatabaseService metaDatabaseService;
@RequestMapping(value = "/", method = RequestMethod.POST)
public ResultVO save(@ModelAttribute MetaDatabase metaDatabase) {
metaDatabaseService.save(metaDatabase);
return ResultVOUtil.success();
}
@RequestMapping(value = "/", method = RequestMethod.GET)
public ResultVO query() {
return ResultVOUtil.success(metaDatabaseService.query());
}
}
具体需要解释的@RestController、@RequestMapping两个注解。其中@RestController注解相当于@Controller加上@ResponseBody,@Controller用于标注控制层组件,@ResponseBody注解的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来为客户端返回JSON数据或者是XML数据,它通常使用在controller层的方法上。@RequestMapping注解用来映射请求的路径,它既可以用于某个类,也可以用于某个方法上。以上面这个代码为例,MetaDatabaseController的URL指向了“http://localhost:7777/scala-boot/meta”,而方法里的URL指向了“http://localhost:7777/scala-boot/meta/”。
至于RequestMethod中的GET方法和POST方法,在这篇文章中有很深入的解释,浅显的摘抄如下:
在大万维网世界中,TCP就像汽车,我们用TCP来运输数据,它很可靠,从来不会发生丢件少件的现象。
但是如果路上跑的全是看起来一模一样的汽车,那这个世界看起来是一团混乱,送急件的汽车可能被前面满载货物的汽车拦堵在路上,整个交通系统一定会瘫痪。
为了避免这种情况发生,交通规则HTTP诞生了。HTTP给汽车运输设定了好几个服务类别,有GET, POST, PUT, DELETE等等,HTTP规定,当执行GET请求的时候,要给汽车贴上GET的标签(设置method为GET),而且要求把传送的数据放在车顶上(url中)以方便记录。
如果是POST请求,就要在车上贴上POST的标签,并把货物放在车厢里。当然,你也可以在GET的时候往车厢内偷偷藏点货物,但是这是很不光彩;也可以在POST的时候在车顶上也放一些数据,让人觉得傻乎乎的。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。
8.utils
utils文件夹一般是用来存放辅助类的,这里我们存放了两个辅助类,它们的功能分别是定义了http返回对象与boot返回工具类。
package com.imooc.utils;
import java.io.Serializable;
/**
* http请求返回的对象
*/
public class ResultVO<T> implements Serializable {
/**错误码*/
private Integer code;
/**提示信息*/
private String msg;
/**具体内容*/
private T data;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
package com.imooc.utils;
/**
* Boot返回值工具类
*/
public class ResultVOUtil {
public static ResultVO success(Object object) {
ResultVO resultVO = new ResultVO();
resultVO.setData(object);
resultVO.setCode(0);
resultVO.setMsg("成功");
return resultVO;
}
public static ResultVO success() {
return success(null);
}
public static ResultVO error(Integer code, String msg) {
ResultVO resultVO = new ResultVO();
resultVO.setCode(code);
resultVO.setMsg("msg");
return resultVO;
}
}