语言都是相通的,只要搞清楚概念后就可以编写代码了。而概念是需要学习成本的。
不用看《编程思想》,基础语法看 http://www.runoob.com/java/java-basic-syntax.html 就可以了,入门后想干啥干啥,如果感兴趣,如果有时间。
这里讲的web是指提供API(Application Programming Interface)的能力。那么什么是API
API是指server端和client端进行资源交互的通道。Client可以通过API来获取和修改server端的资源(Resource). 实际上,API差不多就是URL的代称,现阶段,推荐采用RESTfull API.
API表现方式就是URL(Uniform Resoure Locator)。RESTfull API是一个概念,规定了应该以什么样的结构去构建API,即应该如何拼接URL。先来看看URL是什么样子的。
资源(Resources)
path中的
和
- groups
都是资源的名称,通过参数来确定资源的位置。
- users
行为/操作(Method)
我们通过约定的
来表示对Resource的操作。
- Http Method
常用的HTTP动词有下面五个(括号里是对应的SQL命令)。
- GET(SELECT):从服务器取出资源(一项或多项)。
- POST(CREATE):在服务器新建一个资源。
- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
- DELETE(DELETE):从服务器删除资源。
还有两个不常用的HTTP动词。
- HEAD:获取资源的元数据。
- OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。
示例:
- GET /zoos:列出所有动物园
- POST /zoos:新建一个动物园
- GET /zoos/ID:获取某个指定动物园的信息
- PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
- PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
- DELETE /zoos/ID:删除某个动物园
- GET /zoos/ID/animals:列出某个指定动物园的所有动物
- DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物
当path的组成仍旧无法准确定位资源的时候,可以通过queryParam来进一步缩小范围。
- ?limit=10:指定返回记录的数量
- ?offset=10:指定返回记录的开始位置。
- ?page=2&per_page=100:指定第几页,以及每页的记录数。
- ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
- ?animal_type_id=1:指定筛选条件
更多关于构建RESTfull API的信息,参阅https://codeplanet.io/principles-good-restful-api-design/
现在的接口都是基于JSON传输的,什么是JSON(JavaScript Object Notation)
一个基于JSON的API的response应该包含以下header
- Content-Type:application/json; charset=utf-8
安装NodeJS
然后,创建app.js,
,
- npm install express --save
, 访问
- node app.js
,
- localhost:3000/
- localhost:3000/json
- // 这句的意思就是引入 `express` 模块,并将它赋予 `express` 这个变量等待使用。
- var express = require('express');
- // 调用 express 实例,它是一个函数,不带参数调用时,会返回一个 express 实例,将这个变量赋予 app 变量。
- var app = express();
- // app 本身有很多方法,其中包括最常用的 get、post、put/patch、delete,在这里我们调用其中的 get 方法,为我们的 `/` 路径指定一个 handler 函数。
- // 这个 handler 函数会接收 req 和 res 两个对象,他们分别是请求的 request 和 response。
- // request 中包含了浏览器传来的各种信息,比如 query 啊,body 啊,headers 啊之类的,都可以通过 req 对象访问到。
- // res 对象,我们一般不从里面取信息,而是通过它来定制我们向浏览器输出的信息,比如 header 信息,比如想要向浏览器输出的内容。这里我们调用了它的 #send 方法,向浏览器输出一个字符串。
- app.get('/', function (req, res) {
- res.send('Hello World');
- });
- app.get('/json', function (req, res) {
- var rs = {};
- rs.id=1;
- rs.name = "Ryan";
- res.send(rs);
- });
- // 定义好我们 app 的行为之后,让它监听本地的 3000 端口。这里的第二个函数是个回调函数,会在 listen 动作成功后执行,我们这里执行了一个命令行输出操作,告诉我们监听动作已完成。
- app.listen(3000, function () {
- console.log('app is listening at port 3000');
- });
Java Web的开源框架中,目前最常用的是SpringBoot. SpringBoot可以提供API,可以渲染页面,是作为API Server的最佳选择。
写了无数遍hello world, 这次还是要从hello world开始。
https://github.com/Ryan-Miao/springboot-demo-gradle
Java Web的包管理工具有maven,gradle。这里将使用gradle作为依赖管理工具。
gradle是继maven之后,Java项目构建工具的集大成者。它管理依赖,为什么要管理依赖?我们的项目中将会使用很多其他的lib,这些lib有我们自己的,也有开源的,甚至大部分都是开源的。当引入这些lib的时候,引入哪个版本?去哪里下载?多个版本产生了冲突怎么办?以及最后我们项目开发完成后,怎么打包?甚至,想使用CI/CD自动化构建工具,如何集成?这就是gradle可以做的事情。
gradle要怎么学?
一般来说不用学,不用理会内置的逻辑,只需要用就好。就好比IDE,你不会深究IDE是c编写的还是Java编写的,但会使用IDE来编写代码。同样,gradle的用法很简单,可以满足我们开发中觉得部分需求。当然,当需要自定义功能的时候,可以使用
来编写gradle脚本。
- groovy
IDEA是目前构建Java Web项目最火IDE。用法和Eclipse还是有不少的区别,刚转过来的时候可能有点不习惯。但根据2-8原则,我们只需要掌握其中一部分用法就可以开发了,剩下的高级用法可以在开发中慢慢摸索。即,其实用法也很简单。
点击
->
- File
->
- New
->
- project
->勾选
- gradle
- Java
如果发现没有JDK,那么new一个就好。
下一步,设置项目标签,group通常是公司名称倒写,比如
,
- com.google
等. ArtifactId就是我们的项目名称,比如这次demo为
- com.alibaba
- springboot-demo
然后一路next,完成后确定。IDEA会下载gradle,下载简单的依赖,完毕后,项目根目录下多出几个文件,目前不用care。
- .
- ├── build.gradle
- ├── gradle
- │ └── wrapper
- │ ├── gradle-wrapper.jar
- │ └── gradle-wrapper.properties
- ├── gradlew
- ├── gradlew.bat
- ├── settings.gradle
- └── src
- ├── main
- │ ├── java
- │ └── resources
- └── test
- ├── java
- └── resources
接下来修改
,这个文件是依赖管理的核心文件
- build.gradle
- buildscript {
- repositories {
- maven {
- url "http://maven.aliyun.com/nexus/content/groups/public/"
- }
- mavenCentral()
- }
- dependencies {
- classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.8.RELEASE")
- }
- }
- apply plugin: 'java'
- apply plugin: 'eclipse'
- apply plugin: 'idea'
- apply plugin: 'org.springframework.boot'
- jar {
- baseName = 'springboot-demo'
- version = '0.1.0'
- }
- repositories {
- maven {
- url "http://maven.aliyun.com/nexus/content/groups/public/"
- }
- mavenCentral()
- }
- sourceCompatibility = 1.8
- targetCompatibility = 1.8
- dependencies {
- compile("org.springframework.boot:spring-boot-starter-web")
- testCompile('org.springframework.boot:spring-boot-starter-test')
- }
里就这么写,不用关心为什么,只需要知道这里这样写就可以引入springboot的版本
- buildscript
是唯一会改变和增加内容的地方,当需要第三方库的时候添加,添加规则就是
- dependencies
, 正好和我们创建项目的时候声明的标签一样
- groupId:artifactId:version
修改
之后就要重新build,在IDEA中,点击右侧的工具栏,gradle,点击刷新按钮。就会自动下依赖,如果没有下载,点击gradle下Task里的
- build.gradle
按钮。
- build
另一个方式就是命令行:
细心可以发现项目根目录下有
和
- gradlew
这个文件,这是分别为linux和windows准备的启动工具,在Linux系统中
- gradlew.bat
- ./gradlew build
- or
- sh gradlew build
在windows中
- gradlew build
编译完成后,在左侧的项目目录下的
下可以看到我们引入的第三方库。为什么这么多?因为依赖是树状的,或者说网状的。lib也有他自己的依赖,gradle会负责把我们引入的lib的依赖也给下载下来。在没有maven和gradle这种构建工具之前,项目开发都是自己下载jar,自己丢进去classpath里,很容遗漏,也很容易造成冲突。gralde会负责下载依赖,还会解决冲突,比如不同版本等问题。
- External Libraties
Springboot的一个优点是约定大于配置,意思是我们都约定好怎么配置,我帮你配置好了,你直接用就好。因此,springmvc时代的大部分配置都可以自动化完成。我们的启动类也只有一行.
可以看到,
这个目录变成蓝色,在IDEA里是指sourceSet,也就是源文件,我们的Java代码就是放在这文件下的,这也是约定好的。
- src/main/java
在该目录下新建
- com.test.demo.Application.java
- package com.test.demo;
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- /**
- * Created by Ryan on 2017/11/13/0013.
- */
- @SpringBootApplication public class Application {
- public static void main(String[] args) {
- SpringApplication.run(Application.class, args);
- }
- }
到这里,我们的服务端就配置完毕了。运行main方法即可启动。
虽然服务端配置好了,但并没有API. 新建
- com.test.demo.controller.HelloController.java
- package com.test.demo.controller;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.ResponseBody;
- /**
- * Created by Ryan on 2017/11/14/0014.
- */
- @Controller
- public class HelloController {
- @ResponseBody
- @GetMapping("/hello")
- public String hello(){
- return "{\"hello\":\"world\"}";
- }
- }
然后,再次运行main方法,启动完毕后,访问 http://localhost:8080/hello, 第一个API开发完毕。
这个注解标注这个类是一个controller,用来接收请求和响应response
- @Controller
标注这个方法是一个路由请求实现,括号里就是我们的路由
- @GetMapping("/hello")
这个注解标注这个API的返回值是json,其实就是再response的header里塞入了contentType, 当然,在这里还涉及到class转json的问题。那么,回到开始的问题,json是什么东西?
- @ResponseBody
JSON在Java里没有这个数据结构,其实就是一个String,遵从JSON规则的String,我们的方法在返回这段String的时候,加上header里的contentType,浏览器就会当做JSON读取。在Javascript去读Ajax的结果就变成了一个JSON对象了。其他的,比如Android,读取出来的还是一个字符串,需要手动反序列化成我们想要的类。
说到序列化,我们不可能每个返回结构都这样拼接字符串吧。所以,
标注的请求还会使用一个jackson的适配器,这些都是springboot内置的。暂时也不需要研究实现原理。jackson是什么鬼?
- ResponseBody
jackson是Java中使用最广泛的一个json解析lib,他可以将一个Java 类转变成一个json字符串,也同样可以把一个json字符串反序列化成一个java对象。Springboot是如何做到的?这就需要去研究源码了。
最简单的是启动就是运行main方法,还可以命令行启动
- gradlew bootRun
debug,最简单的就是以debug启动main方法。当然也可以远程。
- gradlew bootRun--debug - jvm
然后,在IDEA中,点击Edit configurations
选择remote
然后,点击debug
如果想支持热加载,则需要添加
- compile("org.springframework.boot:spring-boot-devtools")
在IDEA里修改Java class后需要,重新build当前class才能生效。快捷键
- ctrl+shif+F9
spring boot默认配置了很多东西,但有时候我们想要修改默认值,比如不想用8080作为端口,因为端口被占用了。
在
下,新建
- resources
, 然后在里面输入
- application.properties
- server.port = 8081
然后,重启项目,发现端口已经生效。
再配置一些common的自定义,比如日志。项目肯定要记录日志的,
远远达不到日志的要求。springboot默认采用
- System.out.println
作为日志处理工具。
- Logback
- spring.output.ansi.enabled=ALWAYS
- logging.file=logs/demo.log
- logging.level.root=INFO
接着,开发和生产环境的配置必然不同的,比如数据库的地址不同,那么可以分配置文件来区分环境。
在resources下新建
,
- application-dev.properties
. spring默认通过后缀不同来识别不同的环境,不加后缀的是base配置。那么如何生效呢?
- application-prod.properties
只要在base的配置文件中
- spring.profiles.active = dev
比如,我们在dev环境中设置loglevel为debug
- logging.level.root = debug
这样,springboot会优先读取base文件,然后读取dev,当dev有相同的配置项时,dev会覆盖base。
这样,本地开发和生产环境隔离,部署也方便。事实上,springboot接收参数的优先级为
<
- resources下的配置文件
. 通常,我们部署项目的脚本会使用命令行参数来覆盖配置文件,这样就可以动态指定配置文件了。
- 命令行参数
新建一个controller, com.test.demo.controller.ParamController
- package com.test.demo.controller;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PathVariable;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestParam;
- import org.springframework.web.bind.annotation.RestController;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- /**
- * Created by Ryan on 2017/11/16/0016.
- */
- @RestController
- @RequestMapping("/param")
- public class ParamController {
- private static final Logger LOGGER = LoggerFactory.getLogger(ParamController.class);
- @GetMapping("/hotels/{htid}/rooms")
- public List<Long> getRooms(
- @PathVariable String htid,
- @RequestParam String langId,
- @RequestParam(value = "limit", required = false, defaultValue = "10") int limit,
- @RequestParam(value = "offset", required = false, defaultValue = "1") int offset
- ){
- final Map<String, Object> params = new HashMap<>();
- params.put("hotelId", htid);
- params.put("langId", langId);
- params.put("limit", limit);
- params.put("offset", offset);
- LOGGER.info("The params is {}", params);
- List<Long> roomIds = new ArrayList<>();
- roomIds.add(1L);
- roomIds.add(2L);
- roomIds.add(3L);
- return roomIds;
- }
- }
可以接收url路径中的参数
- @PathVariable
可以接收
- @RequestParam
后的query参数
- ?
==
- @RestController
, 其实,
- @Controller+@ResponseBody
注解表明这个方法会返回json,会将Java类转换成JSON字符串,默认转换器为Jackason
- @ResponseBody
新建class com.test.demo.entity.Room
- public class Room {
- private Integer roomId;
- private String roomName;
- private String comment;
- public Integer getRoomId() {
- return roomId;
- }
- public void setRoomId(Integer roomId) {
- this.roomId = roomId;
- }
- public String getRoomName() {
- return roomName;
- }
- public void setRoomName(String roomName) {
- this.roomName = roomName;
- }
- public String getComment() {
- return comment;
- }
- public void setComment(String comment) {
- this.comment = comment;
- }
- }
假设,我们需要保存一个Room信息,先来get一个
- @GetMapping("/hotels/{htid}/rooms/{roomId}")
- public Room getRoomById(
- @PathVariable String htid,
- @PathVariable Integer roomId
- ){
- if (htid.equals("6606")){
- final Room room = new Room();
- room.setComment("None");
- room.setRoomId(roomId);
- room.setRoomName("豪华双人间");
- return room;
- }
- return null;
- }
然后保存一个
- @PostMapping("/hotels/{htid}/rooms")
- public Integer addRoom(@RequestBody Room room){
- final Random random = new Random();
- final int id = random.nextInt(10);
- room.setRoomId(id);
- LOGGER.info("Add a room: {}", room);
- return id;
- }
- @GetMapping("/hotels/{htid}/rooms/ids")
- public String getRoomsWithIds(@RequestParam List<Integer> ids){
- String s = ids.toString();
- LOGGER.info(s);
- return s;
- }
浏览器访问 http://localhost:8081/param//hotels/6606/rooms/ids?ids=1,2,3
我们除了一个个的if去判断参数,还可以使用注解
- public class Room {
- private Integer roomId;
- @NotEmpty
- @Size(min = 3, max = 20, message = "The size of room name should between 3 and 20")
- private String roomName;
只要在参数前添加
- javax.validation.Valid
- @PostMapping("/hotels/{htid}/rooms")
- public Integer addRoom(
- @Valid @RequestBody Room room,
- @RequestHeader(name = "transactionId") String transactionId
- ){
在springboot中,static content默认寻找规则是
By default Spring Boot will serve static content from a directory called
(or
- /static
or
- /public
or
- /resources
) in the classpath or from the root of the ServletContext.
- /META-INF/resources
在
下新建文件夹
- resources
, src\main\resources\static\content.html
- static
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Hello static content</title>
- <script src="/js/test.js"></script>
- </head>
- <body>
- <h1>Static Content</h1>
- <p>Static content is the files that render directly, the file is the whole content. The different between template is that
- the template page will be resolved by server and then render out.
- </p>
- </body>
- </html>
浏览器访问: http://localhost:8081/content.html
同理,放在static下的文件都可以通过如此映射访问。
模板文件是指通过服务端生成的文件。比如Jsp,会经过servlet编译后,最终生成一个html页面。Springboot默认支持以下几种模板:
FreeMarker
Groovy
Thymeleaf
Mustache
JSP在jar文件中的表现有问题,除非部署为war。
官方推荐的模板为
, 在depenency中添加依赖:
- Thymeleaf
- compile("org.springframework.boot:spring-boot-starter-thymeleaf")
rebuild.
SpringBoot默认模板文件读取位置为:
. 新建
- src\main\resources\templates
- src\main\resources\templates\home.html
- <!DOCTYPE html>
- <html xmlns:th="http://www.thymeleaf.org">
- <head lang="en">
- <meta charset="UTF-8"/>
- <title>Home</title>
- </head>
- <body>
- <h1>Template content</h1>
- <p th:text="${msg} + ' The current user is:' + ${user.name}">Welcome!</p>
- </body>
- </html>
模板文件只能通过服务端路由渲染,也就是说不能像刚开始静态文件那样直接路由过去。
创建一个controller,
- com.test.demo.controller.HomeController
- package com.test.demo.controller;
- import org.springframework.stereotype.Controller;
- import org.springframework.ui.Model;
- import org.springframework.web.bind.annotation.RequestMapping;
- import java.util.HashMap;
- import java.util.Map;
- /**
- * Created by Ryan on 2017/11/18/0018.
- */
- @Controller public class HomeController {
- @RequestMapping("/home") public String index(Model model, String name) {
- final Map < String,
- Object > user = new HashMap < >();
- user.put("name", name);
- model.addAttribute("user", user);
- model.addAttribute("msg", "Hello World!");
- return "home";
- }
- }
这个和之前的API的接口有一点不同,首先是没有
注解,然后是方法的返回值是一个String,这个String不是value,而是指模板文件的位置,相对于
- @ResponseBody
的位置。
- templates
浏览器访问:http://localhost:8081/home?name=Ryan123
方法参数的
是模板文件的变量来源,模板文件从这个对象里读取变量,将这个类放到参数里,Spring会自动注入这个类,绑定到模板文件。这里,放入两个变量。
- Model
在模板端,就可以读取这个变量了。
为什么要这么做?既然有了静态文件,为什么还要模板文件?
首先,这是早期web开发的做法,之前是没有web 前端这个兵种的,页面从静态页面变成动态页面,代表就是jsp,php等。模板文件的有个好处是,服务端可以控制页面,比如从session中拿到用户信息,放入页面。这个在静态页面是做不到的。
然而,现在前后端的分离实践,使得模板文件的作用越来越小。目前主要用于基础数据传递,其他数据则通过客户端的异步请求获得。
当然,随着页面构建复杂,异步请求太多,首屏渲染时间越来越长,严重影响了用户体验,比如淘宝双11的宣传页。这时候,服务端渲染的优势又体现出来了,静态页面直接出数据,不需要多次的ajax请求。
Cross-origin resource sharing (CORS) is a W3C specification implemented by most browsers that allows you to specify in a flexible way what kind of cross domain requests are authorized, instead of using some less secure and less powerful approaches like IFRAME or JSONP.
CORS是浏览器的一种安全保护,隔离不同域名之间的可见度。比如,不允许把本域名下cookie发送给另一个域名,否则cookie被钓鱼后,黑客就可以模拟本人登陆了。更多细节参考MDN
为什么浏览器要拒绝cors?
摘自博客园
cors执行过程摘自自由的维基百科
首先,本地模拟跨域请求。
我们当前demo的域名为
,现在新增一个本地域名, 在HOSTS文件中新增:
- localhost:8081
- 127.0.0.1 corshost
然后,访问http://corshost:8081,即本demo。
新增src\main\resources\static\cors.html
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Test Cors</title>
- </head>
- <body>
- <script src="http://cdn.staticfile.org/jquery/3.2.1/jquery.min.js"></script>
- <script>
- $.ajax({ url: "http://localhost:8081/hello", success: function(data){
- console.log(data);
- }});
- </script>
- </body>
- </html>
访问之前创建的hello接口,可以看到访问失败,
- Failed to load http://localhost:8081/hello: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://corshost:8081' is therefore not allowed access.
这是浏览器正常的行为。
但,由于前后端分离,甚至分开部署,域名肯定不会是同一个了,那么就需要支持跨域。Springboot支持跨域,解决方案如下:
在需要跨域的method上,添加一个
注解即可。
- @CrossOrigin
- @CrossOrigin(origins = {"http://corshost:8081"})
- @ResponseBody
- @GetMapping("/hello")
- public String hello(){
- return "{\"hello\":\"world\"}";
- }
如果是全局配置允许跨域,新建
- com.test.demo.config.CorsConfiguration
- package com.test.demo.config;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.web.servlet.config.annotation.CorsRegistry;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
- /**
- * Created by Ryan on 2017/11/18/0018.
- */
- @Configuration
- public class CorsConfiguration {
- @Bean
- public WebMvcConfigurer corsConfigurer() {
- return new WebMvcConfigurerAdapter() {
- @Override
- public void addCorsMappings(CorsRegistry registry) {
- registry.addMapping("/api/**")
- .allowedOrigins("http://domain2.com")
- .allowedMethods("PUT", "DELETE")
- .allowedHeaders("header1", "header2", "header3")
- .exposedHeaders("header1", "header2")
- .allowCredentials(false).maxAge(3600);
- }
- };
- }
- }
来源: http://www.cnblogs.com/woshimrf/p/java-web-springboot.html