kotlin基础

基础入门

声明变量
1
2
3
val a :String = "hello"

val b = 2
  1. val:只读; var:可读写

  2. 自动类型推导

Long标记

必须大写L

1
val a = 222L
类型转换

int转long不能直接转(跟java不同)

1
2
3
val a = 2
val b :Long = a // error
val b :Long = a.toLong() //ok
无符号类型

java没有

UInt ,UShort,ULong

1
2
3
val a = 1u

val b = 10000000000u
String
1
2
3
4
5
6
7
8
9
10
val content = "hello kotlin"
val html = """
<html>
<title>
</title>
<body>
<div> $content </div>
</body>
</html>
""".trimIndent()
  • Kotlin 中 “==”比较的是内容,“===” 比较的是引用
  • 字符串拼接可以使用“$”
  • 格式化string,如上所示,trimIndent()方法的作用是去掉公共的缩进
数组
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
/**
* 定义
*/
val a = intArrayOf(1, 2)
val b = IntArray(2) { it + 1 }
print(b.contentToString())
print(a.size)

val c = arrayOf("hello", "kotlin")
c[1] = "world"
println("${c[0]},${c[1]}")

/**
* 遍历
*/
val d = floatArrayOf(1f, 2f)
for (item in d) {
print(item)
}

d.forEach { item ->
print(item)
}

//带下标
for (i in a.indices){
print("$i ,${a[i]}")
}

/**
* 判断元素是否存在
*/
if (1f in d) {
print("exist")
}

if (3f !in d) {
print("not exist")
}
区间
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
/**
* 定义
*/
val a = 1..10 //[1,10]

val b = 1 until 10 //[1,10)

val c = 10 downTo 1 //[10,1]

val d = 1..10 step 2 //[1,3,5,6,9]

val e = 1.0..2.0 //连续的,DoubleRange

print(a.joinToString())

/**
* 判断是否存在
*/
if (1.1 in e) {
print("exist")
}

/**
* 遍历
*/
for (i in a) {
print(i)
}
集合框架
  • 增加了不可变的集合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 定义
    */
    val listOf = listOf(1, 23) //不可变
    val mutableListOf = mutableListOf<Int>()

    val mapOf = mapOf("name" to "wzq", "age" to 18)
    val mutableMapOf = mutableMapOf("name" to "wzq", "age" to 18)

    /**
    * 添加
    */
    mutableListOf.add(3)
    mutableListOf += 4
    mutableListOf -= 4

    /**
    * 访问
    */
    listOf[0]

    mapOf["name"]
    mapOf.get("age")
  • 增加了一些语法糖 filter,map等

  • 基于Java,类型别名typealias,给java中的java.util.ArrayList在kotlin中起了一个别名ArrayList

    1
    2
    3
    4
    5
    typealias ArrayList<E> = java.util.ArrayList<E>
    typealias LinkedHashMap<K, V> = java.util.LinkedHashMap<K, V>
    typealias HashMap<K, V> = java.util.HashMap<K, V>
    typealias LinkedHashSet<E> = java.util.LinkedHashSet<E>
    typealias HashSet<E> = java.util.HashSet<E>
  • 新类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /**
    * 新类型pair
    */
    val pair = "key" to "value"
    val pair1 = Pair("key", "value")

    //读取内容
    pair.first
    pair.second
    val (x, y) = pair //解构表达式


    /**
    * 新类型triple
    */
    val tt = Triple(1, 2, 3)
    val (m, n, z) = tt //解构表达式
    tt.first
    tt.second
    tt.third
函数基本概念

kotlin的函数有自己的类型,所以是“一等公民”。可以赋值,传递并在合适的条件下调用。

  • 方法跟函数(概念性)

  • 定义在类中的函数是方法

  • 函数类型,函数引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 类型 ()->Unit
    // 引用 ::foo
    fun foo(){}

    // (String)->Int
    // ::foo
    fun foo(str:String):Int{}

    class Foo{
    // Foo.(String)->Long (Foo,String)->Long
    // Foo::bar
    fun bar(str: String):Long{}
    }

    val f: ()->Unit = ::foo
    val h: (Foo,String)->Long = Foo::bar

    val foo = Foo()
    val a: (String)->Long = foo::bar

    //通过引用调用
    a.invoke("wzq")
    //或者
    a("wzq")
  • 参数:变长,默认,具名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 变长参数
    */
    fun mutiParams(vararg mutiParam: Int): Int {
    return mutiParam.sum()
    }

    /**
    * 默认参数
    */
    fun defaltParams(name: String, type: Int = 1) {
    print("$name,$type")
    }

    @Test
    fun test() {
    defaltParams("wzq")
    /**
    * 具名参数
    */
    defaltParams(name = "zpy", type = 2)
    print(mutiParams(1, 2))
    }
  • 多返回值(借助Pair,Triple)

类和接口
  1. kotlin中默认都是public的

  2. kotlin中类的成员变量必须初始化(在构造器中完成初始化,或者定义的时候直接赋值)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //默认是public的  
    class SimpleClass {
    /**
    * 这里不用初始化是因为在构造器中完成了
    **/
    private var x: Int
    /**
    * 这里的y必须初始化,kotlin强调类必须对自己的成员变量负责。
    **/
    private var y: Int = 0
    /**
    * 构造器方式(副构造器)
    **/
    constructor(x: Int) {
    this.x = x
    }
    }
  3. 构造器,分为主构造器跟副构造器。新建类实例的路径必须经过主构造器(如果有的话)

    以上类从副构造器转为主构造器如下:

    1
    2
    3
    4
    5
    6
    /**
    * 主构造器
    **/
    class SimpleClass(private var x: Int) {
    private var y: Int = 0
    }
  4. 在kotlin定义一个属性 = get方法+set方法+属相

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class SimpleClass(x: Int) {
    var x = x
    /**
    * 以下默认生成,需要自定义的话可以改成自己的逻辑
    */
    get() {
    return field //backing field
    }
    set(value) {
    field = value
    }
    }

    fun test() {
    val simpleClass = SimpleClass(1)

    /**
    * 属相引用
    */
    val kProperty1 = simpleClass::x
    kProperty1.set(2)
    print(kProperty1.get())
    }
  5. 类中的方法默认都是final的,如果子类想要重写,那么需要在类的方法前面加open关键字

  6. 接口中可以定义属性

  7. 继承类跟接口使用“:”,类的话需要带主构造,具体如下:

    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
    abstract class AbstractClass {
    abstract fun xxx()
    open fun canReWrite() {}

    /**
    * 没有open子类不能重写
    */
    fun cannotReWrite() {}
    }

    interface Interface {
    fun yyy()

    /**
    * 接口可以定义属相
    */
    val y: Int
    }


    class SimpleClass(private val x: Int) : AbstractClass(), Interface {
    /**
    * 实现接口定义的属相y
    */
    override val y: Int
    get() = TODO("Not yet implemented")

    override fun yyy() {
    TODO("Not yet implemented")
    }

    override fun xxx() {
    TODO("Not yet implemented")
    }

    override fun canReWrite() {
    super.canReWrite()
    print("xxx")
    }
    }
空安全类型&平台类型
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
fun test() {
/**
* a不可为null
*/
var a: String = ""
/**
* b可以为null
*/
var b: String? = ""
/**
* 报错
*/
b.length
/**
* 安全访问 ?.
* ?: 如果b为null那么b?.length 返回null,使用?:可以设置默认值
*/
val length = b?.length ?: 0
/**
* 强转 !! 如果确认b不为null,那么可以强转
*/
b!!.length

/**
* 平台类型 String! :调用java中的方法返回java的String时,类型为String!,这时需要我们自己去判断是否 为空
* 我们无法自己创建
*
* 如下返回的类型是 TimeZone!
*/
val default = TimeZone.getDefault()
}
扩展方法&属相

kotlin支持在不修改类代码的情况下,动态为类添加属性(扩展属性)和方法(扩展方法)

  • 扩展方法定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class StringUtils {
    var name: String = "StringUtils"
    }

    fun StringUtils.CustomGetLenth(): Int {
    return this.name.length
    }

    fun main(args: Array<String>) {
    val lenth = StringUtils().CustomGetLenth()
    }
  • 扩展方法本质

    1
    2
    3
    4
    5
    6
    7
    public final class StringUtilsKt
    {
    public static final int CustomGetLenth(@NotNull StringUtils $receiver)
    {
    Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");return $receiver.getName().length();
    }
    }

    如果父类和子类都扩展了同名的一个扩展方法,引用类型均为父类的情况下,会调用父类的扩展方法。

  • 扩展属相没有backing field,(接口中的属相也没有)

  • 参考:https://www.cnblogs.com/nicolas2019/p/10932131.html

