乐趣区

SpringBoot数据库操作

本篇概述
上一篇中我们已经介绍了在 SpringBoot 项目中怎么修改默认配置参数,并且我们还掌握了怎么获取配置文件中自定义参数。在这一篇中我们将介绍 SpringBoot 对数据库的操作。既然是对数据库的操作,那难免有一些配置的参数。例如数据库的连接、数据库账号及数据库密码等。所以掌握上篇中的内容很重要。除此之外,我们还要介绍一下用什么样的技术来操作数据库。操作数据库的技术有很多例如比较常见的 JDBC、Mybatis、Hibernate 等。在 SpringBoot 的项目中为我们提供了另外一种操作数据库的技术,也就是 JPA。我们可以通过 JAP 中提供的方式来非常方便的操作数据库。下面我们首先添加 SpringBoot 对数据库的配置参数。具体参数如下:
数据库配置
spring:
profiles:
active: dev
datasource:
url: jdbc:mysql://localhost:3306/springboot?useSSL=false&characterEncoding=utf8
username: root
password: jilinwula
driver-class-name: com.mysql.jdbc.Driver
jpa:
hibernate:
ddl-auto: create
show-sql: true
spring.jpa.hibernate.ddl-auto 参数详解
上面的配置参数比较简单,我们就不详细介绍了,我们只介绍 spring.jpa.hibernate.ddl-auto 参数。该参数的作用是自动操作表结构。且该参数有 4 种选项,下面我们详细介绍一下这 4 种的区别。

create: 当我们启动 SpringBoot 项目时,会自动为我们创建与实体类对应的表,不管表中是否有数据。也就是如果指定参数为 create 时,当项目启动后,该表的数据一定为空。因为该参数的处理方式,是先将表删除后,然后在创建新的表。
create-drop: 当我们启动项目时也会我们自动创建表,但当我们的项目运行停止后,它会自动为我们删除表,并且该参数为 create 一样在启动时也会先把表删除后,然后在创建。
update: 每当我们启动项目时,如果表不存在,则会根据实体类自动帮我们创建一张表。如果表存在,则会根据实体类的变化来决定是不是需要更新表,并且不管更不更新表都不会清空原数据。
validate: 当我们启动项目时会验证实体类中的属性与数据库中的字段是否匹配,如不匹配则报错。

添加相关依赖
如果我们按照上面的方式配置完后,则会发现上面的 driver-class-name 参数会报红显示,原因是没有找到相关的依赖。并且在 SpringBoot 的项目中如果想用 JPA 功能的除了要引入 Mysql 的依赖外,还要引入 Jpa 的依赖。具体依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
当我们添加完上面的依赖后发现配置文件中的 driver-class-name 参数已经不报红显示了,这就表示我们的依赖引入成功了。
spring.jpa.hibernate.ddl-auto 参数验证
下面我们创建一个实体类,来验证一下,上面的 spring.jpa.hibernate.ddl-auto 参数是不是上面说的那样。我们首选验证当参数为 create 时。下面为实体类的代码:
package com.jilinwula.springboot.helloworld.entity;

import lombok.Data;

import javax.persistence.*;

@Data
@Entity
@Table(name = “user_info”)
public class UserInfoEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String username;

private String password;
}
上面实体类中我们指定了几个注解,下面我们详细介绍一下它们的作用:
实体类详解

@Data: 自动生成 GET 和 SET 方法的注解,在上一篇中我们已经介绍过了,该注解可以在类上和属性中添加。如果添加在类上,是表示自动为该实体类的所有属性创建 GET 和 SET 方法。如果添加到属性中则表示只会为该属性添加 GET 和 SET 方法。这样我们就没必要为代码中生成大量的 GET 和 SET 烦恼了。这时可能有人会想到那如果我只想让它帮我生成 GET 方法或者只想生成 SET 方法时应该怎么办呢?别着急,既然你想到了,那么开发这个注解的人也想到了,我们只需要将上面的 @Data 注解修改为相应的 @Getter 或者 @Setter 注解即可。它们正好对应的生成 GET 和 SET 方法。
@Entity: 实体类注解。只有标识该注解的类,JPA 才能自动将这个类中的属性和数据库进行映射。
@Table: 标识该实体类和数据库中哪个表进行映射。
@Id: 标识该自动为主键标识。
@GeneratedValue: 标识主键的生成规则。这个在后面的文章中在做详细介绍。

