开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Java代理模式及动态代理详解

发表于 2020-03-21

Java的动态代理在实践中有着广泛的使用场景,比如最场景的Spring AOP、Java注解的获取、日志、用户鉴权等。本篇文章带大家了解一下代理模式、静态代理以及基于JDK原生动态代理。

代理模式

无论学习静态代理或动态代理,我们都要先了解一下代理模式。

先看百度百科的定义:

代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

直接看定义可能有些难以理解,我们就以生活中具体的实例来说明一下。

我们都去过超市购买过物品,超市从厂商那里购买货物之后出售给我们,我们通常并不知道货物从哪里经过多少流程才到超市。

在这个过程中,等于是厂商“委托”超市出售货物,对我们来说是厂商(真实对象)是不可见的。而超市(代理对象)呢,作为厂商的“代理者”来与我们进行交互。

同时,超市还可以根据具体的销售情况来进行折扣等处理,来丰富被代理对象的功能。

通过代理模式,我们可以做到两点:

1、隐藏委托类的具体实现。

2、实现客户与委托类的解耦,在不改变委托类代码的情况下添加一些额外的功能(日志、权限)等。

代理模式角色定义

在上述的过程中在编程的过程中我们可以定义为三类对象:

  • Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法。比如:广告、出售等。
  • RealSubject(真实主题角色):真正实现业务逻辑的类。比如实现了广告、出售等方法的厂家(Vendor)。
  • Proxy(代理主题角色):用来代理和封装真实主题。比如,同样实现了广告、出售等方法的超时(Shop)。

以上三个角色对应的类图如下:

Java代理及动态代理详解

静态代理实例

静态代理是指代理类在程序运行前就已经存在,这种情况下的代理类通常都是我们在Java代码中定义的。

下面我们就以具体的实例来演示一下静态代理。

首先定义一组接口Sell,用来提供广告和销售等功能。然后提供Vendor类(厂商,被代理对象)和Shop(超市,代理类),它们分别实现了Sell接口。

Sell接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码/**
* 委托类和代理类都实现了Sell接口
* @author sec
* @version 1.0
* @date 2020/3/21 9:30 AM
**/
public interface Sell {

/**
* 出售
*/
void sell();

/**
* 广告
*/
void ad();
}

Vendor类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码/**
* 供应商
* @author sec
* @version 1.0
* @date 2020/3/21 9:30 AM
**/
public class Vendor implements Sell{

@Override
public void sell() {
System.out.println("Shop sell goods");
}

@Override
public void ad() {
System.out.println("Shop advert goods");
}
}

Shop类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码/**
* 超市,代理类
* @author sec
* @version 1.0
* @date 2020/3/21 9:30 AM
**/
public class Shop implements Sell{

private Sell sell;

public Shop(Sell sell){
this.sell = sell;
}

@Override
public void sell() {
System.out.println("代理类Shop,处理sell");
sell.sell();
}

@Override
public void ad() {
System.out.println("代理类Shop,处理ad");
sell.ad();
}
}

其中代理类Shop通过聚合的方式持有了被代理类Vendor的引用,并在对应的方法中调用Vendor对应的方法。在Shop类中我们可以新增一些额外的处理,比如筛选购买用户、记录日志等操作。

下面看看在客户端中如何使用代理类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码/**
* 静态代理类测试方法
* @author sec
* @version 1.0
* @date 2020/3/21 9:33 AM
**/
public class StaticProxy {

public static void main(String[] args) {

// 供应商---被代理类
Vendor vendor = new Vendor();

// 创建供应商的代理类Shop
Sell sell = new Shop(vendor);

// 客户端使用时面向的是代理类Shop。
sell.ad();
sell.sell();
}
}

在上述代码中,针对客户看到的是Sell接口提供了功能,而功能又是由Shop提供的。我们可以在Shop中修改或新增一些内容,而不影响被代理类Vendor。

静态代理的缺点

静态代理实现简单且不侵入原代码,但当场景复杂时,静态代理会有以下缺点:

1、当需要代理多个类时,代理对象要实现与目标对象一致的接口。要么,只维护一个代理类来实现多个接口,但这样会导致代理类过于庞大。要么,新建多个代理类,但这样会产生过多的代理类。

2、当接口需要增加、删除、修改方法时,目标对象与代理类都要同时修改,不易维护。

于是,动态代理便派上用场了。

动态代理

动态代理是指代理类在程序运行时进行创建的代理方式。这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据Java代码中的“指示”动态生成的。

相比于静态代理,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类的函数。

基于JDK原生动态代理实现

实现动态代理通常有两种方式:JDK原生动态代理和CGLIB动态代理。这里,我们以JDK原生动态代理为例来进行讲解。

JDK动态代理主要涉及两个类:java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler。

InvocationHandler接口定义了如下方法:

1
2
3
4
5
6
复制代码/**
* 调用处理程序
*/
public interface InvocationHandler {
Object invoke(Object proxy, Method method, Object[] args);
}

顾名思义,实现了该接口的中介类用做“调用处理器”。当调用代理类对象的方法时,这个“调用”会转送到invoke方法中,代理类对象作为proxy参数传入,参数method标识了具体调用的是代理类的哪个方法,args为该方法的参数。这样对代理类中的所有方法的调用都会变为对invoke的调用,可以在invoke方法中添加统一的处理逻辑(也可以根据method参数对不同的代理类方法做不同的处理)。

Proxy类用于获取指定代理对象所关联的调用处理器。

下面以添加日志为例来演示一下动态代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;

public class LogHandler implements InvocationHandler {
Object target; // 被代理的对象,实际的方法执行者

public LogHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args); // 调用 target 的 method 方法
after();
return result; // 返回方法的执行结果
}
// 调用invoke方法之前执行
private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
// 调用invoke方法之后执行
private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
}

客户端编写程序使用动态代理代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码import java.lang.reflect.Proxy;

/**
* 动态代理测试
*
* @author sec
* @version 1.0
* @date 2020/3/21 10:40 AM
**/
public class DynamicProxyMain {

public static void main(String[] args) {
// 创建中介类实例
LogHandler logHandler = new LogHandler(new Vendor());
// 设置该变量可以保存动态代理类,默认名称$Proxy0.class
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

// 获取代理类实例Sell
Sell sell = (Sell) (Proxy.newProxyInstance(Sell.class.getClassLoader(), new Class[]{Sell.class}, logHandler));

// 通过代理类对象调用代理类方法,实际上会转到invoke方法调用
sell.sell();
sell.ad();
}
}

执行之后,打印日志如下:

1
2
3
4
5
6
复制代码调用方法sell之【前】的日志处理
Shop sell goods
调用方法sell之【后】的日志处理
调用方法ad之【前】的日志处理
Shop advert goods
调用方法ad之【后】的日志处理

经过上述验证,我们发现已经成功为我们的被代理类统一添加了执行方法之前和执行方法之后的日志。

在上述实例中为了看一下生成的动态代理类的代码,我们添加了下面的属性设置(在生产环境中需要去掉该属性)。

1
2
复制代码// 设置该变量可以保存动态代理类,默认名称$Proxy0.class
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

那么,我们可以执行main方法之后,还生成了一个名字为$Proxy0.class类文件。通过反编译可看到如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
复制代码package com.sun.proxy;

import com.choupangxia.proxy.Sell;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Sell {
private static Method m1;
private static Method m2;
private static Method m4;
private static Method m3;
private static Method m0;

public $Proxy0(InvocationHandler var1) throws {
super(var1);
}

public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final void ad() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final void sell() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m4 = Class.forName("com.choupangxia.proxy.Sell").getMethod("ad");
m3 = Class.forName("com.choupangxia.proxy.Sell").getMethod("sell");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

可以看到$Proxy0(代理类)继承了Proxy类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法。

由于动态代理类继承了Proxy类,所以每个代理类都会关联一个InvocationHandler方法调用处理器。

类和所有方法都被public final修饰,所以代理类只可被使用,不可以再被继承。

每个方法都有一个Method对象来描述,Method对象在static静态代码块中创建,以“m+数字”的格式命名。

调用方法的时候通过super.h.invoke(this,m1,(Object[])null);调用。其中的super.h.invoke实际上是在创建代理的时候传递给Proxy.newProxyInstance的LogHandler对象,它继承InvocationHandler类,负责实际的调用处理逻辑。

小结

关于代理和动态代理相关的内容,我们就讲这么多。了解了代理模式可以让我们的系统设计的更加具有可扩展性。而动态代理的应用就更广了,各类框架及业务场景都在使用。有了两个基础,就能够更好的学习其他框架。

关于CGLIB动态代理的内容,我们下篇文章再来聊一聊。


程序新视界:精彩和成长都不容错过

程序新视界-微信公众号

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Nestjs 从零到壹系列(三):使用 JWT 实现注册、

发表于 2020-03-19

前言

上一篇介绍了如何使用 Sequelize 连接 MySQL,接下来,在原来代码的基础上进行扩展,实现用户的注册和登录功能。

这里简单提一下 JWT:

JWT

JWT(JSON Web Token)是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。该 Token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 Token 也可直接被用于认证,也可被加密。

具体原理可以参考《JSON Web Token 入门教程 - 阮一峰》

所以 JWT 实现【登录】的大致流程是:

  1. 客户端用户进行登录请求;
  2. 服务端拿到请求,根据参数查询用户表;
  3. 若匹配到用户,将用户信息进行签证,并颁发 Token;
  4. 客户端拿到 Token 后,存储至某一地方,在之后的请求中都带上 Token ;
  5. 服务端接收到带 Token 的请求后,直接根据签证进行校验,无需再查询用户信息;

下面,就开始我们的实战:

GitHub 项目地址,欢迎各位大佬 Star。

一、编写加密的工具函数

在 src 目录下,新建文件夹 utils,里面将存放各种工具函数,然后新建 cryptogram.ts 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码import * as crypto from 'crypto';

/**
* Make salt
*/
export function makeSalt(): string {
return crypto.randomBytes(3).toString('base64');
}

/**
* Encrypt password
* @param password 密码
* @param salt 密码盐
*/
export function encryptPassword(password: string, salt: string): string {
if (!password || !salt) {
return '';
}
const tempSalt = Buffer.from(salt, 'base64');
return (
// 10000 代表迭代次数 16代表长度
crypto.pbkdf2Sync(password, tempSalt, 10000, 16, 'sha1').toString('base64')
);
}

上面写了两个方法,一个是制作一个随机盐(salt),另一个是根据盐来加密密码。

这两个函数将贯穿注册和登录的功能。

二、用户注册

在写注册逻辑之前,我们需要先修改一下上一篇写过的代码,即 user.service.ts 中的 findeOne() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
复制代码// src/logical/user/user.service.ts
import { Injectable } from '@nestjs/common';
import * as Sequelize from 'sequelize'; // 引入 Sequelize 库
import sequelize from '../../database/sequelize'; // 引入 Sequelize 实例

@Injectable()
export class UserService {
/**
* 查询是否有该用户
* @param username 用户名
*/
async findOne(username: string): Promise<any | undefined> {
const sql = `
SELECT
user_id userId, account_name username, real_name realName, passwd password,
passwd_salt salt, mobile, role
FROM
admin_user
WHERE
account_name = '${username}'
`; // 一段平淡无奇的 SQL 查询语句
try {
const user = (await sequelize.query(sql, {
type: Sequelize.QueryTypes.SELECT, // 查询方式
raw: true, // 是否使用数组组装的方式展示结果
logging: true, // 是否将 SQL 语句打印到控制台
}))[0];
// 若查不到用户,则 user === undefined
return user;
} catch (error) {
console.error(error);
return void 0;
}
}
}

现在,findOne() 的功能更符合它的方法名了,查到了,就返回用户信息,查不到,就返回 undefined。

接下来,我们开始编写注册功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
复制代码// src/logical/user/user.service.ts
import { Injectable } from '@nestjs/common';
import * as Sequelize from 'sequelize'; // 引入 Sequelize 库
import sequelize from '../../database/sequelize'; // 引入 Sequelize 实例

import { makeSalt, encryptPassword } from '../../utils/cryptogram'; // 引入加密函数

@Injectable()
export class UserService {
/**
* 查询是否有该用户
* @param username 用户名
*/
async findOne(username: string): Promise<any | undefined> {
...
}

/**
* 注册
* @param requestBody 请求体
*/
async register(requestBody: any): Promise<any> {
const { accountName, realName, password, repassword, mobile } = requestBody;
if (password !== repassword) {
return {
code: 400,
msg: '两次密码输入不一致',
};
}
const user = await this.findOne(accountName);
if (user) {
return {
code: 400,
msg: '用户已存在',
};
}
const salt = makeSalt(); // 制作密码盐
const hashPwd = encryptPassword(password, salt); // 加密密码
const registerSQL = `
INSERT INTO admin_user
(account_name, real_name, passwd, passwd_salt, mobile, user_status, role, create_by)
VALUES
('${accountName}', '${realName}', '${hashPwd}', '${salt}', '${mobile}', 1, 3, 0)
`;
try {
await sequelize.query(registerSQL, { logging: false });
return {
code: 200,
msg: 'Success',
};
} catch (error) {
return {
code: 503,
msg: `Service error: ${error}`,
};
}
}
}

编写好后,在 user.controller.ts 中添加路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码// src/logical/user/user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
constructor(private readonly usersService: UserService) {}

// @Post('find-one')
// findOne(@Body() body: any) {
// return this.usersService.findOne(body.username);
// }

@Post('register')
async register(@Body() body: any) {
return await this.usersService.register(body);
}
}

现在,我们使用 Postman 来测试一下,先故意输入不一样的密码和已存在的用户名:

如图,密码不一致的校验触发了。

然后,我们把密码改成一致的:

如图,已有用户的校验触发了。

然后,我们再输入正确的参数:

我们再去数据库看一下:

发现已经将信息插入表中了,而且密码也是加密后的,至此,注册功能已基本完成。

三、JWT 的配置与验证

为了更直观的感受处理顺序,我在代码中加入了步骤打印

1. 安装依赖包

1
复制代码$ yarn add passport passport-jwt passport-local @nestjs/passport @nestjs/jwt -S

2. 创建 Auth 模块

1
2
复制代码$ nest g service auth logical
$ nest g module auth logical

3. 新建一个存储常量的文件

在 auth 文件夹下新增一个 constants.ts,用于存储各种用到的常量:

1
2
3
4
复制代码// src/logical/auth/constats.ts
export const jwtConstants = {
secret: 'shinobi7414' // 秘钥
};

4. 编写 JWT 策略

在 auth 文件夹下新增一个 jwt.strategy.ts,用于编写 JWT 的验证策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码// src/logical/auth/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}

// JWT验证 - Step 4: 被守卫调用
async validate(payload: any) {
console.log(`JWT验证 - Step 4: 被守卫调用`);
return { userId: payload.sub, username: payload.username, realName: payload.realName, role: payload.role };
}
}

5. 编写 auth.service.ts 的验证逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
复制代码// src/logical/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import { encryptPassword } from '../../utils/cryptogram';

@Injectable()
export class AuthService {
constructor(private readonly usersService: UserService, private readonly jwtService: JwtService) {}

// JWT验证 - Step 2: 校验用户信息
async validateUser(username: string, password: string): Promise<any> {
console.log('JWT验证 - Step 2: 校验用户信息');
const user = await this.usersService.findOne(username);
if (user) {
const hashedPassword = user.password;
const salt = user.salt;
// 通过密码盐,加密传参,再与数据库里的比较,判断是否相等
const hashPassword = encryptPassword(password, salt);
if (hashedPassword === hashPassword) {
// 密码正确
return {
code: 1,
user,
};
} else {
// 密码错误
return {
code: 2,
user: null,
};
}
}
// 查无此人
return {
code: 3,
user: null,
};
}

// JWT验证 - Step 3: 处理 jwt 签证
async certificate(user: any) {
const payload = { username: user.username, sub: user.userId, realName: user.realName, role: user.role };
console.log('JWT验证 - Step 3: 处理 jwt 签证');
try {
const token = this.jwtService.sign(payload);
return {
code: 200,
data: {
token,
},
msg: `登录成功`,
};
} catch (error) {
return {
code: 600,
msg: `账号或密码错误`,
};
}
}
}

此时保存文件,控制台会报错:

可以先不管,这是因为还没有把 JwtService 和 UserService 关联到 auth.module.ts 中。

5. 编写本地策略

这一步非必须,根据项目的需求来决定是否需要本地策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码// src/logical/auth/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}