智能类型转换
  • a as B

  • a as? B (失败返回null)

  • a is B

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    open class Parent

    class Son(val name: String) : Parent() {

    }

    fun test() {
    val x: Parent = Son("wzq")
    /**
    * x is Son 之后,如果返回true,那么x就已经强转完成了
    */
    if (x is Son) {
    print(x.name)
    }
    }
if,when,try catch 是表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var b: Boolean = false

var a = if (b) 1 else 2

var c = when (b) {
true -> 1
false -> 2
}

var d = when {
b -> 1
else -> 2
}

var e = try {
//xxx
1
} catch (e: Exception) {
2
}
LAMBDA 表达式
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
fun name() {
print("xxx")
}

/**
* 匿名函数(去掉名字)
*/
val func = fun() {
print("xxx")
}

/**
* lambda是一种特殊的匿名函数
* () -> Unit
*/
val func1 = {
print("xxx")
}

/**
* (Int,String) -> String
*/
val func2 = { p: Int, p1: String ->
print("$p1,$p")
p1
}

/**
* 如果只有一个参数的话用it就行
*/
val func3: (Int) -> Unit = {
print("$it")
}

fun main() {
/**
* IntArray接收一个lambda表达式(只有一个参数)用来初始化数组
*/
IntArray(5, { it })
/**
* 另一种格式,简化版
*/
IntArray(5) { it }
}
== & hashcode

版本1:

1
2
3
4
5
6
7
8
9
10
11
12
class Person(var name: String, var age: Int)

fun main() {
val hashSet = HashSet<Person>()
(0..5).forEach {
hashSet += Person("wzq", 18)
}
/**
* 输出6
*/
print(hashSet.size)
}

版本2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person(var name: String, var age: Int) {
override fun equals(other: Any?): Boolean {
val person = other as? Person ?: return false
return person.age == age && person.name == name
}

override fun hashCode(): Int {
return age + name.hashCode()
}
}

fun main() {
val hashSet = HashSet<Person>()
(0..5).forEach {
hashSet += Person("wzq", 18)
}
/**
* 输出1
*/
print(hashSet.size)
}

版本3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val hashSet = HashSet<Person>()
val person = Person("wzq", 18)
hashSet += person
/**
* 输出1
*/
print(hashSet.size)
person.name = "zpy"
hashSet -= person
/**
* 输出1
*/
print(hashSet.size)
}

把Person的name跟age改为 val之后可以避免这种错误,因为val不允许修改

版本4:

1
2
3
4
5
6
7
8
9
10
class Person(val name: String, val age: Int) {
override fun equals(other: Any?): Boolean {
val person = other as? Person ?: return false
return person.age == age && person.name == name
}

override fun hashCode(): Int {
return age + name.hashCode()
}
}

函数进阶

高阶函数
  • 概念:参数是函数或者返回值是函数的函数

  • 例子:

    1
    2
    // IntArray的构造方法
    public inline constructor(size: Int, init: (Int) -> Int)
内联函数
  • 概念:在程序编译时能将程序中内联函数的调用表达式直接替换成内联函数的函数体。

  • 例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    fun doSomething(m: () -> Unit) {
    print("doSomething start")
    m()
    print("doSomething end")
    }


    fun main() {
    print("main start")
    doSomething { print("lambda") }
    print("main end")
    }

    转为java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static final void main() {
    String var0 = "main start";
    System.out.print(var0);
    /**
    * 1. 调用了doSomething方法,2. new了一个Function0的实例
    **/
    doSomething((Function0)null.INSTANCE);
    var0 = "main end";
    System.out.print(var0);
    }

    doSomething添加inline之后转为java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static final void main() {
    String var0 = "main start";
    System.out.print(var0);
    // 第一次内联:少了doSomething的调用
    String var7 = "doSomething start";
    System.out.print(var7);
    // 第二次内联,少了lambda函数的调用
    String var4 = "lambda";
    System.out.print(var4);
    var7 = "doSomething end";
    System.out.print(var7);
    var0 = "main end";
    System.out.print(var0);
    }

    相比较

    1. 少了新建实例的开销
    2. 少了方法调用(进出方法栈)的开销,一个是doSomething方法的调用,一个是lambda函数的调用
  • 高阶函数跟内联函数更配

  • 内联函数的局部return跟非局部return

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    fun main() {
    intArrayOf(1,2,3).forEach {
    if(it == 2)
    /**
    * 局部return,这里相当于continue
    */
    return@forEach
    println(it)
    }
    println("bbb")
    return
    }
    // 打印结果
    1
    3
    bbb
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fun main() {
    intArrayOf(1,2,3).forEach {
    if(it == 2)
    /**
    * 全局return。直接退出main函数
    **/
    return
    println(it)
    }
    println("bbb")
    return
    }
    // 打印结果
    1

    因为foreach是内联函数,转为java之后

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public static final void main() {
    int[] $this$forEach$iv = new int[]{1, 2, 3};
    int[] var2 = $this$forEach$iv;
    int var3 = $this$forEach$iv.length;

    for(int var4 = 0; var4 < var3; ++var4) {
    int element$iv = var2[var4];
    if (element$iv == 2) {
    return;
    }
    System.out.println(element$iv);
    }
    String var9 = "bbb";
    System.out.println(var9);
    }
  • noinline关键字可以取消函数参数的内联

    1
    2
    3
    4
    5
    6
    7
    8
    inline fun limit( noinline p1:()->Unit){
    print("limit")
    p1()
    }

    fun main() {
    limit { print("xxx") }
    }

    转为java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static final void main() {
    Function0 p1$iv = (Function0)null.INSTANCE;
    int $i$f$limit = false;
    //函数内联了
    String var2 = "limit";
    System.out.print(var2);
    //函数参数没有内联
    p1$iv.invoke();
    }
  • crossinline关键字不允许非局部返回

    非局部return的时候return的是调用的地方,lambda函数就没有了返回值,这种情况在某些情景下是有问题的,所以就有了crossinline关键字

    为什么要有crossinline可以参考:https://blog.csdn.net/u013009899/article/details/78584994,这里有例子。

    说白了,就是我们如果直接在lambda参数中结束当前函数,而不给lambda提供一个返回值,这种情况是不被允许的

  • 内联函数的没有被声明为noinline的函数(lambda)参数不能被存储

    1
    2
    3
    4
    inline fun limit(p1:()->Unit){
    //编译出错
    val a = p1
    }
几个有用的高阶函数
  1. let 返回值是block的返回值

    比如fragment的oncreate方法中解析bundle的内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    arguments?.let { // it
    imageId = it.getInt(ARG_IMAGE)
    tip = it.getString(ARG_TIP)
    bgColor = it.getInt(ARG_BG_COLOR)
    smallBgImg = it.getInt(ARG_SMALL_BG_IMAGE)
    }
    }
  2. run 返回值是block的返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    arguments?.run { //this
    imageId = getInt(ARG_IMAGE)
    tip = getString(ARG_TIP)
    bgColor = getInt(ARG_BG_COLOR)
    smallBgImg = getInt(ARG_SMALL_BG_IMAGE)
    }
    }
  1. also 返回值跟调用者是同一个实例

  2. apply T.apply(block: T.() -> Unit): T 返回值跟调用者是同一个实例(比如下面例子,loadingDialog实例调用的,返回值还是那个实例)

    例子1: fragment的newInstance方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    companion object {
    @JvmStatic
    fun newInstance(
    @DrawableRes imageId: Int,
    tip: String? = null,
    @ColorRes bgColor: Int = -1,
    @DrawableRes smallBgImg: Int = -1
    ) =
    LoadingDialog().apply { // this
    arguments = Bundle().apply { // this
    putInt(ARG_IMAGE, imageId)
    putString(ARG_TIP, tip)
    putInt(ARG_BG_COLOR, bgColor)
    putInt(ARG_SMALL_BG_IMAGE, smallBgImg)
    }
    }
    }

    例子2: build模式 ( OkHttpClient源码 )

    1
    2
    3
    4
    5
    6
    7
    fun addInterceptor(interceptor: Interceptor) = apply {
    interceptors += interceptor
    }

    fun addNetworkInterceptor(interceptor: Interceptor) = apply {
    networkInterceptors += interceptor
    }
  3. use

    不用自己关闭流

    1
    2
    3
    4
    File("build.gradle").inputStream().reader().buffered()
    .use {
    print(it.readLine())
    }
集合变换与序列
  • filter

    过滤集合

  • map

    元素一对一映射之后拼接

  • flatmap

    每个元素映射为一个集合,之后把所有的集合拼接起来

  • asSequence

    懒汉式序列

eg:饿汉式如下

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
arr.filter {
println("filter $it")
it % 2 == 0
}.map {
println("map $it")
it * 2
}.flatMap {
println("flatMap $it")
0..it
}.forEach {
println("foreach $it")
}
//输出:
filter 1
filter 2
filter 3
filter 4
map 2
map 4
flatMap 4
flatMap 8
foreach 0
foreach 1
foreach 2
foreach 3
foreach 4
foreach 0
foreach 1
foreach 2
foreach 3
foreach 4
foreach 5
foreach 6
foreach 7
foreach 8