现在我们一切准备就绪了,我们只要启动一下 SpringBoot 的项目就可以了,看看会不会自动为我们创建一张 userinfo 的表。(备注:数据库需要我们自己创建)。我们首先确认一下数据库中确实没有 userinfo 这张表。

下面我们启动一下 SpringBoot 的项目,看一下数据库中有没有创建新的表。


我们看 JPA 确实为我们创建了一张和实体类中 @Table 注解指定的表,并且表中的字段和实体类中的属性一致。下面我们手动向表中添加一条数据,然后重新启动项目,看看项目启动后,这条新增的数据还是否存在。

我们只新增了一条数据,然后重启启动项目后,在看一下数据中的 userinfo 表,看看该数据还有没有。

我们发现刚刚添加的那条数据已经没有了,这也就恰恰证明了,我们上面所有说当 spring.jpa.hibernate.ddl-auto 参数为 create 时,每当项目启动时,都会将原先的表删除,然后在通过实体类重新生成新的表,既然已经是将原先的表都删除了,那曾经添加的数据当然不存在了。如果我们仔细查看 SpringBoot 项目的启动日志,发现启动日志中已经输出了相应的删除及建表的语句,下面为项目启动的时操作表的日志。
Hibernate: drop table if exists user_info
Hibernate: create table user_info (id bigint not null auto_increment, password varchar(255), username varchar(255), primary key (id))
下面我们将 spring.jpa.hibernate.ddl-auto 参数修改为 create-drop 来验证一下 create-drop 参数的特性。我们首先先把表删除掉,以免刚刚的 create 参数影响 create-drop 的验证。

我们还是和刚刚一样启动项目后查看数据库中是不是自动为我们创建一张 userinfo 表。

我们看一样还是自动为我们创建了一个新的表。下面我们将服务执行后,在看一下该表还是不是存在。

