前后端分离的 Web 架构中,后端一般是一个提供 Restful 接口的服务器,为前端提供所需数据。
目前主流的 Restful 后端的语言与框架有:
- JavaScript,运行于 Node.js 环境,有 Express,Restify 等框架
- Java,有 Spring Boot 等框架
- Python,有Django, Flask 等框架
- Ruby,有 Sinatra 等框架
- PHP,有 Laravel 等框架
课程 PJ 不限制后端的实现语言与框架,同学们可以自由选择。
本次 Lab 贴合课程内容,以 Spring Boot 为例,开发一个简单的 Restful 服务。
没有 Java 环境的电脑需要同学们去安装 JDK 与 JRE
Maven 是 Java 的库管理工具,其功能与 npm 比较类似。
如果不安装 Maven,我们也可以通过手动将 Jar 包放到程序的 classpath 中来添加依赖文件,就像前端也可以手动下载 js 库并添加至页面中一样。但是这样的方式不利于管理依赖文件,因此推荐使用 Maven。
下载 Maven: http://maven.apache.org/download.cgi 或解压 Lab 目录中的 apache-maven-3.6.1-bin.zip
安装 Maven: http://maven.apache.org/install.html
新建 Spring Boot 工程参考官方教程:https://spring.io/guides/gs/spring-boot/。
使用 IntelliJ IDEA Ultimate 版本的同学新建工程时也可以参考:https://www.jetbrains.com/help/idea/creating-spring-boot-projects.html
搭建完 Spring Boot 工程后,首先运行 mvn install
安装所需依赖。
工程目录中的 Application.java
为工程的入口,运行此类便可运行我们的 Spring Boot 工程。
代码如下:
package hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
SpringApplication.run(Application.class, args);
一行运行了我们的 Spring 工程。Spring Boot 会搜索工程中所有的 Controller,并注册对应的接口。然后 Spring Boot 会运行内置的 Tomcat 服务器来提供服务。
在 hello 文件夹中已经包含一个接口,查看 hello/HelloController.java
文件:
package hello;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String index() {
return "Hello, World!";
}
}
首先,@RestController
这个 Annotation 标记了这个类为 Restful Service 的一个 Controller,这是每个 Controller 在类名前必须加的 Annotation。
@RequestMapping("/")
表示 index
这个方法注册了 /
这个 path,可以通过在浏览器中打开 http://localhost:8080/
来访问这个接口。接口的 HTTP method 默认为 GET
,其他 method 会在后面讲述。
注意,这个接口返回的只是一个 String,不是 JSON,所以这个接口并不是一个符合 Restful 标准的接口。在写代码时要避免出现这种不标准的情况。
接下来我们创建一个简单的 Restful 接口。
接口 path 为 /greeting
,接收一个 url 参数 name
,方法为 GET
。
样例 URL:http://localhost:8080/greeting?name=Peter
。
样例返回:
{
"id":1,
"name":"Hello, Peter!"
}
实现方式如下:
首先为返回的 JSON 对象创建对应的 Bean 类,新建 response
包并新建 GreetingResponse
,代码如下:
package hello.response;
public class GreetingResponse {
private final long id;
private final String name;
public GreetingResponse(long id, String name) {
this.id = id;
this.name = name;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
}
注意这个类当中,最主要的是 getId
和 getName
这两个 getter 方法,这两个方法决定了返回的 JSON 的样式。
Java Bean 和 JSON 直接的对应关系是由
com.fasterxml.jackson.core
这个包提供的,这个包被包含在了我们的spring-boot-starter-web
中,如果是使用的spring-boot
包,需要手动添加jackson
包才能支持 Java Bean 和 JSON 之间的 Mapping。具体包依赖关系在 pom.xml 中的 dependencies 中配置。
接下去让我们创建接口函数。在 HelloController
中添加如下代码:
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public @ResponseBody GreetingResponse greeting(@RequestParam(value = "name", defaultValue = "World") String name){
return new GreetingResponse(counter.incrementAndGet(), "Hello, " + name + "!");
}
counter
是用来从 1 开始计数的,每次接口被调用时将 counter
加一。
@RequestMapping("/greeting")
将本接口注册到 /greeting
上。
@ResponseBody GreetingResponse
指定了接口返回的数据类型。
方法参数@RequestParam(value = "name", defaultValue = "World") String name
中,首先参数是 String name
,@RequestParam
指定了这个参数是在 URL 的 path 上的一个参数,而非在 header 中或者 body 中。 value = "name"
指定了这个变量对应 path 上的 name
参数。defaultValue = "World"
表示如果没有 name
参数,则将变量的值赋值为 World
。
函数内部可以自由编写,最后返回一个 GreetingResponse 对象即可。
完成后重新加载服务器,通过curl可以测试该接口
curl localhost:8080/greeting?name="xingyu"
{"id":1,"name":"Hello, xingyu!"}
目前我们的后端不能被前端直接调用,这是因为我们还没有设置跨域访问的权限。
比如当我们在浏览器新的 Tab 页面的 console 中使用 ajax 调用后端服务,会出现如下报错:
XMLHttpRequest cannot load http://localhost:8080/hello. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin '...' is therefore not allowed access.
为了让前端能够调用后端服务,我们可以将前端域名添加到头部 Access-Control-Allow-Origin
属性中。
在接口函数前添加 @CrossOrigin(origins = "<前端 hostname,如 http://localhost:9000>")
,或者方便起见允许来自所有源的请求:
@CrossOrigin(origins = "*")
@RequestMapping("/hello")
public String index() {
return "Hello, World!";
}
之前我们的接口没有指定接口方法,这些接口会默认方法为 GET
。
修改 @RequestMapping
的参数可以定义其他方法,如
@RequestMapping(value = "/register", method = RequestMethod.POST)
是注册在 /register 的 POST 方法上的。
对于同一个 path,可以注册多个方法不一样的接口。
@RequestParam
用来接收 path 中的变量,一个接口可以定义多个@RequestParam
变量,如:
public @ResponseBody GreetingResponse greeting(@RequestParam(value = "name") String name, @RequestParam(value = "message") String message)
@RequestBody
用来接收 body 中的变量,该变量一般是 JSON 对象,我们可以像定义 GreetingResponse
一样定义 body 的输入。
比如当 body 的数据是用户注册数据时,body 数据如下:
{
"username": "new-user",
"password": "123456",
"email": "a@b.com",
"phone": "12345678900"
}
我们对应新建一个 UserRegisterRequest.java
:
package hello.request;
public class UserRegisterRequest {
private String username;
private String password;
private String email;
private String phone;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
// 省略剩下的 getter 与 setter。
}
注意:对应 RequestBody 的类不能有 Constructor,这是 jackson 库的一个限制。
与上述两个类似, @RequestHeader
可以获取 Header 中的数据。一般不常用
除了 @RequestBody
只能出现一次外,@RequestHeader
和 @RequestParam
都可以出现多次。
标准的 Restful 服务返回的数据必须是 JSON,不能是一个字符串或者一个数字。
有时同一个接口可能返回不同结构的 JSON 数据,比如用户注册可能有注册成功和注册失败两种情况,会返回不同的数据。一个解决方法是把返回的数据类型改为 Object
,代码如下:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "/register", method = RequestMethod.POST)
public @ResponseBody Object register(@RequestBody UserRegisterRequest request){
if (/* success */ true) {
return new RegisterResponse(/* ... */); //RegisterResponse类请同学们自行定义
} else {
return new ErrorResponse(/* ... */); //ErrorResponse类请同学们自行定义
}
}
}
在类之前也可以加上 @RequestMapping。这样做的话,所有函数的 @RequestMapping 的 path 之前都会加上这个 path。比如上面代码中,注册的 path 是 /user/register。
对于 GET 方法,我们可以通过浏览器很方便地测试,但是对于其他方法,就无法这样测试了。因此推荐使用 Postman 进行接口测试。
Postman 可以从 Google 网上应用店中下载:https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=zh-CN
Postman 使用方法简单说明,以 /user/register 为例:
首先指定 HTTP method 为 POST,然后输入 url 为 localhost:8080/user/register。
接下去填写 body 数据。选择 Body,选择 raw,在右侧类型选择 JSON(application/json)
,然后输入 body 数据。
最后点击 Send
发送请求,在下方可以查看到运行结果
Postman 可以团队协作、分享,使用样例可以让前端开发者快速了解掌握后端 API 的使用方式。
Restful 服务的用户身份认证推荐使用 token 验证的方式。
Reference:
简单来说,token 验证是在用户登录成功后将用户名、token 发行时间和过期时间等信息进行加密,并将生成的密文发回给客户端作为身份认证的凭证。这个 token 密文只有服务端能解密,当用户进行权限相关的操作时,需要将 token 发回给服务端,服务端解密验证通过后进行相关操作。在这个过程中,服务端不需要储存任何用户登录状态,相比传统的 session 的方式,实现了服务端的无状态化。
传统的 session 验证仍旧是一个优秀的身份认证机制。同学们可以使用 Spring Session,或者自己手动编写自定义的 session 管理代码来进行用户身份认证。
Spring Session: http://projects.spring.io/spring-session/
Get started: https://spring.io/guides/gs/spring-boot/
Full reference: http://docs.spring.io/spring-boot/docs/2.0.0.BUILD-SNAPSHOT/reference/htmlsingle/#getting-started-installing-the-cli
Learn more about spring: https://spring.io/
MyBatis 是一个后端数据库层面的框架,类似的框架还有 Hibernate。同学们可以在 PJ 中自由选择后端数据库层面的实现方式,但不太推荐
MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。
MyBatis 支持各种数据库,本次 Lab 中以 MySQL 为例。
将下列两个依赖加入到 pom.xml 的 dependencies 中:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
然后运行 mvn install
安装这两个包。
MyBatis 的官方文档将 MyBatis 的各方面阐述得很清楚,本 Lab 中不重复其中的内容。
请同学们首先阅读 MyBatis 官方文档来学习 MyBatis。Lab 将展示一个结合 Spring Boot 的样例程序。
中文:http://www.mybatis.org/mybatis-3/zh/index.html
英文:http://www.mybatis.org/mybatis-3/index.html
我们接着上文中 Spring Boot 创建的工程,将 MyBatis 加入进来。
- 创建 MyBatis 核心配置文件
SqlMapConfig.xml
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties">
</properties>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
- properties 在
db.properties
文件中定义:
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/adweb_lab3?characterEncoding=utf-8
username=root
password=123456
- 对应地,在 MySQL 中创建新的 Schema
adweb-lab3
。 - 使用
lab3.sql
创建表:
DROP TABLE IF EXISTS Article;
DROP TABLE IF EXISTS User;
CREATE TABLE IF NOT EXISTS User (
userID INT(11) NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password VARCHAR(50) NOT NULL,
email VARCHAR(100) DEFAULT NULL,
phone VARCHAR(20) DEFAULT NULL,
PRIMARY KEY (UserID)
);
INSERT INTO User (userID, username, password, email, phone) VALUES
(1, 'kaiyudai', '12345678', 'kydai@fudan.edu.cn', '13666666666'),
(2, 'fanliu', '12345678', 'liufan@fudan.edu.cn', '13888888888'),
(3, 'xingyuzhang', '12345678', NULL, NULL);
- 新建
SqlSessionLoader.java
来载入 MyBatis:
package hello.mybatis;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class SqlSessionLoader {
private static SqlSessionFactory sqlSessionFactory;
public static SqlSessionFactory getSqlSessionFactory() throws IOException {
if (sqlSessionFactory == null) {
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
return sqlSessionFactory;
}
public static SqlSession getSqlSession() throws IOException {
return getSqlSessionFactory().openSession();
}
}
- 为了使程序能够找到
SqlMapConfig.xml
文件,在pom.xml
的build
标签中添加如下代码:
<resources>
<resource>
<directory>src/main/java/hello/mybatis</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
- 创建mybatis/mapper目录,在目录下创建
UserMapper.xml
来定义对 User 表的操作映射:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="hello.UserMapper">
<select id="findUserById" parameterType="int" resultType="hello.mybatis.po.User">
select * from User where userID = #{userID}
</select>
<select id="findUserByUsername" parameterType="java.lang.String" resultType="hello.mybatis.po.User">
select * from User where username = #{username}
</select>
<insert id="addUser" parameterType="hello.mybatis.po.User" useGeneratedKeys="true" keyProperty="userID">
insert into User (username, password, email, phone)
values (#{username}, #{password}, #{email}, #{phone})
</insert>
</mapper>
Mapper 是 MyBatis 核心功能,注意仔细阅读相关文档,理解 Mapper 的工作原理。
- 创建 Plain Object
User.java
:
package hello.mybatis.po;
public class User {
private int userID;
private String username;
private String password;
private String email;
private String phone;
public User(int userID, String username, String password, String email, String phone) {
this.userID = userID;
this.username = username;
this.password = password;
this.email = email;
this.phone = phone;
}
public User(String username, String password, String email, String phone) {
this.username = username;
this.password = password;
this.email = email;
this.phone = phone;
}
public int getUserID() {
return userID;
}
public void setUserID(int userID) {
this.userID = userID;
}
/* 省略其他 setter 与 getter */
}
- 修改 UserController.java 中注册用户的对应代码:
@RequestMapping(value = "/register", method = RequestMethod.POST)
public @ResponseBody Object register(@RequestBody UserRegisterRequest request) throws IOException {
SqlSession sqlSession = SqlSessionLoader.getSqlSession();
User user = sqlSession.selectOne("hello.UserMapper.findUserByUsername", request.getUsername());
if (user != null) {
sqlSession.close();
return new ErrorResponse("The username is already used");
} else {
sqlSession.insert("hello.UserMapper.addUser", new User(request.getUsername(), request.getPassword(), request.getEmail(), request.getPhone()));
sqlSession.commit();
sqlSession.close();
return new RegisterResponse("abc"); // use your generated token here.
}
}
通过以上 9 个步骤,我们将 MyBatis 加入到了 Spring Boot 工程中,并进行了简单的运用。
以上代码有些地方写的比较粗糙,在结构上有改进空间。
目前,Lab 工程已经完成了用户注册的流程,请同学们设计 用户登录
和 列举所有用户
这两个接口,结合Spring Boot 和 MyBatis 编写对应的代码。
仍有剩余时间的同学可以尝试添加用户身份认证相关代码,继续学习 Spring Boot 和 MyBatis 或将 Lab 所学内容运用于 PJ 中。
截止时间:2018-6-10 23:59:59 提交⽅式:将项目部署到云上,将云地址写在⼀份文档里,将注册,用户登录,列举所有用户,接口的url写在文档里(或者做一个简单的可视化页面),⽂档里也可以简要介绍下lab过程中遇到的问题收获以及你新添加的功能(可选,比如可以简要介绍下你所用的身份认证机制),将该⽂档提交到超星指定的lab作业栏⾥。
任何问题,欢迎随时联系TA email: 15307110273@fudan.edu.edu