添加 asSequence之后变为懒汉,foreach类似水龙头开关,没有的话,啥也不会输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arr.asSequence()
.filter {
println("filter $it")
it % 2 == 0
}.map {
println("map $it")
it * 2
}.forEach {
println("foreach $it")
}

//输出:
filter 1
filter 2
map 2
foreach 4
filter 3
filter 4
map 4
foreach 8
  • sum 求和

  • reduce (没有初始值,返回值跟item一个类型)

    源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public inline fun IntArray.reduce(operation: (acc: Int, Int) -> Int): Int {
    if (isEmpty())
    throw UnsupportedOperationException("Empty array can't be reduced.")
    var accumulator = this[0]
    for (index in 1..lastIndex) {
    accumulator = operation(accumulator, this[index])
    }
    return accumulator
    }

    例子:求和

    1
    2
    3
    4
    val arr = intArrayOf(1, 2, 3, 4)
    arr.reduce { acc, i ->
    acc + i
    }
  • fold(给一个初始值,对每一个item做同样的operation,返回值跟初始值一个类型)

    源码:

    1
    2
    3
    4
    5
    public inline fun <R> IntArray.fold(initial: R, operation: (acc: R, Int) -> R): R {
    var accumulator = initial
    for (element in this) accumulator = operation(accumulator, element)
    return accumulator
    }

    例子:求和

    1
    2
    3
    4
    5
    6
    val arr = intArrayOf(1, 2, 3, 4)
    val fold = arr.fold(0) { acc, i ->
    acc + i
    }
    print(fold)
    //10
sam转换(single abstract method)

eg:匿名内部类Runnable

1
2
3
4
5
6
7
Thread(
object : Runnable {
override fun run() {
print("run")
}
}
).start()

简写如下:

1
2
3
Thread(
Runnable { print("run") }
).start()

SAM之后

1
2
Thread { print("run") }
.start()

eg:设置点击监听

1
2
3
4
5
callWithDialog.setOnClickListener(object:View.OnClickListener{
override fun onClick(v: View?) {
println("onclick")
}
})

sam之后,可以简写为

1
callWithDialog.setOnClickListener { println("onclick") }

调用java的时候可以,如果单方法接口是kotlin,那么不支持。

实例:统计文本中字符(不含空格)出现个数
1
2
3
4
5
6
7
8
9
10
11
File("build.gradle")
.readText()
.toCharArray()
.filterNot {
it.isWhitespace()
}
.groupBy { it }
.map { it.key to it.value.size }
.let {
print(it)
}
tip

this@MainActivity : 内部类获取context的方法

后台开发

环境
  1. node

  2. npm

  3. yarn(取代npm)

node安装(自带npm):https://www.cnblogs.com/mumusen/p/9760732.html

yarn安装:https://www.cnblogs.com/mumusen/p/9760732.html

技术栈

vue

vue-router

vuex

antd pro :https://pro.loacg.com/docs/getting-started

springboot(kotlin)

登录

https://www.jianshu.com/p/ffe2790578fe

vue.ls

一个vue封装的本地储存的方法

https://www.jianshu.com/p/ab7f67878279

vue.config.js

https://www.jianshu.com/p/b358a91bdf2d

验证码
  1. 前端请求验证码:生成一个unique传递给后端
  2. 后端画图生成验证码,返回给前端:将{unique: versifycode }使用redis缓存
  3. 前端请求验证 :带着unique跟输入的verifyCode
  4. 后端验证:根据unique获取verifyCode,比对
antd pro访问本地springboot服务
  1. 禁用mock

    Main.js中注释掉 import ‘./mock’

  2. 设置query的url ,为springboot的server地址

    env.development:

    1
    2
    3
    NODE_ENV=development
    VUE_APP_PREVIEW=false
    VUE_APP_API_BASE_URL=http://localhost:8082

修改vue.config.js

1
2
3
4
5
6
7
8
9
10
11
devServer: {
// development server port 8888
port: 8000,
proxy: {
'/api': { // 匹配模式,所有api开始的连接都匹配到下面服务器地址
target: VUE_APP_API_BASE_URL, // 这是开发服务器的地址
ws: false,
changeOrigin: true,
}
}
},
  1. 此时因为跨域问题会报错:

    has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

  2. 后端解决:https://juejin.im/entry/5a0a52ab6fb9a045016777bf

后端登录逻辑

参见开发文档

正式环境配置

后端:

  • mysql(使用lszy小程序的,需要导入admin_api相关的table)oss备份
  • redis(使用lszy小程序的,暂时没有需要处理的)
  • jenkins : 新建task(拉取代码之后,使用maven构建生成jar包,之后在容器中运行该jar包)
  • 后端容器:运行服务端程序(jar)
  • nginx代理(使用lszy小程序的,增加admin_api的代理)

前端:

  • Node:尝试新建node镜像跟容器,完成前端项目构建
    • dockerFile(nodejs,yarn)
    • 启动镜像,设置挂载
    • 启动之后自动执行打包命令
  • nginx代理:使用lszy小程序的,增加admin_web的代理
  • nginx server(前端容器):部署admin_web
  • jenkins:新建task(拉取代码之后使用node容器构建,之后部署到nginx server容器)