我们发现,刚刚创建的表已经自动删除了,这就是 create-drop 参数的特性。我们查看日志,也可以发现当停止服务时,自动执行的删表语句。下面的日志。
Hibernate: drop table if exists user_info
Hibernate: create table user_info (id bigint not null auto_increment, password varchar(255), username varchar(255), primary key (id))
2019-01-18 16:56:27.033 INFO 6956 — [main] org.hibernate.tool.hbm2ddl.SchemaExport : HHH000230: Schema export complete
2019-01-18 16:56:27.058 INFO 6956 — [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit ‘default’
2019-01-18 16:56:27.400 INFO 6956 — [main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@21de60b4: startup date [Fri Jan 18 16:56:23 CST 2019]; root of context hierarchy
2019-01-18 16:56:27.482 INFO 6956 — [main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped “{[/error]}” onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2019-01-18 16:56:27.483 INFO 6956 — [main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped “{[/error],produces=}” onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2019-01-18 16:56:27.511 INFO 6956 — [main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2019-01-18 16:56:27.512 INFO 6956 — [main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2019-01-18 16:56:27.553 INFO 6956 — [main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2019-01-18 16:56:27.888 INFO 6956 — [main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2019-01-18 16:56:27.973 INFO 6956 — [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8081 (http)
2019-01-18 16:56:27.978 INFO 6956 — [main] JilinwulaSpringbootHelloworldApplication : Started JilinwulaSpringbootHelloworldApplication in 5.928 seconds (JVM running for 7.512)
2019-01-18 17:00:50.630 INFO 6956 — [Thread-16] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@21de60b4: startup date [Fri Jan 18 16:56:23 CST 2019]; root of context hierarchy
2019-01-18 17:00:50.661 INFO 6956 — [Thread-16] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown
2019-01-18 17:00:50.664 INFO 6956 — [Thread-16] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit ‘default’
2019-01-18 17:00:50.666 INFO 6956 — [Thread-16] org.hibernate.tool.hbm2ddl.SchemaExport : HHH000227: Running hbm2ddl schema export
Hibernate: drop table if exists user_info
在日志中我们发现一共执行了两次删除表的语句,第一次是在启动前,第二次是在服务停止时。

下面我们把 spring.jpa.hibernate.ddl-auto 参数修改为 update 来验证 update 参数的特性。同样我们还是事先要把刚刚生成的表删除掉,因为 create-drop 参数在停止服务时,已经把刚刚的表删除掉了,所以我们就不用手动删除了,我们直接把 spring.jpa.hibernate.ddl-auto 参数修改为 update,然后直接启动项目。

我们看上图当我们把 spring.jpa.hibernate.ddl-auto 参数设置为 update 时,也会自动为我们创建表。并且我们停止服务时,该表依然还存在,并且不会清除数据。下面我们向表中添加一条新数据。然后修改实体类中的结构,看看刚刚新增的数据还存在不存在。

实体类修改如下:
package com.jilinwula.springboot.helloworld.entity;

import lombok.Data;

import javax.persistence.*;

@Data
@Entity
@Table(name = “user_info”)
public class UserInfoEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String username;

private String password;

private String nickname;
}
我们新增了一个 nickname 属性,然后我们重新启动服务看看这个新的字段会不会自动在表中创建,并且没有更改原先表中的数据。

我们看新的字段已经自动创建成功了,并且没有删除原先表中的数据,这就是 update 参数的作用,在实际的开发中,把 spring.jpa.hibernate.ddl-auto 参数设置为 update,是比较常见的配置方式。

下面我们验证最后一个参数也就是 validate 参数。因为之前的操作我们现在 userinfo 表中一其有 3 个字段,现在我们将实体类中的 nickname 字段注释掉,然后我们在启动服务,看一看项目启动是否正常。下面为实体类源码:
import javax.persistence.*;

@Data
@Entity
@Table(name = “user_info”)
public class UserInfoEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String username;

private String password;

// private String nickname;
}
当我们启动项目时,发现项目是可以正常启动。然后查看数据库中表,我们发现表中的结构没有任何变化。也就是说,我们注释掉数据库中已有的字段然后启动项目时,项目是可以正常启动的。这又是为什么呢?validate 参数的作用不就是验证实体类中的属性与数据库中的字段不匹配时抛出异常吗?为什么当我们这么设置时没有抛出异常呢?这是因为 validate 参数的特性是只有实体类中的属性比数据库中的字段多时才会报错,如实体类中的属性比数据库中字段少,则不会报错。刚刚我们将 nickname 属性给注释了,但 validate 是不会更改表结构的,所以数据库中还是会有 nickname 字段的,这就导致数据中的字段比实体类中的属性多,所以当我们启动项目时是不会抛出异常。但反之,如果我们在实体类中新增一个字段,然后我们在启动项目时,项目就会抛出异常。实体类源码:
@Data
@Entity
@Table(name = “user_info”)
public class UserInfoEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String username;

private String password;

private String nickname;

private Long roleId;
}
我们新增了一个 roleId 字段,并且该字段在数据库中是没有的,然后我们启动项目。查看日志发现项目已经启动失败了。下面为日志:
Caused by: org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing column [role_id] in table [user_info]
at org.hibernate.tool.schema.internal.SchemaValidatorImpl.validateTable(SchemaValidatorImpl.java:85) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
at org.hibernate.tool.schema.internal.SchemaValidatorImpl.doValidation(SchemaValidatorImpl.java:50) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
at org.hibernate.tool.hbm2ddl.SchemaValidator.validate(SchemaValidator.java:91) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:475) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:444) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:879) ~[hibernate-entitymanager-5.0.12.Final.jar:5.0.12.Final]
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:60) ~[spring-orm-4.3.21.RELEASE.jar:4.3.21.RELEASE]
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:360) ~[spring-orm-4.3.21.RELEASE.jar:4.3.21.RELEASE]
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:384) ~[spring-orm-4.3.21.RELEASE.jar:4.3.21.RELEASE]
… 20 common frames omitted
下面我们在数据库中将 roleId 字段手动添加上,然后我们在重新启动项目,在看一下启动时项目还是否报错。

