Golang之mirco框架部分浅析
在实习中使用 micro
框架,但是挺多不懂的,看了部分源码搞懂了一些,还是有一些比较复杂没搞懂。
第一部分:初始化 service 并修改端口
main.go
// waitgroup is a handler wrapper which adds a handler to a sync.WaitGroup
func waitgroup(wg *util.WaitGroupWrapper) server.HandlerWrapper {
return func(h server.HandlerFunc) server.HandlerFunc {
return func(ctx context.Context, req server.Request, rsp interface{}) error {
wg.Add(1)
defer wg.Done()
return h(ctx, req, rsp)
}
}
}
func main() {
// ...
var wg util.WaitGroupWrapper
var service micro.Service
// 这里在下面给出解释
service = micro.NewService(
// 这里返回了四个函数闭包,同时也是Option类型,NewService的参数是可变长度的Option数组
micro.Name("xxx"),
micro.WrapHandler(waitgroup(&wg)),
micro.RegisterTTL(30*time.Second),
micro.RegisterInterval(10*time.Second),
)
// 先对 service 初始化
service.Init()
// 因为在 go run *.go 的时候回传入参数 --server_address=":8880" 这个时候可以这样将传入的参数取出,之后给webservice使用
addr := service.Server().Options().Address
// 将这个的端口赋为默认地址,即:0
service.Server().Init(server.Address(server.DefaultAddress))
// ...
}
go-micro.go
type Service interface {
Init(...Option)
Options() Options
Client() client.Client
Server() server.Server
Run() error
String() string
}
type Option func(*Options)
// NewService creates and returns a new Service based on the packages within.
func NewService(opts ...Option) Service {
return newService(opts...)
}
这里定义了一个 function type
,一开始对这个不太熟悉,看了 这篇文章 之后懂了点。定义这个 Option
之后的初始化操作就是将 Option
当做参数,传入到 NewXXX
中,在这些 NewXXX
方法又会遍历传入的 []Option
数组,然后调用这个 Option
在里面套一个 Init()
方法或者直接对变量进行赋值来完成对参数 *Options
的部分变量的初始化。
service.go
func newService(opts ...Option) Service {
// newOptions 的操作在下面的 options.go 文件中
options := newOptions(opts...)
// 包装client,添加一些信息
options.Client = &clientWrapper{
options.Client,
metadata.Metadata{
HeaderPrefix + "From-Service": options.Server.Options().Name,
},
}
return &service{
opts: options,
}
}
type service struct {
opts Options
once sync.Once
}
func (s *service) Init(opts ...Option) {
// process options
for _, o := range opts {
o(&s.opts)
}
// 能保证once只执行一次,无论你是否更换once.Do(xx)这里的方法,这个sync.Once块只会执行一次。
s.once.Do(func() {
// save user action
action := s.opts.Cmd.App().Action
// 下面注释给出 cmd.App().Action 的初始化
// func newCmd(opts ...Option) Cmd {
// options := Options{
// Broker: &broker.DefaultBroker,
// Client: &client.DefaultClient,
// Registry: ®istry.DefaultRegistry,
// Server: &server.DefaultServer,
// Selector: &selector.DefaultSelector,
// Transport: &transport.DefaultTransport,
// Brokers: DefaultBrokers,
// Clients: DefaultClients,
// Registries: DefaultRegistries,
// Selectors: DefaultSelectors,
// Servers: DefaultServers,
// Transports: DefaultTransports,
// }
// ...
// cmd := new(cmd)
// cmd.opts = options
// cmd.app = cli.NewApp()
// ...
// cmd.app.Action = func(c *cli.Context) {} 这里
// ...
// return cmd
// set service action
s.opts.Cmd.App().Action = func(c *cli.Context) {
// set register interval
if i := time.Duration(c.GlobalInt("register_interval")); i > 0 {
s.opts.RegisterInterval = i * time.Second
}
// user action
action(c)
}
// Initialise the command flags, overriding new service
_ = s.opts.Cmd.Init(
cmd.Broker(&s.opts.Broker),
cmd.Registry(&s.opts.Registry),
cmd.Transport(&s.opts.Transport),
cmd.Client(&s.opts.Client),
cmd.Server(&s.opts.Server),
)
})
}
options.go
func newOptions(opts ...Option) Options {
opt := Options {
Broker: broker.DefaultBroker,
Cmd: cmd.DefaultCmd,
Client: client.DefaultClient,
Server: server.DefaultServer,
Registry: registry.DefaultRegistry,
Transport: transport.DefaultTransport,
Context: context.Background(),
}
// 第一个值是下标,第二个值(o)是相当于value,这里是一个Option类型的函数。主要就是对 opt 赋值初始化
for _, o := range opts {
o(&opt)
}
return opt
}
// 下面四个函数的返回一个闭包,然后在 newOptions 的时候调用完成初始化操作
// Name of the service
func Name(n string) Option {
return func(o *Options) {
o.Server.Init(server.Name(n))
}
}
// RegisterTTL specifies the TTL to use when registering the service
func RegisterTTL(t time.Duration) Option {
return func(o *Options) {
o.Server.Init(server.RegisterTTL(t))
}
}
// RegisterInterval specifies the interval on which to re-register
func RegisterInterval(t time.Duration) Option {
return func(o *Options) {
o.RegisterInterval = t
}
}
// WrapHandler adds a handler Wrapper to a list of options passed into the server
func WrapHandler(w ...server.HandlerWrapper) Option {
return func(o *Options) {
var wrappers []server.Option
for _, wrap := range w {
wrappers = append(wrappers, server.WrapHandler(wrap))
}
// Init once
// 根据这些option初始化server
o.Server.Init(wrappers...)
}
}
第二部分:订阅Topic
main.go
func subEvent(ctx context.Context, stats *proto.Event) error {
golog.Logf("Received event %+v\n", stats)
return nil
}
func main() {
// ...
// subEvent是自定义函数
micro.RegisterSubscriber("TOPIC", service.Server(), subEvent)
// ...
}
go-micro.go
func RegisterSubscriber(topic string, s server.Server, h interface{}, opts ...server.SubscriberOption) error {
// 调用 server.go 的函数
return s.Subscribe(s.NewSubscriber(topic, h, opts...))
}
server.go
var (
DefaultServer Server = newRpcServer()
)
func NewSubscriber(topic string, h interface{}, opts ...SubscriberOption) Subscriber {
return DefaultServer.NewSubscriber(topic, h, opts...)
}
func Subscribe(s Subscriber) error {
// 这里的 DefaultServer 是 RpcServer,调用下面 rpc_server.go 的函数
return DefaultServer.Subscribe(s)
}
rpc_server.go
// 调用 subscriber.go 中的函数
func (s *rpcServer) NewSubscriber(topic string, sb interface{}, opts ...SubscriberOption) Subscriber {
return newSubscriber(topic, sb, opts...)
}
subscriber.go
type handler struct {
method reflect.Value
reqType reflect.Type
ctxType reflect.Type
}
type subscriber struct {
topic string
rcvr reflect.Value
typ reflect.Type
subscriber interface{}
handlers []*handler
endpoints []*registry.Endpoint
opts SubscriberOptions
}
func newSubscriber(topic string, sub interface{}, opts ...SubscriberOption) Subscriber {
// 利用传进来的 SubcriberOption 初始化,这个和第一部分的类似,略过
var options SubscriberOptions
for _, o := range opts {
o(&options)
}
var endpoints []*registry.Endpoint
var handlers []*handler
// 利用反射去对传进来的 interface{} 操作,我在前面最开始传的是 subEvent 函数
if typ := reflect.TypeOf(sub); typ.Kind() == reflect.Func {
// 因为我传入的是 Func 类型,所以会进入这里
h := &handler{
// 获取方法
method: reflect.ValueOf(sub),
}
// 判断参数个数,并分别给对应位置的类型赋值
switch typ.NumIn() {
case 1:
h.reqType = typ.In(0)
case 2:
h.ctxType = typ.In(0)
h.reqType = typ.In(1)
}
handlers = append(handlers, h)
endpoints = append(endpoints, ®istry.Endpoint{
Name: "Func",
Request: extractSubValue(typ),
Metadata: map[string]string{
"topic": topic,
"subscriber": "true",
},
})
} else {
hdlr := reflect.ValueOf(sub)
name := reflect.Indirect(hdlr).Type().Name()
for m := 0; m < typ.NumMethod(); m++ {
method := typ.Method(m)
h := &handler{
method: method.Func,
}
switch method.Type.NumIn() {
case 2:
h.reqType = method.Type.In(1)
case 3:
h.ctxType = method.Type.In(1)
h.reqType = method.Type.In(2)
}
handlers = append(handlers, h)
endpoints = append(endpoints, ®istry.Endpoint{
Name: name + "." + method.Name,
Request: extractSubValue(method.Type),
Metadata: map[string]string{
"topic": topic,
"subscriber": "true",
},
})
}
}
return &subscriber{
rcvr: reflect.ValueOf(sub),
typ: reflect.TypeOf(sub),
topic: topic,
subscriber: sub,
handlers: handlers,
endpoints: endpoints,
opts: options,
}
}
rpc_server.go
func (s *rpcServer) Subscribe(sb Subscriber) error {
sub, ok := sb.(*subscriber)
if !ok {
return fmt.Errorf("invalid subscriber: expected *subscriber")
}
if len(sub.handlers) == 0 {
return fmt.Errorf("invalid subscriber: no handler functions")
}
// 验证是否合法
if err := validateSubscriber(sb); err != nil {
return err
}
s.Lock()
defer s.Unlock()
_, ok = s.subscribers[sub]
if ok {
return fmt.Errorf("subscriber %v already exists", s)
}
// 置为nil,nil也是值
s.subscribers[sub] = nil
return nil
}
第三部分:运行 service
main.go
// 启动订阅服务
if err := service.Run(); err != nil {
log.Fatal(err)
}
调用 service.go
的 Run()
方法。
service.go
func (s *service) Run() error {
if err := s.Start(); err != nil {
return err
}
// start reg loop
ex := make(chan bool)
// 看下面
go s.run(ex)
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
select {
// wait on kill signal
case <-ch:
// wait on context cancel
case <-s.opts.Context.Done():
}
// exit reg loop
close(ex)
return s.Stop()
}
func (s *service) Start() error {
// 遍历在开始前的需要执行的函数并执行
for _, fn := range s.opts.BeforeStart {
if err := fn(); err != nil {
return err
}
}
// 调用 rpc_server 的 Start()
if err := s.opts.Server.Start(); err != nil {
return err
}
// 调用 rpc_server 的 Register()
if err := s.opts.Server.Register(); err != nil {
return err
}
// 遍历在开始后的需要执行的函数并执行
for _, fn := range s.opts.AfterStart {
if err := fn(); err != nil {
return err
}
}
return nil
}
func (s *service) run(exit chan bool) {
if s.opts.RegisterInterval <= time.Duration(0) {
return
}
t := time.NewTicker(s.opts.RegisterInterval)
for {
select {
case <-t.C:
err := s.opts.Server.Register()
if err != nil {
log.Log("service run Server.Register error: ", err)
}
case <-exit:
t.Stop()
return
}
}
}
func (s *service) Stop() error {
var gerr error
for _, fn := range s.opts.BeforeStop {
if err := fn(); err != nil {
gerr = err
}
}
// 取消注册
if err := s.opts.Server.Deregister(); err != nil {
return err
}
// 停止
if err := s.opts.Server.Stop(); err != nil {
return err
}
for _, fn := range s.opts.AfterStop {
if err := fn(); err != nil {
gerr = err
}
}
return gerr
}
Start()
方法会调用 server.go
的 Start()
方法
server.go
func (s *rpcServer) Start() error {
registerDebugHandler(s)
config := s.Options()
// 返回一个监听该地址的 listener
ts, err := config.Transport.Listen(config.Address)
if err != nil {
return err
}
log.Logf("Listening on %s", ts.Addr())
s.Lock()
s.opts.Address = ts.Addr()
s.Unlock()
// 启动一条协程接收信息,内部调用了 net 包的 Accept,看下面的 accept()
go ts.Accept(s.accept)
go func() {
// wait for exit
ch := <-s.exit
// wait for requests to finish
if wait(s.opts.Context) {
s.wg.Wait()
}
// close transport listener
ch <- ts.Close()
// disconnect the broker
config.Broker.Disconnect()
}()
// TODO: subscribe to cruft
return config.Broker.Connect()
}
func (s *rpcServer) accept(sock transport.Socket) {
defer func() {
// close socket
sock.Close()
if r := recover(); r != nil {
log.Log("panic recovered: ", r)
log.Log(string(debug.Stack()))
}
}()
for {
var msg transport.Message
// 接收信息存放到msg中
if err := sock.Recv(&msg); err != nil {
return
}
// we use this Timeout header to set a server deadline
to := msg.Header["Timeout"]
// we use this Content-Type header to identify the codec needed
ct := msg.Header["Content-Type"]
// 将原来的 Content-Type 转换为可以被 rpc 使用的 Type
cf, err := s.newCodec(ct)
// TODO: needs better error handling
if err != nil {
sock.Send(&transport.Message{
Header: map[string]string{
"Content-Type": "text/plain",
},
Body: []byte(err.Error()),
})
return
}
// 返回一个新的 codec
codec := newRpcPlusCodec(&msg, sock, cf)
// strip our headers
hdr := make(map[string]string)
for k, v := range msg.Header {
hdr[k] = v
}
delete(hdr, "Content-Type")
delete(hdr, "Timeout")
ctx := metadata.NewContext(context.Background(), hdr)
// set the timeout if we have it
if len(to) > 0 {
if n, err := strconv.ParseUint(to, 10, 64); err == nil {
ctx, _ = context.WithTimeout(ctx, time.Duration(n))
}
}
// add to wait group
s.wg.Add(1)
defer s.wg.Done()
// TODO: needs better error handling
// 里面包括了许多方法:
// server.readRequest() // 从codec中读取请求
// server.sendResponse() // 最后会将response的信息通过socket发送
// service.call()
if err := s.rpc.serveRequest(ctx, codec, ct); err != nil {
log.Logf("Unexpected error serving request, closing socket: %v", err)
return
}
}
}
func (s *rpcServer) Register() error {
// parse address for host, port
config := s.Options()
var advt, host string
var port int
// check the advertise address first
// if it exists then use it, otherwise
// use the address
if len(config.Advertise) > 0 {
advt = config.Advertise
} else {
advt = config.Address
}
parts := strings.Split(advt, ":")
if len(parts) > 1 {
host = strings.Join(parts[:len(parts)-1], ":")
port, _ = strconv.Atoi(parts[len(parts)-1])
} else {
host = parts[0]
}
addr, err := addr.Extract(host)
if err != nil {
return err
}
// register service
node := ®istry.Node{
Id: config.Name + "-" + config.Id,
Address: addr,
Port: port,
Metadata: config.Metadata,
}
node.Metadata["transport"] = config.Transport.String()
node.Metadata["broker"] = config.Broker.String()
node.Metadata["server"] = s.String()
node.Metadata["registry"] = config.Registry.String()
s.RLock()
// Maps are ordered randomly, sort the keys for consistency
// 生成 handlerList
var handlerList []string
for n, e := range s.handlers {
// Only advertise non internal handlers
if !e.Options().Internal {
handlerList = append(handlerList, n)
}
}
sort.Strings(handlerList)
// 生成 subscriberList
var subscriberList []*subscriber
for e := range s.subscribers {
// Only advertise non internal subscribers
if !e.Options().Internal {
subscriberList = append(subscriberList, e)
}
}
sort.Slice(subscriberList, func(i, j int) bool {
return subscriberList[i].topic > subscriberList[j].topic
})
var endpoints []*registry.Endpoint
for _, n := range handlerList {
endpoints = append(endpoints, s.handlers[n].Endpoints()...)
}
for _, e := range subscriberList {
endpoints = append(endpoints, e.Endpoints()...)
}
s.RUnlock()
service := ®istry.Service{
Name: config.Name,
Version: config.Version,
Nodes: []*registry.Node{node},
Endpoints: endpoints,
}
s.Lock()
registered := s.registered
s.Unlock()
if !registered {
log.Logf("Registering node: %s", node.Id)
}
// create registry options
rOpts := []registry.RegisterOption{registry.RegisterTTL(config.RegisterTTL)}
if err := config.Registry.Register(service, rOpts...); err != nil {
return err
}
// already registered? don't need to register subscribers
if registered {
return nil
}
s.Lock()
defer s.Unlock()
s.registered = true
for sb, _ := range s.subscribers {
handler := s.createSubHandler(sb, s.opts)
var opts []broker.SubscribeOption
if queue := sb.Options().Queue; len(queue) > 0 {
opts = append(opts, broker.Queue(queue))
}
// 订阅
sub, err := config.Broker.Subscribe(sb.Topic(), handler, opts...)
if err != nil {
return err
}
// 放入之前设为 nil 的 map 里面
s.subscribers[sb] = []broker.Subscriber{sub}
}
return nil
}
第四部分:初始化WebService
main.go
// 对主页服务
// 自定义的Handler函数 serveHome
func serveHome(w http.ResponseWriter, r *http.Request) {
log.Println(r.URL)
if r.URL.Path != "/" {
http.Error(w, "Not found", http.StatusNotFound)
return
}
log.Println(r.URL.Path[1:])
http.ServeFile(w, r, "./view/login.html")
}
func main() {
// ...
// 初始化操作和前面类似,略过
webService := web.NewService(
web.Name("xxx"),
web.RegisterTTL(30*time.Second),
web.RegisterInterval(10*time.Second),
web.Address(addr),
web.Registry(service.Options().Registry),
)
// 部署静态资源,看下面
// http.FileServer()返回一个Handler,将 "" 目录(即当前目录)下的资源部署
webService.Handle("/view/", http.FileServer(http.Dir("")))
// 主页
// 自定义的Handler函数 serveHome
webService.HandleFunc("/", serveHome)
if err := webService.Run(); err != nil {
log.Fatal(err)
}
// ...
}
service.go
func (s *service) Handle(pattern string, handler http.Handler) {
var seen bool
for _, ep := range s.srv.Endpoints {
if ep.Name == pattern {
seen = true
break
}
}
if !seen {
s.srv.Endpoints = append(s.srv.Endpoints, ®istry.Endpoint{
Name: pattern,
})
}
// 底层调用http包的方法
s.mux.Handle(pattern, handler)
}
func (s *service) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
var seen bool
for _, ep := range s.srv.Endpoints {
if ep.Name == pattern {
seen = true
break
}
}
if !seen {
s.srv.Endpoints = append(s.srv.Endpoints, ®istry.Endpoint{
Name: pattern,
})
}
s.mux.HandleFunc(pattern, handler)
}
net/http/server.go
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// HandlerFunc 实现了 ServeHTTP 方法,因此实现了Handler接口
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
if pattern == "" {
panic("http: invalid pattern")
}
if handler == nil {
panic("http: nil handler")
}
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
mux.m[pattern] = muxEntry{h: handler, pattern: pattern}
if pattern[0] != '/' {
mux.hosts = true
}
}
附无用Demo
sync.Once
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
for i := 0; i < 10; i++ {
once.Do(func() {
fmt.Println("once :", i)
})
}
}
输出:
once : 0
type function
package main
import (
"fmt"
)
type Fun func(s string)
func A(s string) {
fmt.Println("a : ", s)
}
func B(s string) {
fmt.Println("b : ", s)
}
func C(s string) {
fmt.Println("c : ", s)
}
func D(s string) {
fmt.Println("d : ", s)
}
func main() {
f := []Fun{A, B, C, D}
for k, v := range f {
fmt.Println(k)
v("233")
}
}
输出:
0
a : 233
1
b : 233
2
c : 233
3
d : 233
Golang之mirco框架部分浅析的更多相关文章
- 【GoLang】golang 微服务框架 go-kit
golang-Microservice Go kit - A toolkit for microservices kubernetes go-kit_百度搜索 Peter Bourgon谈使用Go和& ...
- Golang 网络爬虫框架gocolly/colly 四
Golang 网络爬虫框架gocolly/colly 四 爬虫靠演技,表演得越像浏览器,抓取数据越容易,这是我多年爬虫经验的感悟.回顾下个人的爬虫经历,共分三个阶段:第一阶段,09年左右开始接触爬虫, ...
- Golang 网络爬虫框架gocolly/colly 三
Golang 网络爬虫框架gocolly/colly 三 熟悉了<Golang 网络爬虫框架gocolly/colly一>和<Golang 网络爬虫框架gocolly/colly二& ...
- Golang 网络爬虫框架gocolly/colly 二 jQuery selector
Golang 网络爬虫框架gocolly/colly 二 jQuery selector colly框架依赖goquery库,goquery将jQuery的语法和特性引入到了go语言中.如果要灵活自如 ...
- Golang 网络爬虫框架gocolly/colly 一
Golang 网络爬虫框架gocolly/colly 一 gocolly是用go实现的网络爬虫框架,目前在github上具有3400+星,名列go版爬虫程序榜首.gocolly快速优雅,在单核上每秒可 ...
- Golang 网络爬虫框架gocolly/colly 五 获取动态数据
Golang 网络爬虫框架gocolly/colly 五 获取动态数据 gcocolly+goquery可以非常好地抓取HTML页面中的数据,但碰到页面是由Javascript动态生成时,用goque ...
- [Golang] GoConvey测试框架使用指南
GoConvey 是一款针对Golang的测试框架,可以管理和运行测试用例,同时提供了丰富的断言函数,并支持很多 Web 界面特性. GoConvey 网站 : http://smartystreet ...
- 流式处理框架storm浅析(下篇)
本文来自网易云社区 作者:汪建伟 举个栗子 1 实现的目标 设计一个系统,来实现对一个文本里面的单词出现的频率进行统计. 2 设计Topology结构: 这是一个简单的例子,topology也非常简单 ...
- golang的beego框架开发时出现的问题纪录
golang的beego框架开发时出现的问题纪录1.数据库并发时问题:[ORM]2017/02/20 23:44:05 -[Queries/default] - [FAIL / db.Query / ...
随机推荐
- MIT墙上的格言(如果你把任务留到最后一分钟,那么你一定能在一分钟内完成任务)
1,永远不能忘记傅立叶变换. Never far no can forget Fuliye changer. 2,盲目的研究者就像法拉第和麦克斯韦之间的电学家一样无所适从. Blind Eye's r ...
- springboot 使用日志
spring boot 默认使用日志打印到console 添加application.properties文件在src/main/resoures文件夹下 logging.file=my.log 将日 ...
- dedecms织梦出现“Upload filetype not allow”原因和解决方案
资源:武汉科技有限公司倍频猫 我最近升级DEDE5.6出现此问题后.和解决的方法. 假设有同学是按某些朋友用删除代码的方法解决这个问题的话,建议你修正此做法,毕竟这是有非常大安全风险的.而DEDE本身 ...
- 简化连接Buffer对象的过程
上述一大段代码仅只完成了一件事情,就是连接多个Buffer对象,而这种场景需求将会在多个地方发生,所以,采用一种更优雅的方式来完成该过程是必要的.笔者基于以上的代码封装出一个bufferhelper模 ...
- EF学习目录
EF EF性能优化 EF延迟加载LazyLoading EF相关报错 EF 事务 Entity相互关系 Entity种类(动态代理) DbContext Entity States Code Firs ...
- 将自定义控件加载到RichTextbox并进行交互
先前遇到一个问题,将自定义控件加载到RichTextbox后,无法触发自定义控件的各种事件,最终找到解决办法:设置RichTextbox的IsDocumentEnabled="True&qu ...
- https://www.jianshu.com/p/4da29fa310d2
wampserver显示红色.橙色的解决方案 拿笔的小鑫 关注 2016.10.15 14:38* 字数 2643 阅读 5083评论 0喜欢 3赞赏 1 </br></br&g ...
- Oracle报错:不是单组分组函数
报错:不是单组分组函数 实例:select sum(HWJZ) ,rq from JcChargeInfo 原因: 1.如果程序中使用了分组函数,则有两种情况可以使用: 程序中存在group by, ...
- 【JDK源码分析】String的存储区与不可变 专题
<Think in Java>中说:“关系操作符生成的是一个boolean结果,它们计算的是操作数的值之间的关系”. "=="判断的是两个对象的内存地址是否一样,适用于 ...
- PopupWindow设置动画效果
创建popupwindow的方法 Button menu; private void showPopupWindow() { //设置contentView float density = Densi ...