jenkins 关联git
数据库8.0连接问题
分页(mybatis)
  1. 定义mapper

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    interface UserMapper {
    fun list(start: Int, pageSize: Int, phone: String?,
    startTime: String?, endTime: String?, nick_name: String?,
    gender: Int = 0, grade: Int = 0, provinceId: Int = 0,
    cityId: Int = 0, countyId: Int = 0, schoolId: Int = 0): List<User>?

    fun count(phone: String?,
    startTime: String?, endTime: String?, nick_name: String?,
    gender: Int = 0, grade: Int = 0, provinceId: Int = 0,
    cityId: Int = 0, countyId: Int = 0, schoolId: Int = 0): Int
    }
  2. 定义xml

    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
    84
    85
    86
    87
    88
    89
    90
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.lingshiedu.lszyAdmin.mapper.UserMapper">
    <select id="list" resultType="com.lingshiedu.lszyAdmin.model.User">
    select
    ls_user.id,
    ls_user.nick_name,
    ls_user.phone,
    ls_user.province_id,
    ls_user.city_id,
    ls_user.county_id,
    ls_user.school_id,
    ls_user.grade_id,
    ls_user.state,
    ls_schools.name as school_name,
    ls_user.gender,
    ls_user.adv_score,
    ls_user.occurrence_Time
    from ls_user
    INNER JOIN ls_schools ON ls_user.school_id = ls_schools.id
    <where>
    <if test="phone != null">
    AND phone=#{phone}
    </if>
    <if test="nick_name != null">
    AND nick_name LIKE CONCAT('%', #{nick_name}, '%')
    </if>
    <if test="gender != 0">
    AND gender=#{gender}
    </if>
    <if test="grade != 0">
    AND grade_id=#{grade}
    </if>
    <if test="startTime!=null and endTime!=null ">
    occurrence_Time BETWEEN #{startTime} and #{endTime}
    </if>
    <if test="provinceId != 0">
    AND province_id = #{provinceId}
    </if>
    <if test="cityId != 0">
    AND city_id = #{cityId}
    </if>
    <if test="countyId != 0">
    AND county_id = #{countyId}
    </if>
    <if test="schoolId != 0">
    AND school_id = #{schoolId}
    </if>
    </where>
    order by ls_user.occurrence_Time
    LIMIT #{start},
    #{pageSize}
    </select>

    <select id="count" resultType="int">
    select
    COUNT(*)
    from ls_user
    INNER JOIN ls_schools ON ls_user.school_id = ls_schools.id
    <where>
    <if test="phone != null">
    AND phone=#{phone}
    </if>
    <if test="nick_name != null">
    AND nick_name LIKE CONCAT('%', #{nick_name}, '%')
    </if>
    <if test="gender != 0">
    AND gender=#{gender}
    </if>
    <if test="grade != 0">
    AND grade_id=#{grade}
    </if>
    <if test="startTime!=null and endTime!=null ">
    occurrence_Time BETWEEN #{startTime} and #{endTime}
    </if>
    <if test="provinceId != 0">
    AND province_id = #{provinceId}
    </if>
    <if test="cityId != 0">
    AND city_id = #{cityId}
    </if>
    <if test="countyId != 0">
    AND county_id = #{countyId}
    </if>
    <if test="schoolId != 0">
    AND school_id = #{schoolId}
    </if>
    </where>
    </select>
    </mapper>
nginx命令
  • nginx -s reopen #重启Nginx

  • nginx -s reload #重新加载Nginx配置文件,然后以优雅的方式重启Nginx

  • nginx -s stop #强制停止Nginx服务

  • nginx -s quit #优雅地停止Nginx服务(即处理完所有请求后再停止服务)

  • nginx -t #检测配置文件是否有语法错误,然后退出

  • nginx -?,-h #打开帮助信息

  • nginx -v #显示版本信息并退出

  • nginx -V #显示版本和配置选项信息,然后退出

  • nginx -t #检测配置文件是否有语法错误,然后退出

  • nginx -T #检测配置文件是否有语法错误,转储并退出

  • nginx -q #在检测配置文件期间屏蔽非错误信息

  • nginx -p prefix #设置前缀路径(默认是:/usr/share/nginx/)

  • nginx -c filename #设置配置文件(默认是:/etc/nginx/nginx.conf)

  • nginx -g directives #设置配置文件外的全局指令

  • killall nginx #杀死所有nginx进程

docker nginx 挂载nginx.conf文件不同步问题:

https://blog.csdn.net/qq_41980563/article/details/90408460?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

dockerfile 传参(区分正式环境跟测试环境)
1
2
3
4
5
6
7
8
9
10
11
12
FROM node:13.11.0
RUN npm install -y yarn
WORKDIR /app/lszy_admin_web
COPY package.json ./
COPY yarn.lock ./
RUN yarn install
COPY ./ ./
# 定义arg参数
ARG arg=""
# 定义容器启动之后的参数build_env
ENV build_env=${arg}
CMD yarn ${build_env} && cp -rf dist dist_
1
2
3
4
5
# arg设置为build(正式环境)
docker build --build-arg arg=build . -t node_lszy_admin_web

# arg设置为build(测试环境)
docker build --build-arg arg=build:test . -t node_lszy_admin_web
后台管理员以及权限

antdesign流程:

  1. main.js中引入router模块并设置基础路由

    1
    2
    3
    4
    export default new Router({
    ...
    routes: constantRouterMap
    })
  2. permission.js中添加router拦截器

    1. 如果有token(token使用vue.ls存放在本地)
      1.  如果路由目的地是login,那么重定向到defaultPage
       2.  否则,如果可达的页面为空,那么调用store中的GetAdminUserInfo,成功之后先修改store中的pages信息之后调用store中的GenerateRoutes根据服务端返回的数据更新路由。路由更新完毕之后根据路由跳转
      
    2. 如果没有token,那么跳转登录页面(在免登录名单中的不用登录)

    具体代码如下:

    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
    router.beforeEach((to, from, next) => {
    NProgress.start() // start progress bar
    to.meta && (typeof to.meta.title !== 'undefined' && setDocumentTitle(`${to.meta.title} - ${domTitle}`))
    if (Vue.ls.get(ACCESS_TOKEN)) {
    /* has token */
    if (to.path === '/admin/login') {
    next({ path: defaultRoutePath })
    NProgress.done()
    } else {
    if (store.getters.pages.length === 0) {
    store
    .dispatch('GetAdminUserInfo')
    .then(res => {
    const pages = res.data.pages
    store.dispatch('GenerateRoutes', { pages }).then(() => {
    // 根据roles权限生成可访问的路由表
    // 动态添加可访问路由表
    router.addRoutes(store.getters.addRouters)
    // 请求带有 redirect 重定向时,登录自动重定向到该地址
    const redirect = decodeURIComponent(from.query.redirect || to.path)
    if (to.path === redirect) {
    // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
    next({ ...to, replace: true })
    } else {
    // 跳转到目的路由
    next({ path: redirect })
    }
    })
    })
    } else {
    next()
    }
    }
    } else {
    if (whiteList.includes(to.name)) {
    // 在免登录白名单,直接进入
    next()
    } else {
    next({ path: '/admin/login', query: { redirect: to.fullPath } })
    NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
    }
    }
    })
vue中ref使用(跟子组件通信+emit)

https://www.jianshu.com/p/623c8b009a85

后端日志(log4j2)

https://www.cnblogs.com/LemonFive/p/10737658.html

登陆服务器快捷方式
客户端
  1. 打开~/.ssh,如果没有则创建文件夹

  2. 执行ssh-keygen -t rsa

    Enter file in which to save the key:输入保存私钥的文件名称(id_rsa_host)

    Enter passphrase (empty for no passphrase): (可以不写)

    Enter same passphrase again:(可以不写)

    此时会生成:id_rsa_host文件

  3. ssh-add ~/.ssh/id_rsa_host,通过私钥创建公钥

    此时会生成:id_rsa_host.pub文件

服务端
  1. 打开~/.ssh,如果没有则创建文件夹

  2. 打开authorized_keys,将客户端第3步生成的文件中的公钥保存到此文件中,如果有多个则,换行处理

快捷连接设置

在客户端,~/.ssh下创建config文件

内容如下

Host dev
HostName hostIp
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa_host
User username

完成,测试

ssh dev

回车即可

Mysql添加索引解决速度问题

问题:执行update的时候where后面跟的查询条件是school_id=’xx’ and major_id=’xx’,表里面有4万多条数据,执行起来非常慢,最快需要2s。

解决:

将school_id跟major_id设置成唯一索引(普通索引也可以),总之就是把这两列作为联合索引就行。之后执行飞快,基本上几毫秒就ok。

1.添加PRIMARY KEY(主键索引)

mysql>ALTER TABLE table_name ADD PRIMARY KEY ( column )

2.添加UNIQUE(唯一索引)
mysql>ALTER TABLE table_name ADD UNIQUE ( column )

3.添加INDEX(普通索引)

mysql>ALTER TABLE table_name ADD INDEX index_name ( column )

4.添加FULLTEXT(全文索引)

mysql>ALTER TABLE table_name ADD FULLTEXT ( column)

5.添加多列索引
mysql>ALTER TABLE table_name ADD INDEX index_name ( column1, column2, column3 )

索引操作参考:

https://www.w3cschool.cn/mysql/mysql-index.html

Charles 抓包

报错:Charles cannot configure your proxy settings while it is on a read-only volume.

解决方法:

在Mac终端中输入

1
2
sudo chown -R root "/Applications/Charles.app/Contents/Resources"
sudo chmod -R u+s "/Applications/Charles.app/Contents/Resources"

Android studio:connect refuse

https://www.jianshu.com/p/89872a0ee16f

AndroidStudio同步文件时同步失败,查找是不是开了代理,在AndroidStudio的Proxy中看到了这个提示,但是找到gradle.properties并没有这句话,一翻查找后找到了解决方案:

  • 命令行中输入
    JAVA_OPTS="$JAVA_OPTS -DsocksProxyPort"
  • AndoridStudio中InvalidaCaches/Restart

参考链接:https://stackoverflow.com/questions/35520337/how-to-remove-jvm-property-https-proxyhost

经常开Charls代理或者vpn的时容易出现这种情况。记录一下,以后好找。

参考文旦

https://blog.csdn.net/shadowyspirits/article/details/79756274

spring boot log 配置

​ 使用log4j2

Q1:过滤掉系统日志??
1
2
3
4
5
6
7
8
9
10
11
12
<loggers>
<!-- 过滤掉org.springframework包下面的日志-->
<!-- name对应代码的包 additivity设置为false表示不会到root的log里面 -->
<logger name="org.springframework" level="INFO" additivity="false">
</logger>
<root level="INFO">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileInfo"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
</root>
</loggers>
Q2: 不同级别的日志输出到不同的文件??

https://github.com/bycuimiao/springboot2-log4j2-demo/blob/master/src/main/resources/log4j2.xml

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
<RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log"
filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
<Filters>
<!-- 表示对于warn以及warn级别以上的日志 deny,对于warn一下子的日志不拦截 -->
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL"/>
<!-- 表示info以及以上的日志接受,以下的deny掉 (因为info的以上的日志都被上面的filter过滤掉了,所以只有info的日志可以通过这两个filter)-->
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<SizeBasedTriggeringPolicy size="1M"/>
</Policies>
</RollingFile>
<RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log"
filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
<Filters>
<ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL"/>
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<SizeBasedTriggeringPolicy size="1M"/>
</Policies>
</RollingFile>
<RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log"
filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<SizeBasedTriggeringPolicy size="1M"/>
</Policies>
</RollingFile>
Q3:不同环境日志文件位置问题??

对于日志的位置,不能使用绝对路径,原因是多人开发,每个人机器上的路径不一致,这个时候可以使用

1
<property name="FILE_PATH" value="${sys:user.home}/home/logs"/>

${sys:user.home}表示用户的目录,cd ~

在docker容器中对应/root 目录

Q4:日志打印

利用servelet拦截器

https://www.jianshu.com/p/afdd31bfbf94

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package com.lingshiedu.lszy.filter

import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.springframework.stereotype.Component
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper
import org.springframework.web.util.WebUtils
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import java.util.*
import javax.servlet.*
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
class LogFilter : Filter {
val Logger = LoggerFactory.getLogger(LogFilter::class.java)
val SPLIT_STRING_M = "="
val SPLIT_STRING_DOT = ", "
override fun destroy() {
}

override fun doFilter(req: ServletRequest?, res: ServletResponse?, filterChain: FilterChain?) {
val request = req as HttpServletRequest
val response = res as HttpServletResponse
MDC.clear()
//给本次请求分配一个唯一的tradeId
MDC.put("trade_id", UUID.randomUUID().toString().replace("-", ""))

val wrapperRequest = ContentCachingRequestWrapper(request)
val wrapperResponse = ContentCachingResponseWrapper(response)

Logger.info("method[{}] | queryUrl[{}] | headers[{}] | queryParams[{}] | body:{}", request.method, request.requestURL, getHeaders(req), getRequestParams(request), getRequestBody(wrapperRequest))
filterChain!!.doFilter(wrapperRequest, wrapperResponse)
Logger.info("resp body:{}", getResponseBody(wrapperResponse))

wrapperResponse.copyBodyToResponse()
}

private fun getRequestBody(request: ContentCachingRequestWrapper): String? {
val wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper::class.java)
if (wrapper != null) {
val buf = wrapper.contentAsByteArray
if (buf.isNotEmpty()) {
val payload: String = try {
String(buf, 0, buf.size, Charset.forName(wrapper.characterEncoding))
} catch (e: UnsupportedEncodingException) {
"[unknown]"
}

return payload.replace("\\n".toRegex(), "")
}
}

return ""
}

private fun getHeaders(req: HttpServletRequest): String? {
var header = ""
for (key in req.headerNames) {
val value = req.getHeader(key)
header += " [key:${key} value:${value}] "
}

return header
}

private fun getResponseBody(response: ContentCachingResponseWrapper): String? {
val wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper::class.java)
if (wrapper != null) {
val buf = wrapper.contentAsByteArray
if (buf.isNotEmpty()) {
val payload: String = try {
String(buf, 0, buf.size, Charset.forName(wrapper.characterEncoding))
} catch (e: UnsupportedEncodingException) {
"[unknown]"
}

return payload
}
}

return ""
}

fun getRequestParams(request: HttpServletRequest): String? {
val sb = StringBuilder()
val enu = request.parameterNames
//获取请求参数
while (enu.hasMoreElements()) {
val name = enu.nextElement()
sb.append(name + SPLIT_STRING_M).append(request.getParameter(name))
if (enu.hasMoreElements()) {
sb.append(SPLIT_STRING_DOT)
}
}

return sb.toString()
}

override fun init(p0: FilterConfig?) {
}
}

