JavaWeb基础入门前言Java基础Web参考

作者:@Ryan-Miao
本文为作者原创,转载请注明出处:www.cnblogs.com/woshimrf/p/…


目录

RESTfull API
ContentType
NodeJS Web
Java Web
demo source
Gradle是什么
IntelIj
IDEA
新建一个gradle项目
开始编写服务端配置
编写第一个API
启动和调试
配置文件
接收参数,响应JSON
参数校验
静态文件
模板文件
跨域
部署
引入MySQL
JDBCTemplate
分层架构
DI
面向接口编程
编写测试
集成CI
登陆拦截
OAuth2.0
事物
JPA
缓存

前言

语言都是相通的,只要搞清楚概念后就可以编写代码了。而概念是需要学习成本的。

Java基础

不用看《编程思想》,基础语法看 www.runoob.com/java/java-b… 就可以了,入门后想干啥干啥,如果感兴趣,如果有时间。

Web

这里讲的web是指提供API(Application Programming Interface)的能力。那么什么是API?

API是指server端和client端进行资源交互的通道。Client可以通过API来获取和修改server端的资源(Resource). 实际上,API差不多就是URL的代称,现阶段,推荐采用RESTfull API.

RESTfull API

API表现方式就是URL(Uniform Resoure Locator)。RESTfull API是一个概念,规定了应该以什么样的结构去构建API,即应该如何拼接URL。先来看看URL是什么样子的。

资源(Resources)
path中的groupsusers都是资源的名称,通过参数来确定资源的位置。

行为/操作(Method)
我们通过约定的Http Method来表示对Resource的操作。

常用的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/

ContentType

现在的接口都是基于JSON传输的,什么是JSON(JavaScript Object Notation)?

一个基于JSON的API的response应该包含以下header

Content-Type:application/json; charset=utf-8复制代码

NodeJS Web

安装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

Java Web的开源框架中,目前最常用的是SpringBoot. SpringBoot可以提供API,可以渲染页面,是作为API Server的最佳选择。

写了无数遍hello world, 这次还是要从hello world开始。

demo source

github.com/Ryan-Miao/s…

Java Web的包管理工具有maven,gradle。这里将使用gradle作为依赖管理工具。

Gradle是什么

gradle是继maven之后,Java项目构建工具的集大成者。它管理依赖,为什么要管理依赖?我们的项目中将会使用很多其他的lib,这些lib有我们自己的,也有开源的,甚至大部分都是开源的。当引入这些lib的时候,引入哪个版本?去哪里下载?多个版本产生了冲突怎么办?以及最后我们项目开发完成后,怎么打包?甚至,想使用CI/CD自动化构建工具,如何集成?这就是gradle可以做的事情。

gradle要怎么学?

一般来说不用学,不用理会内置的逻辑,只需要用就好。就好比IDE,你不会深究IDE是c编写的还是Java编写的,但会使用IDE来编写代码。同样,gradle的用法很简单,可以满足我们开发中觉得部分需求。当然,当需要自定义功能的时候,可以使用groovy来编写gradle脚本。

IntelIj IDEA

IDEA是目前构建Java Web项目最火IDE。用法和Eclipse还是有不少的区别,刚转过来的时候可能有点不习惯。但根据2-8原则,我们只需要掌握其中一部分用法就可以开发了,剩下的高级用法可以在开发中慢慢摸索。即,其实用法也很简单。

新建一个gradle项目

点击File->New->project->gradle->勾选Java

如果发现没有JDK,那么new一个就好。

下一步,设置项目标签,group通常是公司名称倒写,比如com.googlecom.alibaba等. ArtifactId就是我们的项目名称,比如这次demo为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')
}复制代码
  • maven是一个仓库,一些开源的第三方库lib都从这里下载,这里引用了aliyun镜像,因为maven在国内访问比较慢,如果在国外可以移除这个节点
  • buildscript里就这么写,不用关心为什么,只需要知道这里这样写就可以引入springboot的版本
  • dependencies是唯一会改变和增加内容的地方,当需要第三方库的时候添加,添加规则就是groupId:artifactId:version, 正好和我们创建项目的时候声明的标签一样

修改build.gradle之后就要重新build,在IDEA中,点击右侧的工具栏,gradle,点击刷新按钮。就会自动下依赖,如果没有下载,点击gradle下Task里的build按钮。

另一个方式就是命令行:

细心可以发现项目根目录下有gradlewgradlew.bat这个文件,这是分别为linux和windows准备的启动工具,在Linux系统中

./gradlew build
or
sh gradlew build复制代码