async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}

6. 关联 Module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码// src/logical/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UserModule } from '../user/user.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';

@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '8h' }, // token 过期时效
}),
UserModule,
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}

此时保存文件,若还有上文的报错,则需要去 app.module.ts,将 AuthService 从 providers 数组中移除,并在 imports 数组中添加 AuthModule 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './logical/user/user.module';
// import { AuthService } from './logical/auth/auth.service';
import { AuthModule } from './logical/auth/auth.module';

@Module({
imports: [UserModule, AuthModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

7. 编写 login 路由

此时,回归到 user.controller.ts,我们将组装好的 JWT 相关文件引入,并根据验证码来判断用户状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
复制代码// src/logical/user/user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from '../auth/auth.service';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
constructor(private readonly authService: AuthService, private readonly usersService: UserService) {}

// JWT验证 - Step 1: 用户请求登录
@Post('login')
async login(@Body() loginParmas: any) {
console.log('JWT验证 - Step 1: 用户请求登录');
const authResult = await this.authService.validateUser(loginParmas.username, loginParmas.password);
switch (authResult.code) {
case 1:
return this.authService.certificate(authResult.user);
case 2:
return {
code: 600,
msg: `账号或密码不正确`,
};
default:
return {
code: 600,
msg: `查无此人`,
};
}
}

@Post('register')
async register(@Body() body: any) {
return await this.usersService.register(body);
}
}

此时保存文件,同样的报错又出现了:

这次我们先去 user.module.ts 将 controllers 注释掉:

此时看控制台,没有 User 相关的路由,我们需要去 app.module.ts 将 Controller 添加回去:

这么做是因为如果在 user.module.ts 中引入 AuthService 的话,就还要将其他的策略又引入一次,个人觉得很麻烦,就干脆直接用 app 来统一管理了。

四、登录验证

前面列了一大堆代码,是时候检验效果了,我们就按照原来注册的信息,进行登录请求:

图中可以看到,已经返回了一长串 token 了,而且控制台也打印了登录的步骤和用户信息。前端拿到这个 token,就可以请求其他有守卫的接口了。

接下来我们试试输错账号或密码的情况:

五、守卫

既然发放了 Token,就要能验证 Token,因此就要用到 Guard(守卫)了。

我们拿之前的注册接口测试一下,修改 user.controller.ts 的代码,引入 UseGuards 和 AuthGuard,并在路由上添加 @UseGuards(AuthGuard('jwt')):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码// src/logical/user/user.controller.ts
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from '../auth/auth.service';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
constructor(private readonly authService: AuthService, private readonly usersService: UserService) {}

@Post('login')
async login(@Body() loginParmas: any) {
...
}

@UseGuards(AuthGuard('jwt')) // 使用 'JWT' 进行验证
@Post('register')
async register(@Body() body: any) {
return await this.usersService.register(body);
}
}

然后,我们先来试试请求头没有带 token 的情况:

可以看到,返回 401 状态码,Unauthorized 表示未授权,也就是判断你没有登录。

现在,我们试试带 Token 的情况,把登录拿到的 Token 复制到 Postman 的 Authorzation 里(选择 Bearer Token):

然后再请求接口:

此时,已经可以正常访问了,再看看控制台打印的信息,步骤也正如代码中注释的那样:

至此,登录功能已基本完成。

总结

本篇介绍了如何使用 JWT 对用户登录进行 Token 签发,并在接受到含 Token 请求的时候,如何验证用户信息,从而实现了登录验证。

当然,实现登录验证并不局限于 JWT,还有很多方法,有兴趣的读者可以自己查阅。

这里也说一下 JWT 的缺点,主要是无法在使用同一账号登录的情况下,后登录的,挤掉先登录的,也就是让先前的 Token 失效,从而保证信息安全(至少我是没查到相关解决方法,如果有大神解决过该问题,还请指点),只能使用一些其他黑科技挤掉 Token(如 Redis)。

现在,注册、登录功能都有了,接下来应该完善一个服务端应有的其他公共功能。

下一篇将介绍拦截器、异常处理以及日志的收集。

本篇收录于NestJS 实战教程,更多文章敬请关注。

`

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

2020年大厂Java面试前复习的正确姿势(800+面试题附

发表于 2020-03-19

前言

个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、 丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!

本篇分享的面试题内容包括:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Redis、MySQL、Spring、Spring Boot、Spring Cloud、RabbitMQ、Kafka、Linux 等技术栈。

1、Java基础系列面试题

Java面试题基础系列228道(1),快看看哪些你还不会?

Java面试题基础系列228道(2),查漏补缺!

Java面试题基础系列228道(3),查漏补缺!

Java面试题基础系列228道(4),快看看哪些你还不会?

Java面试题基础系列228道(5),快看看哪些你还不会?

Java面试题基础系列228道(6)

Java面试题基础系列228道(7)

Java面试题基础系列228道(8)

2、Spring系列面试题

全网最全Spring系列面试题129道(附答案解析)

3、Java并发系列面试题

2万字Java并发编程面试题合集(含答案,建议收藏)

4、JVM与调优面试题

2020年薪30W的Java程序员都要求熟悉JVM与性能调优!

5、Redis面试题

面试还搞不懂redis,快看看这40道面试题(含答案和思维导图)

6、MyBatis面试题

2020面试还搞不懂MyBatis?快看看这27道面试题!(含答案和思维导图)

7、ZooKeeper面试题

面试官最喜欢问的28道ZooKeeper面试题

8、Spring Boot面试题

Spring Boot面试都问了什么?快看看这22道面试题!

9、SpringCloud面试题

查漏补缺:2020年搞定SpringCloud面试(含答案和思维导图)

10、Java微服务面试题

85道Java微服务面试题整理(助力2020面试)

11、kafka面试题

18道kafka高频面试题哪些你还不会?(含答案和思维导图)

12、RabbitMQ面试题

12道RabbitMQ高频面试题你都会了吗?(含答案解析)

13、Dubbo面试题

Dubbo你掌握的如何?快看看这30道高频面试题!

14、ElasticSearch面试题

常见ElasticSearch 面试题解析(上)

常见Elasticsearch 面试题答案详细解析(下)

15、Linux面试题

常见的Linux面试题及答案解析,哪些你还不会?

最后

上面的这些面试题都整理成了PDF文档,希望能帮助到你面试前的复习且找到一个好的工作,也节省你在网上搜索资料的时间来学习!

欢迎关注我的公众号:程序员追风 ,回复888领取这份整理好的Java面试题资料!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Nestjs 从零到壹系列(二):使用 Sequelize

发表于 2020-03-19

前言

上一篇介绍了如何创建项目、路由的访问以及如何创建模块,这篇来讲讲数据库的连接与使用。

既然是后端项目,当然要能连上数据库,否则还不如直接写静态页面。

本教程使用的是 MySQL,有人可能会问为啥不用 MongoDB。。。呃,因为公司使用 MySQL,我也是结合项目经历写的教程,MongoDB 还没踩过坑,所以就不在这误人子弟了。

GitHub 项目地址,欢迎各位大佬 Star。

一、MySQL 准备

首先要确保你有数据库可以连接,如果没有,可以在 MySQL 官网下载一个,本地跑起来。安装教程这里就不叙述了,“百度一下,你就知道”。

推荐使用 Navicat Premium 可视化工具来管理数据库。

用 Navicat 连接上数据库后,新建一个库:

点开我们刚创建的库 nest_zero_to_one,点开 Tables,发现里面空空如也,接下来我们创建一张新表,点开上面工具栏的 Query,并新增查询:

将下列代码复制到框内,点击上面的运行,即可完成表的创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码CREATE TABLE `admin_user` (
`user_id` smallint(6) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`account_name` varchar(24) NOT NULL COMMENT '用户账号',
`real_name` varchar(20) NOT NULL COMMENT '真实姓名',
`passwd` char(32) NOT NULL COMMENT '密码',
`passwd_salt` char(6) NOT NULL COMMENT '密码盐',
`mobile` varchar(15) NOT NULL DEFAULT '0' COMMENT '手机号码',
`role` tinyint(4) NOT NULL DEFAULT '3' COMMENT '用户角色:0-超级管理员|1-管理员|2-开发&测试&运营|3-普通用户(只能查看)',
`user_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-失效|1-有效|2-删除',
`create_by` smallint(6) NOT NULL COMMENT '创建人ID',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` smallint(6) NOT NULL DEFAULT '0' COMMENT '修改人ID',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`user_id`),
KEY `idx_m` (`mobile`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='后台用户表';

然后我们可以看到,左边的 Tables 下多出了 admin_user 表,点开就可以看到字段信息了:

我们先随便插入2条数据,方便后面的查询:

二、项目的数据库配置

先在项目根目录创建一个文件夹 config(与 src 同级),专门放置各种配置。

然后新建一个文件 db.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码// config/db.ts
const productConfig = {
mysql: {
port: '数据库端口',
host: '数据库地址',
user: '用户名',
password: '密码',
database: 'nest_zero_to_one', // 库名
connectionLimit: 10, // 连接限制
},
};

const localConfig = {
mysql: {
port: '数据库端口',
host: '数据库地址',
user: '用户名',
password: '密码',
database: 'nest_zero_to_one', // 库名
connectionLimit: 10, // 连接限制
},
};

// 本地运行是没有 process.env.NODE_ENV 的,借此来区分[开发环境]和[生产环境]
const config = process.env.NODE_ENV ? productConfig : localConfig;

export default config;

Ps:这个文件是不同步到 github 的,需要各位读者结合实际情况配置

市面上有很多连接数据库的工具,笔者这里使用的是 Sequelize,先安装依赖包:

1
2
3
复制代码$ npm i sequelize sequelize-typescript mysql2 -S
或
$ yarn add sequelize sequelize-typescript mysql2 -S

然后在 src 目录下创建文件夹 database,然后再创建 sequelize.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码// src/database/sequelize.ts
import { Sequelize } from 'sequelize-typescript';
import db from '../../config/db';

const sequelize = new Sequelize(db.mysql.database, db.mysql.user, db.mysql.password || null, {
// 自定义主机; 默认值: localhost
host: db.mysql.host, // 数据库地址
// 自定义端口; 默认值: 3306
port: db.mysql.port,
dialect: 'mysql',
pool: {
max: db.mysql.connectionLimit, // 连接池中最大连接数量
min: 0, // 连接池中最小连接数量
acquire: 30000,
idle: 10000, // 如果一个线程 10 秒钟内没有被使用过的话,那么就释放线程
},
timezone: '+08:00', // 东八时区
});

// 测试数据库链接
sequelize
.authenticate()
.then(() => {
console.log('数据库连接成功');
})
.catch((err: any) => {
// 数据库连接失败时打印输出
console.error(err);
throw err;
});

export default sequelize;

三、数据库连接测试

好了,接下来我们来测试一下数据库的连接情况。

我们重写 user.service.ts 的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
复制代码// src/logical/user/user.service.ts
import { Injectable } from '@nestjs/common';
import * as Sequelize from 'sequelize'; // 引入 Sequelize 库
import sequelize from '../../database/sequelize'; // 引入 Sequelize 实例

@Injectable()
export class UserService {
async findOne(username: string): Promise<any | undefined> {
const sql = `
SELECT
user_id id, real_name realName, role
FROM
admin_user
WHERE
account_name = '${username}'
`; // 一段平淡无奇的 SQL 查询语句
try {
const res = await sequelize.query(sql, {
type: Sequelize.QueryTypes.SELECT, // 查询方式
raw: true, // 是否使用数组组装的方式展示结果
logging: true, // 是否将 SQL 语句打印到控制台,默认为 true
});
const user = res[0]; // 查出来的结果是一个数组,我们只取第一个。
if (user) {
return {
code: 200, // 返回状态码,可自定义
data: {
user,
},
msg: 'Success',
};
} else {
return {
code: 600,
msg: '查无此人',
};
}
} catch (error) {
return {
code: 503,
msg: `Service error: ${error}`,
};
}
}
}

保存文件,就会看到控制台刷新了(前提是使用 yarn start:dev 启动的),并打印下列语句:

这说明之前的配置生效了,我们试着用之前的参数请求一下接口:

返回“查无此人”,说明数据库没有叫“Kid”的用户。

我们改成正确的已存在的用户名再试试:

然后观察一下控制台,我们的查询语句已经打印出来了,通过 logging: true,可以在调试 Bug 的时候,更清晰的查找 SQL 语句的错误,不过建议测试稳定后,上线前关闭,不然记录的日志会很繁杂:

再对照一下数据库里的表,发现查出来的数据和数据库里的一致,至此,MySQL 连接测试完成,以后就可以愉快的在 Service 里面搬砖了。

总结

这篇介绍了 MySQL 的数据准备、Sequelize 的配置、Nest 怎么通过 Sequelize 连接上 MySQL,以及用一条简单的查询语句去验证连接情况。

在这里,强烈建议使用写原生 SQL 语句去操作数据库。

虽然 Sequelize 提供了很多便捷的方法,具体可去 Sequelize v5 官方文档 浏览学习。但笔者通过观察 logging 打印出来的语句发现,其实多了很多无谓的操作,在高并发的情况下,太影响性能了。

而且如果不使用原生查询,那么就要建立对象映射到数据库表,然后每次工具更新,还要花时间成本去学习,如果数据库改了字段,那么映射关系就会出错,然后项目就会疯狂报错以致宕机(亲身经历)。

而使用原生 SQL,只需要学一种语言就够了,换个工具,也能用,而且就算改了字段,也只会在请求接口的时候报错,到时候再针对那个语句修改就好了,而且现在查找替换功能这么强大,批量修改也不是难事。

最重要的是,如果你是从前端转后端,或者根本就是0基础到后端,还是建议先把 SQL 的基础打牢,不然连 JOIN、LEFT JOIN 和 RIGHT JOIN 的区别都分不清(我们公司就有个三年经验的后端,乱用 LEFT JOIN,然后被 DB 主管一顿痛骂。。。真事儿)。

多写、多分析、多看控制台报错、多从性能上考虑,才是最快入门的途径。

注意:在写 UPDATE 更新语句的时候,一定要加上 WHERE 条件,一定要加上 WHERE 条件,一定要加上 WHERE 条件,重要的事情说3遍,血与泪的教训!!!

下一篇,将介绍如何使用 JWT(Json Web Token)进行单点登录。

本篇收录于NestJS 实战教程,更多文章敬请关注。

`

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Nestjs 从零到壹系列(一):项目创建&路由设置&模块

发表于 2020-03-18

前言

本系列将以前端的视角进行书写,分享自己的踩坑经历。教程主要面向前端或者毫无后端经验,但是又想尝试 Node.js 的读者,当然,也欢迎后端大佬斧正。

Nest 是一个用于构建高效,可扩展的 Node.js 服务器端应用程序的框架。它使用渐进式 JavaScript,内置并完全支持 TypeScript(但仍然允许开发人员使用纯 JavaScript 编写代码)并结合了 OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素。

在底层,Nest使用强大的 HTTP Server 框架,如 Express(默认)和 Fastify。Nest 在这些框架之上提供了一定程度的抽象,同时也将其 API 直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。

Nest 是我近半年接触的一款后端框架,之前接触的是 Koa2,但因为老项目被“资深”前端写的乱七八糟,所以我就选择了这款以 TypeScript 为主的、最近在国内兴起的框架重构了。截止目前,Github 上的 nestjs 拥有 25.2k 个 Star,主要用户在国外,所以侧面可以证明其一定的稳定性。

Nest 采用 MVC 的设计模式,如果有 Angular 项目经验的读者,应该会觉得熟悉。我没写过 Angular,所以当初学的时候,走了一些弯路,主要是接受这种类 Spring 的设计理念。

GitHub 项目地址,欢迎各位大佬 Star。

好了,碎碎念到此为止,开始吧:

一、项目创建

项目环境:

  • node.js: 11.13.0+
  • npm: 6.13.4+
  • nestjs: 7.0.3
  • typescript: 3.8.3

先确操作系统上安装了 Node.js(>= 10.13.0),然后安装 Nest.js,然后新建项目,输入如下指令:

1
2
复制代码$ npm i -g @nestjs/cli
$ nest new project-name

输入完后,会初始化,此时,会问你使用哪一种方式来管理依赖包:

我选择的是 yarn,主要是国内的 npm 下载得比较慢。如果没有 yarn 的,可以下载一个,也可以使用 npm,不过本系列教程都使用 yarn。

等鸡啄完了米,等狗舔完了面,等火烧断了锁,就会得到下列信息:

按照提示,进入项目,不出意外,目录应该是这个样子的:

运行 yarn run start 或 yarn start,会看到控制台输出如下信息,表示服务已启动:

二、Hello World!

1. 路由指向

打开 src 下的 main.ts,不出意外,应该会看到下列代码:

1
2
3
4
5
6
7
8
复制代码import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();

await NestFactory.create(AppModule); 表示使用 Nest 的工厂函数创建了 AppModule,关于 Module 稍后会介绍。

await app.listen(3000) 表示监听的是 3000 端口,这个可以自定义。若 3000 端口被占用导致项目启动失败,可以修改成其他端口。

然后我们通过 Postman 访问本地的3000端口,会发现出现如下信息:

然后我们需要做的就是,找到为什么会出现 Hello World! 的原因。

打开 src 下的 app.service.ts,会看到如下代码:

1
2
3
4
5
6
7
8
9
复制代码// src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

发现这里有个方法 getHello(),返回了 Hello World! 字符串,那么它在哪里被调用呢?

打开 src 下的 app.controller.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Get()
getHello(): string {
return this.appService.getHello();
}
}