docker

入门

虚拟化技术

主要目的:可以在一个物理设备上做到环境隔离

主机级别虚拟化

两种类型

  • 在裸(物理)机上装hostOs,之后在hostOs上hypervisor(eg:vmware),之后在hypervisor上装各种系统
  • 在裸(物理)机上直接装hypervisor,之后在hypervisor上装各种系统

img

假设需要运行一个web服务,为此我们必须先安装hypervisor,再安装系统,之后在系统上安装nginx或者tomcat;

如果是第一种类型,使用物理资源还需要经过两个内核的调度,这样开销是比较大的。

容器级别虚拟化

linux分为内核跟用户空间,linux内核有两个功能:

  • namespace
  • cgroup

通过这两个功能,linux可以做到对用户空间的隔离。这些隔离的用户空间就是容器。

每一个虚拟机都有自己的ip,域名,文件系统等等,一个主机有的它基本都有。

容器也需要有独立的:

  • 主机名:域名
  • 文件系统
  • 进程通信ipc
  • 进程id(pid)
  • 用户组
  • net(ip地址,端口,等)

这个就是通过命名空间(namespace)实现的;自己理解可以类比写代码时的namespace(相同的类名在不同的namespace下是隔离的)

然而不同namespace之间物理资源(更多是硬件资源,cpu,内存等)还是相互竞争的。仍然需要管理每个container可以使用的资源。cgroups 实现了对资源的配额和度量,通过它我们可以实现资源的隔离分配。

docker

容器级别虚拟化相对主机级别虚拟化

  • 开销小,因为直接运行在宿主机上
  • 隔离性上差点意思

这两种虚拟化技术都有一个问题:迁移性。

docker利用镜像跟仓库很好的解决了这个问题。

有了docker之后我们可以一次编写到处运行,只要安装了docker,这一点跟jvm挺像的。

最大的优点:分发部署

Docker基本用法

docker架构图

img

c/s架构,使用远程api的方式管理跟创建Docker容器。

  1. dockerClient

    类似postman,可以发送get,post,put等等各种请求给host,通过api的方式增删改查image跟container

  2. dockerHost

    • dockerDeamon:用来监听套接字,一般在宿主主机的后台运行,等待接受客户端的请求(docker命令),类比服务端程序。
    • image: 用来生成容器的
      • 只读的
      • 分层构建的(可以通过dockerFile理解),可以根据自己的需求在已有得镜像的基础上随意扩展
      • 静态的
    • container:隔离的用户空间,一个独立于主机的隔离的进程,由image创建
  3. Registry

    主要功能是作为repository存放image,docker官方提供了dockerhub,但是服务器在国外,访问比较慢,在国内也提供了,但是也不快。可以使用阿里或者科大或者清华的镜像仓库。个人配置的是阿里的,体验挺好的(需要注册一个阿里账户,之后会分配一个地址)。

    阿里镜像加速:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

image跟container的关系:类似程序跟进程的关系,类似class跟实体的关系

流程

在终端输入 docker container create –name n3 nginx,

  1. host的deamon收到请求(http的方式)
  2. 在本地检查是否有nginx镜像
  3. 如果有,那么根据镜像创建n3容器
  4. 如果么有,那么发请求到(https)registry中拉去nginx镜像到本地,之后在本地创建n3容器

常用命令

docker
  • docker verison
  • docker info (镜像加速配置完之后可以通过它来查看是否配置成功了)
  • docker search 镜像名 :dockerhub上查找镜像
  • docker inspect 容器名或者镜像名 : 查看容器或者镜像的详细信息(可以查看容器的ip地址,挂载信息等等)
  • docker exec -it 容器名 /bin/bash : 进入容器终端
  • docker logs 容器名
  • docker cp 容器名:容器文件或文件夹 宿主机文件或文件夹 :将容器中的文件或文件夹复制出来
  • docker tag 给镜像打标签
  • docker commit 通过容器创建镜像
image
  • docker image pull 镜像名:tag : 拉取镜像,tag可以缺省
  • docker image ls : 查看所有的镜像
  • docker image rm 镜像名 :删除镜像
container
  • docker container ls -a : 查看所有容器

  • docker container ls -s : 可以查看容器的size

  • docker container create –name n1 nginx :创建nginx容器n1

  • docker container start n1 :启动n1

  • docker container stop xxx :停止容器

  • docker container rm xxx :删除stop的容器

  • docker container run -it –name n2 nginx /bin/bash :创建并启动nginx容器n2,并进入容器终端

  • doker container run -it –name xxx –rm -v xxx1:xxx2 镜像名 /bin/bash

    • it:可以交互的终端
    • rm:运行完自动删除
    • d: 运行在后台
    • name xxx: 容器名
    • v : 文件挂载 主机的xxx1挂载到容器的xxx2
tip
  • Netstat -tnl 查看监听的端口

Docker 镜像管理基础

镜像就是把业务代码,可运行环境进行整体的打包。

镜像包含了启动容器需要的文件系统跟其他内容,因此,镜像用来创建并启动容器。

参考:https://zhuanlan.zhihu.com/p/70424048

思考题

  • 我们基于同一个镜像(ubuntu 18.4)启动了两个容器,会占用两倍磁盘空间吗?
  • 我们在容器内修改或者新建了某个文件,要修改原镜像吗?
  • 我们基于某镜像(ubuntu 18.04)新建一个镜像(myubuntu),需要将原镜像文件全部拷贝到新镜像中吗?

首先,让我们尝试思考下,如果我们去做,该如何高效的解决这些问题?

  • 问题 1,只要将同一个镜像文件加载到内存不同位置就行了,没必要在磁盘上存储多份,可以节省大量存储空间。
  • 问题 2,我们可以参考 Linux 内核管理内存的 Copy-On-Write 策略,也即读时大家共用一份文件,如果需要修改再复制一份进行修改,而大部分文件是其实不会修改的,这样可以最大限度节省空间,提升性能。
  • 问题 3,我们可以将镜像文件分为多个独立的层,然后新镜像文件只要引用基础镜像文件就可以了,这样可以节省大量空间。至于修改基础镜像文件的情况,参考问题 2

镜像分层