下面我们重新启动项目,然后在看一下日志, 发现项目已经成功启动了,这就是 validate 参数的作用。
2019-01-19 17:17:41.160 INFO 1034 — [main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2019-01-19 17:17:41.201 INFO 1034 — [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8081 (http)
2019-01-19 17:17:41.204 INFO 1034 — [main] JilinwulaSpringbootHelloworldApplication : Started JilinwulaSpringbootHelloworldApplication in 2.596 seconds (JVM running for 3.233)

实体类注解高级配置
上述内容我们基本已经将 spring.jpa.hibernate.ddl-auto 参数的的使用介绍完了,下面我们介绍一下实体类中的高级注解。因为我们在上面的测试中我们发现,当我们把 spring.jpa.hibernate.ddl-auto 参数设置为 create 时,虽然成功的创建了实体类中指定的表,但是我们发现自动创建的表只是字段和实体类中的属性一致,但例如表中的字段长度、字段的描述、表的索引,这些高级的配置,是需要我们在实体类中添加新的注解,才能设置的。下面我们将实体类中的代码修改一下,添加上面说的注解,并且验证上面注解是否可以正确设置表中的长度、描述及索引。(备注: 别忘记将 spring.jpa.hibernate.ddl-auto 参数设置为 create)下面为实体类源码:
package com.jilinwula.springboot.helloworld.entity;

import lombok.Data;

import javax.persistence.*;

@Data
@Entity
@Table(name = “user_info”, indexes = @Index(columnList = “username”))
public class UserInfoEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = “id”, columnDefinition = “bigint(10) comment ‘ 主键 '”)
private Long id;

@Column(name = “username”, columnDefinition = “varchar(10) not null default ” comment ‘ 账号 '”)
private String username;

@Column(name = “password”, columnDefinition = “varchar(10) not null default ” comment ‘ 密码 '”)
private String password;

@Column(name = “nickname”, columnDefinition = “varchar(10) not null default ” comment ‘ 妮称 '”)
private String nickname;

@Column(name = “role_id”, columnDefinition = “bigint(10) not null default 0 comment ‘ 角色 '”)
private Long roleId;
}
上面我们介绍过当在类中添加 @Entity 注解后,JPA 会自动将实体类中的属性映射为数据库中表里的字段。但在实际的开发中我们可能会遇到实体类中的属性与数据库中的字段不一致的情况。这时我们就要使用 @Column 注解了,该注解的参数有很多,我们要掌握 2 个就可以了。一个参数为 name 因为 JAP 在映射属性到数据库时,如果没有指定 @Column 参数,则默认使用和实体类中的属性一样的名字,如果指定了 @Column 则使用该注解中的 name 参数。第二个参数为 columnDefinition 参数,该参数则是可以直接将我们创建表中的语句写在该参数中,这样我们可以很方便的控制字段的长度及类型。还有一个注解为 @indexes。该注解可指定我们指定属性为表中的索引,这里要注意一下如果表中字段名字和实体类中的属性名字不一致,@indexes 注解需要指定的是实体类中的属性名,则不是真正表中的字段名。下面我们启动项目,看一下数据库中的表结构是不是和我们实体类中映射的一样。。

我们现在看数据库中的映射,除了创建索引时自动生成的索引名不一样,其它的字段映射类型长度及其描述都和我们实体类中的一致。

JpaRepository 接口
下面我们介绍一下怎么使用 JPA 来操作数据库。我们以增删改查为例,分别介绍它们的使用。在使用 JAP 操作数据库时,我们需要创建一个和实体类相对应的接口,并且让该接口继承 JAP 中已经提供的 JpaRepository(有很多个接口暂时只介绍这一个) 接口。这样我们就可以通过这个接口来操作数据库了。下面我们看一下该接口的源码。
package com.jilinwula.springboot.helloworld.Repository;

import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserInfoRepository extends JpaRepository<UserInfoEntity, Long> {
}
在我们继承 JpaRepository 接口时需要我们指定两个参数,第一个参数表示我们要操作的实体类是哪一个,第二个参数表示我们实体类中的主键类型,其次我们还需要添加 @Repository 注解,这样 JPA 才能操作数据库。下面我们创建一个测试用例,分别介绍数据库中的增删改查。下面为测试用例源码:
package com.jilinwula.springboot.helloworld;

import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;
import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class JilinwulaSpringbootHelloworldApplicationTests {

@Autowired
private UserInfoRepository userInfoRepository;

@Test
public void save() {
UserInfoEntity userInfoEntity = new UserInfoEntity();
userInfoEntity.setUsername(“ 吉林乌拉 ”);
userInfoEntity.setPassword(“jilinwula”);
userInfoEntity.setNickname(“ 二十四分之七倍根号六 ”);
userInfoEntity.setRoleId(1L);

userInfoRepository.save(userInfoEntity);
}

@Test
public void contextLoads() {
}

}
数据库的增删改查
我们暂时只写一个新增的方法,并且我们发现,虽然我们的 UserInfoRepository 接口中没有写任何方法,但我们居然可以直接调用 save 方法了。这是因为当我们将 UserInfoRepository 接口继承 JpaRepository 接口时,是默认继承了该接口的一些方法,所以这些基本的增删改查操作,是不需要我们写任何代码的。下面我们执行一下测试用例,看看该条数据能否正确的插入到数据库中。

我们看数据已经成功的添加到了数据库中。下面我们在添加一条,方便我们以后测试。

。下面我们编写一下查询语句,看看能否正确查出数据。
@Test
public void select() {
UserInfoEntity userInfoEntity = userInfoRepository.findOne(1L);
System.out.println(userInfoEntity);
}
我们看同样,我们还是没有写 findOne 方法,但是我们居然可以直接使用。findOne 方法是 JPA 为我们提供通过主键查询数据的方法,所以该方法的返回值是实体类对象,因为只能返回一条数据。下面我们执行一下该测试用例,看看能否正确查询出数据。
UserInfoEntity(id=1, username= 吉林乌拉, password=jilinwula, nickname= 二十四分之七倍根号六, roleId=1)
我们看已经成功的查询出数据了。这时有人会说,如果我们想查所有的数据应该怎么办呢?别着急,JPA 中除了提供了 findOne 方法,还提供了 findAll 方法,顾名思义,该方法就是查询所有数据的。既然是所有数据,所以该方法的返回值为 List。下面为测试用例源码, 及其执行日志。
@Test
public void selectAll() {
List<UserInfoEntity> userInfoEntitys = userInfoRepository.findAll();
System.out.println(userInfoEntitys);
}
[UserInfoEntity(id=1, username= 吉林乌拉, password=jilinwula, nickname= 二十四分之七倍根号六, roleId=1), UserInfoEntity(id=2, username= 阿里巴巴, password=alibaba, nickname= 淘宝, roleId=2)]
下面我们介绍一下更新方法,在 JPA 中更新方法和 save 方法一样,唯一的区别就是如果我们在实体类中设置了主键,则调用 sava 方法时,JPA 执行的就是更新。如果不设置主键,则 JPA 执行的就是新增。下面为测试用例源码:
@Test
public void update() {
UserInfoEntity userInfoEntity = new UserInfoEntity();
userInfoEntity.setId(1L);
userInfoEntity.setUsername(“ 阿里巴巴 ”);
userInfoEntity.setPassword(“alibaba”);
userInfoEntity.setNickname(“ 淘宝 ”);
userInfoEntity.setRoleId(2L);

userInfoRepository.save(userInfoEntity);
}
现在我们在查询一下数据库,如果更新语句成功,那么此时数据库中则会有两条一样的数据。

我们看,数据库中的确有两条一模一样的数据了,这就证明了我们刚刚的更新语句成功了。下面我们介绍一下最后一个删除语句,该语句也同样比较简单,因为 JPA 也同样为我们提供了该方法,下面为测试用例。
@Test
public void delete() {
userInfoRepository.delete(1L);
}
我们在查询一下数据库,看看 id 为 1 的数据是否还在数据库中存在。

我们看该数据成功的删除了。这就是 JPA 对数据库的增删改查的基本操作。当然 JPA 中还提供了很多复杂的语法,例如级联查询、分页查询等等。这些高级的功能我们在后续的文章中在做详细介绍。这就是本篇的全部内容,如有疑问,欢迎留言,谢谢。

项目源码
下面为项目源码:https://github.com/jilinwula/jilinwula-springboot-helloworld3

原文链接
下面为项目源码:http://jilinwula.com/article/24338

退出移动版