尊重原创,转载注明出处,原文地址:http://www.cnblogs.com/cishengchongyan/p/6129971.html 

  本文将不会对netty中每个点分类讲解,而是一个服务端启动的代码走读,在这个过程中再去了解和学习,这也是博主自己的学习历程。下面开始正文~~~~

  众所周知,在写netty服务端应用的时候一般会有这样的启动代码:

  1. (代码一)
    1 EventLoopGroup bossGroup = new NioEventLoopGroup(1);
  2. EventLoopGroup workerGroup = new NioEventLoopGroup();
  3. try {
  4. ServerBootstrap bootStrap = new ServerBootstrap();
  5. bootStrap.group(bossGroup, workerGroup)
  6.   .channel(NioServerSocketChannel.class)
  7.   .childHandler(new WebsocketChatServerInitializer())
  8.   .option(ChannelOption.SO_BACKLOG, 128)
  9.   .childOption(ChannelOption.SO_KEEPALIVE, true);
  10.  
  11. ChannelFuture f = bootStrap.bind(port).sync();
  12. f.channel().closeFuture().sync();
  13. } finally {
  14. ...
  15. }

  本文将沿着这条主线来走读代码,但是在走读之前首先要先认识一下Netty中的reactor模式是怎么玩的。

  首先先借用Doug Lea在Scalable IO in Java中的经典的图示:

  

  这张图是经典的运用了多路复用的Reactor模式,也大致说明了在netty中各线程的工作模式,mainReactor负责处理客户端的请求,subReacor负责处理I/O的读写操作,同时还会有一些用户的线程,用于异步处理I/O数据,在整个过程中通过角色细化,有效地将线程资源充分利用起来,构建了一条无阻塞通道,最后将耗时的业务逻辑交由业务线程去处理。本文不会对reactor做过多的解读,而是结合netty的线程池模式来学习。

  回到刚刚的主题,在服务端启动的时候首先会new两个NioEventLoopGroup,一个叫bossGroup(boss线程池),一个叫workerGroup(worker线程池),而这两个就分别对应了上述的mainReactor和subReacor。接下来我们来看在new的过程中发生了什么。

  代码走到MultithreadEventLoopGroup的构造方法中:

  1. (代码二)
    1 public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup {
  2.  
  3. private static final int DEFAULT_EVENT_LOOP_THREADS;
  4.  
  5. static {
  6. DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
  7. "io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
  8.  
  9. if (logger.isDebugEnabled()) {
  10. logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
  11. }
  12. }
  13.  
  14. protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
  15. super(nThreads == 0? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args);
  16. }
  17. ...
  18. }

  

  可以看到如果参数传入了thread个数就取这个数目,如果没有传入就取可用处理器(CPU)个数的2倍。因此【代码一】中boss只有1个线程,而worker有2*cpu个数个线程。

  继续往下走到了核心代码MultithreadEventExecutorGroup中:

  1. (代码三)
    1 public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup {
  2.  
  3. private final EventExecutor[] children;
  4. private final AtomicInteger childIndex = new AtomicInteger();
  5. private final AtomicInteger terminatedChildren = new AtomicInteger();
  6. private final Promise<?> terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE);
  7. private final EventExecutorChooser chooser;
  8.  
  9. protected MultithreadEventExecutorGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
  10. if (nThreads <= 0) {
  11. throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
  12. }
  13.  
  14. if (threadFactory == null) {
  15. threadFactory = newDefaultThreadFactory();
  16. }
  17.  
  18. children = new SingleThreadEventExecutor[nThreads];
  19. if (isPowerOfTwo(children.length)) {
  20. chooser = new PowerOfTwoEventExecutorChooser();
  21. } else {
  22. chooser = new GenericEventExecutorChooser();
  23. }
  24.  
  25. for (int i = 0; i < nThreads; i ++) {
  26. boolean success = false;
  27. try {
  28. children[i] = newChild(threadFactory, args);
  29. success = true;
  30. } catch (Exception e) {
  31. // TODO: Think about if this is a good exception type
  32. throw new IllegalStateException("failed to create a child event loop", e);
  33. } finally {
  34. if (!success) {
  35. ... }
  36. }
  37. }
  38.  
  39. final FutureListener<Object> terminationListener = new FutureListener<Object>() {
  40. @Override
  41. public void operationComplete(Future<Object> future) throws Exception {
  42. if (terminatedChildren.incrementAndGet() == children.length) {
  43. terminationFuture.setSuccess(null);
  44. }
  45. }
  46. };
  47.  
  48. for (EventExecutor e: children) {
  49. e.terminationFuture().addListener(terminationListener);
  50. }
  51. }

  

  首先new一个线程工厂newDefaultThreadFactory,然后给变量children赋值【PS:children是线程执行器的集合,几个线程就会有几个EventExecutor。因此EventExecutor是Reactor模式中真正执行工作的对象,它继承自ScheduledExecutorService,所以应该明白它本质上是什么了吧】

  children是赋值new了给定线程数数量的SingleThreadEventExecutor,看其内部代码,SingleThreadEventExecutor构造方法:

  1. (代码四)
    1 public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor {
  2. ...
  3. private final EventExecutorGroup parent;
  4. private final Queue<Runnable> taskQueue;
  5. private final Thread thread;
  6. ...
  7. protected SingleThreadEventExecutor(
  8. EventExecutorGroup parent, ThreadFactory threadFactory, boolean addTaskWakesUp) {
  9.  
  10. if (threadFactory == null) {
  11. throw new NullPointerException("threadFactory");
  12. }
  13.  
  14. this.parent = parent;
  15. this.addTaskWakesUp = addTaskWakesUp;
  16.  
  17. thread = threadFactory.newThread(new Runnable() {
  18. @Override
  19. public void run() {
  20. boolean success = false;
  21. updateLastExecutionTime();
  22. try {
  23. SingleThreadEventExecutor.this.run();
  24. success = true;
  25. } catch (Throwable t) {
  26. logger.warn("Unexpected exception from an event executor: ", t);
  27. } finally {
  28. ...
  29. }
  30. }
  31. });
  32. threadProperties = new DefaultThreadProperties(thread);
  33. taskQueue = newTaskQueue();
  34. }
  35. ...
  36. }

  回到刚刚的主题(代码三),发现在children[i] = newChild(threadFactory, args);而newChild是抽象方法,由于最开始我们初始化的是NioEventLoopGroup,因此是在NioEventLoopGroup中调用的:

  1. (代码五)
    1 protected EventExecutor newChild(
  2. ThreadFactory threadFactory, Object... args) throws Exception {
  3. return new NioEventLoop(this, threadFactory, (SelectorProvider) args[0]);
  4. }

  因此相当于我们有多少个work或boss线程就有多少个NioEventLoop,而每一个NioEventLoop都绑定了一个selector。所以,相当于一个NioEventLoopGroup有自定义线程数量的NioEventLoop。

  【PS:EventLoopGroup顾名思义是EventLoop的group,即包含了一组EventGroup。在实际的业务处理中,EventLoopGroup会通过EventLoop next()方法选择一个 EventLoop,然后将实际的业务处理交给这个被选出的EventLoop去做。对于 NioEventLoopGroup来说,其真实功能都会交给EventLoopGroup去实现。】

  接下来我们重点去看一下EventLoop和EventLoopGroup,自己画了这一块的UML图来理一下类关系:

  可以看出,EventLoop也继承自EventLoopGroup,因此也是EventLoopGroup的一种。同时看到,这一堆类都实现自ScheduledExecutorService,那么大家应该理解EventLoop和EventLoopGroup本质上是什么东西了吧。这里先不铺展开,下文中在讲注册逻辑时会对EventLoopGroup做一个更详细的了解。

  我们先回到【代码五主线】,我们接下来继续看初始化逻辑:

  1. (代码六)
    1 NioEventLoop(NioEventLoopGroup parent, ThreadFactory threadFactory, SelectorProvider selectorProvider) {
  2. super(parent, threadFactory, false);
  3. if (selectorProvider == null) {
  4. throw new NullPointerException("selectorProvider");
  5. }
  6. provider = selectorProvider;
  7. selector = openSelector();
  8. }

  初始化NioEventLoop时调用了openSelector来打开当前操作系统中一个默认的selector实现。

  回到【代码一主线】,服务端初始化了boss和worker线程之后调用ServerBootstrap.group()来绑定两个线程池调度器。接下来调用ServerBootstrap.channel(NioServerSocketChannel.class)。这块逻辑很简单就是在bootstrap内部初始化了一个class类型是NioServerSocketChannel的ChannelFactory,【PS:ChannelFactory不会指定生产对象的具体类型,只要继承自Channel就可以了】。

  接下来,ServerBootstrap.childHandler()作用就是设置ChannelHandler来响应Channel的请求。一般这里都会设置抽象类ChannelInitializer,并且实现模板方法initChannel,在ChannelHandler注册(初始化)的时候会调用initChannel来完成ChannelPipeline的初始化。

  1. (代码七)
    1 public abstract class ChannelInitializer<C extends Channel> extends ChannelInboundHandlerAdapter {
  2.  
  3. protected abstract void initChannel(C ch) throws Exception;
  4.  
  5. @Override
  6. @SuppressWarnings("unchecked")
  7. public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
  8. initChannel((C) ctx.channel());
  9. ctx.pipeline().remove(this);
  10. ctx.fireChannelRegistered();
  11. }
  12. ...
  13. }

  关于ChannelHandler我们后面会做详细的介绍,这里只需要了解到此就可以了。

  回到【代码一主线】,接下来bootStrap.option()和childOption()分别是给boss线程和worder线程设置参数,这里先忽略。

  然后是绑定端口ChannelFuture f = bootStrap.bind(port);在这一步中不仅仅是绑定端口,实际上需要做大量的初始化工作。我们先看一下AbstractBootstrap中的核心代码:

  1. (代码八)
    1   private ChannelFuture doBind(final SocketAddress localAddress) {
  2. final ChannelFuture regFuture = initAndRegister();
  3. final Channel channel = regFuture.channel();
  4. if (regFuture.cause() != null) {
  5. return regFuture;
  6. }
  7.  
  8. if (regFuture.isDone()) {
  9. ChannelPromise promise = channel.newPromise();
  10. doBind0(regFuture, channel, localAddress, promise);
  11. return promise;
  12. } else {
  13. final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
  14. regFuture.addListener(new ChannelFutureListener() {
  15. @Override
  16. public void operationComplete(ChannelFuture future) throws Exception {
  17. Throwable cause = future.cause();
  18. if (cause != null) {
  19. promise.setFailure(cause);
  20. } else {
  21. promise.executor = channel.eventLoop();
  22. }
  23. doBind0(regFuture, channel, localAddress, promise);
  24. }
  25. });
  26. return promise;
  27. }
  28. }

  

  【代码八主线】首先是initAndRegister(),看一下代码:

  1. (代码九)
    1 final ChannelFuture initAndRegister() {
  2. final Channel channel = channelFactory().newChannel();
  3. try {
  4. init(channel);
  5. } catch (Throwable t) {
  6. channel.unsafe().closeForcibly();
  7. return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
  8. }
  9.  
  10. ChannelFuture regFuture = group().register(channel);
  11. if (regFuture.cause() != null) {
  12. if (channel.isRegistered()) {
  13. channel.close();
  14. } else {
  15. channel.unsafe().closeForcibly();
  16. }
  17. }
  18.  
  19. return regFuture;
  20. }

  

  首先调用工厂方法生成一个新Channel,我们刚刚说过,ChannelFactory不限定Channel的具体类型,而我们注册的是NioServerSocketChannel,那么这里生产的就是该类型的Channel,然后调用init(),具体实现在ServerBootstrap中:

  1. (代码十)
    1   @Override
  2. void init(Channel channel) throws Exception {
  3. final Map<ChannelOption<?>, Object> options = options();
  4. synchronized (options) {
  5. channel.config().setOptions(options);
  6. }
  7.  
  8. final Map<AttributeKey<?>, Object> attrs = attrs();
  9. synchronized (attrs) {
  10. for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
  11. @SuppressWarnings("unchecked")
  12. AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
  13. channel.attr(key).set(e.getValue());
  14. }
  15. }
  16.  
  17. ChannelPipeline p = channel.pipeline();
  18.  
  19. final EventLoopGroup currentChildGroup = childGroup;
  20. final ChannelHandler currentChildHandler = childHandler;
  21. final Entry<ChannelOption<?>, Object>[] currentChildOptions;
  22. final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
  23. synchronized (childOptions) {
  24. currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
  25. }
  26. synchronized (childAttrs) {
  27. currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
  28. }
  29.  
  30. p.addLast(new ChannelInitializer<Channel>() {
  31. @Override
  32. public void initChannel(Channel ch) throws Exception {
  33. ChannelPipeline pipeline = ch.pipeline();
  34. ChannelHandler handler = handler();
  35. if (handler != null) {
  36. pipeline.addLast(handler);
  37. }
  38. pipeline.addLast(new ServerBootstrapAcceptor(
  39. currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
  40. }
  41. });
  42. }

  在init()中做了大致这么几件事:1,配置channel的option;2,配置channel的attr;3,ChannelPipeline增加两个Handler,一个是bootstrap中的私有handler,一个是ServerBootstrapAcceptor(这个Handler用于接收客户连接后设置其初始化参数)。

  【代码九主线】完成了init之后调用EventLoopGroup.register(channel)完成了channel的注册,实际上就是将channel注册到EventLoop中的selector上。这块我们可以了解一下其中的实现:

  先看一下EventLoopGroup接口:

  1. (代码十一)
    1 public interface EventLoopGroup extends EventExecutorGroup {
  2.  
  3. @Override
  4. EventLoop next();
  5.  
  6. ChannelFuture register(Channel channel);
  7.  
  8. ChannelFuture register(Channel channel, ChannelPromise promise);
  9. }

  其中next方法返回EventLoopGroup里的一个EventLoop,register用于注册Channel到EventLoop里。【PS:EventLoopGroup顾名思义是EventLoop的group,即包含了一组EventGroup。在实际的业务处理中,EventLoopGroup会通过EventLoop next()方法选择一个 EventLoop,然后将实际的业务处理交给这个被选出的EventLoop去做。对于 NioEventLoopGroup来说,其真实功能都会交给EventLoopGroup去实现】

  我们详细看一下register到底如何实现的,往下看是在SingleThreadEventLoop里实现了该方法:  

  1. (代码十二)
    1 public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor implements EventLoop {
  2.   ...
  3. @Override
  4. public ChannelFuture register(Channel channel) {
  5. return register(channel, new DefaultChannelPromise(channel, this));
  6. }
  7.  
  8. @Override
  9. public ChannelFuture register(final Channel channel, final ChannelPromise promise) {
  10. if (channel == null) {
  11. throw new NullPointerException("channel");
  12. }
  13. if (promise == null) {
  14. throw new NullPointerException("promise");
  15. }
  16.  
  17. channel.unsafe().register(this, promise);
  18. return promise;
  19. }
  20.   ...
  21. }

  注意,在这里调用了Channel的Unsafe内部类完成了注册,因此接下来的东西都是NIO中的 【PS:Unsafe是定义在Channel中的内部接口,是不会被用户代码调用到的,但是在channel的I/O操作中实际上都是由unsafe来完成的。Unsafe不论是接口还是类,都会定义到channel的内部(例如Channel接口中定义了Unsafe接口,AbstractChannel抽象类中定义了AbstractUnsafe抽象类),因此如果将nio类比为一个linux系统的话,那么unsafe就是其中的内核空间】

  具体的register操作是在AbstractUnsafe中完成,在register()方法中调用了模板方法,我们看一下在AbstractNioChannel中的核心实现:

  1. (代码十三)
    1 @Override
  2. protected void doRegister() throws Exception {
  3. boolean selected = false;
  4. for (;;) {
  5. try {
  6. selectionKey = javaChannel().register(eventLoop().selector, 0, this);
  7. return;
  8. } catch (CancelledKeyException e) {
  9. if (!selected) {
  10. eventLoop().selectNow();
  11. selected = true;
  12. } else {
  13. throw e;
  14. }
  15. }
  16. }
  17. }
  18. }

  

  这里实际上调用的是SelectableChannel中的register方法,作用就是将本channel注册到本channel的eventLoop的Selector中,那么问题又来了,什么是SelectableChannel?【PS:它实现Channel接口,代码注释说明其是一种可以被Selector使用用于多路复用的Channel,SelectableChannel可以通过 register方法将自己注册在Selector上,并提供其所关注的事件类型。因此,继承自SelectableChannel的Channel才可以真正和Selector打交道,例如ServerSocketChannel和SocketChannel】

  继续看其中的SelectableChannel中的实现:

  

  1. (代码十四)
    1 public final SelectionKey register(Selector sel, int ops,
  2. Object att)throws ClosedChannelException{
  3. synchronized (regLock) {
  4. ...
  5. SelectionKey k = findKey(sel);
  6. if (k != null) {
  7. k.interestOps(ops);
  8. k.attach(att);
  9. }
  10. if (k == null) {
  11. // New registration
  12. synchronized (keyLock) {
  13. if (!isOpen())
  14. throw new ClosedChannelException();
  15. k = ((AbstractSelector)sel).register(this, ops, att);
  16. addKey(k);
  17. }
  18. }
  19. return k;
  20. }
  21. }

  这里的逻辑很清晰,如果该channel有在Selector中注册过(有对应的SelectionKey),那么将这个key强制绑定到入参的Channel中(可能会导致之前绑定失效),如果该channel没有在Selector中注册过,那么调用AbstractSelector(底层JDK实现)该register逻辑。至此我们完成了register逻辑代码的走读。

  继续回归【代码八主线】,我们已经完成了initAndRegister逻辑,如果不出意外那么regFuture.isDone()将是true,接下来调用了doBind0():

  1. (代码十五)
    1   private static void doBind0(
  2. final ChannelFuture regFuture, final Channel channel,
  3. final SocketAddress localAddress, final ChannelPromise promise) {
  4.  
  5. channel.eventLoop().execute(new Runnable() {
  6. @Override
  7. public void run() {
  8. if (regFuture.isSuccess()) {
  9. channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
  10. } else {
  11. promise.setFailure(regFuture.cause());
  12. }
  13. }
  14. });
  15. }

  

  这里有必要了解一下ChannelPromise,它扩展了Promise和ChannelFuture,是一个可写入的ChannelFuture。我再在网上搜了很多资料里说它具备监听器的功能。但是我自己不这么认为,我们看Promise接口在future的基础上增加了setSuccess(), setFailure()这些方法,而ChannelFuture里success和failuer都是不可写的。为什么呢?从定义上来看,ChannelFuture本来就是异步执行的结果,既然已经异步了那么在返回的时候本来就无法确定其成功或者失败,而有的时候我们做校验或者写一些业务逻辑的时候可以确定其结果,因此我觉得ChannelPromise作为一个可写的ChannelFuture是对其的一个补充,可以标记异步任务成功或者失败,因此它是netty异步框架中实际使用的异步执行结果。在这里调用channel.bind(localAddress, promise);作用很明确就是给该channel绑定端口,然后该方法会立即返回一个ChannelPromise(不论这个实际的异步操作有没有做完)。一般用法也是这样的,方法定义时返回值都是ChannelFuture,而实现时实际返回的都是ChannelPromise。

  最后给立即返回的这个ChannelFuture添加一个listener。netty中有两种方式获取异步执行的真正结果,一种是调用老祖宗Future的get方法来获取(阻塞等待),一种是添加listener(异步回调),netty中推荐使用第二种方式,在整个的netty异步框架中也大量使用了这种方式。刚刚添加的那个listener的作用是:如果注册失败了,那么就关闭该Channel。最后bind返回异步的ChannelPromise,完成整个bind流程。

  至此【代码一主线】走读完毕,我们大致浏览了一遍server端bootstrap启动流程。

  最后大致总结一下服务端启动的主流程:

  1. 初始化boss和worker线程调度器NioEventLoopGroup,打开其中的Selector对象并配置相关参数。
  2. ServerBootstrap绑定这两个NioEventLoopGroup。
  3. 为server端确定绑定Channel的class类型(即将要使用什么类型),在本文的例子中绑定的是NioServerSocketChannel,实质上只是初始化ChannelFactory。(此时还没有初始化该Channel,也没有为Selector注册该Channel)。
  4. 初始化用户定义的ChannelInitializer,也就是在ChannelPipeline中添加用户自己的ChannelHandler(此时还没有注册,只是初始化变量而已)。
  5. 调用bind(port)启动监听,整个bind的过程非常复杂,做了最核心的初始化工作:

    1) ChannelFactory生成核心的NioServerSocketChannel实例,为该Channel初始化参数,然后为NioServerSocketChannel的pipeline中再添加两个netty框架的Handler。

    2) 将NioServerSocketChannel实例绑定到boss线程调度器的Selector中,此时boss线程被激活并开始接受I/O请求,同时所有的Pipeline中的Handler也会完成注册。

    3) 异步为NioServerSocketChannel绑定注册的端口。

  

  至此,ServerBootstrap启动完毕,开始接收I/O请求。本文大致走读了一遍服务端启动的代码,在走读的过程中对一些概念进行解读,相信大家在大脑中对netty的基本成员已经有了一个轮廓。那么服务端启动之后,netty是如何接收并分发socket请求,pipeline中又是如何组织并调用handler,以及boss和worker如何协同工作将在下一篇博客中进行解读。

  

  

  

  