img

  1. 镜像分层,除了最上层的容器层是可读写的,其他层都是只读的。如果容器层需要读写其他层的数据,需要用到copy-on-write技术
  2. 只读层是可以共享的
  3. 每个容器都有自己的读写层,容器被删之后读写层也会被删除。如果想要持久化数据,可以使用docker挂载(volume):比如log日志
  4. 执行docker ps -s(或者docker container ls -s)可以看到容器最后一列有个size(2B (virtual 127MB)),这里的2B就是读写层的大小,127M是读写层加只读层的大小。
UnionFS

docker可以实现分层依赖的就是unionFs,所谓 UnionFS 就是把不同物理位置的目录合并到同一个目录中。对应docker,就是把下层的文件“合并”到上层,如果在上层想要修改下层的文件,需要先把下层文件copy到上层,之后再修改,但是下层的文件是不变的。

14年的时候linux内核中合并了overlayFs,也是现在docker使用的存储方案,执行docker info可以看到。

https://zhuanlan.zhihu.com/p/70424048 这篇文章中使用mount命令在ubuntu中模拟了docker的分层。

镜像构建分发

构建
  1. 通过容器构建

    1
    docker commit -p m1 kj/mysql:v1
    • -p 暂停容器
    • m1 容器名
    • kj/mysql 镜像仓库名
    • v1 镜像版本
  2. 通过dockerfile构建

    • 可以跟github,docker仓库(阿里云或者dockerhub)联动
  • 上传dockerFile到github,docker仓库检测到dockerfile的变化,会去github上拉代码然后build好镜像
    • 底层也是基于容器构建的,只不过是docker帮我们新建了一个容器,dockerfile实际是在这个容器里面执行的
分发
  1. 上传到仓库分发

    1
    2
    docker login 
    docker push
    • Dockerhub镜像的仓库名必须跟hub上的一致
    • 阿里云
  2. 本地打包分发

    1
    2
    docker save
    docker load

容器虚拟网络

基本概念

Linux内核支持二层以及三层设备的虚拟化。

  1. 虚拟网卡

    • linux内核支持虚拟网卡的创建(ip命令)

    • 每一个虚拟网卡是成对出现的,可以模拟成一根网线(下图中的eth0跟veth就是虚拟网卡的两端)

    作用:连接虚拟网桥跟容器

  2. 虚拟网桥(交换机)

    • Docker0

    • ip,mac映射

    • linux内核也支持虚拟交换机的创建(brctl 命令)

    • 通过交换机,同一个网段不同的容器可以通信

    作用:将主机以及所有的容器放在同一个局域网内

  3. nat

    网络地址转化

    作用

    • 将容器的私有地址转化为物理网卡的地址(snat)

    • 将物理网卡的地址转化为容器的地址(dnat)

docker的四种网络模式

bridge

当Docker进程启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的Docker容器会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中。

从docker0子网中分配一个IP给容器使用,并设置docker0的IP地址为容器的默认网关。在主机上创建一对虚拟网卡veth pair设备,Docker将veth pair设备的一端放在新创建的容器中,并命名为eth0(容器的网卡),另一端放在主机中,以vethxxx这样类似的名字命名,并将这个网络设备加入到docker0网桥中。可以通过brctl show命令查看。

bridge模式是docker的默认网络模式,不写–net参数,就是bridge模式。使用docker run -p时,docker实际是在iptables做了DNAT规则,实现端口转发功能。可以使用iptables -t nat -vnL查看

img

host

如果启动容器的时候使用host模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。但是,容器的其他方面,如文件系统、进程列表等还是和宿主机隔离的。

container

这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。

none

使用none模式,Docker容器拥有自己的Network Namespace,但是,并不为Docker容器进行任何网络配置。也就是说,这个Docker容器没有网卡、IP、路由等信息。需要我们自己为Docker容器添加网卡、配置IP等。

bridge模型验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@iZ2ze4qtfwa4w5rqbr2e83Z ~]# ifconfig

docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536

veth4c07805: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500

veth718f748: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500

vethce1977e: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500

vethf87afa7: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500

docker0就是虚拟网桥

vethxxx是虚拟网卡的一端,都是连接在docker0上的,使用brctl验证如下:

1
2
3
4
5
6
7
8
[root@iZ2ze4qtfwa4w5rqbr2e83Z ~]# yum -y install bridge-utils

[root@iZ2ze4qtfwa4w5rqbr2e83Z ~]# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.0242e24e01a3 no veth4c07805
veth718f748
vethce1977e
vethf87afa7

验证网卡连接:

1
2
3
4
5
6
7
8
[root@iZ2ze4qtfwa4w5rqbr2e83Z ~]# ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT
5: vethce1977e@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0
9: vethf87afa7@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0
17: veth718f748@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master
19: veth4c07805@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master

5,9,17,19是4个网卡,以“vethce1977e@if4”为例,vethce1977e是连接在docker0上的,if4是放在容器上的。

进入容器查看网卡:

1
2
3
4
5
6
7
8
9
10
wangzeqi@wangzeqideMacBook-Pro ~ % docker run -it --name b1 busybox /bin/sh
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:03
inet addr:172.17.0.3 Bcast:172.17.255.255 Mask:255.255.0.0

lo Link encap:Local Loopback

/ # ping 172.17.0.1
PING 172.17.0.1 (172.17.0.1): 56 data bytes
64 bytes from 172.17.0.1: seq=0 ttl=64 time=2.169 ms

eth0就是虚拟网卡的另一端,ping 虚拟网桥docker0可以ping通。

nat:查看iptables规则

1
[root@iZ2ze4qtfwa4w5rqbr2e83Z ~]# iptables -t nat -vnL

所有收到的源地址为172.18.0.0/16(除了docker0)目标地址为任意地址的请求,都做snat(局域网内部私有地址转化为主机物理网卡的地址) tip: MASQUERADE的翻译是化妆,非常形象

主机端口通过dnat转发到容器端口

参考:

https://www.cnblogs.com/yy-cxd/p/6553624.html

https://blog.csdn.net/nia305/article/details/82775384

tip

Q:Docker For Mac没有docker0网桥

在使用Docker时,要注意平台之间实现的差异性,如Docker For Mac的实现和标准Docker规范有区别,Docker For Mac的Docker Daemon是运行于虚拟机(xhyve)中的, 而不是像Linux上那样作为进程运行于宿主机,因此Docker For Mac没有docker0网桥,不能实现host网络模式,host模式会使Container复用Daemon的网络栈(在xhyve虚拟机中),而不是与Host主机网络栈,这样虽然其它容器仍然可通过xhyve网络栈进行交互,但却不是用的Host上的端口(在Host上无法访问)。bridge网络模式 -p 参数不受此影响,它能正常打开Host上的端口并映射到Container的对应Port。

Docker存储卷

写时复制

  • 镜像是分层的,docker容器启动的时候会去加载镜像的只读层,然后在最上层添加一个读写层。
  • 容器中的io都是针对读写层
  • 如果要对只读层的内容进行修改或者删除,需要将只读层的内容copy到读写层,之后在读写层操作(tip:删除的话只是在读写层标记为删除)

概念

考虑我们做一个mysql的容器

  1. 数据读写都在容器中的读写层,存储效率低。
  2. 数据会随着容器的删除而丢失

此时就需要存储卷

  1. “宿主机上”有个存储目录A,容器有个存储目录B,把这两个目录关联起来,A跟B的内容始终是同步的。那么A就是存储卷。
  2. 在容器删除的时候数据不会丢失
  3. 两个容器可以通过这种方式通信
  4. 1中“宿主机”并不准确,这个目录也可以是共享目录,比如nfs服务器上的目录,这样容器就不用局限在一个宿主机上。

两种类型

  1. 绑定挂载卷:宿主机的目录跟容器的目录都是run的时候指定的
  2. docker管理卷:容器中的目录是run的时候指定的,宿主机目录由docker deamon自动指定

演示

  1. 启动容器c1:关联宿主机目录/Users/wangzeqi/container/data 跟 容器目录/data
  2. 在/data目录下新建一个文件
  3. 删除容器c1,查看/Users/wangzeqi/container/data目录
  4. 启动容器c2,关联/Users/wangzeqi/container/data跟/data/temp,
  5. 查看/data/temp目录
  6. 查看容器信息
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
#1
docker container run --name c1 --rm -it -v /Users/wangzeqi/container/data:/data busybox
#2
cd /data
echo "hello" >> test.txt
#3
exit
cd /Users/wangzeqi/container/data
cat test.txt
#4
docker container run --name c2 --rm -it -v /Users/wangzeqi/container/data:/data/temp busybox
#5
cd data/temp/
cat test.txt
#6
docker inspect c2

