zoukankan      html  css  js  c++  java
  • 19.go语言基础学习(下)——2019年12月16日

    2019年12月16日16:57:04

    5.接口

    2019年11月01日15:56:09

    5.1 duck typing

    1.

    image-20191101160041371

    2.

    image-20191101160535039

    接口

    3.介绍

    Go 语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现。

    而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。

    编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现。

    每个接口类型由数个方法组成。接口的形式代码如下:

    type 接口类型名 interface{
        方法名1( 参数列表1 ) 返回值列表1
        方法名2( 参数列表2 ) 返回值列表2
        …
    }
    

    对各个部分的说明:

    • 接口类型名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer,有关闭功能的接口叫 Closer 等。
    • 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
    • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略,例如:
    type writer interface{
        Write([]byte) error
    }
    

    3.接口由使用者定义

    image-20191101160723791

    4.

    image-20191101174144581

    接口的值类型

    5.type assertion 类型断言

    判断r的真实类型,是不是mock.Retriever类型实现的。

    r.(*mock.Retriever)

    r.(type)

    // Type assertion
    	if mockRetriever, ok := r.(*mock.Retriever); ok {
    		fmt.Println(mockRetriever.Contents)
    	} else {
    		fmt.Println("r is not a mock retriever")
    	}
    
    	fmt.Println(
    		"Try a session with mockRetriever")
    	fmt.Println(session(&mockRetriever))
    }
    

    image-20191103130020382

    6.interface里一般有类型和值

    fmt.println("%T %v ",r,r)

    func inspect(r Retriever) {
      
    	fmt.Println("Inspecting", r)
    	fmt.Printf(" > Type:%T Value:%v
    ", r, r)
    	fmt.Print(" > Type switch: ")
      
    	switch v := r.(type) {
    	case *mock.Retriever:
    		fmt.Println("Contents:", v.Contents)
    	case *real.Retriever:
    		fmt.Println("UserAgent:", v.UserAgent)
    	}
    	fmt.Println()
    }
    
    

    7.接口变量里有什么

    image-20191103131212162

    8.接口变量自带指针

    9.接口变量同样采用值传递,几乎不需要使用接口的指针

    10.指针接受者实现只能以指针方式使用;值接受者都可以

    var r Retriever
    
    	mockRetriever := mock.Retriever{
    		Contents: "this is a fake imooc.com"}
    	r = &mockRetriever
    	inspect(r)
    
    	r = &real.Retriever{
    		UserAgent: "Mozilla/5.0",
    		TimeOut:   time.Minute,
    	}
    

    11.查看接口类型

    interface{}支持任何类型

    type Queue []interface{}
    
    func (q *Queue) Push(v interface{}) {
    	*q=append(*q,v)
    
    }
    

    Type Assertion

    Type Switch

    2019年11月03日

    5.2 接口的组合

    2019年11月03日13:33:08

    1.新定义一个接口——为了组合

    type Poster interface {
    	Post(url string,
    		form map[string]string) string
    }
    
    func post(poster Poster) {
    	poster.Post(url,
    		map[string]string{
    			"name":   "ccmouse",
    			"course": "golang",
    		})
    }
    

    2.定义一个组合接口

    type RetrieverPoster interface {
    	Retriever
    	Poster
    }
    

    3.mockRetriever接口实现了Post方法

    //mockretriever.go
    func (r *Retriever) Post(url string,
    	form map[string]string) string {
    	r.Contents = form["contents"]
    	return "ok"
    }
    
    
    //main.go
    func session(s RetrieverPoster) string {
    	s.Post(url, map[string]string{
    		"contents": "another faked imooc.com",
    	})
    	return s.Get(url)
    }
    
    
    func main() {
    	var r Retriever
    
    	mockRetriever := mock.Retriever{
    		Contents: "this is a fake imooc.com"}
    	r = &mockRetriever
    	inspect(r)
    
    	r = &real.Retriever{
    		UserAgent: "Mozilla/5.0",
    		TimeOut:   time.Minute,
    	}
    	inspect(r)
    
    	fmt.Println(
    		"Try a session with mockRetriever")
    	fmt.Println(session(&mockRetriever))
    }
    

    6.函数式编程

    2019年11月03日14:53:11

    1.

    image-20191103150018372
    
    #### 2.闭包
    
    <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8kwses02tj313u0u0h1k.jpg" alt="image-20191103150946598" style="zoom:25%;" />
    
    #### 3.匿名函数
    
    Go语言支持匿名函数,即在需要使用函数时再定义函数,匿名函数没有函数名只有函数体,函数可以作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式传递,这与C语言的回调函数比较类似,不同的是,Go语言支持随时在代码里定义匿名函数。
    
    **定义一个匿名函数**
    
    匿名函数的定义格式如下:
    
    

    func(参数列表)(返回参数列表){
    函数体
    }

    
    匿名函数的定义就是没有名字的普通函数定义。
    
    **在定义时调用匿名函数**
    
    匿名函数可以在声明后调用,例如:
    
    ```go
    func(data int) {    
    	fmt.Println(
    	"hello", data)
    }(100)
    

    注意第3行} (100)

    将匿名函数赋值给变量

    匿名函数可以被赋值,例如:

    // 将匿名函数体保存到f()中
    f := func(data int) {
        fmt.Println("hello", data)
    }
    
    // 使用f()调用
    f(100)
    

    匿名函数的用途非常广泛,它本身就是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。

    4.闭包的记忆效应

    被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。

    累加器的实现:

    package main
    import (
        "fmt"
    )
    // 提供一个值, 每次调用函数会指定对值进行累加
    func Accumulate(value int) func() int {
        // 返回一个闭包
        return func() int {
            // 累加
            value++
            // 返回一个累加值
            return value
        }
    }
    func main() {
        // 创建一个累加器, 初始值为1
        accumulator := Accumulate(1)
        // 累加1并打印
        fmt.Println(accumulator())
        fmt.Println(accumulator())
        // 打印累加器的函数地址
        fmt.Printf("%p
    ", accumulator)
        // 创建一个累加器, 初始值为1
        accumulator2 := Accumulate(10)
        // 累加1并打印
        fmt.Println(accumulator2())
        // 打印累加器的函数地址
        fmt.Printf("%p
    ", accumulator2)
    }
    

    代码说明如下:

    • 第 8 行,累加器生成函数,这个函数输出一个初始值,调用时返回一个为初始值创建的闭包函数。
    • 第 11 行,返回一个闭包函数,每次返回会创建一个新的函数实例。
    • 第 14 行,对引用的 Accumulate 参数变量进行累加,注意 value 不是第 11 行匿名函数定义的,但是被这个匿名函数引用,所以形成闭包。
    • 第 17 行,将修改后的值通过闭包的返回值返回。
    • 第 24 行,创建一个累加器,初始值为 1,返回的 accumulator 是类型为 func()int 的函数变量。
    • 第 27 行,调用 accumulator() 时,代码从 11 行开始执行匿名函数逻辑,直到第 17 行返回。
    • 第 32 行,打印累加器的函数地址。

    对比输出的日志发现 accumulator 与 accumulator2 输出的函数地址不同,因此它们是两个不同的闭包实例。

    7.错误处理和资源管理

    7.1 defer调用

    2019年11月03日16:00:50

    1.defer调用来实现资源管理

    2.确保调用在函数结束时发生

    3.参数在defer语句时计算

    4.defer列表为后进先出

    5.何时使用defer调用

    Open/Close

    Lock/unlock

    PrintHeader/PrintFooter

    6.源码

    package main
    
    import (
    	"fmt"
    	"os"
    	"u2pppw/src/functional/fib"
    
    	"bufio"
    
    
    )
    
    func tryDefer() {
    	for i := 0; i < 100; i++ {
    		defer fmt.Println(i)
    		if i == 30 {
    			// Uncomment panic to see
    			// how it works with defer
    			// panic("printed too many")
    		}
    	}
    }
    
    
    func writeFile(filename string) {
    	file, err := os.OpenFile(filename,
    		os.O_EXCL|os.O_CREATE|os.O_WRONLY, 0666)
    
    	if err != nil {
    		if pathError, ok := err.(*os.PathError); !ok {
    			panic(err)
    		} else {
    			fmt.Printf("%s, %s, %s
    ",
    				pathError.Op,
    				pathError.Path,
    				pathError.Err)
    		}
    		return
    	}
    	defer file.Close()
    
    	writer := bufio.NewWriter(file)
    	defer writer.Flush()
    
    	f := fib.Fibonacci()
    	for i := 0; i < 20; i++ {
    		fmt.Fprintln(writer, f())
    	}
    }
    
    func main() {
    	tryDefer()
    	writeFile("fib.txt")
    }
    
    

    7.2 错误处理概念

    2019年11月03日16:57:12

    1.为了防止程序挂掉——错误处理

    	if err != nil {
    		if pathError, ok := err.(*os.PathError); !ok {
    			panic(err)
    		} else {
    			fmt.Printf("%s, %s, %s
    ",
    				pathError.Op,
    				pathError.Path,
    				pathError.Err)
    		}
    		return
    	}
    

    服务器统一出错处理

    1.如何实现统一的错误处理逻辑

    2.起一个服务端读取内容

    读取方法

    const prefix = "/list/"
    
    func HandleFileList(writer http.ResponseWriter,
    	request *http.Request) error {
    	fmt.Println()
    	if strings.Index(
    		request.URL.Path, prefix) != 0 {
    		return userError(
    			fmt.Sprintf("path %s must start "+
    				"with %s",
    				request.URL.Path, prefix))
    	}
    	path := request.URL.Path[len(prefix):]
    	file, err := os.Open(path)
    	if err != nil {
    		return err
    	}
    	defer file.Close()
    
    	all, err := ioutil.ReadAll(file)
    	if err != nil {
    		return err
    	}
    
    	writer.Write(all)
    	return nil
    }
    
    

    处理错误的包装方法——使用函数式编程

    type appHandler func(writer http.ResponseWriter,
    	request *http.Request) error
    
    func errWrapper(
    	handler appHandler) func(
    	http.ResponseWriter, *http.Request) {
    	return func(writer http.ResponseWriter,
    		request *http.Request) {
    		// panic
    		defer func() {
    			if r := recover(); r != nil {
    				log.Printf("Panic: %v", r)
    				http.Error(writer,
    					http.StatusText(http.StatusInternalServerError),
    					http.StatusInternalServerError)
    			}
    		}()
    
    		err := handler(writer, request)
    
    		if err != nil {
    			log.Printf("Error occurred "+
    				"handling request: %s",
    				err.Error())
    
    			// user error
    			if userErr, ok := err.(userError); ok {
    				http.Error(writer,
    					userErr.Message(),
    					http.StatusBadRequest)
    				return
    			}
    
    			// system error
    			code := http.StatusOK
    			switch {
    			case os.IsNotExist(err):
    				code = http.StatusNotFound
    			case os.IsPermission(err):
    				code = http.StatusForbidden
    			default:
    				code = http.StatusInternalServerError
    			}
    			http.Error(writer,
    				http.StatusText(code), code)
    		}
    	}
    }
    

    main方法

    func main() {
    	http.HandleFunc("/",
    		errWrapper(filelisting.HandleFileList))
    
    	err := http.ListenAndServe(":8888", nil)
    	if err != nil {
    		panic(err)
    	}
    }
    
    

    3.源码

    main.go

    package main
    
    import (
    	"log"
    	"net/http"
    	_ "net/http/pprof"
    	"os"
    	"u2pppw/src/errhandling/filelistingserver/filelisting"
    )
    
    type appHandler func(writer http.ResponseWriter,
    	request *http.Request) error
    
    func errWrapper(
    	handler appHandler) func(
    	http.ResponseWriter, *http.Request) {
    	return func(writer http.ResponseWriter,
    		request *http.Request) {
    		// panic
    		defer func() {
    			if r := recover(); r != nil {
    				log.Printf("Panic: %v", r)
    				http.Error(writer,
    					http.StatusText(http.StatusInternalServerError),
    					http.StatusInternalServerError)
    			}
    		}()
    
    		err := handler(writer, request)
    
    		if err != nil {
    			log.Printf("Error occurred "+
    				"handling request: %s",
    				err.Error())
    
    			// user error
    			if userErr, ok := err.(userError); ok {
    				http.Error(writer,
    					userErr.Message(),
    					http.StatusBadRequest)
    				return
    			}
    
    			// system error
    			code := http.StatusOK
    			switch {
    			case os.IsNotExist(err):
    				code = http.StatusNotFound
    			case os.IsPermission(err):
    				code = http.StatusForbidden
    			default:
    				code = http.StatusInternalServerError
    			}
    			http.Error(writer,
    				http.StatusText(code), code)
    		}
    	}
    }
    
    type userError interface {
    	error
    	Message() string
    }
    
    func main() {
    	http.HandleFunc("/",
    		errWrapper(filelisting.HandleFileList))
    
    	err := http.ListenAndServe(":8888", nil)
    	if err != nil {
    		panic(err)
    	}
    }
    
    

    handle.go

    package filelisting
    
    import (
    	"fmt"
    	"io/ioutil"
    	"net/http"
    	"os"
    	"strings"
    )
    
    const prefix = "/list/"
    
    type userError string
    
    func (e userError) Error() string {
    	return e.Message()
    }
    
    func (e userError) Message() string {
    	return string(e)
    }
    
    func HandleFileList(writer http.ResponseWriter,
    	request *http.Request) error {
    	fmt.Println()
    	if strings.Index(
    		request.URL.Path, prefix) != 0 {
    		return userError(
    			fmt.Sprintf("path %s must start "+
    				"with %s",
    				request.URL.Path, prefix))
    	}
    	path := request.URL.Path[len(prefix):]
    	file, err := os.Open(path)
    	if err != nil {
    		return err
    	}
    	defer file.Close()
    
    	all, err := ioutil.ReadAll(file)
    	if err != nil {
    		return err
    	}
    
    	writer.Write(all)
    	return nil
    }
    
    

    7.3panic和recover

    2019年11月04日14:00:22

    1. panic

    1.停止当前函数执行

    2.一直向上返回,执行每一层的defer

    3.如果没有遇见recover,程序退出

    2. recover

    1.仅在defer调用中使用

    defer func() {
    		err:=recover()
    	}()
    

    2.获取panic的值

    如果无法处理,就可以重新panic

    package main
    
    import (
    	"fmt"
    )
    
    func tryRecover() {
    	defer func() {
    		r := recover()
    		if r == nil {
    			fmt.Println("Nothing to recover. " +
    				"Please try uncomment errors " +
    				"below.")
    			return
    		}
    		if err, ok := r.(error); ok {
    			fmt.Println("Error occurred:", err)
    		} else {
    			panic(fmt.Sprintf(
    				"I don't know what to do: %v", r))
    		}
    	}()
    
    	// Uncomment each block to see different panic
    	// scenarios.
    	// Normal error
    	//panic(errors.New("this is an error"))
    
    	// Division by zero
    	//b := 0
    	//a := 5 / b
    	//fmt.Println(a)
    
    	// Causes re-panic
    	panic(123)
    }
    
    func main() {
    	tryRecover()
    }
    
    

    image-20191104140851073

    8.goroutine

    2019年11月04日14:49:18

    1.并发编程

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func main() {
    	for i := 0; i < 1000; i++ {
    		go func(i int) {
    			for {
    				fmt.Printf("Hello from "+
    					"goroutine %d
    ", i)
    			}
    		}(i)
    	}
    	time.Sleep(time.Minute)
    }
    

    2.协程 Coroutine

    轻量级线程

    非抢占式多任务处理,由协程主动交出控制权

    多个协程可能在一个或多个线程上运行

    func main() {
    
    	var a [10]int
    	for i:=0;i<10;i++{
    		go func(i int){
    			for{
    				//fmt.Println("hello %d
    ",i)
    				a[i]++
    				runtime.Gosched()
    			}
    		}(i)
    	}
    
    	time.Sleep(time.Millisecond)
    	fmt.Println(a)
    	
    }
    

    3.任何函数只需加上go就能送给调度器运行

    4.不需要在定义时区分时都是异步函数

    5.调度器在合适的点进行切换

    6.使用-race来检测数据访问冲突

    7.goroutine可能的切换点

    I/O,select

    channel

    等待锁

    函数调用

    runtime,Gosched()

    image-20191104152620188

    9.channel

    2019年11月04日15:30:28

    1.介绍

    Go语言中的通道(channel)是一种特殊的类型。在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。

    通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。

    2.声明通道类型

    通道本身需要一个类型进行修饰,就像切片类型需要标识元素类型。通道的元素类型就是在其内部传输的数据类型,声明如下:

    var 通道变量 chan 通道类型
    
    • 通道类型:通道内的数据类型。
    • 通道变量:保存通道的变量。

    chan 类型的空值是 nil,声明后需要配合 make 后才能使用。

    3.创建通道

    通道是引用类型,需要使用 make 进行创建,格式如下:

    通道实例 := make(chan 数据类型)
    
    • 数据类型:通道内传输的元素类型。
    • 通道实例:通过make创建的通道句柄。

    请看下面的例子:

    ch1 := make(chan int)                 // 创建一个整型类型的通道
    ch2 := make(chan interface{})         // 创建一个空接口类型的通道, 可以存放任意格式
    type Equip struct{ /* 一些字段 */ }
    ch2 := make(chan *Equip)             // 创建Equip指针类型的通道, 可以存放*Equip
    

    4.发送数据

    // 创建一个空接口通道
    ch := make(chan interface{})
    // 将0放入通道中
    ch <- 0
    // 将hello字符串放入通道中
    ch <- "hello"
    

    10.beego介绍及搭建

    2019年12月06日13:28:50

    1.介绍

    • Beego是一个能够快速开发Go应用程序的HTTP框架, 它可以用来迅速的开发API, 网络APP(网站)和后端服务
    • Beego是一个MVC的框架

    beego有八个模块,分别是

    1. cache
    2. config:
    3. context:
    4. httplibs: curl函数
    5. logs:
    6. orm
    7. session
    8. toolbox

    2.优势

    beego是一个类似tornado的Go应用框架,采用了RESTFul的方式来实现应用框架,是一个超轻量级的框架,主要有如下的特点:

    • 支持MVC的方式,用户只需要关注逻辑,实现对应method的方法即可
    • 支持websocket,通过自定义Handler实现集成sockjs等方式实现
    • 支持自定义路由,支持各种方式的路由,正则、语意均支持,类似sinatra
    • session集成,支持memory、file、redis、mysql等存储
    • 表单处理自动化解析,用户可以很方便的获取数据
    • 日志分级系统,用户可以很方便的调试和应用日志记录
    • 自定义配置文件,支持ini格式的文本配置,可以方便的在系统中调参数
    • 采用了Go内置的模板,集成实现了很多Web开发中常用的函数

    3.环境搭建

    beego 的安装是典型的 Go 安装包的形式:

    go get github.com/astaxie/beego
    

    Go 升级,通过该方式用户可以升级 beego 框架,强烈推荐该方式:

    go get -u github.com/astaxie/beego
    

    源码下载升级,用户访问 https://github.com/astaxie/beego ,下载源码,然后覆盖到 $GOPATH/src/github.com/astaxie/beego 目录,然后通过本地执行安装就可以升级了:

    go install  github.com/astaxie/beego
    

    bee 工具简介

    bee 工具是一个为了协助快速开发 beego 项目而创建的项目,通过 bee 您可以很容易的进行 beego 项目的创建、热编译、开发、测试、和部署。

    bee 工具的安装

    您可以通过如下的方式安装 bee 工具:

    go get github.com/beego/bee
    

    安装完之后,bee 可执行文件默认存放在 $GOPATH/bin 里面,所以您需要把 $GOPATH/bin 添加到您的环境变量中,才可以进行下一步。

    new 命令

    new 命令是新建一个 Web 项目,我们在命令行下执行 bee new <项目名> 就可以创建一个新的项目。但是注意该命令必须在 $GOPATH/src 下执行。最后会在 $GOPATH/src 相应目录下生成如下目录结构的项目:

    bee new myproject
    [INFO] Creating application...
    /gopath/src/myproject/
    /gopath/src/myproject/conf/
    /gopath/src/myproject/controllers/
    /gopath/src/myproject/models/
    /gopath/src/myproject/static/
    /gopath/src/myproject/static/js/
    /gopath/src/myproject/static/css/
    /gopath/src/myproject/static/img/
    /gopath/src/myproject/views/
    /gopath/src/myproject/conf/app.conf
    /gopath/src/myproject/controllers/default.go
    /gopath/src/myproject/views/index.tpl
    /gopath/src/myproject/main.go
    13-11-25 09:50:39 [SUCC] New application successfully created!
    myproject
    ├── conf
    │   └── app.conf
    ├── controllers
    │   └── default.go
    ├── main.go
    ├── models
    ├── routers
    │   └── router.go
    ├── static
    │   ├── css
    │   ├── img
    │   └── js
    ├── tests
    │   └── default_test.go
    └── views
        └── index.tpl
    
    8 directories, 4 files
    

    api 命令

    上面的 new 命令是用来新建 Web 项目,不过很多用户使用 beego 来开发 API 应用。所以这个 api 命令就是用来创建 API 应用的,执行命令之后如下所示:

    bee api apiproject
    create app folder: /gopath/src/apiproject
    create conf: /gopath/src/apiproject/conf
    create controllers: /gopath/src/apiproject/controllers
    create models: /gopath/src/apiproject/models
    create tests: /gopath/src/apiproject/tests
    create conf app.conf: /gopath/src/apiproject/conf/app.conf
    create controllers default.go: /gopath/src/apiproject/controllers/default.go
    create tests default.go: /gopath/src/apiproject/tests/default_test.go
    create models object.go: /gopath/src/apiproject/models/object.go
    create main.go: /gopath/src/apiproject/main.go
    

    这个项目的目录结构如下:

    apiproject
    ├── conf
    │   └── app.conf
    ├── controllers
    │   └── object.go
    │   └── user.go
    ├── docs
    │   └── doc.go
    ├── main.go
    ├── models
    │   └── object.go
    │   └── user.go
    ├── routers
    │   └── router.go
    └── tests
        └── default_test.go
    

    从上面的目录我们可以看到和 Web 项目相比,少了 static 和 views 目录,多了一个 test 模块,用来做单元测试的。

    同时,该命令还支持一些自定义参数自动连接数据库创建相关 model 和 controller:
    bee api [appname] [-tables=""] [-driver=mysql] [-conn="root:@tcp(127.0.0.1:3306)/test"]
    如果 conn 参数为空则创建一个示例项目,否则将基于链接信息链接数据库创建项目。

    run 命令

    我们在开发 Go 项目的时候最大的问题是经常需要自己手动去编译再运行,bee run 命令是监控 beego 的项目,通过 fsnotify监控文件系统。但是注意该命令必须在 $GOPATH/src/appname 下执行。
    这样我们在开发过程中就可以实时的看到项目修改之后的效果:

    bee run
    13-11-25 09:53:04 [INFO] Uses 'myproject' as 'appname'
    13-11-25 09:53:04 [INFO] Initializing watcher...
    13-11-25 09:53:04 [TRAC] Directory(/gopath/src/myproject/controllers)
    13-11-25 09:53:04 [TRAC] Directory(/gopath/src/myproject/models)
    13-11-25 09:53:04 [TRAC] Directory(/gopath/src/myproject)
    13-11-25 09:53:04 [INFO] Start building...
    13-11-25 09:53:16 [SUCC] Build was successful
    13-11-25 09:53:16 [INFO] Restarting myproject ...
    13-11-25 09:53:16 [INFO] ./myproject is running...
    

    我们打开浏览器就可以看到效果 http://localhost:8080/

  • 相关阅读:
    Linux安装MySQL5.7.25
    Linux配置jdk
    SpringBoot junit 测试
    SpringBoot jasypt 对配置文件项加密
    关于Hibernate级联操作的总结
    GROUP BY 和 ORDER BY一起使用时,要注意的问题!
    web.xml 中的listener、 filter、servlet 加载顺序及其详解
    Hibernate对象的生命周期(补充)
    hibernate--持久对象的生命周期介绍
    开发属于你自己的标签库!
  • 原文地址:https://www.cnblogs.com/oneapple/p/12050549.html
Copyright © 2011-2022 走看看