(二)Netty源码学习笔记之服务端启动的更多相关文章

  1. Netty 源码(一)服务端启动

    Netty 源码(一)服务端启动 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) ServerBootstap 创建时序图如 ...

  2. (三)Netty源码学习笔记之boss线程处理流程

    尊重原创,转载注明出处,原文地址:http://www.cnblogs.com/cishengchongyan/p/6160194.html  本文我们将先从NioEventLoop开始来学习服务端的 ...

  3. (一)Netty源码学习笔记之概念解读

    尊重原创,转载注明出处,原文地址:http://www.cnblogs.com/cishengchongyan/p/6121065.html  博主最近在做网络相关的项目,因此有契机学习netty,先 ...

  4. Netty源码学习笔记

    1.ByteBuf

  5. 【Netty源码学习】DefaultChannelPipeline(三)

    上一篇博客中[Netty源码学习]ChannelPipeline(二)我们介绍了接口ChannelPipeline的提供的方法,接下来我们分析一下其实现类DefaultChannelPipeline具 ...

  6. Underscore.js 源码学习笔记(上)

    版本 Underscore.js 1.9.1 一共 1693 行.注释我就删了,太长了… 整体是一个 (function() {...}());  这样的东西,我们应该知道这是一个 IIFE(立即执行 ...

  7. Netty源码学习系列之4-ServerBootstrap的bind方法

    前言 今天研究ServerBootstrap的bind方法,该方法可以说是netty的重中之重.核心中的核心.前两节的NioEventLoopGroup和ServerBootstrap的初始化就是为b ...

  8. JUC源码学习笔记2——AQS共享和Semaphore,CountDownLatch

    本文主要讲述AQS的共享模式,共享和独占具有类似的套路,所以如果你不清楚AQS的独占的话,可以看我的<JUC源码学习笔记1> 主要参考内容有<Java并发编程的艺术>,< ...

  9. Spring 源码学习笔记10——Spring AOP

    Spring 源码学习笔记10--Spring AOP 参考书籍<Spring技术内幕>Spring AOP的实现章节 书有点老,但是里面一些概念还是总结比较到位 源码基于Spring-a ...

随机推荐

  1. jQuery遍历checkbox

    $("input[type='checkbox']").each(function(){ var value = $(this).val(); //获得值 $(this).attr ...

  2. 第一次源码分析: 图片加载框架Picasso源码分析

    使用: Picasso.with(this) .load("http://imgstore.cdn.sogou.com/app/a/100540002/467502.jpg") . ...

  3. 怎么用XMind辅助电商工作

    随着电商的迅猛发展,突出了电商运营管理的重要性.能不能做好电商,就要看他的管理做的好不好,全面不全面!电商的突破与进步,离不开全方位的协同管理.之前,小编有给大家介绍过电商量化管理的四大利器:XMin ...

  4. Android基础总结(九)

    多媒体概念(了解) 文字.图片.音频.视频 计算机图片大小的计算(掌握) 图片大小 = 图片的总像素 * 每个像素占用的大小 单色图:每个像素占用1/8个字节 16色图:每个像素占用1/2个字节 25 ...

  5. LAMP(1) 在VirtualBox里安装Ubuntu Server

    问题0.虚拟机中安装lamp环境 问题解决: 来自百度经验 问题1. 用putty远程登陆linux系统,显示network error connection refused 问题解决 问题2. my ...

  6. 小猪cms模块继承

    TextAction  继承 UserAction 继承 BaseAction 继承 Action TextAction 路径 PigCms\Lib\Action\User UserAction 路径 ...

  7. bzoj3506 [Cqoi2014]排序机械臂

    bzoj3506 此题是一道比较简单的spaly题目. 用splay维护序列,将每个点排到对应的位置之后删除,这样比较容易区间翻转. 我的指针写法在洛谷上AC了,但在bzoj上RE. #include ...

  8. SVD的几何意义,以及在去噪,推荐系统中的应用

    很多文章说到奇异值分解的时候总是大概罗列下它的功能,并没有对功能及物理意义进行过多的阐述,现在我来对奇异值进行整理一下. 一 奇异值分解 对任意的矩阵A∈Fmn,rank(A)=r(矩阵的秩),总可以 ...

  9. 如何解决Maven和SBT下载Jar包太慢

    国内:如何解决Maven和SBT下载Jar包太慢 Maven 远程仓库 <mirror> <id>ui</id> <mirrorOf>central&l ...

  10. mybatis : trim标签, “等于==”经验, CDATA标签 ,模糊查询CONCAT,LIKE

    一.My Batis trim标签有点类似于replace效果. trim 属性, prefix:前缀覆盖并增加其内容 suffix:后缀覆盖并增加其内容 prefixOverrides:前缀判断的条 ...