#可以看到如下信息
"Mounts": [
{
"Type": "bind", #存储卷类型
"Source": "/Users/wangzeqi/container/data", # 宿主机目录
"Destination": "/data/temp", # 容器目录
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],

#6 或者
docker inspect -f {{.Mounts}} c2
#打印如下
[{bind /Users/wangzeqi/container/data /data/temp true rprivate}]

tip

  1. -v的目录如果不存在docker会自动创建

  2. 两个容器可以共享一个存储卷

    • –volumes-from c1 复制容器c1的挂载关联规则
  3. 两个容器可以共享一个网络空间(docker网络模式中的container模式)

    • –network container:c1 加入c1的网络空间
  4. 为了不用每次都指定容器的网络配置或者挂载配置,假如需要搭建nmt (nginx ,tomcat,mysql )架构或者更加复杂的,可以

    • 创建一个基础架构容器,新建其他容器的时候从基础架构容器复制配置信息
    • 或者使用容器编排工具(docker-compose),这个也支持创建基础架构容器
    • 或者使用k8s编排

Dockerfile

构建镜像的源码。镜像是分层的,dockerfile中每一条命令对应一层。

工作逻辑

  1. 选定一个工作目录

  2. 工作目录下放Dockerfile文件

  3. 如果构建过程需要访问某个文件,那个这个文件必须在工作目录下

  4. 构建的时候工作目录下的文件都会打包到镜像。

  5. .dockerignore文件中声明的文件不会打包到镜像

指令

先看一个实例,看注释即可(以下是在node容器中构建ant design vue项目的dockerfile)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 定义基础镜像
FROM node:13.11.0
# install yarn
RUN npm install -y yarn
# 指定工作目录
WORKDIR /app/lszy_admin_web
# 安装依赖
COPY package.json yarn.lock ./
RUN yarn install
# 将工程copy到工作目录
COPY ./ ./
# 声明build的arg,测试环境:build:test;正式环境:build
ARG env
# build
RUN yarn ${env}
# 将dist复制到存储卷
CMD cp -rf dist dist_copy
FROM
  1. 定义基础镜像
  2. 默认情况下build命令会先在本地查看是否有该基础镜像,没有的话会去dockerHub拉取(如果设置了阿里的镜像加速,那么会从加速地址去拉取)或者也可以指定拉取的地址(registry)
  3. 格式:镜像仓库名:tag 或者 镜像仓库名@digest , 第二种更加安全
MAINTAINER

废弃了,设置作者信息

LABLE
  1. 替代maintainer
  2. key-value形式
  3. 用来设置说明信息,类似注释
COPY && ADD
  1. 从文件或者文件夹复制到镜像的指定目录
  2. ADD可以复制url,COPY只能复制宿主机的内容
  3. ADD复制tar文件时,会自动解压,copy不会自动解压
  4. 一条命令中源文件可以有多个,目标文件只能一个
  5. 复制文件夹的时候是复制文件夹下所有内容,文件夹本身不会复制过去
  6. 目标文件必须“/”结尾
WORKDIR
  1. 指定容器启动之后的工作目录
  2. 容器启动之后默认会进入该目录
  3. DockerFile中WORKDIR声明工作路径之后,下面所有命令中涉及到镜像中的路径时都可以使用相对该工作目录的相对路径
ARG && ENV
1
2
3
4
5
6
7
8
9
10
11
12
13
14
wangzeqi@wangzeqideMacBook-Pro workspace % docker build -t "test" .
Sending build context to Docker daemon 5.12kB
Step 1/3 : FROM busybox
---> 6d5fcfe5ff17
Step 2/3 : WORKDIR /
---> Running in 2630e64e0b23
Removing intermediate container 2630e64e0b23
---> 70c2a25a7355
Step 3/3 : RUN mkdir test && cd test && echo "hello" >> test.txt
---> Running in 91496fb114ae
Removing intermediate container 91496fb114ae
---> ddd723329232
Successfully built ddd723329232
Successfully tagged test:latest
  1. 使用dockerfile构建镜像的过程如上,(注意每一层执行之后都有一个removing操作)每一层都会新建一个临时容器,在容器上运行该层的命令,运行完成之后删除容器

  2. ARG用于定义镜像构建时传递的参数:在构建的过程中如果我们需要动态的传递参数,那么可以使用ARG命令指定形参,在build的时候传递实参

    1
    2
    3
    4
    5
    # Dockerfile
    ARG arg1

    # 执行构建
    docker build --build-arg arg1=build:test . -t node_lszy_admin_web
  3. ENV则是定义容器运行时传递的参数

    1
    2
    3
    4
    5
    # Dockerfile
    ENV env1

    # 运行容器
    docker run --name test --rm --env env1=build test
CMD && RUN
1. CMD 是在容器启动之后默认执行的命令,如果我们在docker run 之后加了命令,那么dockerfile中的的cmd将会被替换掉
 2. RUN 是在构建镜像是执行的命令
VOLOMN
  1. 定义容器中的存储卷
  2. run的时候不需要使用-v指定存储卷的位置
  3. 对应的宿主机上的挂载卷是docker自己指定的(对应存储卷的第二种类型)
EXPOSE
  1. 定义对外(宿主机)暴露的端口
  2. run的时候直接-P(注意是大写P),docker会自动分配宿主机上的一个端口映射到容器暴露的端口
  3. 如果需要自己指定端口映射,运行时使用-p 3306:3306 (小写p)即可

tip:linux环境变量(arg跟env命令可能会用到)

1
2
3
4
5
wangzeqi@wangzeqideMacBook-Pro dist % echo ${name:-wzq}
wzq
wangzeqi@wangzeqideMacBook-Pro dist % name=zpy
wangzeqi@wangzeqideMacBook-Pro dist % echo $name
zpy
  • “-wzq”如果name为空,那么name=wzq
  • “+wzq”如果name不为空,那么name=wzq

其他

  1. 一文教您如何通过 Docker 快速搭建各种测试环境(Mysql, Redis, Elasticsearch, MongoDB) https://juejin.im/post/5ce531b65188252d215ed8b7

Kotlin

第一章(入门)

基础
  1. var 可变变量 val 不可变变量

  2. 可以不用指定类型,会自动推导 eg:

    1
    2
    3
    var v1: Int = 10
    var v2 = 11
    val v3 = 12
  3. String 跟 String?是两种不同的类型,String 类型不能为空,String?可以空

  4. 强转符号 !! eg:

    1
    2
    3
    4
    5
    6
    7
    8
    var s1: String = "123"
    var s2: String? = null

    fun main(args: Array<String>) {
    //需要强转
    s1 = s2!!
    s2 = s1
    }
  5. 函数声明 fun xxx(param: type): returnType{ … } eg:

    1
    2
    3
    4
    fun printlen(str: String): String {
    println("stringValue: $str")
    return str
    }
  6. kotlin中if else 是一个表达式(有返回值),不是语句

    1
    2
    3
    fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
    }
  7. kotlin中赋值是语句,不是表达式

  8. 字符串模版 ->占位符$

    1
    2
    3
    4
    5
    //case 1
    val str = "kotlin"
    println("hello,$str")
    //case 2 :表达式
    println("hello,${if(str=="kotlin") "kotlin" else "java"}")
  9. 类定义

    1. Java vs kotlin

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class Person {
      private String name;

      public Person(String name) {
      this.name = name;
      }

      public String getName() {
      return name;
      }
      }

      转成kotlin: 值对象->只包含数据,不包含对数据的操作

      1
      class Person(val name: String)
    2. 类属性:

      • 声明了属性,就隐式声明了该属性的get跟set
      • 如果是val属性,那么只有get
      • 如果是is开头的属性,get方法也是is开头的
    3. 自定义类属性的get跟set方法:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      class Rectangle(val height: Int, val width: Int) {
      //方式1
      val isSquare: Boolean
      get() {
      return height == width
      }
      //方式2
      val isRect: Boolean
      get() = height > 0 && width > 0
      }

      fun main(args: Array<String>) {
      val rectangle = Rectangle(10, 10)
      println(rectangle.isRect)
      println(rectangle.isSquare)
      }
  10. 包结构

    1. kotlin中可以将多个类,多个方法放在一个kt文件中
    2. 不同包下使用类的时候跟java一致,不同的是kotlin可以导入方法
  11. 枚举

    java使用enum关键字即可,kotlin需要使用enum class

    1. java

      1
      2
      3
      public enum Gender {
      Man, Woman
      }
    2. kotlin

      1
      2
      3
      enum class Gender {
      Man, Woman
      }
  12. when

    1. When 替代switch case;when跟if else一样是个表达式,不是语句

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      enum class Gender {
      Man, Woman
      }

      fun getGender(gender: Gender): String {
      return when (gender) {
      Gender.Man -> "男"
      Gender.Woman -> "女"
      }
      }

      fun main(args: Array<String>) {
      val gender = getGender(Gender.Man)
      println("性别:$gender")
      }
    2. 组合的情况,用逗号隔开

    3. when允许任何对象作为分支条件

    4. when可以不指定参数,分支条件可以为任意的boolean表达式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      fun getLevel(score: Int)  =
      when {
      (score in 0..59) -> "不及格"
      (score in 60..100) -> "及格"
      else -> throw Exception("分数不正确")
      }

      fun main(args: Array<String>) {
      println(getLevel(10))
      }
  13. 类的智能转换:java中做类型转换前会使用instanceOf() 方法判断,之后再进行强转,kotlin中使用(a is b)进行判断,并且判断之后a对象自动转换为b类型,不需要强转

  14. while 跟java完全一致

  15. forin替代java中的for循环

    1. 整数遍历

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      for (i in 1..10) {
      println(i)
      }

      for (i in 10 downTo 1) {
      println(i)
      }

      for (i in 1..10 step 2) {
      println(i)
      }
      //不包括11
      for (i in 1 until 11) {
      println(i)
      }
    2. 遍历数组

      1
      2
      3
      4
      val arrayList = arrayListOf<String>("1", "2")
      for ((index,item) in arrayList.withIndex()){
      println("$index,$item")
      }
    3. 遍历map

      1
      2
      3
      4
      5
      6
      7
      val hashMap = HashMap<String, String>()
      hashMap["1"] = "123"
      hashMap["2"] = "234"

      for ((key,value) in hashMap){
      println("$key,$value")
      }
  16. in 跟 ! in

    1. 是否在范围内,范围可以是数字,字符,也可以是实现了Java.lang.comparable接口的类对象实例
    2. 是否在集合内
    1
    2
    3
    4
    5
    6
    7
    8
    9
    fun isNumber(c: Char) = c in '0'..'9'

    fun main(args: Array<String>) {
    println(isNumber('0'))
    //string 实现了Java.lang.comparable
    println("javascript" in "java".."kotlin")

    println("java" !in setOf("java", "kotlin"))
    }
  17. 异常处理

    1. 跟java 一样用try catch finally
    2. 它是表达式
    3. kotlin中函数不需要throws异常
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fun readNumber(read: BufferedReader): Int? {
    val line = read.readLine()
    return try {
    Integer.parseInt(line)
    } catch (e: Exception) {
    null
    } finally {
    read.close()
    }
    }

    fun main(args: Array<String>) {
    print(readNumber(BufferedReader(StringReader("123q"))))
    }

    以上例子中,如果是java,那么需要throws IOException( 因为read.close()方法 ),但是在kotlin中不需要

  18. 创建集合

    1. kotlin没有自己的集合类,跟java公用
    2. kotlin在java的基础上增加了一些语法糖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    fun main(args: Array<String>) {
    val hashSet = hashSetOf(1, 2, 3)
    val hashMap = hashMapOf(1 to '1', 2 to '2')
    val arrayList = arrayListOf(1, 2, 3)

    println(hashSet.javaClass)
    println(hashMap.javaClass)
    println(arrayList.javaClass)

    println(arrayList.last())
    println(hashSet.max())
    println(hashMap[1])
    }

    //输出
    class java.util.HashSet
    class java.util.HashMap
    class java.util.ArrayList
    3
    3
    1
  19. 函数声明时候的默认参数

    1. 在java的类中会出现大量的方法重载,kotlin的函数参数可以有默认值,这个特性可以减少很多模版化的重载
    2. kotlin调用方法的时候可以指定参数名称,而且可以不用按方法声明的参数顺序去写,增加了代码易读性。
    3. 因为java中不具有该特性,java调用该代码的时候必须指定全部参数;@JvmOverloads注解可以让这个方法生成对应的java重载方法,这样就java调用的时候就不用指定全部参数了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @JvmOverloads
    fun doSomething(
    param1: String,
    param2: String = "default"
    ) {
    println(param1)
    println(param2)
    }


    fun main(args: Array<String>) {
    doSomething("1")
    doSomething("1", param2 = "2")
    doSomething(param2 = "1", param1 = "2")
    }