在windows中

gradlew build复制代码

编译完成后,在左侧的项目目录下的External Libraties下可以看到我们引入的第三方库。为什么这么多?因为依赖是树状的,或者说网状的。lib也有他自己的依赖,gradle会负责把我们引入的lib的依赖也给下载下来。在没有maven和gradle这种构建工具之前,项目开发都是自己下载jar,自己丢进去classpath里,很容遗漏,也很容易造成冲突。gralde会负责下载依赖,还会解决冲突,比如不同版本等问题。

开始编写服务端配置

Springboot的一个优点是约定大于配置,意思是我们都约定好怎么配置,我帮你配置好了,你直接用就好。因此,springmvc时代的大部分配置都可以自动化完成。我们的启动类也只有一行.

可以看到,src/main/java这个目录变成蓝色,在IDEA里是指sourceSet,也就是源文件,我们的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

虽然服务端配置好了,但并没有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这个注解标注这个类是一个controller,用来接收请求和响应response
  • @GetMapping("/hello")标注这个方法是一个路由请求实现,括号里就是我们的路由
  • @ResponseBody这个注解标注这个API的返回值是json,其实就是再response的header里塞入了contentType, 当然,在这里还涉及到class转json的问题。那么,回到开始的问题,json是什么东西?

JSON在Java里没有这个数据结构,其实就是一个String,遵从JSON规则的String,我们的方法在返回这段String的时候,加上header里的contentType,浏览器就会当做JSON读取。在Javascript去读Ajax的结果就变成了一个JSON对象了。其他的,比如Android,读取出来的还是一个字符串,需要手动反序列化成我们想要的类。

说到序列化,我们不可能每个返回结构都这样拼接字符串吧。所以,ResponseBody标注的请求还会使用一个jackson的适配器,这些都是springboot内置的。暂时也不需要研究实现原理。jackson是什么鬼?

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的自定义,比如日志。项目肯定要记录日志的,System.out.println远远达不到日志的要求。springboot默认采用Logback作为日志处理工具。

spring.output.ansi.enabled=ALWAYS
logging.file=logs/demo.log
logging.level.root=INFO复制代码

接着,开发和生产环境的配置必然不同的,比如数据库的地址不同,那么可以分配置文件来区分环境。

在resources下新建application-dev.properties, application-prod.properties. spring默认通过后缀不同来识别不同的环境,不加后缀的是base配置。那么如何生效呢?

只要在base的配置文件中

spring.profiles.active=dev复制代码

比如,我们在dev环境中设置loglevel为debug

logging.level.root=debug复制代码

这样,springboot会优先读取base文件,然后读取dev,当dev有相同的配置项时,dev会覆盖base。

这样,本地开发和生产环境隔离,部署也方便。事实上,springboot接收参数的优先级为resources下的配置文件<命令行参数. 通常,我们部署项目的脚本会使用命令行参数来覆盖配置文件,这样就可以动态指定配置文件了。

接收参数,响应JSON

新建一个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;
    }
}复制代码
  • LOG: 采用Sl4J接口
  • 参数: @PathVariable 可以接收url路径中的参数
  • 参数: @RequestParam 可以接收?后的query参数
  • 响应: @RestController == @Controller+@ResponseBody, 其实,@ResponseBody注解表明这个方法会返回json,会将Java类转换成JSON字符串,默认转换器为Jackason

参数为JSON

新建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 /static (or /public or /resources or /META-INF/resources) in the classpath or from the root of the ServletContext.

resources下新建文件夹 static,
src\main\resources\static\content.html

<!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。

官方推荐的模板为Thymeleaf, 在depenency中添加依赖:

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的接口有一点不同,首先是没有@ResponseBody注解,然后是方法的返回值是一个String,这个String不是value,而是指模板文件的位置,相对于templates的位置。

浏览器访问:http://localhost:8081/home?name=Ryan123

方法参数的Model是模板文件的变量来源,模板文件从这个对象里读取变量,将这个类放到参数里,Spring会自动注入这个类,绑定到模板文件。这里,放入两个变量。

在模板端,就可以读取这个变量了。

为什么要这么做?既然有了静态文件,为什么还要模板文件?

首先,这是早期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的域名为localhost:8081,现在新增一个本地域名, 在HOSTS文件中新增:

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);
            }
        };
    }
}
复制代码

部署

引入MySQL

JDBCTemplate

分层架构

DI

面向接口编程

编写测试

集成CI

登陆拦截

OAuth2.0

事物

JPA

缓存

参考

So do it,and change it,no regret!