喔,原来如此,这里引入了 app.service.ts 中的 AppService 类,并实例化,然后通过 @Get() 修饰 AppController 里的 getHello() 方法,表示这个方法会被 GET 请求调用。

我们修改一下路由,就是在 @Get() 括号里面写上字符串:

1
2
3
4
5
6
7
8
9
10
复制代码// src/app.controller.ts
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Get('hello-world')
getHello(): string {
return this.appService.getHello();
}
}

然后重启项目(在控制台按下 Ctrl + C 终止项目,然后再输入 yarn start),此时我们再访问 localhost:3000/,就会发现 404 了:

此时,我们输入 localhost:3000/hello-world,熟悉的字符出现了:

这就是 Nest 的路由,是不是很简单?

2. 局部路由前缀

路由还可以设置局部和全局的前缀,使用前缀可以避免在所有路由共享通用前缀时出现冲突的情况。

还是 app.controller.ts,在 @Controller()写入 lesson-1,这样的话就表示当前文件中,所有的路由都有了前缀 lesson-1:

1
2
3
4
5
6
7
8
9
10
复制代码// src/app.controller.ts
@Controller('lesson-1')
export class AppController {
constructor(private readonly appService: AppService) {}

@Get('hello-world')
getHello(): string {
return this.appService.getHello();
}
}

重启项目,此时我们访问 localhost:3000/lesson-1/hello-world,就会指向 getHello() 方法了:

3. 全局路由前缀

这个更简单了,只需要在 main.ts 中加上app.setGlobalPrefix():

1
2
3
4
5
6
7
8
9
10
复制代码// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('nest-zero-to-one'); // 全局路由前缀
await app.listen(3000);
}
bootstrap();

之后只要请求服务,所有的路由都要加上 nest-zero-to-one 前缀:

4. 使用 nodemon 模式启动项目

如果不想频繁重启,可以使用 yarn start:dev 启动项目,它会使用 nodemon 监听文件的变化,并自动重启服务。

如果出现下列信息:

原因是可能之前装过 typescript 或者 nestjs 脚手架,然后新建项目的时候,typescript 版本比较旧,只需在项目中更新到 3.7.0 以上:

1
复制代码$ yarn add typescript -D

出现这个截图,但是没有路由信息,表示 nodemon 的配置需要更改:

1
2
3
4
5
6
7
复制代码package.json:
❌ "start:dev": "concurrently --handle-input \"wait-on dist/main.js && nodemon\" \"tsc -w -p tsconfig.build.json\" ",
✅ "start:dev": "concurrently --handle-input \"wait-on dist/src/main.js && nodemon\" \"tsc -w -p tsconfig.build.json\" ",

nodemon.json:
❌ "exec": "node dist/main"
✅ "exec": "node dist/src/main"

然后再运行 yarn start:dev 就可以了:

或者干脆直接把 main.ts 扔到根目录去(和src同级)

这样再改动什么文件,都会自动重启服务了。

三、新增模块

通过上文,应该熟悉了 NestJS 的设计模式,主要就是 Controller、Service、Module 共同努力,形成了一个模块。

  • Controller:传统意义上的控制器,提供 api 接口,负责处理路由、中转、验证等一些简洁的业务;
  • Service:又称为 Provider, 是一系列服务、repo、工厂方法、helper 的总称,主要负责处理具体的业务,如数据库的增删改查、事务、并发等逻辑代码;
  • Module:负责将 Controller 和 Service 连接起来,类似于 namespace 的概念;

很直观的传统 MVC 结构,有 Spring 开发经验的后端应该不会陌生。

下面我们通过新增一个 User 模块来进行实战:

1. Service

个人习惯先创建 Service,最后再创建 Module,因为 Controller 和 Module 都需要引入 Service,这样引入的时候就可以有提示了(当然,也可以事先写 import 语句,但 ESLint 的检查会冒红点,强迫症患者表示不接受)。

使用 nest-cli 提供的指令可以快速创建文件,语法如下:

1
复制代码$ nest g [文件类型] [文件名] [文件目录(src目录下)]

我们输入:

1
复制代码$ nest g service user logical

就会发现 src 目录下多了 logical/user/ 文件夹(个人喜欢将业务逻辑相关的文件放入 logical)

上图中的 user.service.spec.ts 可以不用管……至少我写了大半年,也没动过这种文件。

然后我们看一下 user.service.ts,用指令创建的文件,基本都长这样:

1
2
3
4
5
复制代码// src/logical/user/user.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {}

于是,我们可以仿照 app.service.ts 来写一个简单的业务了:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码// src/logical/user/user.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
findOne(username: string): string {
if (username === 'Kid') {
return 'Kid is here';
}
return 'No one here';
}
}

2. Controller

现在,我们来写控制器,输入下列命令:

1
复制代码$ nest g controller user logical

初始化的 Controller 基本都长这个样:

1
2
3
4
5
复制代码// src/logical/user/user.controller.ts
import { Controller } from '@nestjs/common';

@Controller('user')
export class UserController {}

接下来,我们把 Service 的业务逻辑引入进来:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码import { Controller, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
constructor(private readonly usersService: UserService) {}

@Post('find-one')
findOne(@Body() body: any) {
return this.usersService.findOne(body.username);
}
}

需要先用构造器实例化,然后才能调用方法,这里使用的是 POST 来接收请求,通过 @Body() 来获取请求体(request.body)的参数。

我们用 Postman 来测试一下,先随意传入一个 username:

再传入 ‘Kid’:

由此可知,我们成功匹配到了路由,并且编写的业务生效了。

至此 70% 的流程已经走完,以后开发业务(搬砖),基本都是在 Service 和 Controller 里面折腾了。。。

注意:千万不要往 Controller 里面添加乱七八糟的东西,尤其不要在里面写业务逻辑,Controller 就应该保持简洁、干净。很多前端刚写 Node 的时候,都喜欢在这里面写逻辑,只为了省事,殊不知这对后期的维护是个灾难。

3. Module

这个是连接 Service 和 Controller 的东东,很多人会奇怪,上文只是创建了 Service 和 Controller,怎么就可以访问了呢?

打开 app.module.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserService } from './logical/user/user.service';
import { UserController } from './logical/user/user.controller';

@Module({
imports: [],
controllers: [AppController, UserController],
providers: [AppService, UserService],
})
export class AppModule {}

发现使用指令创建文件的时候,已经自动帮我们引入 User 相关文件了,而 main.ts 文件里,又已经引入了 AppModule,并使用 NestFactory 创建了实例。

因此,如果是新建无关痛痒的子模块,即使不新建 Module 文件,也能通过路由访问。

但是作为教程,还是大致说一下吧,先创建文件:

1
复制代码$ nest g module user logical

初始化的 Module 基本都长这个样:

1
2
3
4
复制代码import { Module } from '@nestjs/common';

@Module({})
export class UserModule {}

我们把 Service 和 Controller 组装起来:

1
2
3
4
5
6
7
8
9
10
复制代码import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Module({
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}

这样做有什么好处呢,就是其他 Module 想引入 User 的时候,就不用同时引入 Service 和 Controller 了,我们修改一下 app.module.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
// import { UserService } from './logical/user/user.service';
// import { UserController } from './logical/user/user.controller';
import { UserModule } from './logical/user/user.module';

@Module({
imports: [UserModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

保存运行,发现路由依然生效:

当然,Module 还有其他高级玩法,这个就不在这里展开了。

总结

本篇介绍了 Nest.js 项目的创建,路由的访问,以及如何新增模块。

每个模块又可分为 Service、Controller、Module。在本篇中:Service 负责处理逻辑、Controller 负责路由、Module 负责整合。

通过实战可以看出,Nest 还是相对简单的,唯一的障碍可能就是 TypeScript 了。

写惯了 JavaScript 的人,可能不是很能适应这种类型检查,尤其是热衷于使用各种骚操作的,不过既然涉及到了后端领域,还是严谨一点比较好,前期可以避免各种不规范导致的坑。

下一篇将介绍如何连接 MySQL 数据库。

本篇收录于NestJS 实战教程,更多文章敬请关注。

`

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

恕我直言,我怀疑你没怎么用过枚举

发表于 2020-03-17

Profile


我们是否一样?

估计很多小伙伴(也包括我自己)都有这种情况,在自学Java语言看书时,关于枚举enum这一块的知识点可能都有点 “轻敌” ,觉得这块内容非常简单,一带而过,而且在实际写代码过程中也不注意运用。

是的,我也是这样!直到有一天我提的代码审核没过,被技术总监一顿批,我才重新拿起了《Java编程思想》,把枚举这块的知识点重新又审视了一遍。


为什么需要枚举

常量定义它不香吗?为啥非得用枚举?

举个栗子,就以B站上传视频为例,视频一般有三个状态:草稿、审核和发布,我们可以将其定义为静态常量:

1
2
3
4
5
6
7
8
复制代码public class VideoStatus {

public static final int Draft = 1; //草稿

public static final int Review = 2; //审核

public static final int Published = 3; //发布
}

对于这种单值类型的静态常量定义,本身也没错,主要是在使用的地方没有一个明确性的约束而已,比如:

1
2
3
4
5
复制代码void judgeVideoStatus( int status ) {

...

}

比如这里的 judgeVideoStatus 函数的本意是传入 VideoStatus 的三种静态常量之一,但由于没有类型上的约束,因此传入任意一个int值都是可以的,编译器也不会提出任何警告。

但是在枚举类型出现之后,上面这种情况就可以用枚举严谨地去约束,比如用枚举去定义视频状态就非常简洁了:

1
2
3
复制代码public enum VideoStatus {
Draft, Review, Published
}

而且主要是在用枚举的地方会有更强的类型约束:

1
2
3
4
5
6
复制代码// 入参就有明确类型约束
void judgeVideoStatus( VideoStatus status ) {

...

}

这样在使用 judgeVideoStatus 函数时,入参类型就会受到明确的类型约束,一旦传入无效值,编译器就会帮我们检查,从而规避潜在问题。

除此之外,枚举在扩展性方面比普常量更方便、也更优雅。


重新系统认识一下枚举

还是拿前文《答应我,别再if/else走天下了可以吗》中的那个例子来说:比如,在后台管理系统中,肯定有用户角色一说,而且角色一般都是固定的,适合定义成一个枚举:

1
2
3
4
5
6
7
8
复制代码public enum UserRole {

ROLE_ROOT_ADMIN, // 系统管理员

ROLE_ORDER_ADMIN, // 订单管理员

ROLE_NORMAL // 普通用户
}

接下来我们就用这个UserRole为例来说明枚举的所有基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码UserRole role1 = UserRole.ROLE_ROOT_ADMIN;
UserRole role2 = UserRole.ROLE_ORDER_ADMIN;
UserRole role3 = UserRole.ROLE_NORMAL;

// values()方法:返回所有枚举常量的数组集合
for ( UserRole role : UserRole.values() ) {
System.out.println(role);
}
// 打印:
// ROLE_ROOT_ADMIN
// ROLE_ORDER_ADMIN
// ROLE_NORMAL

// ordinal()方法:返回枚举常量的序数,注意从0开始
System.out.println( role1.ordinal() ); // 打印0
System.out.println( role2.ordinal() ); // 打印1
System.out.println( role3.ordinal() ); // 打印2

// compareTo()方法:枚举常量间的比较
System.out.println( role1.compareTo(role2) ); //打印-1
System.out.println( role2.compareTo(role3) ); //打印-2
System.out.println( role1.compareTo(role3) ); //打印-2

// name()方法:获得枚举常量的名称
System.out.println( role1.name() ); // 打印ROLE_ROOT_ADMIN
System.out.println( role2.name() ); // 打印ROLE_ORDER_ADMIN
System.out.println( role3.name() ); // 打印ROLE_NORMAL

// valueOf()方法:返回指定名称的枚举常量
System.out.println( UserRole.valueOf( "ROLE_ROOT_ADMIN" ) );
System.out.println( UserRole.valueOf( "ROLE_ORDER_ADMIN" ) );
System.out.println( UserRole.valueOf( "ROLE_NORMAL" ) );

除此之外,枚举还可以用于switch语句中,而且意义更加明确:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码UserRole userRole = UserRole.ROLE_ORDER_ADMIN;
switch (userRole) {
case ROLE_ROOT_ADMIN: // 比如此处的意义就非常清晰了,比1,2,3这种数字好!
System.out.println("这是系统管理员角色");
break;
case ROLE_ORDER_ADMIN:
System.out.println("这是订单管理员角色");
break;
case ROLE_NORMAL:
System.out.println("这是普通用户角色");
break;
}

自定义扩充枚举

上面展示的枚举例子非常简单,仅仅是单值的情形,而实际项目中用枚举往往是多值用法。

比如,我想扩充一下上面的UserRole枚举,在里面加入 角色名 – 角色编码 的对应关系,这也是实际项目中常用的用法。

这时候我们可以在枚举里自定义各种属性、构造函数、甚至各种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
复制代码public enum UserRole {

ROLE_ROOT_ADMIN( "系统管理员", 000000 ),
ROLE_ORDER_ADMIN( "订单管理员", 100000 ),
ROLE_NORMAL( "普通用户", 200000 ),
;

// 以下为自定义属性

private final String roleName; //角色名称

private final Integer roleCode; //角色编码

// 以下为自定义构造函数

UserRole( String roleName, Integer roleCode ) {
this.roleName = roleName;
this.roleCode = roleCode;
}

// 以下为自定义方法

public String getRoleName() {
return this.roleName;
}

public Integer getRoleCode() {
return this.roleCode;
}

public static Integer getRoleCodeByRoleName( String roleName ) {
for( UserRole enums : UserRole.values() ) {
if( enums.getRoleName().equals( roleName ) ) {
return enums.getRoleCode();
}
}
return null;
}

}

从上述代码可知,在enum枚举类中完全可以像在普通Class里一样声明属性、构造函数以及成员方法。


枚举 + 接口 = ?

比如在我的前文《答应我,别再if/else走天下了可以吗》中讲烦人的if/else消除时,就讲过如何通过让枚举去实现接口来方便的完成。

这地方不妨再回顾一遍:

什么角色能干什么事,这很明显有一个对应关系,所以我们首先定义一个公用的接口RoleOperation,表示不同角色所能做的操作:

1
2
3
复制代码public interface RoleOperation {
String op(); // 表示某个角色可以做哪些op操作
}

接下来我们将不同角色的情况全部交由枚举类来做,定义一个枚举类RoleEnum,并让它去实现RoleOperation接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码public enum RoleEnum implements RoleOperation {

// 系统管理员(有A操作权限)
ROLE_ROOT_ADMIN {
@Override
public String op() {
return "ROLE_ROOT_ADMIN:" + " has AAA permission";
}
},

// 订单管理员(有B操作权限)
ROLE_ORDER_ADMIN {
@Override
public String op() {
return "ROLE_ORDER_ADMIN:" + " has BBB permission";
}
},

// 普通用户(有C操作权限)
ROLE_NORMAL {
@Override
public String op() {
return "ROLE_NORMAL:" + " has CCC permission";
}
};
}

这样,在调用处就变得异常简单了,一行代码就行了,根本不需要什么if/else:

1
2
3
4
5
6
复制代码public class JudgeRole {
public String judge( String roleName ) {
// 一行代码搞定!之前的if/else灰飞烟灭
return RoleEnum.valueOf(roleName).op();
}
}

而且这样一来,以后假如我想扩充条件,只需要去枚举类中加代码即可,而不用改任何老代码,非常符合开闭原则!


枚举与设计模式

什么?枚举还能实现设计模式?

是的!不仅能而且还能实现好几种!

1、单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码public class Singleton {

// 构造函数私有化,避免外部创建实例
private Singleton() {

}

//定义一个内部枚举
public enum SingletonEnum{

SEED; // 唯一一个枚举对象,我们称它为“种子选手”!

private Singleton singleton;

SingletonEnum(){
singleton = new Singleton(); //真正的对象创建隐蔽在此!
}

public Singleton getInstnce(){
return singleton;
}
}

// 故意外露的对象获取方法,也是外面获取实例的唯一入口
public static Singleton getInstance(){
return SingletonEnum.SEED.getInstnce(); // 通过枚举的种子选手来完成
}
}

2、策略模式

这个也比较好举例,比如用枚举就可以写出一个基于策略模式的加减乘除计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
复制代码public class Test {

public enum Calculator {

ADDITION {
public Double execute( Double x, Double y ) {
return x + y; // 加法
}
},

SUBTRACTION {
public Double execute( Double x, Double y ) {
return x - y; // 减法
}
},

MULTIPLICATION {
public Double execute( Double x, Double y ) {
return x * y; // 乘法
}
},


DIVISION {
public Double execute( Double x, Double y ) {
return x/y; // 除法
}
};

public abstract Double execute(Double x, Double y);
}

public static void main(String[] args) {
System.out.println( Calculator.ADDITION.execute( 4.0, 2.0 ) );
// 打印 6.0
System.out.println( Calculator.SUBTRACTION.execute( 4.0, 2.0 ) );
// 打印 2.0
System.out.println( Calculator.MULTIPLICATION.execute( 4.0, 2.0 ) );
// 打印 8.0
System.out.println( Calculator.DIVISION.execute( 4.0, 2.0 ) );
// 打印 2.0
}
}

专门用于枚举的集合类

我们平常一般习惯于使用诸如:HashMap 和 HashSet等集合来盛放元素,而对于枚举,有它专门的集合类:EnumSet和EnumMap

1、EnumSet

EnumSet 是专门为盛放枚举类型所设计的 Set 类型。

还是举例来说,就以文中开头定义的角色枚举为例:

1
2
3
4
5
6
7
8
复制代码public enum UserRole {

ROLE_ROOT_ADMIN, // 系统管理员

ROLE_ORDER_ADMIN, // 订单管理员

ROLE_NORMAL // 普通用户
}

比如系统里来了一批人,我们需要查看他是不是某个角色中的一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码// 定义一个管理员角色的专属集合
EnumSet<UserRole> userRolesForAdmin
= EnumSet.of(
UserRole.ROLE_ROOT_ADMIN,
UserRole.ROLE_ORDER_ADMIN
);

// 判断某个进来的用户是不是管理员
Boolean isAdmin( User user ) {

if( userRoles.contains( user.getUserRole() ) )
return true;

return false;
}

2、EnumMap

同样,EnumMap 则是用来专门盛放枚举类型为key的 Map 类型。

比如,系统里来了一批人,我们需要统计不同的角色到底有多少人这种的话:

1
2
3
4
5
6
7
8
9
10
复制代码Map<UserRole,Integer> userStatisticMap = new EnumMap<>(UserRole.class);

for ( User user : userList ) {
Integer num = userStatisticMap.get( user.getUserRole() );
if( null != num ) {
userStatisticMap.put( user.getUserRole(), num+1 );
} else {
userStatisticMap.put( user.getUserRole(), 1 );
}
}

用EnumMap可以说非常方便了。


总 结

小小的枚举就玩出这么多的花样,不过好在探索和总结的过程还挺有意思的,也复习了很多知识,慢慢来吧。


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java后端学习路线(适合科班、非科班和已工作的仔)

发表于 2020-03-16

点赞再看,养成习惯,微信搜索【三太子敖丙】关注这个互联网苟且偷生的工具人。

本文 GitHub github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

前言

这期我想写很久了,但是因为时间的原因一直拖到了现在,我以为一两天就写完了,结果从构思到整理资料,再到写出来用了差不多一周的时间吧。

你们也知道丙丙一直都是创作鬼才来的,所以我肯定不会一本正经的写,我想了好几个切入点,最后决定用一个完整的电商系统作为切入点,带着大家看看,我们需要学些啥,我甚至还收集配套视频和资料,暖男石锤啊,这期是呕心沥血之作,不要白嫖了。

正文

在写这个文章之前,我花了点时间,自己臆想了一个电商系统,基本上算是麻雀虽小五脏俱全,我今天就用它开刀,一步步剖析,我会讲一下我们可能会接触的技术栈可能不全,但是够用,最后给个学习路线。

Tip:请多欣赏一会,每个点看一下,看看什么地方是你接触过的,什么技术栈是你不太熟悉的,我觉得还算是比较全的,有什么建议也可以留言给我。

不知道大家都看了一下没,现在我们就要庖丁解牛了,我从上到下依次分析。

前端

你可能会会好奇,你不是讲后端学习路线嘛,为啥还有前端的部分,我只能告诉你,傻瓜,肤浅。

我们可不能闭门造车,谁告诉你后端就不学点前端了?

前端现在很多也了解后端的技术栈的,你想我们去一个网站,最先接触的,最先看到的是啥?

没错就是前端,在大学你要是找不到专门的前端同学,去做系统肯定也要自己顶一下前端的,那我觉得最基本的技术栈得熟悉和了解吧,丙丙现在也是偶尔会开发一下我们的管理系统主要是VUE和React。

在这里我列举了我目前觉得比较简单和我们后端可以了解的技术栈,都是比较基础的。

作为一名后端了解部分前端知识还是很有必要的,在以后开发的时候,公司有前端那能帮助你前后端联调更顺畅,如果没前端你自己也能顶一下简单的页面。

HTML、CSS、JS、Ajax我觉得是必须掌握的点,看着简单其实深究或者去操作的话还是有很多东西的,其他作为扩展有兴趣可以了解,反正入门简单,只是精通很难很难。

在这一层不光有这些还有Http协议和Servlet,request、response、cookie、session这些也会伴随你整个技术生涯,理解他们对后面的你肯定有不少好处。

Tip:我这里最后删除了JSP相关的技术,我个人觉得没必要学了,很多公司除了老项目之外,新项目都不会使用那些技术了。

前端在我看来比后端难,技术迭代比较快,知识好像也没特定的体系,所以面试大厂的前端很多朋友都说难,不是技术多难,而是知识多且复杂,找不到一个完整的体系,相比之下后端明朗很多,我后面就开始讲后端了。

网关层:

互联网发展到现在,涌现了很多互联网公司,技术更新迭代了很多个版本,从早期的单机时代,到现在超大规模的互联网时代,几亿人参与的春运,几千亿成交规模的双十一,无数互联网前辈的造就了现在互联网的辉煌。

微服务,分布式,负载均衡等我们经常提到的这些名词都是这些技术在场景背后支撑。

单机顶不住,我们就多找点服务器,但是怎么将流量均匀的打到这些服务器上呢?

负载均衡,LVS

我们机器都是IP访问的,那怎么通过我们申请的域名去请求到服务器呢?

DNS

大家刷的抖音,B站,快手等等视频服务商,是怎么保证同时为全国的用户提供快速的体验?

CDN

我们这么多系统和服务,还有这么多中间件的调度怎么去管理调度等等?

zk

这么多的服务器,怎么对外统一访问呢,就可能需要知道反向代理的服务器。

Nginx

这一层做了反向负载、服务路由、服务治理、流量管理、安全隔离、服务容错等等都做了,大家公司的内外网隔离也是这一层做的。

我之前还接触过一些比较有意思的项目,所有对外的接口都是加密的,几十个服务会经过网关解密,找到真的路由再去请求。

这一层的知识点其实也不少,你往后面学会发现分布式事务,分布式锁,还有很多中间件都离不开zk这一层,我们继续往下看。

服务层:

这一层有点东西了,算是整个框架的核心,如果你跟我帅丙一样以后都是从事后端开发的话,我们基本上整个技术生涯,大部分时间都在跟这一层的技术栈打交道了,各种琳琅满目的中间件,计算机基础知识,Linux操作,算法数据结构,架构框架,研发工具等等。

我想在看这个文章的各位,计算机基础肯定都是学过的吧,如果大学的时候没好好学,我觉得还是有必要再看看的。

为什么我们网页能保证安全可靠的传输,你可能会了解到HTTP,TCP协议,什么三次握手,四次挥手。

还有进程、线程、协程,什么内存屏障,指令乱序,分支预测,CPU亲和性等等,在之后的编程生涯,如果你能掌握这些东西,会让你在遇到很多问题的时候瞬间get到点,而不是像个无头苍蝇一样乱撞(然而丙丙还做得不够)。

了解这些计算机知识后,你就需要接触编程语言了,大学的C语言基础会让你学什么语言入门都会快点,我选择了面向对象的JAVA,但是也不知道为啥现在还没对象。

JAVA的基础也一样重要,面向对象(包括类、对象、方法、继承、封装、抽象、 多态、消息解析等),常见API,数据结构,集合框架,设计模式(包括创建型、结构型、行为型),多线程和并发,I/O流,Stream,网络编程你都需要了解。

代码会写了,你就要开始学习一些能帮助你把系统变得更加规范的框架,SSM可以会让你的开发更加便捷,结构层次更加分明。

写代码的时候你会发现你大学用的Eclipse在公司看不到了,你跟大家一样去用了IDEA,第一天这是什么玩意,一周后,真香,但是这玩意收费有点贵,那免费的VSCode真的就是不错的选择了。

代码写的时候你会接触代码的仓库管理工具maven、Gradle,提交代码的时候会去写项目版本管理工具Git。

代码提交之后,发布之后你会发现很多东西需要自己去服务器亲自排查,那Linux的知识点就可以在里面灵活运用了,查看进程,查看文件,各种Vim操作等等。

系统的优化很多地方没优化的空间了,你可能会尝试从算法,或者优化数据结构去优化,你看到了HashMap的源码,想去了解红黑树,然后在算法网上看到了二叉树搜索树和各种常见的算法问题,刷多了,你也能总结出精华所在,什么贪心,分治,动态规划等。

这么多个服务,你发现HTTP请求已经开始有点不满足你的需求了,你想开发更便捷,像访问本地服务一样访问远程服务,所以我们去了解了Dubbo,Spring cloud。

了解Dubbo的过程中,你发现了RPC的精华所在,所以你去接触到了高性能的NIO框架,Netty。

代码写好了,服务也能通信了,但是你发现你的代码链路好长,都耦合在一起了,所以你接触了消息队列,这种异步的处理方式,真香。

他还可以帮你在突发流量的时候用队列做缓冲,但是你发现分布式的情况,事务就不好管理了,你就了解到了分布式事务,什么两段式,三段式,TCC,XA,阿里的全局事务服务GTS等等。

分布式事务的时候你会想去了解RocketMQ,因为他自带了分布式事务的解决方案,大数据的场景你又看到了Kafka。

我上面提到过zk,像Dubbo、Kafka等中间件都是用它做注册中心的,所以很多技术栈最后都组成了一个知识体系,你先了解了体系中的每一员,你才能把它们联系起来。

服务的交互都从进程内通信变成了远程通信,所以性能必然会受到一些影响。

此外由于很多不确定性的因素,例如网络拥塞、Server 端服务器宕机、挖掘机铲断机房光纤等等,需要许多额外的功能和措施才能保证微服务流畅稳定的工作。

Spring Cloud 中就有 Hystrix 熔断器、Ribbon客户端负载均衡器、Eureka注册中心等等都是用来解决这些问题的微服务组件。

你感觉学习得差不多了,你发现各大论坛博客出现了一些前沿技术,比如容器化,你可能就会去了解容器化的知识,像Docker,Kubernetes(K8s)等。

微服务之所以能够快速发展,很重要的一个原因就是:容器化技术的发展和容器管理系统的成熟。

这一层的东西呢其实远远不止这些的,我不过多赘述,写多了像个劝退师一样,但是大家也不用慌,大部分的技术都是慢慢接触了,工作中慢慢去了解,去深入的。

好啦我们继续沿着图往下看,那再往下是啥呢?

数据层:

数据库可能是整个系统中最值钱的部分了,在我码文字的前一天,刚好发生了微盟程序员删库跑路的操作,删库跑路其实是我们在网上最常用的笑话,没想到还是照进了现实。

这里也提一点点吧,36小时的故障,其实在互联网公司应该是个笑话了吧,权限控制没做好类似rm -rf 、fdisk、drop等等这样的高危命令是可以实时拦截掉的,备份,全量备份,增量备份,延迟备份,异地容灾全部都考虑一下应该也不至于这样,一家上市公司还是有点点不应该。

数据库基本的事务隔离级别,索引,SQL,主被同步,读写分离等都可能是你学的时候要了解到的。

上面我们提到了安全,不要把鸡蛋放一个篮子的道理大家应该都知道,那分库的意义就很明显了,然后你会发现时间久了表的数据大了,就会想到去接触分表,什么TDDL、Sharding-JDBC、DRDS这些插件都会接触到。

你发现流量大的时候,或者热点数据打到数据库还是有点顶不住,压力太大了,那非关系型数据库就进场了,Redis当然是首选,但是MongoDB、memcache也有各自的应用场景。

Redis使用后,真香,真快,但是你会开始担心最开始提到的安全问题,这玩意快是因为在内存中操作,那断点了数据丢了怎么办?你就开始阅读官方文档,了解RDB,AOF这些持久化机制,线上用的时候还会遇到缓存雪崩击穿、穿透等等问题。

单机不满足你就用了,他的集群模式,用了集群可能也担心集群的健康状态,所以就得去了解哨兵,他的主从同步,时间久了Key多了,就得了解内存淘汰机制……

他的大容量存储有问题,你可能需要去了解Pika….

其实远远没完,每个的点我都点到为止,但是其实要深究每个点都要学很久,我们接着往下看。

实时/离线/大数据

等你把几种关系型非关系型数据库的知识点,整理清楚后,你会发现数据还是大啊,而且数据的场景越来越多多样化了,那大数据的各种中间件你就得了解了。

你会发现很多场景,不需要实时的数据,比如你查你的支付宝去年的,上个月的账单,这些都是不会变化的数据,没必要实时,那你可能会接触像ODPS这样的中间件去做数据的离线分析。

然后你可能会接触Hadoop系列相关的东西,比如于Hadoop(HDFS)的一个数据仓库工具Hive,是建立在 Hadoop 文件系统之上的分布式面向列的数据库HBase 。

写多的场景,适合做一些简单查询,用他们又有点大材小用,那Cassandra就再合适不过了。

离线的数据分析没办法满足一些实时的常见,类似风控,那Flink你也得略知一二,他的窗口思想还是很有意思。

数据接触完了,计算引擎Spark你是不是也不能放过……

搜索引擎:

传统关系型数据库和NoSQL非关系型数据都没办法解决一些问题,比如我们在百度,淘宝搜索东西的时候,往往都是几个关键字在一起一起搜索东西的,在数据库除非把几次的结果做交集,不然很难去实现。

那全文检索引擎就诞生了,解决了搜索的问题,你得思考怎么把数据库的东西实时同步到ES中去,那你可能会思考到logstash去定时跑脚本同步,又或者去接触伪装成一台MySQL从服务的Canal,他会去订阅MySQL主服务的binlog,然后自己解析了去操作Es中的数据。

这些都搞定了,那可视化的后台查询又怎么解决呢?Kibana,他他是一个可视化的平台,甚至对Es集群的健康管理都做了可视化,很多公司的日志查询系统都是用它做的。

学习路线

看了这么久你是不是发现,帅丙只是一直在介绍每个层级的技术栈,并没说到具体的一个路线,那是因为我想让大家先有个认知或者说是扫盲吧,我一样用脑图的方式汇总一下吧,如果图片被平台二压了,可以去公众号回复【路线】。

资料/学习网站

JavaFamily:由一个在互联网苟且偷生的男人维护的GitHub

CodeGym :一个在线Java编程课程,80%的内容是练习,适合一窍不通的入门者。

Wibit Online Java Courses :一个非常有趣的编程学习网站,各种生动的动画形象能让人忘记学习的枯燥。在线视频学习,非常适合零基础。

stanford CS106A: Programming Methodology :斯坦福经典课程系列,完全没有编程经验,想学Java语言的,可以看看这个课程。

Bloombenc :一个在线交互式学习平台,老师可以根据你的学习能力和节奏修改他们的教学方法,还可以在平台上编码。

Imooc:慕课网,我大学的C语言就是在这里看的

CodeAcademy :比较实用的Java在线课程,注重的是在找工作时非常有用的技术能力。

PLURALSIGHT:整合了很多Java的视频课程,部分免费,部分付费,可以根据自己的需要挑选。

Lynda Online Java Training Videos:Java进阶课程,包括如何使用JDBC来集成MySQL数据库,Reflection API,管理文件和目录等。

九章基础算法班(Java):中文在线互动课,随时开始学习。

BeginnersBook:Java初学者免费教程,有稍微一些编程基础之后,可以跟着文档里的代码练习。

docs.oracle.com/javase/tuto…:官方Java指南,对了解几乎所有的java技术特性都非常有帮助。

JournalDev:Java相关教程及问答

JavaWorld:最早的一个Java站点,每周更新Java技术文章。

developer.com/java :由Gamelan.com 维护的Java技术文章网站。

IBM Developerworks技术网站:IBM的Develperworks技术网站,这是其中的Java技术主页

Tip:本来这一栏有很多我准备的资料的,但是都是外链,或者不合适的分享方式,博客的运营小姐姐提醒了我,所以大家去公众号回复【路线】好了。

絮叨

如果你想去一家不错的公司,但是目前的硬实力又不到,我觉得还是有必要去努力一下的,技术能力的高低能决定你走多远,平台的高低,能决定你的高度。

如果你通过努力成功进入到了心仪的公司,一定不要懈怠放松,职场成长和新技术学习一样,不进则退。

丙丙发现在工作中发现我身边的人真的就是实力越强的越努力,最高级的自律,享受孤独(周末的歪哥)。

总结

我提到的技术栈你想全部了解,我觉得初步了解可能几个月就够了,这里的了解仅限于你知道它,知道他是干嘛的,知道怎么去使用它,并不是说深入了解他的底层原理,了解他的常见问题,熟悉问题的解决方案等等。

你想做到后者,基本上只能靠时间上的日积月累,或者不断的去尝试积累经验,也没什么速成的东西,欲速则不达大家也是知道的。

技术这条路,说实话很枯燥,很辛苦,但是待遇也会高于其他一些基础岗位。

所实话我大学学这个就是为了兴趣,我从小对电子,对计算机都比较热爱,但是现在打磨得,现在就是为了钱吧,是不是很现实?若家境殷实,谁愿颠沛流离。

但是至少丙丙因为做软件,改变了家庭的窘境,自己日子也向小康一步步迈过去。

说做程序员改变了我和我家人的一生可能夸张了,但是我总有一种下班辈子会因为我选择走这条路而改变的错觉。

我是敖丙,一个在互联网苟且偷生的工具人。

创作不易,本期硬核,不想被白嫖,各位的「三连」就是丙丙创作的最大动力,我们下次见!

文章持续更新,可以微信搜索「 三太子敖丙 」第一时间阅读,回复【资料】【面试】【简历】有我准备的一线大厂面试资料和简历模板,本文 GitHub github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

看完这篇文章,你的Linux基础就差不多了 Linux基础

发表于 2020-03-15

传送门:看完这篇文章,你的Python基础就差不多了

Linux基础

这篇文章基于传智播客的2016年的gitbook资料和视频资料,同时也融合了2018年的视频和课件资料中的一些内容,即以2016年的资料为蓝本,2018年的资料为辅助编写的。

一、Linux介绍

1、操作系统的发展

2、Linux的不同版本

<1>Linux内核版本:内核(kernel)是系统的心脏,是运行程序和管理像磁盘和打印机等硬件设备的核心程序,它提供了一个在裸设备与应用程序间的抽象层。
<2>Linux发行版本:也被叫做 GNU, 通常包含了包括桌面环境、办公套件、媒体播放器、数据库等应用软件。

二、文件和目录

1、Windows和Linux文件系统区别

在 windows 平台下,打开“计算机”,我们看到的是一个个的驱动器盘符:

每个驱动器都有自己的根目录结构,这样形成了多个树并列的情形,如图所示:

在 Linux 下,我们是看不到这些驱动器盘符,我们看到的是文件夹(目录):

就比如我们用的Ubuntu没有盘符这个概念,只有一个根目录/,所有文件都在它下面:

/:根目录,一般根目录下只存放目录,在Linux下有且只有一个根目录。所有的东西都是从这里开始。当你在终端里输入“/home”,你其实是在告诉电脑,先从/(根目录)开始,再进入到home目录。
/bin: /usr/bin: 可执行二进制文件的目录,如常用的命令ls、tar、mv、cat等。
/boot:放置linux系统启动时用到的一些文件,如Linux的内核文件:/boot/vmlinuz,系统引导管理器:/boot/grub。
/dev:存放linux系统下的设备文件,访问该目录下某个文件,相当于访问某个设备,常用的是挂载光驱 mount /dev/cdrom /mnt。
/etc:系统配置文件存放的目录,不建议在此目录下存放可执行文件,重要的配置文件有 /etc/inittab、/etc/fstab、/etc/init.d、/etc/X11、/etc/sysconfig、/etc/xinetd.d。
/home:系统默认的用户家目录,新增用户账号时,用户的家目录都存放在此目录下,表示当前用户的家目录,edu 表示用户 edu 的家目录。
/lib: /usr/lib: /usr/local/lib:系统使用的函数库的目录,程序在执行过程中,需要调用一些额外的参数时需要函数库的协助。
/lost+fount:系统异常产生错误时,会将一些遗失的片段放置于此目录下。
/mnt: /media:光盘默认挂载点,通常光盘挂载于 /mnt/cdrom 下,也不一定,可以选择任意位置进行挂载。
/opt:给主机额外安装软件所摆放的目录。
/proc:此目录的数据都在内存中,如系统核心,外部设备,网络状态,由于数据都存放于内存中,所以不占用磁盘空间,比较重要的目录有 /proc/cpuinfo、/proc/interrupts、/proc/dma、/proc/ioports、/proc/net/* 等。
/root:系统管理员root的家目录。
/sbin: /usr/sbin: /usr/local/sbin:放置系统管理员使用的可执行命令,如fdisk、shutdown、mount 等。与 /bin 不同的是,这几个目录是给系统管理员 root使用的命令,一般用户只能”查看”而不能设置和使用。
/tmp:一般用户或正在执行的程序临时存放文件的目录,任何人都可以访问,重要数据不可放置在此目录下。
/srv:服务启动之后需要访问的数据目录,如 www 服务需要访问的网页数据存放在 /srv/www 内。
/usr:应用程序存放目录,/usr/bin 存放应用程序,/usr/share 存放共享数据,/usr/lib 存放不能直接运行的,却是许多程序运行所必需的一些函数库文件。/usr/local: 存放软件升级包。/usr/share/doc: 系统说明文件存放目录。/usr/share/man: 程序说明文件存放目录。
/var:放置系统执行过程中经常变化的文件,如随时更改的日志文件 /var/log,/var/log/message:所有的登录文件存放目录,/var/spool/mail:邮件存放的目录,/var/run:程序或服务启动后,其PID存放在该目录下。

2、用户目录

位于/home/user,称之为用户工作目录或家目录,表示方式:

/home/user
~

3、相对路径和绝对路径

绝对路径:从/目录开始描述的路径为绝对路径,如:/home
相对路径:从当前位置开始描述的路径为相对路径,如:../../
.和.. :每个目录下都有.和..(可用ls -a查看);. 表示当前目录;.. 表示上一级目录,即父目录;根目录下的.和..都表示当前目录

4、文件权限

文件权限就是文件的访问控制权限,即哪些用户和组群可以访问文件以及可以执行什么样的操作。
在 Unix/Linux中的每一个文件或目录都包含有访问权限,这些访问权限决定了谁能访问和如何访问这些文件和目录。

<1>访问用户

通过设定权限可以从以下三种访问方式限制访问权限:
只允许用户自己访问(所有者) 所有者就是创建文件的用户,用户是所有用户所创建文件的所有者,用户可以允许所在的用户组能访问用户的文件。
允许一个预先指定的用户组中的用户访问(用户组) 用户都组合成用户组,例如,某一类或某一项目中的所有用户都能够被系统管理员归为一个用户组,一个用户能够授予所在用户组的其他成员的文件访问权限。
允许系统中的任何用户访问(其他用户) 用户也将自己的文件向系统内的所有用户开放,在这种情况下,系统内的所有用户都能够访问用户的目录或文件。在这种意义上,系统内的其他所有用户就是 other 用户类。

<2>访问权限

用户能够控制一个给定的文件或目录的访问程度,一个文件或目录可能有读、写及执行权限:
读权限(r) 对文件而言,具有读取文件内容的权限;对目录来说,具有浏览目录的权限。
写权限(w) 对文件而言,具有新增、修改文件内容的权限;对目录来说,具有删除、移动目录内文件的权限。
可执行权限(x) 对文件而言,具有执行文件的权限;对目录了来说该用户具有进入目录的权限。
注意:通常,Unix/Linux系统只允许文件的属主(所有者)或超级用户改变文件的读写权限。

<3>示例说明:利用ls -lh查看

第1个字母代表文件的类型:“d” 代表文件夹、“-” 代表普通文件、“c” 代表硬件字符设备、“b” 代表硬件块设备、“s”表示管道文件、“l” 代表软链接文件。 后 9 个字母分别代表三组权限:文件所有者、用户者、其他用户拥有的权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码handy@ubuntu:~$ ls -l
total 44
drwxr-xr-x 2 handy handy 4096 Aug 29 06:45 Desktop
drwxr-xr-x 2 handy handy 4096 Aug 29 06:45 Documents
drwxr-xr-x 2 handy handy 4096 Aug 29 06:45 Downloads
-rw-r--r-- 1 handy handy 8980 Aug 29 06:39 examples.desktop
drwxr-xr-x 2 handy handy 4096 Aug 29 06:45 Music
drwxr-xr-x 2 handy handy 4096 Aug 29 06:45 Pictures
drwxr-xr-x 2 handy handy 4096 Aug 29 06:45 Public
drwxr-xr-x 2 handy handy 4096 Aug 29 06:45 Templates
drwxr-xr-x 2 handy handy 4096 Aug 29 06:45 Videos
handy@ubuntu:~$ ls -lh
total 44K
drwxr-xr-x 2 handy handy 4.0K Aug 29 06:45 Desktop
drwxr-xr-x 2 handy handy 4.0K Aug 29 06:45 Documents
drwxr-xr-x 2 handy handy 4.0K Aug 29 06:45 Downloads
-rw-r--r-- 1 handy handy 8.8K Aug 29 06:39 examples.desktop
drwxr-xr-x 2 handy handy 4.0K Aug 29 06:45 Music
drwxr-xr-x 2 handy handy 4.0K Aug 29 06:45 Pictures
drwxr-xr-x 2 handy handy 4.0K Aug 29 06:45 Public
drwxr-xr-x 2 handy handy 4.0K Aug 29 06:45 Templates
drwxr-xr-x 2 handy handy 4.0K Aug 29 06:45 Videos
handy@ubuntu:~$

每一个用户都有它自身的读、写和执行权限。
第一组权限控制访问自己的文件权限,即所有者权限。
第二组权限控制用户组访问其中一个用户的文件的权限。
第三组权限控制其他所有用户访问一个用户的文件的权限。
这三组权限赋予用户不同类型(即所有者、用户组和其他用户)的读、写及执行权限就构成了一个有9种类型的权限组。

三、常用基本命令

Linux 提供了大量的命令,利用它可以有效地完成大量的工作,如磁盘操作、文件存取、目录操作、进程管理、文件权限设定等。Linux 发行版本最少的命令也有 200 多个,这里只介绍比较重要和使用频率最多的命令。

1、命令使用方法

Linux命令格式:
command [-options] [parameter1] …
命令 选项 参数
说明:
command: 命令名,相应功能的英文单词或单词的缩写 [-options]:选项,可用来对命令进行控制,也可以省略,[]代表可选 parameter1 …:传给命令的参数:可以是零个一个或多个.

2、查看帮助文档

###<1>–help
一般是linux命令自带的帮助信息,如:ls --help

<2>man

man是linux提供的一个手册,包含了绝大部分的命令、函数使用说明
该手册分成很多章节(section),使用man时可以指定不同的章节来浏览。
例:man ls ; man 2 printf
man中各个section意义如下:
1: Standard commands(标准命令)
2: System calls(系统调用,如open,write)
3: Library functions(库函数,如printf,fopen)
4:Special devices(设备文件的说明,/dev下各种设备)
5: File formats(文件格式,如passwd)
6:Games and toys(游戏和娱乐)
7:Miscellaneous(杂项、惯例与协定等,例如Linux档案系统、网络协定、ASCII 码;environ全局变量)
8: Administrative Commands(管理员命令,如ifconfig)
man是按照手册的章节号的顺序进行搜索的。
man设置了如下的功能键:

例如:

3、自动补全

在敲出命令的前几个字母的同时,按下tab键,系统会自动帮我们补全命令

4、历史命令

当系统执行过一些命令后,可按上下键翻看以前的命令,history将执行过的命令列举出来。

四、文件、磁盘管理的常用命令

<1>查看文件信息:ls

ls是英文单词list的简写,其功能为列出目录的内容,是用户最常用的命令之一,它类似于DOS下的dir命令。
Linux文件或者目录名称最长可以有265个字符,“.”代表当前目录,“..”代表上一级目录,以“.”开头的文件为隐藏文件,需要用-a参数才能显示。

ls常用参数

ls -l

与DOS下的文件操作类似,在Unix/Linux系统中,也同样允许使用特殊字符来同时引用多个文件名,这些特殊字符被称为通配符。

<2>输出重定向命令:>

Linux允许将命令执行结果重定向到一个文件,本应显示在终端上的内容保存到指定文件中。
如:ls > test.txt ( test.txt 如果不存在,则创建,存在则覆盖其内容 )
注意: >输出重定向会覆盖原来的内容,>>输出重定向则会追加到文件的尾部。

<3>分屏显示:more

查看内容时,在信息过长无法在一屏上显示时,会出现快速滚屏,使得用户无法看清文件的内容,此时可以使用more命令,每次只显示一页,按下空格键可以显示下一页,按下q键退出显示,按下h键可以获取帮助。

more 示例

<4>管道:|

管道:一个命令的输出可以通过管道做为另一个命令的输入。
管道我们可以理解现实生活中的管子,管子的一头塞东西进去,另一头取出来,这里“ | ”的左右分为两端,左端塞东西(写),右端取东西(读)。

管道示例

<5>清屏:clear

clear作用为清除终端上的显示(类似于DOS的cls清屏功能),也可使用快捷键:Ctrl + l ( “l” 为字母 )。

<6>切换工作目录: cd

在使用Unix/Linux的时候,经常需要更换工作目录。cd命令可以帮助用户切换工作目录。Linux所有的目录和文件名大小写敏感。
cd后面可跟绝对路径,也可以跟相对路径。如果省略目录,则默认切换到当前用户的主目录。

cd 示例

注意:如果路径是从根路径开始的,则路径的前面需要加上 “ / ”,如 “ /mnt ”,通常进入某个目录里的文件夹,前面不用加 “ / ”。

<7>显示当前路径:pwd

使用pwd命令可以显示当前的工作目录,该命令很简单,直接输入pwd即可,后面不带参数。

<8>创建目录:mkdir

通过mkdir命令可以创建一个新的目录。参数-p可递归创建目录。

示例

需要注意的是新建目录的名称不能与当前目录中已有的目录或文件同名,并且目录创建者必须对当前目录具有写权限。

<补>创建文件:touch

命令格式:touch 文件名
如果文件不存在,可以创建一个空白文件。
如果文件存在,可以修改文件的末次修改日期。

<9>删除目录:rmdir

可使用rmdir命令删除一个目录。必须离开目录,并且目录必须为空目录,不然提示删除失败。

<10>删除文件:rm

可通过rm删除文件或目录。使用rm命令要小心,因为文件删除后不能恢复。为了防止文件误删,可以在rm后使用-i参数以逐个确认要删除的文件。

常用参数

注意递归删除文件夹要加-r,而删除文件可以不加。
示例

<11>建立链接文件:ln

Linux链接文件类似于Windows下的快捷方式。
链接文件分为软链接(有-s)和硬链接。
软链接:软链接不占用磁盘空间,源文件删除则软链接失效,源文件要用绝对路径。ln -s 源文件 链接文件
硬链接:硬链接只能链接普通文件,不能链接目录,相当于文件“小名”,日常是不用的,只有文件的硬链接数(用ls -l查看)为0时,文件才被真正删除。 ln 源文件 链接文件

硬链接示例及软链接的tree示意

文件软硬链接示意图

在Linux中,文件数据和文件名是分开存储的。

<12>查看或者合并文件内容:cat

对应英文是concatenate,用于查看文件内容(适合内容较少的,较多的用more)、创建文件、文件合并、追加文件内容等。

常用参数

Linux中还有一个nl命令,和cat -b效果等价。

示例

<13>文本搜索:grep

Linux系统中grep命令是一种强大的文本搜索工具,grep允许对文本文件进行模式查找。如果找到匹配模式, grep打印包含模式的所有行。
grep一般格式为:grep [-选项] ‘搜索内容串’文件名
在grep命令中输入字符串参数时,最好引号或双引号括起来。例如:grep‘a ’1.txt。

常用选项

grep搜索内容串可以是正则表达式,也就是模式查找。
正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
grep常用正则表达式

grep示例

<14>查找文件:find

find支持文件名的正则表达式查找,按文件修改时间查找,按文件大小查找,按文件权限查找,按文件类型查找等,查找到以后还支持直接对查找到的文件使用命令,功能非常强大。

典型的find命令的写法是:find 查找路径 查找的标准 查找到之后的动作。
比如: find /home -type d -ls,意思是: 找出/home/下所有的目录,并显示目录的详细信息。

后继命令(查找到之后的动作):

1
2
3
4
5
6
复制代码-print: 显示
-ls:类似ls -l的形式显示每一个文件的详细
-quit:查找到一个就退出
-delete:删除匹配到的行
-ok COMMAND {} \:每一次操作都需要用户确认,{}表示引用找到的文件,是占位符,对于(find等输出的一个列表的内容)依次循环每一个;\是表示 -exec 命令终结的的符号。
-exec COMMAND {} \:每次操作无需确认

常用用法

<15>拷贝文件:cp

cp命令的功能是将给出的文件或目录复制到另一个文件或目录中,相当于DOS下的copy命令。

常用选项

<16>移动文件:mv

用户可以使用mv命令来移动文件或目录,也可以给文件或目录重命名。

常用选项

<17>归档管理:tar

计算机中的数据经常需要备份,tar是Unix/Linux中最常用的备份工具,此命令可以把一系列文件归档到一个大文件中,也可以把档案文件解开以恢复数据。
tar使用格式tar [选项] 打包文件名 文件
tar命令很特殊,其选项前面可以使用“-”,也可以不使用。

常用选项

注意:除了f需要放在参数的最后,其它参数的顺序任意。
一般来说,我们的选项主要要用cvf和xvf。

  • 文件打包:tar -cvf ***.tar 1.py 2.py 3.txt *.c
  • 文件解包:tar -xvf ***.tar -C ~/Desktop

<18>文件压缩解压:gzip

tar与gzip命令结合使用实现文件打包、压缩。 tar只负责打包文件,但不压缩,用gzip压缩tar打包后的文件,其扩展名一般用xxxx.tar.gz。
gzip使用格式:gzip [选项] 被压缩文件
常用选项:-d解压、-r压缩所有子目录
tar这个命令并没有压缩的功能,它只是一个打包的命令,但是在tar命令中增加一个选项(-z)可以调用gzip实现了一个压缩的功能,实行一个先打包后压缩的过程。

  • 压缩用法:tar -zcvf 压缩包包名 文件1 文件2 ...

-z :指定压缩包的格式为:file.tar.gz

  • 解压用法: tar -zxvf 压缩包包名
    解压到指定目录:-C (大写字母“C”)

<19>文件压缩解压:bzip2

tar与bzip2命令结合使用实现文件打包、压缩(用法和gzip一样)。
tar只负责打包文件,但不压缩,用bzip2压缩tar打包后的文件,其扩展名一般用xxxx.tar.gz2。
在tar命令中增加一个选项(-j)可以调用bzip2实现了一个压缩的功能,实行一个先打包后压缩的过程。

  • 压缩用法:tar jcvf 压缩包包名 文件...(tar jcvf bk.tar.bz2 *.c)
  • 解压用法:tar jxvf 压缩包包名 (tar jxvf bk.tar.bz2)
    示例

<20>文件压缩解压:zip、unzip

通过zip压缩文件的目标文件不需要指定扩展名,默认扩展名为zip。

  • 压缩文件:zip [-r] 目标文件(没有扩展名) 源文件
  • 解压文件:unzip -d 解压后目录文件 压缩文件

<21>查看命令位置:which

bin 和 sbin

示例

五、用户、权限管理的常用命令

用户是Unix/Linux系统工作中重要的一环,用户管理包括用户与组账号的管理。
在Unix/Linux系统中,不论是由本机或是远程登录系统,每个系统都必须拥有一个账号,并且对于不同的系统资源拥有不同的使用权限。
Unix/Linux系统中的root账号通常用于系统的维护和管理,它对Unix/Linux操作系统的所有部分具有不受限制的访问权限。
在Unix/Linux安装的过程中,系统会自动创建许多用户账号,而这些默认的用户就称为“标准用户”。
在大多数版本的Unix/Linux中,都不推荐直接使用root账号登录系统。

<1>查看当前用户:whoami

whoami命令用户查看当前系统当前账号的用户名。可通过cat /etc/passwd查看系统用户信息。
由于系统管理员通常需要使用多种身份登录系统,例如通常使用普通用户登录系统,然后再以su命令切换到root身份对传统进行管理。这时候就可以使用whoami来查看当前用户的身份。

<2>查看登录用户:who

who命令用于查看当前所有登录系统的用户信息。

常用选项

示例

<3>退出登录账户: exit

  • 如果是图形界面,退出当前终端(Terminal);
  • 如果是使用ssh远程登录,退出登陆账户;
  • 如果是切换后的登陆用户,退出则返回上一个登陆账号。
    exit示意图

<4>切换用户:su

可以通过su命令切换用户,su后面可以加“-”。su和su –命令不同之处在于,su -切换到对应的用户时会将当前的工作目录自动转换到切换后的用户主目录:

示例

注意:如果是ubuntu平台,需要在命令前加sudo,如果在某些操作需要管理员才能操作,ubuntu无需切换到root用户即可操作,只需加sudo即可。sudo是ubuntu平台下允许系统管理员让普通用户执行一些或者全部的root命令的一个工具,减少了root 用户的登陆和管理时间,提高了安全性。
su用法

Ubuntu下示例

<5>添加、删除组账号:groupadd、groupdel

groupadd 组名 新建组账号 groupdel 组名 删除组账号 cat /etc/group 查看用户组信息

示例

<6>修改用户所在组:usermod

  • 主组:通常在新建用户时指定,在 /etc/passwd 的第四列GID对应的组。
  • 附加组:在 /etc/group 的最后一列表示该组的用户列表,用于指定用户的附加权限。
    usermod可以用来设置用户的 主组/附加组 和 登陆 Shell ,命令格式如下:

<7>添加用户账号:useradd

在Unix/Linux中添加用户账号可以使用adduser或useradd命令,因为adduser命令是指向useradd命令的一个链接,因此,这两个命令的使用格式完全一样。
useradd命令的使用格式如下: useradd [选项及参数] 新建用户名

创建用户、设置密码、删除用户、确认用户信息

useradd示例

注意:

  • 创建用户时忘记加上-m的解决方法是:删除用户,重新创建(不必考虑设置权限问题)。
  • 创建用户时会默认创建一个和 用户名 同名的组。
  • 用户信息保存在/etc/passwd文件中。
  • 默认使用useradd添加的用户没有sudo权限,需要用命令sudo usermod -G sudo 用户名,将用户添加到sudo附加组中。
    usermod修改附加组示例

<8>设置用户密码:passwd

在Unix/Linux中,超级用户可以使用passwd命令为普通用户设置或修改用户口令。用户也可以直接使用该命令来修改自己的口令,而无需在命令后面使用用户名。

passwd示例

<9>删除用户:userdel

userdel命令用法

<补>查看用户UID和GID:id

命令格式:id 用户名

passwd文件说明

id示例

<10>修改文件权限:chmod

chmod 修改文件权限有两种使用格式:字母法与数字法。
字母法:chmod u/g/o/a +/-/= rwx 文件

ugoa

+-=

rwx

chmod o+w file 给文件file的其它用户增加写权限:

chmod u-r file 给文件file的拥有者减去读的权限:

chmod g=x file设置文件file的同组用户的权限为可执行,同时去除读、写权限:

数字法:“rwx” 这些权限也可以用数字来代替
chmod数字表示法

例如,chmod 777 file:所有用户拥有读、写、执行权限

注意要递归修改权限的话,需要加上-R。
chmod示例

以下是对修改文件的属主、属组、权限的总结:

<11>修改文件所有者:chown

命令格式:chown 用户名 文件名|目录名

<12>修改文件所属组:chgrp

命令格时:chgrp 组名 文件名|目录名

注意要递归修改的话,需要加上-R。

chown、chgrp示例

修改文件的命令总结

六、系统、远程管理的常用命令

<1>查看当前日历:cal

cal(calendar)命令用于查看当前日历,-y显示整年日历:

cal示例

<2>显示或设置时间:date

设置时间格式(需要管理员权限):date [MMDDhhmm[[CC]YY][.ss]] +format
CC为年前两位yy为年的后两位,前两位的MM为月,后两位的mm为分钟,dd为天,hh为小时,ss为秒。如: date 010203042016.55。
显示时间格式(date '+%y,%m,%d,%H,%M,%S'):

date示例

<3>查看进程信息:ps

进程是一个具有一定独立功能的程序,它是操作系统动态执行的基本单元。
ps(process status)命令可以查看进程的详细状况,常用选项(选项可以不加“-”)如下:

注意:ps默认只会显示当前用户通过终端启动的应用程序。
ps示例

<4>动态显示进程:top

top命令用来动态显示运行中的进程。top命令能够在运行后,在指定的时间间隔更新显示信息。可以在使用top命令时加上-d来指定显示信息更新的时间间隔。
在top命令执行后,可以按下按键得到对显示的结果进行排序:

top示例

<5>终止进程:kill

kill命令指定进程号的进程,需要配合 ps 使用。
使用格式:kill [-signal] pid
信号值从0到15,其中9为绝对终止,可以处理一般信号无法终止的进程。

kill示例

有些进程不能直接杀死,这时候我们需要加一个参数-9,“ -9 ” 代表强制结束。

<6>关机重启:reboot、shutdown、init

shutdown命令格式:shutdown -选项 时间
shutdown可以 安全关闭 或 重新启动系统
注意:

  • 当选项是-r时,表示重新启动。
  • 当选项是-c时,表示取消操作。
  • 不指定选项和参数时,默认一分钟后关闭电脑。
  • 远程维护服务器时,最好不要关闭系统,而应该重新启动系统。
    常用命令示例:

<7>检测磁盘空间:df

df(disk free)命令用于检测文件系统的磁盘空间占用和空余情况,可以显示所有文件系统对节点和磁盘块的使用情况。

df常用选项

df示例

<8>检测目录所占磁盘空间:du

du(disk usage)命令用于统计目录或文件所占磁盘空间的大小,该命令的执行结果与df类似,du更侧重于磁盘的使用状况。
du命令的使用格式如下: du [选项] 目录或文件名

du常用选项

du示例

<9>查看或配置网卡信息:ifconfig

如果,我们只是敲:ifconfig,它会显示所有网卡的信息:

可以通过管道快速查看IP地址:ifconfig | grep 'inet'。
提示:一台计算机中可能有一个物理网卡和多个虚拟网卡,在Linux中物理网卡的名字通常以ensXX表示。127.0.0.1被称为本地回环/环回地址,一般用来测试本机网卡是否正常。

<10>测试远程主机连通性:ping

ping一般用于检测当前计算机到目标计算机之间的网络是否通畅,数值越大,速度越慢。
ping的工作原理与潜水艇的声纳相似,它就是取自声纳的声音。
网络管理员之间也通常将ping作为动词——ping一下计算机x,看它是否还开着。

提示:在Linux中,要想终止一个终端程序的执行,绝大多数都可以使用ctrl c。

<补>SSH基础

通过ssh客户端可以连接到安装了ssh服务器的远程机器上。

ssh客户端是一种使用secure shell(SSH)协议连接到远程计算机的程序。
利用SSH协议,可以防止信息泄露,防止DNS欺骗和IP欺骗(加密),并提高传输速度(压缩)。

1)域名 和 端口号

域名:由一串用点分隔的名字组成,是IP地址的别名,方便用户记忆,例如www.baidu.com。
IP地址:通过IP地址找到网络上的计算机。
端口号:通过端口号找到计算机上运行的应用程序。

SSH服务器的默认端口号是22,如果是默认端口号,在连接时可以省略。

常见服务器端口号

2)SSH服务器的安装配置

安装ssh相关服务的客户端:openssh-client,服务端(Ubuntu自带):openssh-server,可以用命令:sudo apt-get install openssh-client openssh-server。

  • Ubuntu中其实只需安装SSH服务器:sudo apt-get install openssh-server,启动服务service sshd start,查看服务状态:service sshd status,设置有root权限的用户的登陆应该修改配置文件:vi /etc/ssh/sshd_config,如下:
1
2
3
4
5
6
> 复制代码# Authentication:
> LoginGraceTime 120
> PermitRootLogin yes
> StrictModes yes
>
>

然后要记得重启服务:service sshd restart,也可以sudo /etc/init.d/ssh stop , sudo /etc/init.d/ssh start。

可能需要:关闭Ubuntu的防火墙:

1
2
3
4
5
6
7
> > 复制代码sudo ufw disable #关闭防火墙
> >
> > sudo ufw enable #开启防火墙
> >
> > sudo ufw status #查看防火墙状态
> >
> >

还有可能的错误:/etc/passwd文件中用户的shell设置的不对。(我之前的错误在这里,下图是在Ubuntu中用service sshd status查看日志得到的)

  • Ubuntu中配置openssh-server开机自动启动:打开/etc/rc.local文件,在exit 0语句前加入:/etc/init.d/ssh start

3)SSH客户端的简单使用

ssh [-p port] user@remote命令中有三个要素:

  • user是远程机器上的用户名,如果不指定的话默认为当前用户。
  • remote是远程机器的地址,可以是 IP 或 域名 ,或者是后面会提到的 别名。
  • port是SSH Server监听的端口,如果不指定,就为默认值22。

提示:

  • 使用exit退出当前用户的登陆。
  • ssh这个终端命令只能在Unix或Linux中使用,在Windows中要安装客户端软件。

4)Windows下SSH客户端软件的安装和使用

提示:建议从官网下载。
PuTTy:www.chiark.greenend.org.uk/~sgtatham/p…
XShell:www.netsarang.com/download/ma…

PuTTy

XShell

5)SSH高级使用

有关ssh配置的信息都放在用户家目录下.ssh目录下。

免密码登陆:

配置别名:

每次都输入ssh -p port user@remote,时间久了会觉得很麻烦,特别是当user, remote和port都得输入,而且还不好记忆,而** 配置别名 **可以让我们进一步偷懒,譬如用:ssh mac来替代上面这么一长串,那么就在~/.ssh/config里面追加以下内容:

1
2
3
4
复制代码Host mac
HostName ip地址
User itheima
Port 22

保存之后,即可用ssh mac实现远程登录了,scp同样可以使用
提示:touch config 之后 gedit config或者vi config,然后就可以追加了。

<补>远程拷贝文件:scp

scp 就是 secure copy,是一个在Linux下来进行远程拷贝文件的命令。
它的地址格式与ssh基本相同,需要注意的是,在指定端口时用的是大写的-P。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码# 把本地当前目录下的 01.py 文件 复制到 远程 家目录下的 Desktop/01.py
# 注意:`:` 后面的路径如果不是绝对路径,则以用户的家目录作为参照路径
scp -P port 01.py user@remote:Desktop/01.py

# 把远程 家目录下的 Desktop/01.py 文件 复制到 本地当前目录下的 01.py
scp -P port user@remote:Desktop/01.py 01.py

# 加上 -r 选项可以传送文件夹
# 把当前目录下的 demo 文件夹 复制到 远程 家目录下的 Desktop
scp -r demo user@remote:Desktop

# 把远程 家目录下的 Desktop 复制到 当前目录下的 demo 文件夹
scp -r user@remote:Desktop demo

scp常用选项

注意:

  • scp这个终端命令只能在Unix或Linux系统中使用。
  • 如果在Windows系统中,可以安装PuTTy使用pscp命令行工具,或者安装FileZilla(filezilla-project.org/)使用FTP进行文件传…
    FileZilla
  • 除了用scp命令拷贝,还可以用ftp服务上传,安装ftp服务:sudo apt-get install vsftpd。
  • ftp服务的配置:sudo vi /etc/vsftpd.conf,在配置文件中查找并修改以下信息
1
2
3
4
5
6
7
8
9
10
> 复制代码anonymous_enable=NO #不允许匿名用户登陆
> local_enable=YES #允许本机登陆
> local_root=/home/handy/ftp #指定ftp上传下载目录,所有用户访问统一个目录
> ####一般使用FileZilla时只需要更改###
> write_enable=YES #允许上传文件到ftp服务器!!!
> ###################################
> chroot_list_enable=YES # 不允许跳出家目录
> chroot_list_file=/etc/vsftpd.chroot_list #允许vsftpd.chroot_list文件中的用户登陆ftp服务器
>
>

更改配置文件完成后,要重启服务:service vsftpd restart

测试上传功能,登陆ftp服务器:ftp IP
上传命令,可以把文件上传到ftp服务器:put somefile
下载命令,可以把ftp服务器上的文件下载到本地:get somefile
也可用图形界面的FileZilla或Xftp。

解决上传的中文乱码文件的删除、改名等操作
ls -i:显示文件索引节点号(inode)。一个索引节点代表一个文件;
find -inum 节点号 -delete:查找符合指定的inode编号的文件并删除,不能删除非空目录;
更名:find . -inum 节点号 -exec mv {} 新名字 \
删除:find ./ -inum 节点号 -exec rm -rf {} \
{}是占位符,find出来的,每一个文件,的意思。对于每一个文件,占座,等find出来后,放到对应的{}的位置。\是表示 -exec 命令终结的的符号。

七、Ubuntu软件安装的常用命令

<1>安装/卸载软件:apt

apt是advanced packaging tool的缩写,是Linux下的一款安装包管理工具,可以在终端中安装/卸载/更新软件包。

apt常用命令

提示:apt的安装命令其实不用记忆,在终端中如果没有这个命令,系统会提示安装。

<2>配置软件源

如果希望在Ubuntu中安装软件更加快速,可以通过设置镜像源,选择一个访问网速更快的服务器,来提供软件下载/安装服务。
提示:更换服务器之后,需要一个相对比较长时间的更新过程,需要耐心等待。更新完成后,再安装软件就会从新设置的服务器下载安装了。

所谓镜像源,就是所有服务器的内容是相同的(镜像),但是其所在位置不同,国内的服务器一般会快些。

软件和更新

八、vi编辑器的常用命令

<1> vi简介

在工作中,要对 服务器 上的文件进行 简单 的修改,可以使用 ssh 远程登录到服务器上,并且使用 vi 进行快速的编辑即可
常见需要修改的文件包括:源程序、配置文件,例如 ssh 的配置文件 ~/.ssh/config

在没有图形界面的环境下,要编辑文件,vi 是最佳选择!
每一个要使用 Linux 的程序员,都应该或多或少的学习一些 vi 的常用命令

在很多 Linux 发行版中,直接把 vi 做成 vim 的软连接

vi

vi 是 Visual interface 的简称,是 Linux 中 最经典 的文本编辑器
vi 的核心设计思想 —— 让程序员的手指始终保持在键盘的核心区域,就能完成所有的编辑操作

vi 的特点:没有图形界面 的 功能强大 的编辑器、只能是编辑 文本内容,不能对字体、段落进行排版、不支持鼠标操作、没有菜单、只有命令、vi 编辑器在 系统管理、服务器管理 编辑文件时,其功能永远不是图形界面的编辑器能比拟的。

vim

vim = vi improved
vim 是从 vi 发展出来的一个文本编辑器,支持 代码补全、编译 及 错误跳转 等方便编程的功能特别丰富,在程序员中被广泛使用,被称为 编辑器之神

<2> 打开和新建文件

1
复制代码$ vi 文件名
  • 如果文件已经存在,会直接打开该文件
  • 如果文件不存在,会新建一个文件

<3>打开文件并且定位行

在日常工作中,有可能会遇到 打开一个文件,并定位到指定行 的情况
例如:在开发时,知道某一行代码有错误,可以 快速定位 到出错代码的位置
,这个时候,可以使用以下命令打开文件

1
复制代码$ vi 文件名 +行数

提示:如果只带上 + 而不指定行号,会直接定位到文件末尾,如果不带+号,那么会直接定位到文件开头。

<4>异常处理

如果 vi 异常退出,在磁盘上可能会保存有 交换文件

下次再使用 vi 编辑该文件时,会看到以下屏幕信息,按下字母 d 可以 删除交换文件 即可,之前的异常退出涉及的修改消失。

提示:按下键盘时,注意输入法为英文状态。

<5> vi 的三种工作模式

vi 有三种基本工作模式:

  • 命令模式
  • 打开文件首先进入命令模式*,是使用 vi 的 入口
    通过 命令 对文件进行常规的编辑操作,例如:定位、翻页、复制、粘贴、删除……
    在其他图形编辑器下,通过 快捷键 或者 鼠标 实现的操作,都在 命令模式 下实现
  • 末行模式 —— 执行 保存、退出 等操作
    要退出 vi 返回到控制台,需要在末行模式下输入命令
    末行模式 是 vi 的 出口
  • 编辑模式 —— 正常的编辑文字

[图片上传失败…(image-44394c-1551450393896)]

提示:在 Touch Bar 的 Mac 电脑上 ,按 ESC 不方便,可以使用 CTRL + [ 替代

末行模式命令:

命令 英文 功能
w write 保存
q quit 退出,如果没有保存,不允许退出
q! quit 强行退出,不保存退出
wq write & quit 保存并退出
x 保存并退出

<6> 常用命令

命令学习线路图

  1. 重复命令多次
    • 在命令模式下,先输入一个数字,再跟上一个命令,可以让该命令 重复执行指定次数
  2. 移动和选择(多练)
    • vi 之所以快,关键在于 能够快速定位到要编辑的代码行
    • 移动命令 能够 和 编辑操作 命令 组合使用
  3. 编辑操作
    • 删除、复制、粘贴、替换、缩排
  4. 撤销和重复
  5. 查找替换

1. 移动(基本)

  • 要熟练使用 vi,首先应该学会怎么在 命令模式 下样快速移动光标
  • 编辑操作命令,能够和 移动命令 结合在一起使用
1) 上、下、左、右
命令 功能 手指
h 向左 食指
j 向下 食指
k 向上 中指
l 向右 无名指

移动光标

2) 行内移动
命令 英文 功能
w word 向后移动一个单词
b back 向前移动一个单词
0 行首
^ 行首,第一个不是空白字符的位置
$ 行尾
3) 行数移动
命令 英文 功能
gg go 文件顶部
G go 文件末尾
数字gg go 移动到 数字 对应行数
数字G go 移动到 数字 对应行数
:数字 移动到 数字 对应行数
4) 屏幕移动
命令 英文 功能
Ctrl + b back 向上翻页
Ctrl + f forward 向下翻页
H Head 屏幕顶部
M Middle 屏幕中间
L Low 屏幕底部

2. 移动(程序)

1) 段落移动
  • vi 中使用 空行 来区分段落
  • 在程序开发时,通常 一段功能相关的代码会写在一起 —— 之间没有空行
命令 功能
{ 上一段
} 下一段
2) 括号切换
  • 在程序世界中,()、[]、{} 使用频率很高,而且 都是成对出现的
命令 功能
% 括号匹配及切换
3) 标记
  • 在开发时,某一块代码可能需要稍后处理,例如:编辑、查看
  • 此时先使用 m 增加一个标记,这样可以 在需要时快速地跳转回来 或者 执行其他编辑操作
  • 标记名称 可以是 a~z 或者 A~Z 之间的任意 一个 字母
  • 添加了标记的 行如果被删除,标记同时被删除
  • 如果 在其他行添加了相同名称的标记,之前添加的标记也会被替换掉
命令 英文 功能
mx mark 添加标记 x,x 是 az 或者 AZ 之间的任意一个字母
‘x 直接定位到标记 x 所在位置

3. 选中文本(可视模式)

  • 在 vi 中要选择文本,需要先使用 Visual 命令切换到 可视模式
  • vi 中提供了 三种 可视模式,可以方便程序员选择 选中文本的方式
  • 按 ESC 可以放弃选中,返回到 命令模式
命令 模式 功能
v 可视模式 从光标位置开始按照正常模式选择文本
V 可视行模式 选中光标经过的完整行
Ctrl + v 可视块模式 垂直方向选中文本

注意:可视模式下,可以和 移动命令 连用,例如:ggVG 能够选中所有内容

4. 撤销和恢复撤销

  • 在学习编辑命令之前,先要知道怎样撤销之前一次 错误的 编辑动作!
命令 英文 功能
u undo 撤销上次命令
CTRL + r redo 恢复撤销的命令

5. 删除文本

命令 英文 功能
x cut 删除光标所在字符,或者选中文字
d(移动命令) delete 删除移动命令对应的内容
dd delete 删除光标所在行,可以 ndd 复制多行
D delete 删除至行尾

提示:如果使用 可视模式 已经选中了一段文本,那么无论使用 d 还是 x,都可以删除选中文本

  • 删除命令可以和 移动命令 连用,以下是常见的组合命令:
    dw # 从光标位置删除到单词末尾
    d0 # 从光标位置删除到一行的起始位置
    d} # 从光标位置删除到段落结尾
    ndd # 从光标位置向下连续删除 n 行
    d代码行G # 从光标所在行 删除到 指定代码行 之间的所有代码
    d’a # 从光标所在行 删除到 标记a 之间的所有代码

提示:可使用:set nu和:set nonu设置行号的显示与否。

6. 复制、粘贴

  • vi 中提供有一个 被复制文本的缓冲区
    • 复制 命令会将选中的文字保存在缓冲区
    • 删除 命令删除的文字会被保存在缓冲区
    • 在需要的位置,使用 粘贴 命令可以将缓冲区的文字插入到光标所在位置
命令 英文 功能
y(移动命令) copy 复制
yy copy 复制一行,可以 nyy 复制多行
p paste 粘贴

提示

  • 命令 d、x 类似于图形界面的 剪切操作 —— CTRL + X
  • 命令 y 类似于图形界面的 复制操作 —— CTRL + C
  • 命令 p 类似于图形界面的 粘贴操作 —— CTRL + V
  • vi 中的 文本缓冲区同样只有一个,如果后续做过 复制、剪切 操作,之前缓冲区中的内容会被替换

注意

  • vi 中的 文本缓冲区 和系统的 剪贴板 不是同一个
  • 所以在其他软件中使用 CTRL + C 复制的内容,不能在 vi 中通过 P 命令粘贴
  • 可以在 编辑模式 下使用 鼠标右键粘贴

7. 替换

命令 英文 功能 工作模式
r replace 替换当前字符 命令模式
R replace 替换当前行光标后的字符 替换模式
  • R 命令可以进入 替换模式,替换完成后,按下 ESC 可以回到 命令模式
  • 替换命令 的作用就是不用进入 编辑模式,对文件进行 轻量级的修改

8. 缩排和重复执行

命令 功能
>> 向右增加缩进
<< 向左减少缩进
. 重复上次命令
  • 缩排命令 在开发程序时,统一增加代码的缩进 比较有用!
    • 一次性 在选中代码前增加 4 个空格,就叫做 增加缩进
    • 一次性 在选中代码前删除 4 个空格,就叫做 减少缩进
  • 在 可视模式 下,缩排命令只需要使用 一个 > 或者 <

在程序中,缩进 通常用来表示代码的归属关系

  • 前面空格越少,代码的级别越高
  • 前面空格越多,代码的级别越低

9. 查找

常规查找

命令 功能
/str 查找 str
  • 查找到指定内容之后,使用 Next 查找下一个出现的位置:
    • n: 查找下一个
    • N: 查找上一个
  • 如果不想看到高亮显示,可以随便查找一个文件中不存在的内容即可
单词快速匹配
命令 功能
* 向后查找当前光标所在单词
# 向前查找当前光标所在单词
  • 在开发中,通过单词快速匹配,可以快速看到这个单词在其他什么位置使用过

10. 查找并替换

  • 在 vi 中查找和替换命令需要在 末行模式 下执行
  • 记忆命令格式:
1
复制代码:%s///g
1) 全局替换
  • 一次性替换文件中的 所有出现的旧文本
  • 命令格式如下:
1
复制代码:%s/旧文本/新文本/g
2) 可视区域替换
  • 先选中 要替换文字的 范围
  • 命令格式如下:
1
复制代码:s/旧文本/新文本/g
3) 确认替换
  • 如果把末尾的 g 改成 gc 在替换的时候,会有提示!推荐使用!

c表示conform。

1
复制代码:%s/旧文本/新文本/gc
  1. y - yes 替换
  2. n - no 不替换
  3. a - all 替换所有
  4. q - quit 退出替换
  5. l - last 最后一个,并把光标移动到行首
  6. ^E 向下滚屏
  7. ^Y 向上滚屏

11. 插入命令

  • 在 vi 中除了常用的 i 进入 编辑模式 外,还提供了以下命令同样可以进入编辑模式:
命令 英文 功能 常用
i insert 在当前字符前插入文本 常用
I insert 在行首插入文本 较常用
a append 在当前字符后添加文本
A append 在行末添加文本 较常用
o 在当前行后面插入一空行 常用
O 在当前行前面插入一空行 常用

[图片上传失败…(image-d3e85b-1551450393896)]

要快速打出大写字母,使用:shift 字母。

演练 1 —— 编辑命令和数字连用
  • 在开发中,可能会遇到连续输入 N 个同样的字符

在 Python 中有简单的方法,但是其他语言中通常需要自己输入

  • 例如:********** 连续 10 个星号

要实现这个效果可以在 命令模式 下

  1. 输入 10,表示要重复 10 次
  2. 输入 i 进入 编辑模式
  3. 输入 * 也就是重复的文字
  4. 按下 ESC 返回到 命令模式,返回之后 vi 就会把第 2、3 两步的操作重复 10 次

提示:正常开发时,在 进入编辑模式之前,不要按数字

演练 2 —— 利用 可视块 给多行代码增加注释
  • 在开发中,可能会遇到一次性给多行代码 增加注释 的情况

在 Python 中,要给代码增加注释,可以在代码前增加一个 #

要实现这个效果可以在 命令模式 下

  1. 移动到要添加注释的 第 1 行代码,按 ^ 来到行首
  2. 按 CTRL + v 进入 可视块 模式
  3. 使用 j 向下连续选中要添加的代码行
  4. 输入 I 进入 编辑模式,并在 行首插入,注意:一定要使用 I
  5. 输入 # 也就是注释符号
  6. 按下 ESC 返回到 命令模式,返回之后 vi 会在之前选中的每一行代码 前 插入 #

12. 分屏命令

  • 属于 vi 的高级命令 —— 可以 同时编辑和查看多个文件
末行命令扩展

末行命令 主要是针对文件进行操作的:保存、退出、保存&退出、搜索&替换、另存、新建、浏览文件

命令 英文 功能
:e . edit 会打开内置的文件浏览器,浏览要当前目录下的文件
:n 文件名 new 新建文件
:w 文件名 write 另存为,但是仍然编辑当前文件,并不会切换文件

提示:切换文件之前,必须保证当前这个文件已经被保存!

  • 已经学习过的 末行命令:
命令 英文 功能
:w write 保存
:q quit 退出,如果没有保存,不允许退出
:q! quit 强行退出,不保存退出
:wq write & quit 保存并退出
:x 保存并退出
:%s///gc 确认搜索并替换

在实际开发中,可以使用 w 命令 阶段性的备份代码

分屏命令
  • 使用 分屏命令,可以 同时编辑和查看多个文件
命令 英文 功能
:sp [文件名] split 横向增加分屏
:vsp [文件名] vertical split 纵向增加分屏
1) 切换分屏窗口

分屏窗口都是基于 CTRL + W 这个快捷键的,w 对应的英文单词是 window

命令 英文 功能
w window 切换到下一个窗口
r reverse 互换窗口
c close 关闭当前窗口,但是不能关闭最后一个窗口
q quit 退出当前窗口,如果是最后一个窗口,则关闭 vi
o other 关闭其他窗口
2) 调整窗口大小

分屏窗口都是基于 CTRL + W 这个快捷键的,w 对应的英文单词是 window

命令 英文 功能
+ 增加窗口高度
- 减少窗口高度
> 增加窗口宽度
< 减少窗口宽度
= 等分窗口大小

调整窗口宽高的命令可以和数字连用,例如:5 CTRL + W + 连续 5 次增加高度

13. 常用命令速查图

vimrc
  • vimrc 是 vim 的配置文件,可以设置 vim 的配置,包括:热键、配色、语法高亮、插件 等
  • Linux 中 vimrc 有两个位置,家目录下的配置文件优先级更高
1
2
复制代码/etc/vim/vimrc
~/.vimrc
  • 常用的插件有:
    • 代码补全
    • 代码折叠
    • 搜索
    • Git 集成
    • ……
  • 网上有很多高手已经配置好的针对 python 开发的 vimrc 文件,可以下载过来直接使用,或者等大家多 Linux 比较熟悉后,再行学习!

Tips:

终端中的字体大小更改:放大是ctrl shift +,缩小是ctrl -。
终端中退出某个程序:往往是q,可能是ctrl c或是ctrl d。
以新标签页的形式打开一个终端:ctrl alt T。

全文思维导图

Linux 基础

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一个HashMap能跟面试官扯上半个小时

发表于 2020-03-15

一个HashMap能跟面试官扯上半个小时

关注 安琪拉的博客
1.回复面试领取面试资料
2.回复书籍领取技术电子书
3.回复交流进技术沟通群

前言

HashMap应该算是Java后端工程师面试的必问题,因为其中的知识点太多,很适合用来考察面试者的Java基础。

开场

面试官: 你先自我介绍一下吧!

安琪拉: 我是安琪拉,草丛三婊之一,最强中单(钟馗不服)!哦,不对,串场了,我是**,目前在–公司做–系统开发。

面试官: 看你简历上写熟悉Java集合,HashMap用过的吧?

安琪拉: 用过的。(还是熟悉的味道)

面试官: 那你跟我讲讲HashMap的内部数据结构?

安琪拉: 目前我用的是JDK1.8版本的,内部使用数组 + 链表红黑树;

安琪拉: 方便我给您画个数据结构图吧:

面试官: 那你清楚HashMap的数据插入原理吗?

安琪拉: 呃[做沉思状]。我觉得还是应该画个图比较清楚,如下:

  1. 判断数组是否为空,为空进行初始化;
  2. 不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
  3. 查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
  4. 存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false);
  5. 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;
  6. 如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8, 大于的话链表转换为红黑树;
  7. 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。

面试官: 刚才你提到HashMap的初始化,那HashMap怎么设定初始容量大小的吗?

安琪拉: [这也算问题??] 一般如果new HashMap() 不传值,默认大小是16,负载因子是0.75, 如果自己传入初始大小k,初始化大小为 大于k的 2的整数次方,例如如果传10,大小为16。(补充说明:实现代码如下)

1
2
3
4
5
6
7
8
9
复制代码static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

补充说明:下图是详细过程,算法就是让初始二进制右移1,2,4,8,16位,分别与自己异或,把高位第一个为1的数通过不断右移,把高位为1的后几位全变为1,111111 + 1 = 1000000 = 2^6 (符合大于50并且是2的整数次幂 )

补充说明

面试官: 你提到hash函数,你知道HashMap的哈希函数怎么设计的吗?

安琪拉: [问的还挺细] hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。

面试官: 那你知道为什么这么设计吗?

安琪拉: [这也要问],这个也叫扰动函数,这么设计有二点原因:

  1. 一定要尽可能降低hash碰撞,越分散越好;
  2. 算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;

面试官: 为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode?

[这问题有点刁钻], 安琪拉差点原地💥了,恨不得出biubiubiu 二一三连招。

安琪拉: 因为key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。你想,如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。(来自知乎-胖君)

源码中模运算就是把散列值和数组长度-1做一个”与”操作,位运算比%运算要快。

1
2
3
4
5
复制代码bucketIndex = indexFor(hash, table.length);

static int indexFor(int h, int length) {
return h & (length-1);
}

顺便说一下,这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。

1
2
3
4
复制代码  10100101 11000100 00100101
& 00000000 00000000 00001111
----------------------------------
00000000 00000000 00000101 //高位全部归零,只保留末四位

但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,如果正好让最后几个低位呈现规律性重复,就无比蛋疼。

时候“扰动函数”的价值就体现出来了,说到这里大家应该猜出来了。看下面这个图,

img

右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

最后我们来看一下Peter Lawley的一篇专栏文章《An introduction to optimising a hashing strategy》里的的一个实验:他随机选取了352个字符串,在他们散列值完全没有冲突的前提下,对它们做低位掩码,取数组下标。

img

结果显示,当HashMap数组长度为512的时候(2^9),也就是用掩码取低9位的时候,在没有扰动函数的情况下,发生了103次碰撞,接近30%。而在使用了扰动函数之后只有92次碰撞。碰撞减少了将近10%。看来扰动函数确实还是有功效的。

另外Java1.8相比1.7做了调整,1.7做了四次移位和四次异或,但明显Java 8觉得扰动做一次就够了,做4次的话,多了可能边际效用也不大,所谓为了效率考虑就改成一次了。

下面是1.7的hash代码:

1
2
3
4
复制代码static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

面试官: 看来做过功课,有点料啊!是不是偷偷看了安琪拉的博客, 你刚刚说到1.8对hash函数做了优化,1.8还有别的优化吗?

安琪拉: 1.8还有三点主要的优化:

  1. 数组+链表改成了数组+链表或红黑树;
  2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;
  3. 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
  4. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;

面试官: 你分别跟我讲讲为什么要做这几点优化;

安琪拉: 【咳咳,果然是连环炮】

  1. 1.8使用红黑树:防止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn);
  2. 1.8使用尾插法:因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;

A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环,如下图所示:

在这里插入图片描述

1.7的扩容调用transfer代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; //A线程如果执行到这一行挂起,B线程开始进行扩容
newTable[i] = e;
e = next;
}
}
}
  1. 扩容的时候为什么1.8 不用重新hash就可以直接定位原节点在新数据的位置呢?

这是由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1,怎么理解呢?

扩容前长度为16,用于计算(n-1) & hash 的二进制n-1为0000 1111,扩容为32后的二进制就高位多了1,为0001 1111。

因为是& 运算,1和任何数 & 都是它本身,那就分二种情况,如下图:原数据hashcode高位第4位为0和高位为1的情况;

第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)

在这里插入图片描述

面试官: 那HashMap是线程安全的吗?

安琪拉: 不是,在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题,以1.8为例,看👇的代码,当A线程判断index位置为空后正好挂起,B线程开始往index位置的写入节点数据,这时A线程恢复现场,执行赋值操作,就把A线程的数据给覆盖了;还有++size这个地方也会造成多线程同时扩容等问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
复制代码final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //多线程执行到这里
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) // 多个线程走到这,可能重复resize()
resize();
afterNodeInsertion(evict);
return null;
}

面试官: 那你平常怎么解决这个线程不安全的问题?

安琪拉: Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map。

HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,锁粒度比较大,Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法通过对象锁实现线程安全;ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。

面试官: 那你知道ConcurrentHashMap的分段锁的实现原理吗?

安琪拉: 【天啦撸! 俄罗斯套娃,一个套一个】ConcurrentHashMap成员变量使用volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现赋值操作,多线程操作只会锁住当前操作索引的节点。

如下图,线程A锁住A节点所在链表,线程B锁住B节点所在链表,操作互不干涉。

面试官: 你前面提到链表转红黑树是链表长度达到阈值,这个阈值是多少?

安琪拉: 阈值是8,红黑树转链表阈值为6

面试官: 为什么是8,不是16,32甚至是7 ?又为什么红黑树转链表的阈值是6,不是8了呢?

安琪拉: 【你去问作者啊!天啦撸,biubiubiu 真想213连招】因为作者就这么设计的,哦,不对,因为经过计算,在hash函数设计合理的情况下,发生hash碰撞8次的几率为百万分之6,概率说话。。因为8够用了,至于为什么转回来是6,因为如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的互相转化,为了预防这种情况的发生,设置为6。

面试官: HashMap内部节点是有序的吗?

安琪拉: 是无序的,根据hash值随机插入

面试官: 那有没有有序的Map?

安琪拉: LinkedHashMap 和 TreeMap

面试官: 跟我讲讲LinkedHashMap怎么实现有序的?

安琪拉: LinkedHashMap内部维护了一个单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和 after用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;

/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
//链接新加入的p节点到链表后端
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
//LinkedHashMap的节点类
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public static void main(String[] args) {
Map<String, String> map = new LinkedHashMap<String, String>();
map.put("1", "安琪拉");
map.put("2", "的");
map.put("3", "博客");

for(Map.Entry<String,String> item: map.entrySet()){
System.out.println(item.getKey() + ":" + item.getValue());
}
}
//console输出
1:安琪拉
2:的
3:博客

面试官: 跟我讲讲TreeMap怎么实现有序的?

安琪拉:TreeMap是按照Key的自然顺序或者实现的Comprator接口的比较函数的顺序进行排序,内部是通过红黑树来实现。所以要么key所属的类实现Comparable接口,或者自定义一个实现了Comparator接口的比较器,传给TreeMap用于key的比较。

面试官: 前面提到通过CAS 和 synchronized结合实现锁粒度的降低,你能给我讲讲CAS 的实现以及synchronized的实现原理吗?

安琪拉: 下一期咋们再约时间,OK?

面试官: 好吧,回去等通知吧!


参考资料

  1. An introduction to optimising a hashing strategy
  2. JDK 源码中 HashMap 的 hash 方法原理是什么?
  3. 淡腾的枫-HashMap中的hash函数

后续会更新一些大厂高频面试题,主要是Java后端的。也可以公众号后台回复关键字面试提前领取面试的资料。

1.回复面试领取面试资料

2.回复书籍领取技术电子书

3.回复交流进技术交流群

关注

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用koa+mongodb构建的仿知乎接口(一)

发表于 2020-03-14

之前一直使用koa和express构建过一些小的应用,但是都是没有放到线上去跑。这回,我的想法是把自己那台学生服务器拿来充分利用一下,话不多说,直接直奔主题吧。

使用的技术栈:

  • nodejs
  • koa2(网络编程框架)
  • mongodb(非关系型数据库)
  • jwt(用于鉴权)
  • pm2(用于跑启动脚本)

何为REST?何为restful api?

表现层状态转换(英语:Representational State Transfer,缩写:REST)是Roy Thomas Fielding博士于2000年在他的博士论文中提出来的一种万维网软件架构风格

restful api:则是符合REST风格的api

koa 洋葱模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码app.use(async (ctx, next) => {
console.log(1)
await next()
ctx.body = '1'
})

app.use(async (ctx, next) => {
console.log(2)
await next()
console.log(3)
})

app.use(async (ctx, next) => {
console.log(4)
})

// 1 2 4 3

如何在写一个koa中间件

1
2
3
4
5
6
7
8
9
10
11
12
复制代码const auth = async (ctx,next) => {
try {
const {authorization=''} = ctx.request.header
const token = authorization.replace('Bearer ', '')
console.log('token',token)
const user = jsonwebtoken.verify(token, scret)
ctx.state.user = user
} catch (error) {
ctx.throw(401, error.message)
}
await next()
}

开始搭建目录结构

  1. 起一个简单的服务
1
2
3
4
5
6
7
复制代码const Koa = require('koa')
const app = new Koa()

const port = 3000 || process.env.port
app.listen(port, () => {
console.log(`App is listen on ${port}`)
})
  1. 搭建路由,编写自动读取路由中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码// routing
const fs = require('fs')

module.exports = app => {
fs.readdirSync(__dirname).forEach(file=> {
if (file === 'index.js') {return}
const router = require(`./${file}`)
app.use(router.routes()).use(router.allowedMethods())
})
}

const routing = require('./routes/index')
routing(app)
  1. 解决post请求ctx.request.body为undefined问题
1
2
复制代码const KoaBodyPareser = require('koa-bodyparser')
app.use(KoaBodyPareser())
  1. 连接数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码const mongoose = require('mongoose')
const connectionStr = require('./config').connectionStr

// 连接数据库
mongoose.connect(connectionStr, {
useNewUrlParser: true,
useUnifiedTopology: true
}, () => {
console.log('数据库连接成功')
})
.catch(err => {
console.log(err)
})

mongoose.connection.on(error, console.error)
  1. 错误处理
1
2
3
4
复制代码const error = require('koa-json-error')
app.use(error({
postFormat: ({stack, ...rest}) => process.env.NODE_ENV === 'production' ? rest : {stack, ...rest}
}))
  1. 参数格式校验
1
2
复制代码app.use(parameter(app))
const parameter = require('koa-parameter')

实现用户接口的增删改查

  1. 定义用户的数据层model
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码const mongoose = require('mongoose')
const {Schema, model} = mongoose

const UserSchema = new Schema({
name: {
type: String,
required: true
},
password:{
type: String,
required: true,
select: false
},
__v: {
type: Number,
select: false
}
})

module.exports = model('User', UserSchema)
  1. 用户的路由层router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码const KoaRouter = require('koa-router')
const jwt = require('koa-jwt')

const router = new KoaRouter({prefix: '/users'})
const {find, findById, create, update, del, login, checkOwer} = require('../controller/users')
const secret = require('../config').secret

const auth = jwt({secret})

// 获取所有用户
router.get('/', find)
// 新建用户
router.post('/', create)
// 获取特定用户
router.get('/:id', findById)
// 更新用户
router.patch('/:id',auth, checkOwer, update)
// 删除用户
router.delete('/:id',auth, checkOwer, del)
// 登录
router.post('/login', login)

module.exports = router
  1. 用户的控制器controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
复制代码const jsonwebtoken = require('jsonwebtoken')
const User = require('../models/users')
const secret = require('../config').secret

class UsersCtl {
async find(ctx) {
ctx.body = await User.find()
}
async findById(ctx) {
const user = await User.findById(ctx.params.id)
if (!user) {ctx.throw(404, '用户不存在')}
ctx.body = user
}
async create(ctx) {
ctx.status = 200
ctx.verifyParams({
name: {type: 'string', required: true},
password: {type: 'string', required: true}
})
const {name} = ctx.request.body
const repeatUser = await User.findOne({name})
if (repeatUser) { ctx.throw(409, '用户已被占用') }
const user = await User(ctx.request.body).save()
ctx.body = user
}
async update(ctx) {
ctx.verifyParams({
name: {type: 'string', required: false},
password: {type: 'string', required: false}
})
const user = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
if (!user) {ctx.throw(404, '用户不存在')}
ctx.body = user
}
async del(ctx) {
const user = await User.findByIdAndRemove(ctx.params.id)
if (!user) {ctx.throw(404, '用户不存在')}
ctx.body = user
ctx.status = 204
}
async login(ctx) {
ctx.verifyParams({
name: {type: 'string', required: true},
password: {type: 'string', required: true}
})
const user = await User.findOne(ctx.request.body)
if(!user) {ctx.throw(401, '用户名或者密码不正确')}
const {_id, name} = user
const token = await jsonwebtoken.sign({_id, name}, secret, {expiresIn: '1d'})
ctx.body = {
token
}
}
async checkOwer(ctx, next) {
if (ctx.params.id !== ctx.state.user._id) {
ctx.throw(403, '用户没有权限')
}
await next()
}
}

module.exports = new UsersCtl()

postman的使用

  1. 新建collection
  2. 在collection中新建request
  3. 在登录接口中的Test中设置token为全局变量

1
2
复制代码let jsonData = pm.response.json()
pm.globals.set("token", jsonData.token);
  1. 在其他需要验证的接口中Authization使用token

这就是项目大构建和用户接口的实现了,好记性不如烂笔头,特此总结, 下次将是图片上传几款的实践。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…827828829…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%