kotlin跟java可以随便调用吗
  1. kotlin可以直接在.kt文件中写变量写方法等。

    Test.kt文件如下

    1
    2
    3
    4
    5
    var v1: Int = 10
    fun printlen(str: String): String {
    println("stringValue: $str")
    return str
    }

    java中调用(文件名+Kt是编译之后的class名,getV1跟setV1是编译过程中自动生成的)

    1
    2
    3
    4
    5
    //调用方法
    TestKt.printlen("111");
    //使用变量
    TestKt.getV1();
    TestKt.setV1(2);
  1. kotlin匿名内部类 ,关键字object+类名{ xxx }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Kt {
    //定义匿名内部类
    object Inner{
    fun sayHello(){
    println("hello")
    }
    }

    fun test(){
    //kotlin 调用
    Inner.sayHello()
    }
    }

    public void test(){
    //java调用
    Kt.Inner.INSTANCE.sayHello();
    }

    从java的调用方式可以看出来匿名内部类是单例的。

  1. kotlin使用java中Class对象,kotlin中Class是KClass

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //J是一个java类
    fun test(clazz: Class<J>) {
    println(clazz.simpleName)
    }
    //Kt是一个kotlin类
    fun test1(clazz: KClass<Kt>){
    println(clazz.simpleName)
    }
    //调用
    test(J::class.java)
    test1(Kt::class)
  2. in在kotlin中是关键字,如果调用的java常量名字是in需要加

    1
    J.'in'
新手常见问题
  1. kotlin中默认的只有基础类型,没有封装类型
  2. kotlin是空安全的,调用返回值是String的java方法时需要用String?来接受返回值,不能用var。var接受的话会自动推导成String!,如果用var接受,当方法返回值是null的时候,可能会有空安全问题(比如调用.length())。
  3. kotlin中没有静态变量跟静态方法

第二章 函数跟lambda闭包

函数特性
  1. 函数参数可以有默认值

  2. 如果只有一个语句可以如下

    1
    fun test() = print("hello")
嵌套函数

函数中可以声明函数

1
2
3
4
5
6
7
8
9
10
fun test3(){
var count =3
fun say(){
if(count >0){
println("hello")
count--
}
}
say()
}
扩展函数
  1. 语法

    1
    2
    3
    4
    5
    6
    7
    //扩展java中的File,注意this
    fun File.printFileName() = print(this.name)

    fun main() {
    val file = File("./kotlin_study.iml")
    file.printFileName()
    }
  2. 使用场景

    • 用于扩展第三方的类
    • 用于扩展java的类
    • 。。。感觉就是很强大
  3. 注意点

    • 扩展函数都是静态的(public static final)
    • 不具有重载的特性(看上去是对象在调用,实际是类在调用)

设计模式--策略 观察者 装饰

策略模式
场景

算法簇:不同级别的用户,支付级别不同。

设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface IPay{
void pay();
}

class VipPay implements IPay;

class CustomPay implements IPay;


class User:{

IPay pay;

void setPay(IPay pay);

void pay(){

pay.pay();

}
}
角色
  1. 策略接口
  2. 策略实现
  3. 使用策略的上下文(对应上面例子例子中有需要支付功能的用户)
观察者模式
场景

订阅场景:粉丝时刻关注明星的动态。

设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Subject{  // 明星
ArrayList<Observer> observers;

regist(Observer o);
unregist(Observer o);
notifyAll(Message msg){
for each : observers
observer.onChange(msg);
}
}


interface Observer{
void onChange(Message msg);
}

class fan1 implements Observer{
void onChange(Message msg);
}

class fan2 implements Observer{
void onChange(Message msg);
}
角色

被观察者:subject

观察者:(接口以及实现)

装饰者模式
场景

不用继承扩展原有类的功能:男生->有车的男生-> 有房子的男生。。。

java I/0流:

  • InputStream(实体基类)
  • FileInputStream(实体的实现)
  • FilterInputStream(装饰器基类)
  • BufferedInputStream(装饰器实现)
设计
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
interface Man{
void getDec();
}

class NormalMan implements Man{
void getDec(){
print "normal man";
}
}

class Decorator implements Man{
Man man;
public ManDecorator(Man man){
this.man=man;
}

void getDec(){
man.getDec();
}
}

class CarDecorator extends Decorator{
public CarDecorator(Man man){
super(man);
}
void getDec(){
super.getDec();
print "have car";
}
}

class HouseDecorator extends Decorator{
public CarDecorator(Man man){
super(man);
}
void getDec(){
super.getDec();
print "have house";
}
}

使用:

public static void main(String[] args){
Man man = new NormalMan();
//车子装饰
Man man1 = new CarDecorator(man);
//房子装饰
Man man2 = new HouseDecorator(man1);

man2.getDec();
}
角色
  1. 实体的基类:对应man
  2. 实体的实现:对应normalMan
  3. 装饰器的基类(实现了实体基类):对应decorator
  4. 装饰器的实现:对应CarDecorator. HouseDecorator;