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

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


  • 首页

  • 归档

  • 搜索

用aiohttp强大的异步爬虫 aiohttp 介绍 aio

发表于 2018-05-17

看到现在网络上大多讲的都是requests、scrapy,却没有说到爬虫中的神器:aiohttp

aiohttp 介绍

aiohttp是什么,官网上有这样一句话介绍:Async HTTP client/server for asyncio and Python,翻译过来就是 asyncio和Python的异步HTTP客户端/服务器

主要特点是:

  1. 支持客户端和HTTP服务器。
  2. 无需使用Callback Hell即可支持Server WebSockets和Client WebSockets。
  3. Web服务器具有中间件,信号和可插拔路由。

emmmm,好吧,还是来看代码吧

Client example:

1
复制代码import aiohttpimport asyncioasync def fetch(session, url):    async with session.get(url) as response:        return await response.text()async def main():    async with aiohttp.ClientSession() as session:        html = await fetch(session, 'http://httpbin.org/headers')        print(html)loop = asyncio.get_event_loop()loop.run_until_complete(main())

output:

1
复制代码{"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate","Connection":"close","Host":"httpbin.org","User-Agent":"Python/3.6 aiohttp/3.2.1"}}

Server example:

1
复制代码from aiohttp import webasync def handle(request):    name = request.match_info.get('name', "Anonymous")    text = "Hello, " + name    return web.Response(text=text)app = web.Application()app.add_routes([web.get('/', handle),                web.get('/{name}', handle)])web.run_app(app)

output:

1
复制代码======== Running on http://0.0.0.0:8080 ========(Press CTRL+C to quit)

aiohttp 与 requests

去翻一下官方文档 Client Quickstart,让我感觉非常熟悉,很多用法和requests相似。

1
复制代码async with aiohttp.ClientSession() as session:    async with session.get('http://httpbin.org/get') as resp:        print(resp.status)        print(await resp.text())

首先,官方推荐使用ClientSession来管理会话,这不就是requests中的 session吗?用法也类似,使用session.get()去发送 get请求,返回的resp中就有我们所需要的数据了,用法也和 requests一样,text()文本, .json()直接打印返回的json数据,
headers什么的也一样,更多内容参考官方文档Response object

既然已经有requests了,那为什么还要说 aiohttp了?重点来了,aiohttp是异步的。在python3.5中,加入了 asyncio/await 关键字,使得回调的写法更加直观和人性化。而aiohttp是一个提供异步web服务的库, asyncio可以实现单线程并发IO操作。

requests写爬虫是同步的,是等待网页下载好才会执行下面的解析、入库操作,如果在下载网页时间太长会导致阻塞,使用 multiprocessing或者 threading加速爬虫也是一种方法。

我们现在使用的aiohttp是异步的,简单来说,就是不需要等待,你尽管去下载网页就好了,我不用傻傻的等待你完成才进行下一步,我还有别的活要干。这样就极大的提高了下载网页的效率。

另外,Scrapy也是异步的,是基于Twisted事件驱动的。在任何情况下,都不要写阻塞的代码。阻塞的代码包括:

  1. 访问文件、数据库或者Web
  2. 产生新的进程并需要处理新进程的输出,如运行shell命令
  3. 执行系统层次操作的代码,如等待系统队列

代码实例

这里是使用aiohttp的一个爬虫实例

1
复制代码import asyncioimport aiohttpfrom bs4 import BeautifulSoupimport loggingclass AsnycGrab(object):    def __init__(self, url_list, max_threads):        self.urls = url_list        self.results = {}        self.max_threads = max_threads    def __parse_results(self, url, html):        try:            soup = BeautifulSoup(html, 'html.parser')            title = soup.find('title').get_text()        except Exception as e:            raise e        if title:            self.results[url] = title    async def get_body(self, url):        async with aiohttp.ClientSession() as session:            async with session.get(url, timeout=30) as response:                assert response.status == 200                html = await response.read()                return response.url, html    async def get_results(self, url):        url, html = await self.get_body(url)        self.__parse_results(url, html)        return 'Completed'    async def handle_tasks(self, task_id, work_queue):        while not work_queue.empty():            current_url = await work_queue.get()            try:                task_status = await self.get_results(current_url)            except Exception as e:                logging.exception('Error for {}'.format(current_url), exc_info=True)    def eventloop(self):        q = asyncio.Queue()        [q.put_nowait(url) for url in self.urls]        loop = asyncio.get_event_loop()        tasks = [self.handle_tasks(task_id, q, ) for task_id in range(self.max_threads)]        loop.run_until_complete(asyncio.wait(tasks))        loop.close()if __name__ == '__main__':    async_example = AsnycGrab(['http://edmundmartin.com',               'https://www.udemy.com',               'https://github.com/',               'https://zhangslob.github.io/',               'https://www.zhihu.com/'], 5)    async_example.eventloop()    print(async_example.results)

需要注意的是,你需要时刻在你的代码中使用异步操作,你如果在代码中使用同步操作,爬虫并不会报错,但是速度可能会受影响。

其他异步库

因为爬虫不仅仅只有下载这块,还会有操作数据库,这里提供两个异步库:aioredis、 motor

1
复制代码import asyncioimport aioredisloop = asyncio.get_event_loop()async def go():    conn = await aioredis.create_connection(        'redis://localhost', loop=loop)    await conn.execute('set', 'my-key', 'value')    val = await conn.execute('get', 'my-key')    print(val)    conn.close()    await conn.wait_closed()loop.run_until_complete(go())# will print 'value'

文档:aioredis

1
复制代码import motor.motor_asyncioclient = motor.motor_asyncio.AsyncIOMotorClient('mongodb://localhost:27017')db = client['test_database']collection = db['test_collection']async def do_insert():    document = {'key': 'value'}    result = await db.test_collection.insert_one(document)    print('result %s' % repr(result.inserted_id))async def do_find_one():    document = await db.test_collection.find_one({'i': {'$lt': 1}})    pprint.pprint(document)

文档:motor

本文仅仅介绍了aiohttp作为Client的用法, 有兴趣的朋友可以去研究下作为Server的用法,同样很强大。

本文转载自: 掘金

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

Java面试题整理及参考答案 相关概念 数据类型相关 关于垃

发表于 2018-05-17

下列面试题都是在网上收集的,本人抱着学习的态度找了下参考答案,有不足的地方还请指正,更多精彩内容可以关注我的微信公众号:Java团长

相关概念

面向对象的三个特征

封装,继承,多态.这个应该是人人皆知.有时候也会加上抽象.

多态的好处

允许不同类对象对同一消息做出响应,即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用).主要有以下优点:

  1. 可替换性:多态对已存在代码具有可替换性.
  2. 可扩充性:增加新的子类不影响已经存在的类结构.
  3. 接口性:多态是超类通过方法签名,向子类提供一个公共接口,由子类来完善或者重写它来实现的.
  4. 灵活性.
  5. 简化性.

代码中如何实现多态

实现多态主要有以下三种方式:

  1. 接口实现
  2. 继承父类重写方法
  3. 同一类中进行方法重载

虚拟机是如何实现多态的

动态绑定技术(dynamic binding),执行期间判断所引用对象的实际类型,根据实际类型调用对应的方法.

接口的意义

接口的意义用三个词就可以概括:规范,扩展,回调.

抽象类的意义

抽象类的意义可以用三句话来概括:

  1. 为其他子类提供一个公共的类型
  2. 封装子类中重复定义的内容
  3. 定义抽象方法,子类虽然有不同的实现,但是定义时一致的

接口和抽象类的区别

比较 抽象类 接口
默认方法 抽象类可以有默认的方法实现 java 8之前,接口中不存在方法的实现.
— — —
实现方式 子类使用extends关键字来继承抽象类.如果子类不是抽象类,子类需要提供抽象类中所声明方法的实现. 子类使用implements来实现接口,需要提供接口中所有声明的实现.
构造器 抽象类中可以有构造器, 接口中不能
和正常类区别 抽象类不能被实例化 接口则是完全不同的类型
访问修饰符 抽象方法可以有public,protected和default等修饰 接口默认是public,不能使用其他修饰符
多继承 一个子类只能存在一个父类 一个子类可以存在多个接口
添加新方法 想抽象类中添加新方法,可以提供默认的实现,因此可以不修改子类现有的代码 如果往接口中添加新方法,则子类中需要实现该方法.

父类的静态方法能否被子类重写

不能.重写只适用于实例方法,不能用于静态方法,而子类当中含有和父类相同签名的静态方法,我们一般称之为隐藏.

什么是不可变对象

不可变对象指对象一旦被创建,状态就不能再改变。任何修改都会创建一个新的对象,如 String、Integer及其它包装类。

静态变量和实例变量的区别?

静态变量存储在方法区,属于类所有.实例变量存储在堆当中,其引用存在当前线程栈.

能否创建一个包含可变对象的不可变对象?

当然可以创建一个包含可变对象的不可变对象的,你只需要谨慎一点,不要共享可变对象的引用就可以了,如果需要变化时,就返回原对象的一个拷贝。最常见的例子就是对象中包含一个日期对象的引用.

java 创建对象的几种方式

  1. 采用new
  2. 通过反射
  3. 采用clone
  4. 通过序列化机制

前2者都需要显式地调用构造方法. 造成耦合性最高的恰好是第一种,因此你发现无论什么框架,只要涉及到解耦必先减少new的使用.

switch中能否使用string做参数

在idk 1.7之前,switch只能支持byte,short,char,int或者其对应的封装类以及Enum类型。从idk 1.7之后switch开始支持String.

switch能否作用在byte,long上?

可以用在byte上,但是不能用在long上.

String s1=”ab”,String s2=”a”+”b”,String s3=”a”,String s4=”b”,s5=s3+s4请问s5==s2返回什么?

返回false.在编译过程中,编译器会将s2直接优化为”ab”,会将其放置在常量池当中,s5则是被创建在堆区,相当于s5=new String(“ab”);

你对String对象的intern()熟悉么?

intern()方法会首先从常量池中查找是否存在该常量值,如果常量池中不存在则现在常量池中创建,如果已经存在则直接返回.
比如
String s1=”aa”;
String s2=s1.intern();
System.out.print(s1==s2);//返回true

Object中有哪些公共方法?

  1. equals()
  2. clone()
  3. getClass()
  4. notify(),notifyAll(),wait()
  5. toString

java当中的四种引用

强引用,软引用,弱引用,虚引用.不同的引用类型主要体现在GC上:

  1. 强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象
  2. 软引用:在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。
  3. 弱引用:具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象
  4. 虚引用:顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。

更多了解参见深入对象引用:

http://blog.csdn.net/dd864140130/article/details/49885811

WeakReference与SoftReference的区别?

这点在四种引用类型中已经做了解释,这里简单说明一下即可:
虽然 WeakReference 与 SoftReference 都有利于提高 GC 和 内存的效率,但是 WeakReference ,一旦失去最后一个强引用,就会被 GC 回收,而软引用虽然不能阻止被回收,但是可以延迟到 JVM 内存不足的时候。

为什么要有不同的引用类型

不像C语言,我们可以控制内存的申请和释放,在Java中有时候我们需要适当的控制对象被回收的时机,因此就诞生了不同的引用类型,可以说不同的引用类型实则是对GC回收时机不可控的妥协.有以下几个使用场景可以充分的说明:

  1. 利用软引用和弱引用解决OOM问题:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题.
  2. 通过软引用实现Java对象的高速缓存:比如我们创建了一Person的类,如果每次需要查询一个人的信息,哪怕是几秒中之前刚刚查询过的,都要重新构建一个实例,这将引起大量Person对象的消耗,并且由于这些对象的生命周期相对较短,会引起多次GC影响性能。此时,通过软引用和 HashMap 的结合可以构建高速缓存,提供性能.

java中==和eqauls()的区别,equals()和`hashcode的区别

==是运算符,用于比较两个变量是否相等,而equals是Object类的方法,用于比较两个对象是否相等.默认Object类的equals方法是比较两个对象的地址,此时和==的结果一样.换句话说:基本类型比较用==,比较的是他们的值.默认下,对象用==比较时,比较的是内存地址,如果需要比较对象内容,需要重写equal方法

equals()和hashcode()的联系

hashCode()是Object类的一个方法,返回一个哈希值.如果两个对象根据equal()方法比较相等,那么调用这两个对象中任意一个对象的hashCode()方法必须产生相同的哈希值.
如果两个对象根据eqaul()方法比较不相等,那么产生的哈希值不一定相等(碰撞的情况下还是会相等的.)

a.hashCode()有什么用?与a.equals(b)有什么关系

hashCode() 方法是相应对象整型的 hash 值。它常用于基于 hash 的集合类,如 Hashtable、HashMap、LinkedHashMap等等。它与 equals() 方法关系特别紧密。根据 Java 规范,使用 equal() 方法来判断两个相等的对象,必须具有相同的 hashcode。

将对象放入到集合中时,首先判断要放入对象的hashcode是否已经在集合中存在,不存在则直接放入集合.如果hashcode相等,然后通过equal()方法判断要放入对象与集合中的任意对象是否相等:如果equal()判断不相等,直接将该元素放入集合中,否则不放入.

有没有可能两个不相等的对象有相同的hashcode

有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。如果两个对象相等,必须有相同的hashcode 值,反之不成立.

可以在hashcode中使用随机数字吗?

不行,因为同一对象的 hashcode 值必须是相同的

a==b与a.equals(b)有什么区别

如果a 和b 都是对象,则 a==b 是比较两个对象的引用,只有当 a 和 b 指向的是堆中的同一个对象才会返回 true,而 a.equals(b) 是进行逻辑比较,所以通常需要重写该方法来提供逻辑一致性的比较。例如,String 类重写 equals() 方法,所以可以用于两个不同对象,但是包含的字母相同的比较。

3*0.1==0.3返回值是什么

false,因为有些浮点数不能完全精确的表示出来。

a=a+b与a+=b有什么区别吗?

+=操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,而a=a+b则不会自动进行类型转换.如:
byte a = 127;
byte b = 127;
b = a + b; // error : cannot convert from int to byte
b += a; // ok
(译者注:这个地方应该表述的有误,其实无论 a+b 的值为多少,编译器都会报错,因为
a+b 操作会将 a、b 提升为 int 类型,所以将 int 类型赋值给 byte 就会编译出错)

short s1= 1; s1 = s1 + 1; 该段代码是否有错,有的话怎么改?

有错误,short类型在进行运算时会自动提升为int类型,也就是说s1+1的运算结果是int类型.

short s1= 1; s1 += 1; 该段代码是否有错,有的话怎么改?

+=操作符会自动对右边的表达式结果强转匹配左边的数据类型,所以没错.

& 和 &&的区别

首先记住&是位操作,而&&是逻辑运算符.另外需要记住逻辑运算符具有短路特性,而&不具备短路特性.

1
2
3
4
5
6
7
8
9
10
11
复制代码public class Test{
   static String name;

   public static void main(String[] args){
       if(name!=null&userName.equals("")){
           System.out.println("ok");
       }else{
           System.out.println("erro");
       }
   }
}

以上代码将会抛出空指针异常.

一个.java文件内部可以有类?(非内部类)

只能有一个public公共类,但是可以有多个default修饰的类.

如何正确的退出多层嵌套循环.

  1. 使用标号和break;
  2. 通过在外层循环中添加标识符

内部类的作用

内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立.在单个外围类当中,可以让多个内部类以不同的方式实现同一接口,或者继承同一个类.创建内部类对象的时刻不依赖于外部类对象的创建.内部类并没有令人疑惑的”is-a”关系,它就像是一个独立的实体.

内部类提供了更好的封装,除了该外围类,其他类都不能访问

final,finalize和finally的不同之处

final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变。finalize 方法是在对象被回收之前调用的方法,给对象自己最后一个复活的机会,但是什么时候调用 finalize 没有保证。finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。

clone()是哪个类的方法?

java.lang.Cloneable 是一个标示性接口,不包含任何方法,clone 方法在 object 类中定义。并且需要知道 clone() 方法是一个本地方法,这意味着它是由 c 或 c++ 或 其他本地语言实现的。

深拷贝和浅拷贝的区别是什么?

浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。

深拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。

static都有哪些用法?

几乎所有的人都知道static关键字这两个基本的用法:静态变量和静态方法.也就是被static所修饰的变量/方法都属于类的静态资源,类实例所共享.

除了静态变量和静态方法之外,static也用于静态块,多用于初始化操作:

1
2
3
4
5
复制代码public calss PreCache{
   static{
       //执行相关操作
   }
}

此外static也多用于修饰内部类,此时称之为静态内部类.

最后一种用法就是静态导包,即import static.import static是在JDK 1.5之后引入的新特性,可以用来指定导入某个类中的静态资源,并且不需要使用类名.资源名,可以直接使用资源名,比如:

1
2
3
4
5
6
7
8
9
复制代码import static java.lang.Math.*;

public class Test{

   public static void main(String[] args){
       //System.out.println(Math.sin(20));传统做法
       System.out.println(sin(20));
   }
}

final有哪些用法

final也是很多面试喜欢问的地方,能回答下以下三点就不错了:
1.被final修饰的类不可以被继承
2.被final修饰的方法不可以被重写
3.被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变.
4.被final修饰的方法,JVM会尝试将其内联,以提高运行效率
5.被final修饰的常量,在编译阶段会存入常量池中.

回答出编译器对final域要遵守的两个重排序规则更好:
1.在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序.
2.初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序.

数据类型相关

java中int char,long各占多少字节?

类型 位数 字节数
short 2 16
— — —
int 4 32
long 8 64
float 4 32
double 8 64
char 2 16

64位的JVM当中,int的长度是多少?

Java 中,int 类型变量的长度是一个固定值,与平台无关,都是 32 位。意思就是说,在 32 位 和 64 位 的Java 虚拟机中,int 类型的长度是相同的。

int和Integer的区别

Integer是int的包装类型,在拆箱和装箱中,二者自动转换.int是基本类型,直接存数值,而integer是对象,用一个引用指向这个对象.

int 和Integer谁占用的内存更多?

Integer 对象会占用更多的内存。Integer是一个对象,需要存储对象的元数据。但是 int 是一个原始类型的数据,所以占用的空间更少。

String,StringBuffer和StringBuilder区别

String是字符串常量,final修饰;StringBuffer字符串变量(线程安全);
StringBuilder 字符串变量(线程不安全).

String和StringBuffer

String和StringBuffer主要区别是性能:String是不可变对象,每次对String类型进行操作都等同于产生了一个新的String对象,然后指向新的String对象.所以尽量不在对String进行大量的拼接操作,否则会产生很多临时对象,导致GC开始工作,影响系统性能.

StringBuffer是对对象本身操作,而不是产生新的对象,因此在有大量拼接的情况下,我们建议使用StringBuffer.

但是需要注意现在JVM会对String拼接做一定的优化:
String s=“This is only ”+”simple”+”test”会被虚拟机直接优化成String s=“This is only simple test”,此时就不存在拼接过程.

StringBuffer和StringBuilder

StringBuffer是线程安全的可变字符串,其内部实现是可变数组.StringBuilder是jdk 1.5新增的,其功能和StringBuffer类似,但是非线程安全.因此,在没有多线程问题的前提下,使用StringBuilder会取得更好的性能.

什么是编译器常量?使用它有什么风险?

公共静态不可变(public static final )变量也就是我们所说的编译期常量,这里的 public 可选的。实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变。这种方式存在的一个问题是你使用了一个内部的或第三方库中的公有编译时常量,但是这个值后面被其他人改变了,但是你的客户端仍然在使用老的值,甚至你已经部署了一个新的jar。为了避免这种情况,当你在更新依赖 JAR 文件时,确保重新编译你的程序。

java当中使用什么类型表示价格比较好?

如果不是特别关心内存和性能的话,使用BigDecimal,否则使用预定义精度的 double 类型。

如何将byte转为String

可以使用 String 接收 byte[] 参数的构造器来进行转换,需要注意的点是要使用的正确的编码,否则会使用平台默认编码,这个编码可能跟原来的编码相同,也可能不同。

可以将int强转为byte类型么?会产生什么问题?

我们可以做强制转换,但是Java中int是32位的而byte是8 位的,所以,如果强制转化int类型的高24位将会被丢弃,byte 类型的范围是从-128到128

关于垃圾回收

你知道哪些垃圾回收算法?

垃圾回收从理论上非常容易理解,具体的方法有以下几种:

  1. 标记-清除
  2. 标记-复制
  3. 标记-整理
  4. 分代回收
    更详细的内容参见深入理解垃圾回收算法:

http://blog.csdn.net/dd864140130/article/details/50084471

如何判断一个对象是否应该被回收

这就是所谓的对象存活性判断,常用的方法有两种:1.引用计数法;2:对象可达性分析.由于引用计数法存在互相引用导致无法进行GC的问题,所以目前JVM虚拟机多使用对象可达性分析算法.

简单的解释一下垃圾回收

Java 垃圾回收机制最基本的做法是分代回收。内存中的区域被划分成不同的世代,对象根据其存活的时间被保存在对应世代的区域中。一般的实现是划分成3个世代:年轻、年老和永久。内存的分配是发生在年轻世代中的。当一个对象存活时间足够长的时候,它就会被复制到年老世代中。对于不同的世代可以使用不同的垃圾回收算法。进行世代划分的出发点是对应用中对象存活时间进行研究之后得出的统计规律。一般来说,一个应用中的大部分对象的存活时间都很短。比如局部变量的存活时间就只在方法的执行过程中。基于这一点,对于年轻世代的垃圾回收算法就可以很有针对性.

调用System.gc()会发生什么?

通知GC开始工作,但是GC真正开始的时间不确定.

进程,线程相关

说说进程,线程,协程之间的区别

简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程.进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高.线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位.同一进程中的多个线程之间可以并发执行.

你了解守护线程吗?它和非守护线程有什么区别

程序运行完毕,jvm会等待非守护线程完成后关闭,但是jvm不会等待守护线程.守护线程最典型的例子就是GC线程

什么是多线程上下文切换

多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

创建两种线程的方式?他们有什么区别?

通过实现java.lang.Runnable或者通过扩展java.lang.Thread类.相比扩展Thread,实现Runnable接口可能更优.原因有二:

  1. Java不支持多继承.因此扩展Thread类就代表这个子类不能扩展其他类.而实现Runnable接口的类还可能扩展另一个类.
  2. 类可能只要求可执行即可,因此继承整个Thread类的开销过大.

Thread类中的start()和run()方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

怎么检测一个线程是否持有对象监视器

Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着”某条线程”指的是当前线程。

Runnable和Callable的区别

Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以方便获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务

什么导致线程阻塞

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面让我们逐一分析。

方法 说明
sleep() sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。 典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止
— —
suspend() 和 resume() 两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
yield() yield() 使当前线程放弃当前已经分得的CPU 时间,但不使当前线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程
wait() 和 notify() 两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用.

wait(),notify()和suspend(),resume()之间的区别

初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。上述的核心区别导致了一系列的细节上的区别。

首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用 任意对象的notify()方法则导致从调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。

其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException
异常。

wait() 和 notify() 方法的上述特性决定了它们经常和synchronized关键字一起使用,将它们和操作系统进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于 block 和wakeup 原语(这一对方法均声明为 synchronized)。它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。

关于 wait() 和 notify() 方法最后再说明两点:
第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。

第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。

谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 wait() 方法的调用都可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。

以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了 wait() 和 notify() 方法,因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效率较低,较容易出错。实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目的。

产生死锁的条件

1.互斥条件:一个资源每次只能被一个进程使用。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别

wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

wait()与sleep()的区别

关于这两者已经在上面进行详细的说明,这里就做个概括好了:

  • sleep()来自Thread类,和wait()来自Object类.调用sleep()方法的过程中,线程不会释放对象锁。而 调用 wait 方法线程会释放对象锁
  • sleep()睡眠后不出让系统资源,wait让其他线程可以占用CPU
  • sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒.而wait()需要配合notify()或者notifyAll()使用

为什么wait,nofity和nofityAll这些方法不放在Thread类当中

一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

怎么唤醒一个阻塞的线程

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

什么是多线程的上下文切换

多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

synchronized和ReentrantLock的区别

synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知
另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word.

FutureTask是什么

这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

一个线程如果出现了运行时异常怎么办?

如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放

Java当中有哪几种锁

  1. 自旋锁: 自旋锁在JDK1.6之后就默认开启了。基于之前的观察,共享数据的锁定状态只会持续很短的时间,为了这一小段时间而去挂起和恢复线程有点浪费,所以这里就做了一个处理,让后面请求锁的那个线程在稍等一会,但是不放弃处理器的执行时间,看看持有锁的线程能否快速释放。为了让线程等待,所以需要让线程执行一个忙循环也就是自旋操作。在jdk6之后,引入了自适应的自旋锁,也就是等待的时间不再固定了,而是由上一次在同一个锁上的自旋时间及锁的拥有者状态来决定
  2. 偏向锁: 在JDK1.之后引入的一项锁优化,目的是消除数据在无竞争情况下的同步原语。进一步提升程序的运行性能。 偏向锁就是偏心的偏,意思是这个锁会偏向第一个获得他的线程,如果接下来的执行过程中,改锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。偏向锁可以提高带有同步但无竞争的程序性能,也就是说他并不一定总是对程序运行有利,如果程序中大多数的锁都是被多个不同的线程访问,那偏向模式就是多余的,在具体问题具体分析的前提下,可以考虑是否使用偏向锁。
  3. 轻量级锁: 为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁

如何在两个线程间共享数据

通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的

如何正确的使用wait()?使用if还是while?

wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码:

1
2
3
4
5
复制代码synchronized (obj) {
   while (condition does not hold)
     obj.wait(); // (Releases lock, and reacquires on wakeup)
     ... // Perform action appropriate to condition
}

什么是线程局部变量ThreadLocal

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

ThreadLoal的作用是什么?

简单说ThreadLocal就是一种以空间换时间的做法在每个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了.

生产者消费者模型的作用是什么?

(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用
(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约

写一个生产者-消费者队列

可以通过阻塞队列实现,也可以通过wait-notify来实现.

使用阻塞队列来实现

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
复制代码//消费者
public class Producer implements Runnable{
   private final BlockingQueue<Integer> queue;

   public Producer(BlockingQueue q){
       this.queue=q;
   }

   @Override
   public void run() {
       try {
           while (true){
               Thread.sleep(1000);//模拟耗时
               queue.put(produce());
           }
       }catch (InterruptedException e){

       }
   }

   private int produce() {
       int n=new Random().nextInt(10000);
       System.out.println("Thread:" + Thread.currentThread().getId() + " produce:" + n);
        return n;
   }
}
//消费者
public class Consumer implements Runnable {
   private final BlockingQueue<Integer> queue;

   public Consumer(BlockingQueue q){
       this.queue=q;
   }

   @Override
   public void run() {
       while (true){
           try {
               Thread.sleep(2000);//模拟耗时
               consume(queue.take());
           }catch (InterruptedException e){

           }

       }
   }

   private void consume(Integer n) {
       System.out.println("Thread:" + Thread.currentThread().getId() + " consume:" + n);

   }
}
//测试
public class Main {

   public static void main(String[] args) {
       BlockingQueue<Integer> queue=new ArrayBlockingQueue<Integer>(100);
       Producer p=new Producer(queue);
       Consumer c1=new Consumer(queue);
       Consumer c2=new Consumer(queue);

       new Thread(p).start();
       new Thread(c1).start();
       new Thread(c2).start();
   }
}

使用wait-notify来实现

该种方式应该最经典,这里就不做说明了

如果你提交任务时,线程池队列已满,这时会发生什么

如果你使用的LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。

为什么要使用线程池

避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。

java中用到的线程调度算法是什么

抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

Thread.sleep(0)的作用是什么

由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

什么是CAS

CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功

什么是乐观锁和悲观锁

乐观锁:乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

悲观锁:悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

ConcurrentHashMap的并发度是什么?

ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?

ConcurrentHashMap的工作原理

ConcurrentHashMap在jdk 1.6和jdk 1.8实现原理是不同的.

jdk 1.6:

ConcurrentHashMap是线程安全的,但是与Hashtablea相比,实现线程安全的方式不同。Hashtable是通过对hash表结构进行锁定,是阻塞式的,当一个线程占有这个锁时,其他线程必须阻塞等待其释放锁。ConcurrentHashMap是采用分离锁的方式,它并没有对整个hash表进行锁定,而是局部锁定,也就是说当一个线程占有这个局部锁时,不影响其他线程对hash表其他地方的访问。
具体实现:ConcurrentHashMap内部有一个Segment

jdk 1.8

在jdk 8中,ConcurrentHashMap不再使用Segment分离锁,而是采用一种乐观锁CAS算法来实现同步问题,但其底层还是“数组+链表->红黑树”的实现。

CyclicBarrier和CountDownLatch区别

这两个类非常类似,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:

  • CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
  • CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
  • CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了

java中的++操作符线程安全么?

不是线程安全的操作。它涉及到多个指令,如读取变量值,增加,然后存储回内存,这个过程可能会出现多个线程交差

你有哪些多线程开发良好的实践?

  1. 给线程命名
  2. 最小化同步范围
  3. 优先使用volatile
  4. 尽可能使用更高层次的并发工具而非wait和notify()来实现线程通信,如BlockingQueue,Semeaphore
  5. 优先使用并发容器而非同步容器.
  6. 考虑使用线程池

关于volatile关键字

可以创建Volatile数组吗?

Java 中可以创建 volatile类型数组,不过只是一个指向数组的引用,而不是整个数组。如果改变引用指向的数组,将会受到volatile 的保护,但是如果多个线程同时改变数组的元素,volatile标示符就不能起到之前的保护作用了

volatile能使得一个非原子操作变成原子操作吗?

一个典型的例子是在类中有一个 long 类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。为什么?因为 Java 中读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量的读写是原子。

一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。volatile 修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的应用。简单的说,就是当你写一个 volatile 变量之前,Java
内存模型会插入一个写屏障(write barrier),读一个 volatile 变量之前,会插入一个读屏障(read barrier)。意思就是说,在你写一个 volatile 域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。

volatile类型变量提供什么保证?

volatile 主要有两方面的作用:1.避免指令重排2.可见性保证.例如,JVM 或者 JIT为了获得更好的性能会对语句重排序,但是 volatile 类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。某些情况下,volatile 还能提供原子性,如读 64 位数据类型,像 long 和 double 都不是原子的(低32位和高32位),但 volatile 类型的 double
和 long 就是原子的.

关于集合

Java中的集合及其继承关系

关于集合的体系是每个人都应该烂熟于心的,尤其是对我们经常使用的List,Map的原理更该如此.这里我们看这张图即可:

更多内容可见集合类总结:

http://write.blog.csdn.net/postedit/40826423

poll()方法和remove()方法区别?

poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。

LinkedHashMap和PriorityQueue的区别

PriorityQueue 是一个优先级队列,保证最高或者最低优先级的的元素总是在队列头部,但是 LinkedHashMap 维持的顺序是元素插入的顺序。当遍历一个 PriorityQueue 时,没有任何顺序保证,但是 LinkedHashMap 课保证遍历顺序是元素插入的顺序。

WeakHashMap与HashMap的区别是什么?

WeakHashMap 的工作与正常的 HashMap 类似,但是使用弱引用作为 key,意思就是当 key 对象没有任何引用时,key/value 将会被回收。

ArrayList和LinkedList的区别?

最明显的区别是 ArrrayList底层的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构是双向循环链表,不支持随机访问。使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)。

ArrayList和Array有什么区别?

  1. Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
  2. Array是指定大小的,而ArrayList大小是固定的

ArrayList和HashMap默认大小?

在 Java 7 中,ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的幂)。这就是 Java 7 中 ArrayList 和 HashMap 类的代码片段

1
2
3
4
复制代码private static final int DEFAULT_CAPACITY = 10;

//from HashMap.java JDK 7
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

Comparator和Comparable的区别?

Comparable 接口用于定义对象的自然顺序,而 comparator 通常用于定义用户定制的顺序。Comparable 总是只有一个,但是可以有多个 comparator 来定义对象的顺序。

如何实现集合排序?

你可以使用有序集合,如 TreeSet 或 TreeMap,你也可以使用有顺序的的集合,如 list,然后通过 Collections.sort() 来排序。

如何打印数组内容

你可以使用 Arrays.toString() 和 Arrays.deepToString() 方法来打印数组。由于数组没有实现 toString() 方法,所以如果将数组传递给 System.out.println() 方法,将无法打印出数组的内容,但是 Arrays.toString() 可以打印每个元素。

LinkedList的是单向链表还是双向?

双向循环列表,具体实现自行查阅源码.

TreeMap是实现原理

采用红黑树实现,具体实现自行查阅源码.

遍历ArrayList时如何正确移除一个元素

该问题的关键在于面试者使用的是 ArrayList 的 remove() 还是 Iterator 的 remove()方法。这有一段示例代码,是使用正确的方式来实现在遍历的过程中移除元素,而不会出现 ConcurrentModificationException 异常的示例代码。

什么是ArrayMap?它和HashMap有什么区别?

ArrayMap是Android SDK中提供的,非Android开发者可以略过.
ArrayMap是用两个数组来模拟map,更少的内存占用空间,更高的效率.
具体参考这篇文章:ArrayMap VS HashMap:http://lvable.com/?p=217%5D

HashMap的实现原理

1 HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2 HashMap的数据结构: 在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

当我们往Hashmap中put元素时,首先根据key的hashcode重新计算hash值,根绝hash值得到这个元素在数组中的位置(下标),如果该数组在该位置上已经存放了其他元素,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放入链尾.如果数组中该位置没有元素,就直接将该元素放到数组的该位置上.

需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

你了解Fail-Fast机制吗

Fail-Fast即我们常说的快速失败,

更多内容参看fail-fast机制:http://blog.csdn.net/chenssy/article/details/38151189

Fail-fast和Fail-safe有什么区别

Iterator的fail-fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail->fast的,而java.util.concurrent中的集合类都为fail-safe的。当检测到正在遍历的集合的结构被改变时,Fail-fast迭代器抛出ConcurrentModificationException,而fail-safe迭代器从不抛出ConcurrentModificationException。

关于日期

SimpleDateFormat是线程安全的吗?

非常不幸,DateFormat 的所有实现,包括 SimpleDateFormat 都不是线程安全的,因此你不应该在多线程序中使用,除非是在对外线程安全的环境中使用,如 将 SimpleDateFormat 限制在 ThreadLocal 中。如果你不这么做,在解析或者格式化日期的时候,可能会获取到一个不正确的结果。因此,从日期、时间处理的所有实践来说,我强力推荐 joda-time 库。

如何格式化日期?

Java 中,可以使用 SimpleDateFormat 类或者 joda-time 库来格式日期。DateFormat 类允许你使用多种流行的格式来格式化日期。参见答案中的示例代码,代码中演示了将日期格式化成不同的格式,如 dd-MM-yyyy 或 ddMMyyyy。

关于异常

简单描述java异常体系

相比没有人不了解异常体系,关于异常体系的更多信息可以见

白话异常机制:http://blog.csdn.net/dd864140130/article/details/42504189

什么是异常链

详情直接参见上面的白话异常机制,不做解释了.

throw和throws的区别

throw用于主动抛出java.lang.Throwable 类的一个实例化对象,意思是说你可以通过关键字 throw 抛出一个 Error 或者 一个Exception,如:throw new IllegalArgumentException(“size must be multiple of 2″),
而throws 的作用是作为方法声明和签名的一部分,方法被抛出相应的异常以便调用者能处理。Java 中,任何未处理的受检查异常强制在
throws 子句中声明。

关于序列化

Java 中,Serializable 与 Externalizable 的区别

Serializable 接口是一个序列化 Java 类的接口,以便于它们可以在网络上传输或者可以将它们的状态保存在磁盘上,是 JVM 内嵌的默认序列化方式,成本高、脆弱而且不安全。Externalizable 允许你控制整个序列化过程,指定特定的二进制格式,增加安全机制。

关于JVM

JVM特性

平台无关性.
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用模式Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。

简单解释一下类加载器

有关类加载器一般会问你四种类加载器的应用场景以及双亲委派模型,

更多的内容参看深入理解JVM加载器:

http://blog.csdn.net/dd864140130/article/details/49817357

简述堆和栈的区别

VM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常都比堆小,也不会在多个线程之间共享,而堆被整个 JVM 的所有线程共享。

简述JVM内存分配

  1. 基本数据类型比变量和对象的引用都是在栈分配的
  2. 堆内存用来存放由new创建的对象和数组
  3. 类变量(static修饰的变量),程序在一加载的时候就在堆中为类变量分配内存,堆中的内存地址存放在栈中
  4. 实例变量:当你使用java关键字new的时候,系统在堆中开辟并不一定是连续的空间分配给变量,是根据零散的堆内存地址,通过哈希算法换算为一长串数字以表征这个变量在堆中的”物理位置”,实例变量的生命周期–当实例变量的引用丢失后,将被GC(垃圾回收器)列入可回收“名单”中,但并不是马上就释放堆中内存
  5. 局部变量: 由声明在某方法,或某代码段里(比如for循环),执行到它的时候在栈中开辟内存,当局部变量一但脱离作用域,内存立即释放

其他

java当中采用的是大端还是小端?

XML解析的几种方式和特点

DOM,SAX,PULL三种解析方式:

  • DOM:消耗内存:先把xml文档都读到内存中,然后再用DOM API来访问树形结构,并获取数据。这个写起来很简单,但是很消耗内存。要是数据过大,手机不够牛逼,可能手机直接死机
  • SAX:解析效率高,占用内存少,基于事件驱动的:更加简单地说就是对文档进行顺序扫描,当扫描到文档(document)开始与结束、元素(element)开始与结束、文档(document)结束等地方时通知事件处理函数,由事件处理函数做相应动作,然后继续同样的扫描,直至文档结束。
  • PULL:与 SAX 类似,也是基于事件驱动,我们可以调用它的next()方法,来获取下一个解析事件(就是开始文档,结束文档,开始标签,结束标签),当处于某个元素时可以调用XmlPullParser的getAttributte()方法来获取属性的值,也可调用它的nextText()获取本节点的值。

JDK 1.7特性

然 JDK 1.7 不像 JDK 5 和 8 一样的大版本,但是,还是有很多新的特性,如 try-with-resource 语句,这样你在使用流或者资源的时候,就不需要手动关闭,Java 会自动关闭。Fork-Join 池某种程度上实现 Java 版的 Map-reduce。允许 Switch 中有 String 变量和文本。菱形操作符(<>)用于类型推断,不再需要在变量声明的右边申明泛型,因此可以写出可读写更强、更简洁的代码

JDK 1.8特性

java 8 在 Java 历史上是一个开创新的版本,下面 JDK 8 中 5 个主要的特性:
Lambda 表达式,允许像对象一样传递匿名函数
Stream API,充分利用现代多核 CPU,可以写出很简洁的代码
Date 与 Time API,最终,有一个稳定、简单的日期和时间库可供你使用
扩展方法,现在,接口中可以有静态、默认方法。
重复注解,现在你可以将相同的注解在同一类型上使用多次。

Maven和ANT有什么区别?

虽然两者都是构建工具,都用于创建 Java 应用,但是 Maven 做的事情更多,在基于“约定优于配置”的概念下,提供标准的Java 项目结构,同时能为应用自动管理依赖(应用中所依赖的 JAR 文件.

JDBC最佳实践

  • 优先使用批量操作来插入和更新数据
  • 使用PreparedStatement来避免SQL漏洞
  • 使用数据连接池
  • 通过列名来获取结果集

IO操作最佳实践

  • 使用有缓冲的IO类,不要单独读取字节或字符
  • 使用NIO和NIO 2或者AIO,而非BIO
  • 在finally中关闭流
  • 使用内存映射文件获取更快的IO

我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。

本文转载自: 掘金

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

Spring Task定时任务的配置和使用 spring中使

发表于 2018-05-16

Spring Task

记录下Spring自带的定时任务用法。

spring中使用定时任务

基于xml配置文件使用定时任务

首先配置spring开启定时任务

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
复制代码<beans xmlns="http://www.springframework.org/schema/beans"  
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd">

<task:annotation-driven /> <!-- 定时器开关-->

<bean id="myTask" class="com.spring.task.MyTask"></bean>

<task:scheduled-tasks>
<!-- 这里表示的是每隔五秒执行一次 -->
<task:scheduled ref="myTask" method="show" cron="*/5 * * * * ?" />
<task:scheduled ref="myTask" method="print" cron="*/10 * * * * ?"/>
</task:scheduled-tasks>

<!-- 自动扫描的包名 -->
<context:component-scan base-package="com.spring.task" />

</beans>

定义自己的任务执行逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码package com.spring.task;  

/**
* 定义任务
*/
public class MyTask {

public void show() {
System.out.println("show method 1");
}

public void print() {
System.out.println("print method 1");
}
}

基于注解使用定时任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码package com.spring.task;  
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
* 基于注解的定时器
*/
@Component
public class MyTask2 {

/**
* 定时计算。每天凌晨 01:00 执行一次
*/
@Scheduled(cron = "0 0 1 * * *")
public void show() {
System.out.println("show method 2");
}

/**
* 启动时执行一次,之后每隔2秒执行一次
*/
@Scheduled(fixedRate = 1000*2)
public void print() {
System.out.println("print method 2");
}
}

这样,当项目启动,定时任务就会按照规则按时执行了。

Spring Boot中使用定时任务

Spring Boot中使用更加方便。

  • 引入springboot starter包
1
2
3
4
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
  • 在程序入口启动类添加@EnableScheduling,开启定时任务功能
1
2
3
4
5
6
7
复制代码@SpringBootApplication
@EnableScheduling
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
  • 定义定时任务逻辑
1
2
3
4
5
6
7
8
9
10
复制代码@Component
public class MyTask3 {

private int count=0;

@Scheduled(cron="*/6 * * * * ?")
private void process() {
System.out.println("this is scheduler task runing "+(count++));
}
}

任务执行规则说明

先来看看@Scheduled注解的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {

String cron() default "";

String zone() default "";

long fixedDelay() default -1;

String fixedDelayString() default "";

long fixedRate() default -1;

String fixedRateString() default "";

long initialDelay() default -1;

String initialDelayString() default "";
}

可以看出,注解中可以传8种参数:

  • cron:指定cron表达式
  • zone:默认使用服务器默认时区。可以设置为java.util.TimeZone中的zoneId
  • fixedDelay:从上一个任务完成开始到下一个任务开始的间隔,单位毫秒
  • fixedDelayString:同上,时间值是String类型
  • fixedRate:从上一个任务开始到下一个任务开始的间隔,单位毫秒
  • fixedRateString:同上,时间值是String类型
  • initialDelay:任务首次执行延迟的时间,单位毫秒
  • initialDelayString:同上,时间值是String类型

cron表达式的使用方法

Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:

  • Seconds Minutes Hours DayofMonth Month DayofWeek Year
  • Seconds Minutes Hours DayofMonth Month DayofWeek

每一个域可出现的字符如下:

  • Seconds: 可出现”, - * /“四个字符,有效范围为0-59的整数
  • Minutes: 可出现”, - * /“四个字符,有效范围为0-59的整数
  • Hours: 可出现”, - * /“四个字符,有效范围为0-23的整数
  • DayofMonth: 可出现”, - * / ? L W C”八个字符,有效范围为0-31的整数
  • Month: 可出现”, - * /“四个字符,有效范围为1-12的整数或JAN-DEC
  • DayofWeek: 可出现”, - * / ? L C #”四个字符,有效范围为1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一, 依次类推
  • Year: 可出现”, - * /“四个字符,有效范围为1970-2099年

每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:

  1. *:表示匹配该域的任意值,假如在Minutes域使用*, 即表示每分钟都会触发事件。
  2. ?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和 DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。
  3. -:表示范围,例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次。
  4. /:表示起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次。
  5. ,:表示列出枚举值值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
  6. L:表示最后,只能出现在DayofWeek和DayofMonth域,如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。
  7. W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一 到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份。
  8. LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。
  9. #:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。

参考 cron表达式例子

本文转载自: 掘金

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

聊聊自动化测试路上遇到的挑战

发表于 2018-05-16

去年有写过两篇博客,分别是浅谈UI自动化测试和浅谈接口自动化测试,都是一些基础的方法论的内容,今年转岗专门做自动化测试,有了很多新的发现,当然,也遇到了很多的挑战。

遂重开一篇博客,聊聊最近做自动化测试遇到的一些挑战,以及自己的解决方法和一些思考。。。

一、测试范围

无论是功能测试,还是自动化或者性能测试,第一步要做的,是明确测试范围和需求指标。对于自动化测试来说,特别是UI自动化,并不是所有的功能点都适合做UI自动化。

根据具体的业务情况和项目稳定程度,选择UI自动化+API自动化结合,选择合适的业务点来进行针对性的自动化测试方案设计,才是最佳方案。

对于什么项目适合做UI自动化,在之前的博客浅谈UI自动化测试里已经分析过,这里列几个我个人觉得比较适合做UI自动化的点:

①、使用频次较高,异常判断较多,且最基础的功能,比如用户注册、登录;

②、用户端比较核心的功能,比如订单中心,会员中心;

③、促销活动页面、信息展示页等;

分层测试的概念,之前也介绍过了,分层测试金字塔如下:

从效果来看,单元自动化是收益最高的,但是单元自动化对大多数自动化测试初学者来说,难度太大。

且目前国内大部分中小型企业,开发本身的开发规范、管理等工作也做的不太好,单元自动化,实现的概率,短期内不看好。

而接口自动化,目前来说是性价比最高的一种选择。

UI自动化的投入和产出比,是三种模型中最低的,但对于越来越复杂的大型的系统,UI自动化在某些方面,也可以减少很多发布后冒烟测试的工作量。

二、系统架构

对大多数功能测试人员来说,系统架构不用太多关注,但对于自动化或者性能测试人员来说,系统架构是前期需求分析、技术方案选型设计的一个重点。

比如系统开发的编程语言,使用的数据库类型,通信服务框架(如果要进行API自动化,那么系统所采用的通信协议是绕不过去的一点),应用服务器的部署等,都是需要考虑的。

PS:我目前就职的这家企业,数据库使用的是微软全家桶套餐里面的SQL server,而我测试脚本开发语言是python,它本身对SQL server和Oracle的支持并不是很好,导致在数据库配置时候,踩了很多坑。

当然,我最后也解决了这个问题。关于数据库的支持问题,只是我遇到的问题里面的一个很小的部分。

熟悉系统架构的另一个原因是:比如系统采用的开发语言是java,自动化测试人员本身使用的脚本开发语言也是java,那么兼容性就是很好的,而且有技术问题,也可以找开发同事帮忙解决,

这样无形中也节省了很多时间,而且对自己的技术提升,也是不小的帮助。

熟悉了系统架构后,针对性的考虑自动化测试方案设计,技术方案选型,才是最好的方式,不要用固有的方式来解决不同的问题,而要用不同的技术方案解决不同的问题。

附:python:利用pymmsql模块操作SQL server数据库

三、项目情况

关于这点,个人认为应该通过分析沟通来确认系统是否适合自动化测试工作。那么,什么样的系统适合进行自动化测试?

如上图所示,我大概罗列了一些适合进行自动化测试的项目所具备的一些特征,当然,不需要全部具备,只需要满足几点即可以考虑进行自动化测试(红色线条标注部分为基本的条件)。

当然,项目情况不仅仅是这些,还有其他的一些点,也是需要考虑的,比如:

①、文档管理

文档包括需求文档、测试方案、测试用例、测试规范、开发规范、数据库表设计文档、接口文档等。

如果文档不太完善甚至没有对应的文档,那么自动化测试工作,前期的准备工作就需要投入更多的时间和精力。

PS:比如要进行API自动化,但是没有接口文档,没有数据库表设计文档,那么关于API的接口说明,入参出参说明,这将带来很多附带的工作量,而文档数据的统计,又是一件很麻烦的事情。

有时候需要数据库确认接口对数据的影响,对应的数据库表字段,如果没有相关文档说明,那么自己整理以及和DBA沟通熟悉,就需要花费很多的时间和精力。

而很多企业对自动化的认识太片面,认为自动化可以替代人发现很多BUG,且追求短期的明显效果,而如果没有较完善的文档管理,那么自动化测试人员在前期的准备阶段花费的时间将影响

leader对个人能力和工作效率的怀疑,可以说这也是目前国内很多企业存在的弊端。

问题总归是要解决的,如果遇到这种类似的问题,那么在任务拆分和工时预估时候,可以将这些因素考虑进去,计算在个人工时中,并注明原因,在和leader甚至更高层评估中,抛出存在的问题,

推动问题的解决,这也是自动化测试人员的一种价值体现。

②、流程管理

项目的需求迭代是否稳定?从需求评审到开发、测试、发布上线是否有比较规范的流程?

如果需求迭代较快或者不稳定,那么自动化测试的脚本维护工作量将成倍增长;如果没有较好的流程管理,自动化工作的进展总会遇到很多不可描述的坑,沟通成本,变更成本等,还会影响到

测试方案的设计和规划。

在自动化测试中,需要针对性的熟悉了解这些问题点,做好应对的准备。自动化测试过程中,变化是最大的挑战!!!

四、“目标”一致

这里的“目标”,指的是个人对工作的目标设定和leader对自动化工作的期望,站的角度不同,理解不同,因此期望的结果不同。并不是要达成完全的一致,我这里想表达的是沟通的重要性。

个人针对系统情况,技术架构,测试范围的理解,对应的自动化测试方案设计和目标设定,应该与leader不断沟通,不断调整,达成一定程度上的一致,这样也能避免很多后续的麻烦。

当然,目标的设定,也应该根据具体的自动化测试需求,进行任务拆分,评审,不断调整,这是一个持续不断的调整的过程,需要耐心和坚持!

五、运行环境

自动化测试脚本开发,也需要基于不同的环境,而环境的选择,需要考虑不同的情况,下面一一列举说说我个人的思路:

1、SIT环境:

SIT环境,也称为系统集成环境,就是我们常说的测试环境,如果测试脚本是基于SIT环境开发调试,那么就需要SIT环境相对来说比较稳定。

但是大部分时候,SIT环境因为需要多次的提交测试,交叉影响较大,且SIT环境和生产环境还是有一定差异的(至于为什么,相信测试童鞋,都明白其中的痛点)。

2、UAT环境:

UAT环境,就是我们所说的验收环境,当然类似的,也有灰度环境等。UAT环境相对SIT环境来说,是比较稳定且和生产的一致性较高的,很适合进行自动化测试脚本开发和调试,以及日常的测试回归。

但自动化测试的作用又不限于这点,还可以进行发布后的生产冒烟,定时轮询测试等。

3、生产环境:

如果在生产环境进行测试脚本开发和调试,以及测试执行,那么就需要解决以下几点问题:

①、网络问题

一般我们的SIT和UAT环境都是部署在企业内部,网络环境也是走的内网,请求解析和外网有一定区别。在生产环境运行脚本进行回归冒烟的话,需要考虑真实的用户操作环境,不同的网络对测试结果影响。

而且测试脚本中,sleep最好能不用就不用(特别是UI自动化,强制等待容易带来很多不可控的变数)。

②、数据污染问题

生产环境进行自动化测试回归冒烟,需要对测试所产生的数据进行隔离处理,否则会导致业务受到影响,并且对生产的数据造成污染。常用的数据隔离方法如下:

测试白名单账号、网络标记、线程标记、测试数据落入对应的测试库,某些业务还可以使用MOCK对象和挡板。

关于数据隔离这几点,在性能测试中,也是需要考虑的一个重点,当然性能测试中也经常使用这些方法进行数据隔离,避免生产数据污染带来的影响。

六、服务部署

之前的博客做接口测试需要哪些技能有介绍过,自动化测试的核心是持续集成。那么一个独立的不受影响的持续集成环境是必须的。

持续集成环境也叫作CI环境,类似的还有CD环境(持续交付)。

这些应用,在敏捷测试中,应用的比较多,不过目前行业内,我个人了解到,越来越多的企业开始搭建持续集成环境,这样做的好处是很多的,比如:

①、版本迭代提测,可以通过CI服务一键提交,节省时间,提高效率;

②、如果需要造大量的测试数据,可以通过一个小脚本,只需要进行配置,点击启动按钮即可;

③、发布生产环境时候,可以通过CI服务自动化打包部署,避免了人手工打包配置时候的误操作等问题;

④、自动化测试脚本,部署在CI环境后,只需要点击启动,或者设置出发条件,定时任务的形式,来做到生产定时轮询,发布后冒烟等测试工作;

CI环境如何部署?大家可以参考我之前的博客:linux环境:持续集成服务部署系列

七、测试策略

这里的策略,可以从下面几个角度来说:

1、任务拆分

确认测试范围和需求指标后,将自动化测试任务进行粒度更小的拆分,将工作内容所需时间精确到人/天的范围,按时完成任务,有问题不断调整;

2、优先级

任务拆分后,对任务进行优先级排序,通过评审沟通,确认先完成哪些任务,然后完成哪些等。

测试方案设计中,可以考虑分为第一期、第二期自动化测试任务,比如第一期实现自动化测试在一定程度上的覆盖率,先实现PC端的自动化测试,第二期可以考虑移动端的自动化测试覆盖,

第三期可以考虑自动化测试平台的开发等等。有目标有计划,根据项目变化和进度不断调整。

自动化任务的优先级排序,可以参考这几点:

①、用户使用频率越高的优先级应该最高;

②、系统基础功能应该排在优先级前列;

③、系统核心功能(比如支付、订单)应该较早的覆盖;

④、历史记录中生产出现问题叫多的功能点,应该考虑尽可能的覆盖;

3、工时预估

前期的准备工作(比如范围确认、需求分析、CI环境部署、测试方案设计、技术框架选型、文档整理等)完成后,需要针对具体的拆分后的任务进行工时预估,以确认工作阶段性的进展,

方便随时调整。工时预估需要考虑以下几点:

①、自动化测试人员个人的技术能力,特点;

②、项目情况、流程管理、系统架构的变化程度;

③、测试方案技术实现的难易程度;

八、解决问题

自动化测试道路上,全是坑!!!

工作中我们会遇到很多问题,有沟通问题、管理问题、技术难点、以及一些不可描述的问题,但只有想办法解决这些问题,我们才能在工作中不断提升,完成工作,获得该有的收获。

不同的企业存在不同的各种各样的问题,没有完美的公司,因此工作中,学会适应环境,融洽沟通,也是工作中必须的一项内容。

工作的本质就是拿钱消灾,解决问题,创造价值!!!

所以,良好的心态,坚持不懈的态度,解决问题的思路,是需要自动化测试童鞋所具备的。。。

以上就是我个人在自动化测试过程中遇到的一些问题,挑战,以及自己的一些思考和解决方法,希望能为看到的童鞋提供一定的思路和帮助,仅供参考。。。

当然,其中很多内容,拿出来单独说,也有很多值得探讨的地方,限于篇幅这里就不一一分析,后续会不断更新。。。

Has anything you’ve done made your life better?

本文转载自: 掘金

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

如何准备Java初级和高级的技术面试

发表于 2018-05-16

本人最近几年一直在做java后端方面的技术面试官,而在最近两周,又密集了面试了一些java初级和高级开发的候选人,在面试过程中,我自认为比较慎重,遇到问题回答不好的候选人,我总会再三从不同方面提问,只有当反复确认能力不行才会下结论,相反,如果候选人给我的印象不错,我也会从多个角度来衡量,以免招进会说但不会干活的“大忽悠”。


其实倒也不是我故意要为难候选人,毕竟入职后就是同事,但面试官的职责使然,而且,如果资深的面试官一般也这样。

写到这里,恐怕会吓到一些想要面试的朋友,能力强和能力弱都会被多问,那怎么办?


这就是本文将要讲到的主题:如何准备Java初级和高级的技术面试。

一. 换位思考下,如果你面试官,你会怎么做

  1. 只能通过简历和面试来衡量,别无他法。如果某位大牛确认能力很行,但面试时无法充分地自证能力,那对不起了,过不了,现实就这样。
  1. 如果面试官由于能力不行,招进来一个大忽悠,那估计会被领导骂。而且再也不会被让面试了,给领导的印象就不好了。所以不能评主观印象,而是会有些客观标准,具体而言,就是从多个方面问些题目,答好答坏就看候选人的。


    其实一些题目都差不多,但不同能力的面试官问问题的切入点和渐进程度会不同,而且有经验的面试官会挖掘候选人的优势,并能从候选人的说辞中判断候选人是真懂还是忽悠。

二. 总体上说下准备面试的几个方面点

记得之前考政治,某个大题10分,分5个点,每个点的标准答案不多,也就一两句话。比较取巧的做法是,涵盖点要全,每个点无需多说,但要说到点子上。相反,如果在某个点做得再多,其它点没覆盖到,只能拿这个点的分。

同理,在面试时,应当综合准备 java Core,数据库,框架,分布式等方面的题目。根据我面试的结果,我发现不少候选人走了弯路,他们或者干脆不准备,准备时可能方法不到位,单准备一个方面。比如只准备了算法题,在这方面回答很好,但其它方面就一无所知了。

所以说,没有所谓的一定能成功的面试秘籍,但有可以帮助提升成功率的准备方法。


切记,面试前一定得准备,否则成功的可能性很低,准备时,得综合看各方面的点。至于每个点要到什么程度,后文会讲到。

三. 架构方面需要准备的点

初级开发而言,需要让面试官感觉出如下的要点。

  1. 熟悉SSM架构,至少在项目里做过。

这个的说法是,介绍项目时,用一个业务流程来说spring mvc如何做的。

  1. 知道Spring MVC中的细节,比如@Autowired的用法,如何把url映射到Controller上,ModelAndView对象返回的方式等。
  1. 最好结合项目的用法,说下你是怎么用AOP,拦截器的,比如说可以通过拦截器拦截非法请求,怎么用 AOP输出日志等。
  1. 关于ORM方面,不限用过哪种,但得知道一对一,一多多,多对多等的用法,以及cascade和inverse的用法。
  1. 最好知道声明式事务的做法。

如果你要应聘高级开发,那在上述基础上,最好了解如下的知识点:

  • Spring Bean的周期
  • 最好能通过阅读源代码,说下IOC,AOP以及Spring MVC的工作流程
  • 最好能结合反射,说下IOC等的实现原理
  • Spring Boot和Spring Cloud的一些知识点

四. 数据库方面需要准备的点

不少候选人会看很多SQL的技巧,比如select该怎么写,insert又该怎么写,但仅限于此,不会再准备其它的。

这样就很吃亏,因为面试官会认为,哪怕是初级开发,SQL语句也该会写,所以这块不会多问,而会问如下方面的问题。

  1. 索引怎么建的,怎么用的?比如我建好了一个索引,在where 语句里写 name like ‘123%’会不会走索引,怎么情况下不该建索引,哪些语句不会走索引。
  1. 除了索引之外,你有过哪些SQL优化方面的经验,比如分库分表,或通过执行计划查看SQL的优化点。这最好是能结合你做的项目实际来讲。

这里,我面试下来,大概有70%的候选人只知道基本SQL的写法,所以哪怕你是只有理论经验,会说一些优化点,也是非常有利的。

这块对于高级开发而言,更得了解优化方面的技能。

五. Java Core方面需要准备的点

这块是基础,其实很多问的问题,候选人一定会在项目里用到,但很少能说好说全。

这块主要会从集合,多线程,异常处理流程以及JVM虚拟机这些方面来问。

集合方面:

  1. hashcode有没有重写过?在什么场景下需要重写。如果可以,结合hash表的算法,说下hashmap的实现原理。

对于高级开发而言,最好通过ConcurrentHashMap来说明下并发方面的底层实现代码。

  1. ArrayList,LinkedList的差别,比如一个基于数组,一个基于链表,它们均是线程不安全的,ArrayList的扩容做法等。

对于高级而言,最好看下底层的代码。

  1. Set如何实现防重的,比如TreeSet和HashSet等。
  1. Collection的一些方法,比如比较方法,包装成线程安全的方法等。
  1. 可能有些面试官会问,如何通过ArrayList实现队列或堆栈,这个可以准备下。

多线程方面,其实在项目里不怎么会用到,但会问如下的问题:

  1. synchronized和可重入锁的差别,然后可能会顺便问下信号量等防并发的机制。
  1. 在线程里该如何返回值,其实就是callable runnable 区别。
  1. 一定得通过ThreadLocal或volatile关键字,来说明线程的内存模型。
  1. 线程池方面,会用,了解些常用参数

线程方面,可能问得比较多的就是并发机制,如果是高级开发,可能会问得深些。

虚拟机方面

  1. 结构图和流程可以大致说下。
  1. 一定得了解针对堆的垃圾回收机制,具体而言,可以画个图,说下年轻代年老代等。
  1. 说下垃圾回收的流程,然后针对性地说下如何在代码中优化内存性能。
  1. 最好说下如果出现了OOM异常,该怎么排查?如何看Dump文件。
  1. GC的一些概念,比如强弱软引用,finalize方法等,这些可以准备下。

六. 算法,设计模式等,其实是虚的

这块好准备,不过话说哪怕这些没回答好,但能证明有相关技能的项目经验,一般也会让过。

不过在这块,不少候选人就本末倒置了,比如就准备算法,设计模式,刚才提到的框架,数据库和Java Core方面就不准备了。这样很吃亏,就好比考政治只复习了一个点,其它一点也不准备。

七. 我面试的感受&听到哪类回答就能证明候选人比较资深

  1. 大多数的候选人(大概7成)直接就来了,不做任何准备。要知道,面试和项目其实有些脱节,哪怕项目做得再好,不做准备照样通不过,只要我确认过这类人确实无法达标,我拒掉他们没任何心理负担,谁让他们不准备?
  1. 还有些候选人态度很好,明显准备过,但没准备到位,比如像刚才所说,只准备了算法,或者在Java Core方面,只看了集合方面的面试题。对于这些同学,哪怕是过了,我也会感到惋惜,毕竟如果面试好些的话,工资也能更高些,至于哪些过不了的,我敢说,如果他们准备过,估计就不是这个结果了。

其实我也知道,人无完人,哪怕我自己去面试,也不可能面面俱到,所以,我不会要求候选人什么问题都能回答出,甚至大多答错也没关系,只要能证明自己的能力即可通过面试。

我也和不少面试官交流过,根据我们的经验,如果候选人能说出如下的知识点,即能证明他在这个领域比较资深了,在这块,我可能就不会过多地问问题了。


架构方面

  1. 能证明自己可以干活(这不难),同时能结合底层代码说出IOC,AOP或Spring MVC的流程,只要能说出一个即可。或者能说出拦截器,Controller等的高级用法。
  1. 能证明自己有Spring Boot或Spring Cloud的经验,比如能说出些Spring Cloud组件的用法。
  1. 如果能证明自己有分布式开发的经验,那最好了,其实这不难证明,比如能说出服务的包是放在多台机器上(大多数公司其实都这样),而且能说出如何部署,如何通过nginx等做到负载均衡。

数据库方面,其实讲清楚一个问题即可:如何进行SQL调优,比如通过索引,看执行计划即可,如果有其它的优化点,说清楚即可。

Java Core方面,这里给出些诀窍:

  1. 能结合ConcurrentHashMap的源代码,说出final,volatile,transient的用法,以及在其中如何用Lock对象防止写并发。
  1. 结合一个项目实际,说下设计模式的实践。
  1. 多线程方面,能说出Lock或volatile等高级知识点的用法。
  1. 这块最取巧:说下GC的流程,以及如何通过日志和Dump文件排查OOM异常,如果再高级些的话,说下如何在代码中优化内存代码。

诀窍点归结成一个:能结合源代码或项目实际,说出些比较资深的问题。

八. 本文的侧重点

本文的侧重点是:

  1. 面试一定得准备 (重要的话说三遍,这里已经超过3遍了)
  2. 如何全面充分地准备。

至于为什么要写这个文章?我得不停地总结我作为面试官的技巧,这样我在面试中也能更高效更准确地招到合适的人才。

不过话说回来,这篇文章应该对大家多少有些帮助,因为不是每个面试官都肯泄漏面试内幕的。

我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。

本文转载自: 掘金

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

Go(golang)开发区块链只需180行代码

发表于 2018-05-14

本文你将用Go(golang)语言创建自己的区块链、理解哈希函数是如何保持区块链的完整性、掌握如何用Go(golang)语言创造并添加新的块、实现多个节点通过竞争生成块、通过浏览器来查看整个链、了解所有其他关于区块链的基础知识。

但是,文章中将不会涉及工作量证明算法(PoW)以及权益证明算法(PoS)这类的共识算法,同时为了让你更清楚得查看区块链以及块的添加,我们将网络交互的过程简化了,关于 P2P 网络比如“全网广播”这个过程等内容将在后续文章中补上。

Go(golang)语言开发环境

我们假设你已经具备一点 Go 语言的开发经验。在安装和配置 Go 开发环境后之后,我们还要获取以下一些依赖:

1
复制代码~$ go get github.com/davecgh/go-spew/spew

spew可以帮助我们在终端中中直接查看 struct 和 slice 这两种数据结构。

1
复制代码~$ go get github.com/gorilla/mux

Gorilla 的 mux 包非常流行, 我们用它来写 web handler。

1
复制代码~$ go get github.com/joho/godotenv

godotenv可以帮助我们读取项目根目录中的.env 配置文件,这样就不用将 http端口之类的配置硬编码进代码中了。比如像这样:

1
复制代码ADDR=8080

接下来,我们创建一个 main.go 文件。之后的大部分工作都围绕这个文件,开始写代码吧!

导入依赖包

我们将所有的依赖包以声明的方式导入进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码package main

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"time"

"github.com/davecgh/go-spew/spew"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)

区块链数据模型

接着我们来定义一个结构体,它代表组成区块链的每一个块的数据模型:

1
2
3
4
5
6
7
复制代码type Block struct {
Index int
Timestamp string
BPM int
Hash string
PrevHash string
}
  • Index 是这个块在整个链中的位置
  • Timestamp 显而易见就是块生成时的时间戳
  • Hash 是这个块通过 SHA256 算法生成的散列值
  • PrevHash 代表前一个块的 SHA256 散列值
  • BPM 每分钟心跳数,也就是心率

接着,我们再定义一个结构表示整个链,最简单的表示形式就是一个 Block 的 slice:

1
复制代码var Blockchain []Block

我们使用散列算法(SHA256)来确定和维护链中块和块正确的顺序,确保每一个块的 PrevHash 值等于前一个块中的 Hash 值,这样就以正确的块顺序构建出链:

180行go代码让你彻底理解区块链是什么

区块链散列和生成新块

我们为什么需要散列?主要是两个原因:

  • 在节省空间的前提下去唯一标识数据。散列是用整个块的数据计算得出,在我们的例子中,将整个块的数据通过 SHA256 计算成一个定长不可伪造的字符串。
  • 维持链的完整性。通过存储前一个块的散列值,我们就能够确保每个块在链中的正确顺序。任何对数据的篡改都将改变散列值,同时也就破坏了链。以我们从事的医疗健康领域为例,比如有一个恶意的第三方为了调整“人寿险”的价格,而修改了一个或若干个块中的代表不健康的 BPM 值,那么整个链都变得不可信了。

我们接着写一个函数,用来计算给定的数据的 SHA256 散列值:

1
2
3
4
5
6
7
复制代码func calculateHash(block Block) string {
record := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}

这个 calculateHash 函数接受一个块,通过块中的 Index,Timestamp,BPM,以及 PrevHash 值来计算出 SHA256 散列值。接下来我们就能编写一个生成块的函数:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码func generateBlock(oldBlock Block, BPM int) (Block, error) {
var newBlock Block

t := time.Now()
newBlock.Index = oldBlock.Index + 1
newBlock.Timestamp = t.String()
newBlock.BPM = BPM
newBlock.PrevHash = oldBlock.Hash
newBlock.Hash = calculateHash(newBlock)

return newBlock, nil
}

其中,Index 是从给定的前一块的 Index 递增得出,时间戳是直接通过 time.Now() 函数来获得的,Hash 值通过前面的 calculateHash 函数计算得出,PrevHash 则是给定的前一个块的 Hash 值。

区块链校验块

搞定了块的生成,接下来我们需要有函数帮我们判断一个块是否有被篡改。检查 Index 来看这个块是否正确得递增,检查 PrevHash 与前一个块的 Hash 是否一致,再来通过 calculateHash 检查当前块的 Hash 值是否正确。通过这几步我们就能写出一个校验函数:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}
if oldBlock.Hash != newBlock.PrevHash {
return false
}
if calculateHash(newBlock) != newBlock.Hash {
return false
}
return true
}

除了校验块以外,我们还会遇到一个问题:两个节点都生成块并添加到各自的链上,那我们应该以谁为准?这里的细节我们留到下一篇文章,
这里先让我们记住一个原则:始终选择最长的链:

180行go代码让你彻底理解区块链是什么

通常来说,更长的链表示它的数据(状态)是更新的,所以我们需要一个函数能帮我们将本地的过期的链切换成最新的链:

1
2
3
4
5
复制代码func replaceChain(newBlocks []Block) {
if len(newBlocks) > len(Blockchain) {
Blockchain = newBlocks
}
}

到这一步,我们基本就把所有重要的函数完成了。接下来,我们需要一个方便直观的方式来查看我们的链,包括数据及状态。通过浏览器查看 web 页面可能是最合适的方式!

Go(golang)语言 Web 服务

我猜你一定对传统的 web 服务及开发非常熟悉,所以这部分你肯定一看就会。

借助 Gorilla/mux 包,我们先写一个函数来初始化我们的 web 服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码func run() error {
mux := makeMuxRouter()
httpAddr := os.Getenv("ADDR")
log.Println("Listening on ", os.Getenv("ADDR"))
s := &http.Server{
Addr: ":" + httpAddr,
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}

if err := s.ListenAndServe(); err != nil {
return err
}

return nil
}

其中的端口号是通过前面提到的 .env 来获得,再添加一些基本的配置参数,这个 web 服务就已经可以 listen and serve 了!

接下来我们再来定义不同 endpoint 以及对应的 handler。例如,对“/”的 GET 请求我们可以查看整个链,“/”的 POST 请求可以创建块。

1
2
3
4
5
6
复制代码func makeMuxRouter() http.Handler {
muxRouter := mux.NewRouter()
muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")
return muxRouter
}

GET 请求的 handler:

1
2
3
4
5
6
7
8
复制代码func handleGetBlockchain(w http.ResponseWriter, r *http.Request) {
bytes, err := json.MarshalIndent(Blockchain, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.WriteString(w, string(bytes))
}

为了简化,我们直接以 JSON 格式返回整个链,你可以在浏览器中访问 localhost:8080 或者 127.0.0.1:8080 来查看(这里的8080就是你在 .env 中定义的端口号 ADDR)。

POST 请求的 handler 稍微有些复杂,我们先来定义一下 POST 请求的 payload:

1
2
3
复制代码type Message struct {
BPM int
}

再看看 handler 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码func handleWriteBlock(w http.ResponseWriter, r *http.Request) {
var m Message

decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&m); err != nil {
respondWithJSON(w, r, http.StatusBadRequest, r.Body)
return
}
defer r.Body.Close()

newBlock, err := generateBlock(Blockchain[len(Blockchain)-1], m.BPM)
if err != nil {
respondWithJSON(w, r, http.StatusInternalServerError, m)
return
}
if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
newBlockchain := append(Blockchain, newBlock)
replaceChain(newBlockchain)
spew.Dump(Blockchain)
}

respondWithJSON(w, r, http.StatusCreated, newBlock)

}

我们的 POST 请求体中可以使用上面定义的 payload,比如:

1
复制代码{"BPM":75}

还记得前面我们写的 generateBlock 这个函数吗?它接受一个“前一个块”参数,和一个 BPM 值。POST handler 接受请求后就能获得请求体中的 BPM 值,接着借助生成块的函数以及校验块的函数就能生成一个新的块了!

除此之外,你也可以:

  • 使用spew.Dump 这个函数可以以非常美观和方便阅读的方式将 struct、slice 等数据打印在控制台里,方便我们调试。
  • 测试 POST 请求时,可以使用 POSTMAN 这个 chrome 插件,相比 curl它更直观和方便。

POST 请求处理完之后,无论创建块成功与否,我们需要返回客户端一个响应:

1
2
3
4
5
6
7
8
9
10
复制代码func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {
response, err := json.MarshalIndent(payload, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("HTTP 500: Internal Server Error"))
return
}
w.WriteHeader(code)
w.Write(response)
}

快要大功告成了。

接下来,我们把这些关于区块链的函数,web 服务的函数“组装”起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}

go func() {
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "", ""}
spew.Dump(genesisBlock)
Blockchain = append(Blockchain, genesisBlock)
}()
log.Fatal(run())
}

这里的 genesisBlock (创世块)是 main 函数中最重要的部分,通过它来初始化区块链,毕竟第一个块的 PrevHash 是空的。

哦耶!完成了

可以从这里获得完整的代码:Github repo

让我们来启动它:

1
复制代码~$ go run main.go

在终端中,我们可以看到 web 服务器启动的日志信息,并且打印出了创世块的信息:

180行go代码让你彻底理解区块链是什么

接着我们打开浏览器,访问 localhost:8080 这个地址,我们可以看到页面中展示了当前整个区块链的信息(当然,目前只有一个创世块):

180行go代码让你彻底理解区块链是什么

接着,我们再通过 POSTMAN 来发送一些 POST 请求:

180行go代码让你彻底理解区块链是什么

刷新刚才的页面,现在的链中多了一些块,正是我们刚才生成的,同时你们可以看到,块的顺序和散列值都正确。

180行go代码让你彻底理解区块链是什么

总结

刚刚我们完成了一个自己的区块链,虽然很简单(陋),但它具备块生成、散列计算、块校验等基本能力。接下来你就可以继续深入的学习
区块链的其他重要知识,比如工作量证明、权益证明这样的共识算法,或者是智能合约、Dapp、侧链等等。

目前这个实现中不包括任何 P2P 网络的内容,我们会在下一篇文章中补充这部分内容,当然,我们鼓励你在这个基础上自己实践一遍!

如果你希望高效的学习以太坊DApp开发,可以访问汇智网提供的最热门在线互动教程:

  • 适合区块链新手的以太坊DApp实战入门教程
  • 区块链+IPFS+Node.js+MongoDB+Express去中心化以太坊电商应用开发实战

其他更多内容也可以访问这个以太坊博客。

原文:Code your own blockchain in less than 200 lines of Go!

本文转载自: 掘金

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

Spring Boot的自动化配置原理 文末送书

发表于 2018-05-14


随着Ruby、Groovy等动态语言的流行,相比较之下Java的开发显得格外笨重。繁多的配置、低下的开发效率、复杂的部署流程以及第三方技术集成难度大等问题一直被人们所诟病。随着Spring家族中的新星Spring Boot的诞生,这些问题都在逐渐被解决。

个人觉得Spring Boot中最重要的两个优势就是可以使用starter简化依赖配置和Spring的自动配置。

本文涉及到大量代码,静下心来仔细阅读。阅读之后,你会彻底了解SpringBoot的自动化配置以及starter的原理。

使用starter简化依赖配置

Spring提供了一系列starter来简化Maven配置。其核心原理也就是Maven和Gradle的依赖传递方案。当我们在我们的pom文件中增加对某个starter的依赖时,该starter的依赖也会自动的传递性被依赖进来。而且,很多starter也依赖了其他的starter。例如web starter就依赖了tomcat starter,并且大多数starter基本都依赖了spring-boot-starter。

Spring自动配置

Spring Boot会根据类路径中的jar包、类,为jar包里的类自动配置,这样可以极大的减少配置的数量。简单点说就是它会根据定义在classpath下的类,自动的给你生成一些Bean,并加载到Spring的Context中。自动配置充分的利用了spring 4.0的条件化配置特性,能够自动配置特定的Spring bean,用来启动某项特性。

条件化配置

假设你希望一个或多个bean只有在某种特殊的情况下才需要被创建,比如,一个应用同时服务于中美用户,要在中美部署,有的服务在美国集群中需要提供,在中国集群中就不需要提供。在Spring 4之前,要实现这种级别的条件化配置是比较复杂的,但是,Spring 4引入了一个新的@Conditional注解可以有效的解决这类问题。

1
复制代码@Bean@Conditional(ChinaEnvironmentCondition.class)public ServiceBean serviceBean(){    return new ServiceBean();}

当@Conditional(ChinaEnvironmentCondition.class)条件的值为true的时候,该ServiceBean才会被创建,否则该bean就会被忽略。

@Conditional指定了一个条件。他的条件的实现是一个Java类——ChinaEnvironmentCondition,要实现以上功能就要定义ChinaEnvironmentCondition类,并继承Condition接口并重写其中的matches方法。

1
复制代码class ChinaEnvironmentCondition implements Condition{    public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {        Environment env = context.getEnvironment();        return env.containProperty("ENV_CN");    }}

在上面的代码中,matches方法的内容比较简单,他通过给定的ConditionContext对象进而获取Environment对象,然后使用该对象检查环境中是否存在ENV_CN属性。如果存在该方法则直接返回true,反之返回false。

当该方法返回true的时候,就符合了@Conditional指定的条件,那么ServiceBean就会被创建。反之,如果环境中没有这个属性,那么这个ServiceBean就不会被创建。

除了可以自定义一些条件之外,Spring 4本身提供了很多已有的条件供直接使用,如:

1
复制代码@ConditionalOnBean@ConditionalOnClass@ConditionalOnExpression@ConditionalOnMissingBean@ConditionalOnMissingClass@ConditionalOnNotWebApplication

Spring Boot应用的启动入口

自动配置充分的利用了spring 4.0的条件化配置特性,那么,Spring Boot是如何实现自动配置的?Spring 4中的条件化配置又是怎么运用到Spring Boot中的呢?

这要从Spring Boot的启动类说起。Spring Boot应用通常有一个名为*Application的入口类,入口类中有一个main方法,这个方法其实就是一个标准的Java应用的入口方法。

一般在main方法中使用SpringApplication.run()来启动整个应用。值得注意的是,这个入口类要使用@SpringBootApplication注解声明。@SpringBootApplication是Spring Boot的核心注解,他是一个组合注解。

1
复制代码@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(    excludeFilters = {@Filter(    type = FilterType.CUSTOM,    classes = {TypeExcludeFilter.class}), @Filter(    type = FilterType.CUSTOM,    classes = {AutoConfigurationExcludeFilter.class})})public @interface SpringBootApplication {    // 略}

@SpringBootApplication是一个组合注解,它主要包含@SpringBootConfiguration、@EnableAutoConfiguration等几个注解。

也就是说可以直接在启动类中使用这些注解来代替@ SpringBootApplication注解。 关于Spring Boot中的Spring自动化配置主要是@EnableAutoConfiguration的功劳。该注解可以让Spring Boot根据类路径中的jar包依赖为当前项目进行自动配置。

至此,我们知道,Spring Boot的自动化配置主要是通过@EnableAutoConfiguration来实现的,因为我们在程序的启动入口使用了@SpringBootApplication注解,而该注解中组合了@EnableAutoConfiguration注解。所以,在启动类上使用@EnableAutoConfiguration注解,就会开启自动配置。

那么,本着刨根问底的原则,当然要知道@EnableAutoConfiguration又是如何实现自动化配置的,因为目前为止,我们还没有发现Spring 4中条件化配置的影子。

EnableAutoConfiguration

其实Spring框架本身也提供了几个名字为@Enable开头的Annotation定义。比如@EnableScheduling、@EnableCaching、@EnableMBeanExport等,@EnableAutoConfiguration的理念和这些注解其实是一脉相承的。

@EnableScheduling是通过@Import将Spring调度框架相关的bean定义都加载到IoC容器。

@EnableMBeanExport是通过@Import将JMX相关的bean定义加载到IoC容器。

@EnableAutoConfiguration也是借助@Import的帮助,将所有符合自动配置条件的bean定义加载到IoC容器。

下面是EnableAutoConfiguration注解的源码:

1
复制代码@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import({EnableAutoConfigurationImportSelector.class})public @interface EnableAutoConfiguration {    //略}

观察@EnableAutoConfiguration可以发现,这里Import了@EnableAutoConfigurationImportSelector,这就是Spring Boot自动化配置的“始作俑者”。

至此,我们知道,至此,我们知道,由于我们在Spring Boot的启动类上使用了@SpringBootApplication注解,而该注解组合了@EnableAutoConfiguration注解,@EnableAutoConfiguration是自动化配置的“始作俑者”,而@EnableAutoConfiguration中Import了@EnableAutoConfigurationImportSelector注解,该注解的内部实现已经很接近我们要找的“真相”了。

EnableAutoConfigurationImportSelector

EnableAutoConfigurationImportSelector的源码在这里就不贴了,感兴趣的可以直接去看一下,其实实现也比较简单,主要就是使用Spring 4 提供的的SpringFactoriesLoader工具类。

通过SpringFactoriesLoader.loadFactoryNames()读取了ClassPath下面的META-INF/spring.factories文件。

这里要简单提一下spring.factories文件,它是一个典型的java properties文件,配置的格式为Key = Value形式。

EnableAutoConfigurationImportSelector通过读取spring.factories中的key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的值。

如spring-boot-autoconfigure-1.5.1.RELEASE.jar中的spring.factories文件包含以下内容:

1
复制代码# Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\......org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration

上面的EnableAutoConfiguration配置了多个类,这些都是Spring Boot中的自动配置相关类;在启动过程中会解析对应类配置信息。每个Configuation都定义了相关bean的实例化配置。都说明了哪些bean可以被自动配置,什么条件下可以自动配置,并把这些bean实例化出来。

如果我们新定义了一个starter的话,也要在该starter的jar包中提供spring.factories文件,并且为其配置org.springframework.boot.autoconfigure.EnableAutoConfiguration 对应的配置类。

Configuation

我们从spring-boot-autoconfigure-1.5.1.RELEASE.jar中的spring.factories文件随便找一个Configuration,看看他是如何自动加载bean的。

1
复制代码@Configuration@AutoConfigureAfter({JmxAutoConfiguration.class})@ConditionalOnProperty(    prefix = "spring.application.admin",    value = {"enabled"},    havingValue = "true",    matchIfMissing = false)public class SpringApplicationAdminJmxAutoConfiguration {    @Bean    @ConditionalOnMissingBean    public SpringApplicationAdminMXBeanRegistrar springApplicationAdminRegistrar() throws MalformedObjectNameException {        String jmxName = this.environment.getProperty("spring.application.admin.jmx-name", "org.springframework.boot:type=Admin,name=SpringApplication");        if(this.mbeanExporter != null) {            this.mbeanExporter.addExcludedBean(jmxName);        }        return new SpringApplicationAdminMXBeanRegistrar(jmxName);    }}

看到上面的代码,终于找到了我们要找的东西——Spring 4的条件化配置。

上面SpringApplicationAdminJmxAutoConfiguration 在决定对哪些bean进行自动化配置的时候,使用了两个条件注解:ConditionalOnProperty和ConditionalOnMissingBean。

只有满足这种条件的时候,对应的bean才会被创建。这样做的好处是什么?这样可以保证某些bean在没满足特定条件的情况下就可以不必初始化,避免在bean初始化过程中由于条件不足,导致应用启动失败。

总结

至此,我们可以总结一下Spring Boot的自动化配置的实现:

通过Spring 4的条件配置决定哪些bean可以被配置,将这些条件定义成具体的Configuation,然后将这些Configuation配置到spring.factories文件中,作为key: org.springframework.boot.autoconfigure.EnableAutoConfiguration的值,这时候,容器在启动的时候,由于使用了EnableAutoConfiguration注解,该注解Import的EnableAutoConfigurationImportSelector会去扫描classpath下的所有spring.factories文件,然后进行bean的自动化配置。

所以,如果我们想要自定义一个starter的话,可以通过以上方式将自定义的starter中的bean自动化配置到Spring的上下文中,从而避免大量的配置。

福利时间

为答谢粉丝们长期以来的厚爱,五月份第四次送书,本次送书 5 本。

书籍介绍如下:

《Spring Boot企业级应用开发实战》

本书围绕如何整合以目前最新的 Spring Boot 2 版本为核心的技术栈,来实现一个完整的企业级博客系统而展开。读者可以通过学习构建这个博客系统的整个过程,来达到设计和实现一个企业级 Java EE 应用开发的目的。

最新:本书基于最新的Spring Boot 2 版本编写。

立足实战:跳脱纯理论讲述,案例贯穿全书,一步步讲述使用Spring Boot开发企业系统的方法。读者可以一步步学到项目实战经验!

全面:弥补市面上有关 Spring Boot 学习资料的不足,重新编写整个教学案例,使读者轻松脱离“HelloWorld”阶段,向Spring Boot企业级快速应用开发跃迁。

购买链接

点击下方**阅读原文**,查看更多本书信息。关于Spring Boot 2.0的简单介绍,可以参考我的Spring Boot 2.0发布,新特性一览文章。

参与方式:

活动截止时间 5月19日 10:00。

关注Hollis公众号,本文下方评论,点赞数排名前5名,获得由柳伟卫著、北京大学出版社出版的spring boot 2.0 新书一本。

你可以说说你最近的学习工作状态、对于Hollis的建议、或者你对Hollis的看法等。“我只想要本书”、“求点赞”之类的留言不会被上墙。

- MORE | 更多精彩文章 -

  • ****本想试试看,却拿到了京东的Offer | 文末送书****
  • 常用Git命令清单
  • 乐观锁的实现方式——CAS
  • 统一配置中心的设计方案

如果你看到了这里,说明你喜欢本文。

那么请长按二维码,关注Hollis


转发朋友圈,是对我最大的支持。

本文转载自: 掘金

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

原来你是这样的Websocket--抓包分析 Chrome控

发表于 2018-05-10

之前自己一个人负责完成了公司的消息推送服务,和移动端配合完成了扫码登录、订单消息推送、活动消息广播等功能。为了加深自己对Websocket协议的理解,自己通过进行抓包的方式学习了一番。现在分享出来,希望对大家能有所帮助。

Chrome控制台

(1)F12进入控制台,点击Network,选中ws栏,注意选中Filter。

(2)刷新页面会得到一个ws链接。

(3)点击链接可以查看链接详情

注意红框标出的信息,后面会详细说明。
(4)当然也可以切换到Frames查看发出和接收的消息,但是非常的简陋,只能看到消息内容,数据长度和时间

Fiddler:抓包调试利器

(1)打开Fiddler,点开菜单栏的Rules,选择Customize Rules…

(2)这时会打开CustomRules.js文件,在class Handlers中加入以下代码

1
2
3
4
复制代码static function OnWebSocketMessage(oMsg: WebSocketMessage) {
// Log Message to the LOG tab
FiddlerApplication.Log.LogString(oMsg.ToString());
}

(3)保存后就可以在Fiddler右边栏的Log标签里,看到WebSocket的数据包
下列图中红框标出的Client.1代表客户端发出的第一条消息;对应的Server.1代表服务端发出的第一条消息。MessageType:Text代表正常的通话消息;Close代表会话关闭。
客户端发出的消息:

服务端发出的消息:

然后我们会发现每次会话关闭都是由客户端发起的:

相对于Chrome控制台来说Fiddler抓包更加详细一些,能知道会话消息是由客户端还是服务端发出并且能知道消息类型。但是这仍然满足不了深入理解学习Websocket协议的目的。如果是处理HTTP、HTTPS,还是用Fiddler。其他协议比如TCP,UDP 就用WireShark。TPC/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP、Websocket是应用层协议,主要解决如何包装数据。因为应用层是在传输层的基础上包装数据,所以我们还是从底层开始了解Websocket到底是个啥?是如何工作的?

WireShark

WireShark(前称Ethereal)是一个网络封包分析软件。网络封包分析软件的功能是撷取网络封包,并尽可能显示出最为详细的网络封包资料。WireShark抓包是根据TCP/IP五层协议来的,也就是物理层、数据链路层、网络层、传输层、应用层。我们主要关注传输层和应用层。

TCP三次握手

我们都知道,TCP建立连接时,会有三次握手过程。下图是WireShark截获到的三次握手的三个数据包(虽然叫数据包,但是三次握手包是没有数据的)。

点击上图中的数据包就可以查看每个数据包的详情,这里我们需要明确几个概念才能看懂每个数据包代表啥意义:
SYN:同步比特,建立连接。
ACK:确认比特,置1表示这是一个确认的TCP包,0则不是。
PSH:推送比特,当发送端PSH=1时,接收端应尽快交付给应用进程。

  • 第一次握手

可以看到我们打开的Transmission Control Protocol即为传输层(Tcp)
SYN置为1,客户端向服务端发送连接请求包。

  • 第二次握手

服务器收到客户端发过来的TCP报文,由SYN=1知道客户端要求建立联机,向客户端发送一个SYN=1,ACK=1的TCP报文,将确认序号设置为客户端的序列号加1。

  • 第三次握手

客户端接收到服务器发过来的包后检查确认序列号是否正确,即第一次发送的序号+1,以及标志位ACK是否为1。若正确则再次发送确认包,ACK标志为1。链接建立成功,可以发送数据了。

一次特殊的HTTP请求

紧接着是一次Http请求(第四个包),说明Http的确是使用Tcp建立连接的。

先来看传输层(Tcp): PSH(推送比特)置1,ACK置1,PSH置1说明开始发送数据,同时发送数据ACK要置1,因为需要接收到这个数据包的端给予确认。PSH为1的情况,一般只出现在 DATA内容不为0的包中,也就是说PSH为1表示的是有真正的TCP数据包内容被传递。

再来看应用层(Http):这是一次特殊的Http请求,为什么是一次特殊的Http请求呢?Http请求头中Connection:Upgrade Upgrade:websocket,Upgrade代表升级到较新的Http协议或者切换到不同的协议。很明显WebSocket使用此机制以兼容的方式与HTTP服务器建立连接。WebSocket协议有两个部分:握手建立升级后的连接,然后进行实际的数据传输。首先,客户端通过使用Upgrade: WebSocket和Connection: Upgrade头部以及一些特定于协议的头来请求WebSocket连接,以建立正在使用的版本并设置握手。服务器,如果它支持协议,回复与相同Upgrade: WebSocket和Connection: Upgrade标题,并完成握手。握手完成后,数据传输开始。这些信息在前面的Chrome控制台中也可以看到。

请求:

响应:
响应状态码 101 表示服务器已经理解了客户端的请求,在发送完这个响应后,服务器将会切换到在Upgrade请求头中定义的那些协议。

由此我们可以总结出:
Websocket协议本质上是一个基于TCP的协议。建立连接需要握手,客户端(浏览器)首先向服务器(web server)发起一条特殊的http请求,web server解析后生成应答到浏览器,这样子一个websocket连接就建立了,直到某一方关闭连接。

Websocket的世界

通信协议格式是WebSocket格式,服务器端采用Tcp Socket方式接收数据,进行解析,协议格式如下:

首先我们需要知道数据在物理层,数据链路层是以二进制进行传递的,而在应用层是以16进制字节流进行传输的。

第一个字节:

FIN:1位,用于描述消息是否结束,如果为1则该消息为消息尾部,如果为零则还有后续数据包;
RSV1,RSV2,RSV3:各1位,用于扩展定义的,如果没有扩展约定的情况则必须为0
OPCODE:4位,用于表示消息接收类型,如果接收到未知的opcode,接收端必须关闭连接。

Webdocket数据帧中OPCODE定义:
0x0表示附加数据帧
0x1表示文本数据帧
0x2表示二进制数据帧
0x3-7暂时无定义,为以后的非控制帧保留
0x8表示连接关闭
0x9表示ping
0xA表示pong
0xB-F暂时无定义,为以后的控制帧保留

第二个字节:

MASK:1位,用于标识PayloadData是否经过掩码处理,客户端发出的数据帧需要进行掩码处理,所以此位是1。数据需要解码。
PayloadData的长度:7位,7+16位,7+64位
如果其值在0-125,则是payload的真实长度。
如果值是126,则后面2个字节形成的16位无符号整型数的值是payload的真实长度。
如果值是127,则后面8个字节形成的64位无符号整型数的值是payload的真实长度。

上图是客户端发送给服务端的数据包,其中PayloadData的长度为二进制:01111110——>十进制:126;如果值是126,则后面2个字节形成的16位无符号整型数的值是payload的真实长度。也就是圈红的十六进制:00C1——>十进制:193 byte。所以PayloadData的真实数据长度是193 bytes;

根据我们的分析,客户端到服务端数据包的websocket帧图应该为:

我们再来抓包分析一下服务器到客户端的数据包:

可以发现服务器发送给客户端的数据包中第二个字节中MASK位为0,这说明服务器发送的数据帧未经过掩码处理,这个我们从客户端和服务端的数据包截图中也可以发现,客户端的数据被加密处理,而服务端的数据则没有。(如果服务器收到客户端发送的未经掩码处理的数据包,则会自动断开连接;反之,如果客户端收到了服务端发送的经过掩码处理的数据包,也会自动断开连接)。

掩码处理:

未掩码处理:

根据我们的分析,服务端到客户端数据包的websocket帧图应该为:

TCP KeepAlive

如上图所示,TCP保活报文总是成对出现,包括TCP保活探测报文和TCP保活探测确认报文。
TCP保活探测报文是将之前TCP报文的确认序列号减1,并设置1个字节,内容为“00”的应用层数据,如下图所示:

TCP保活探测确认报文就是对保活探测报文的确认,其报文格式如下:

因为Websocket通过Tcp Socket方式工作,现在考虑一个问题,在一次长连接中,服务器怎么知道消息的顺序呢?这就涉及到tcp的序列号(Sequence Number)和确认号(Acknowledgment Number)。我的理解是序列号是发送的数据长度;确认号是接收的数据长度。这样讲比较抽象,我们从TCP三次握手开始(结合下图)详细分析一下。

包1:
TCP会话的每一端的序列号都从0开始,同样的,确认号也从0开始,因为此时通话还未开始,没有通话的另一端需要确认

包2:
服务端响应客户端的请求,响应中附带序列号0(由于这是服务端在该次TCP会话中发送的第一个包,所以序列号为0)和相对确认号1(表明服务端收到了客户端发送的包1中的SYN)。需要注意的是,尽管客户端没有发送任何有效数据,确认号还是被加1,这是因为接收的包中包含SYN或FIN标志位。

包3:
和包2中一样,客户端使用确认号1响应服务端的序列号0,同时响应中也包含了客户端自己的序列号(由于服务端发送的包中确认收到了客户端发送的SYN,故客户端的序列号由0变为1)此时,通信的两端的序列号都为1。

包4:客户端——>服务器
这是流中第一个携带有效数据的包(确切的说,是客户端发送的HTTP请求),序列号依然为1,因为到上个包为止,还没有发送任何数据,确认号也保持1不变,因为客户端没有从服务端接收到任何数据。需要注意的是,包中有效数据的长度为505字节

包5:服务器——>客户端
当上层处理HTTP请求时,服务端发送该包来确认客户端在包4中发来的数据,需要注意的是,确认号的值增加了505(505是包4中有效数据长度),变为506,简单来说,服务端以此来告知客户端端,目前为止,我总共收到了506字节的数据,服务端的序列号保持为1不变。

包6:服务器——>客户端
这个包标志着服务端返回HTTP响应的开始,序列号依然为1,因为服务端在该包之前返回的包中都不带有有效数据,该包带有129字节的有效数据。

包7:
由于上个数据包的发送,TCP客户端的确认序列号增长至130,从服务端接收了129字节的数据,客户端的确认号由1增长至130
理解了序列号和确认序列号是怎么工作的之后,我们也就知道“TCP保活探测报文是将之前TCP报文的确认序列号减1,并设置1个字节”为什么要这么搞了。减一再加一,是为了保证一次连接中keep alive不影响序列号和确认序列号。Keep alive 中的1byte 00的数据并不是真正要传递的数据,而是tcp keep alive约定俗称的规则。

总结:
WebSocket 是一个独立的基于 TCP 的协议,它与 HTTP 之间的唯一关系就是它的握手请求可以作为一个升级请求(Upgrade request)经由 HTTP 服务器解释。再严谨一点:WebSocket是一个网络通讯协议, 只要理解上面的数据帧格式和握手流程, 都可以完成基于websokect的即时通讯。


微信公众号:关注公众号,获取最新更新

本文转载自: 掘金

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

【从零开始 图文详解】IDEA整合SSM框架:Spring+

发表于 2018-05-08

1 准备

IntelliJ IDEA

Tomcat

JDK

Maven

mysql

spring、springmvc、mybatis 了解

现在假设如上条件你都具备,那么通过我这篇博客 你一定可以整合出 SSM 框架,完成如下效果

这里写图片描述

整个博客共分为三部分:

  • 建立 基于 Maven 的 WEB 项目,启动 Tomcat ,项目启动
  • 整合 Mybatis+Spring ,进行单元测试 可以完成从数据库查询出数据功能
  • 整合 Mybaits+Spring+SpringMVC,输入Url 完成整个 MVC 的流程

2 步骤

因为说的比较细,所以前面第一部分都了解可以跳过。

2.1 第一部分

1.新建基于 Maven 的Web工程

这里写图片描述

2.填写 GroupId 和 ArtifactId,参考

groupId一般分为多个段,这里我只说两段,第一段为域,第二段为公司名称。域又分为org、com、cn等等许多,其中org为非营利组织,com为商业组织。举个apache公司的tomcat项目例子:这个项目的groupId是org.apache,它的域是org(因为tomcat是非营利项目),公司名称是apache,artigactId是tomcat。

这里写图片描述

3.填写 Maven 配置。

这里写图片描述

图片中1、2、3分别:
1:首先你要先配置好 Maven,如果配置好了,去 CMD DOSS 界面下输入mvn --version 命令,如果有显示,则代表已经配置好了,如果没,下载maven,配置环境变量。
maven下载地址

2:选择我们的 maven 文件夹下的 setting,这里特别要注意,可能因为 “墙”的原因,下载 maven jar 包 很容易卡住,所以建议在 setting.xml 配置国内镜像,比如阿里云。

1
2
3
4
5
6
7
复制代码	 <!-- 复制如上一段镜像代码配置,添加阿里云镜像 -->
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>

这里写图片描述

在相应位置填上如上代码即可。

3.这里为了从头开始,我新建了个仓库地址。这里大家随意,也可以和我一样。

这里写图片描述

4.填写工程名,module名,前者是 workspace 后者代表一个项目

这里写图片描述

5.建立架构包

这里写图片描述

如果这里弹出此对话框,记得点击 auto。
这里写图片描述

这里写图片描述

新建文件夹,利用Sources Resources 两个标签分别转化 Java 和 Resources 文件夹

6.新建tomcat并且配置

这里写图片描述

这里写图片描述

7.启动项目

这里写图片描述

2.2 第二部分

**1.添加 maven 依赖。**因为 pom.xml 里面 jar 包太多,太占内容,所以从源码中复制吧。

2.因为SSM三者整合全部放在一个 .xml 中太杂。所以这里拆成三个
spring-mybaits.xml ,spring-service ,spring-web
配置 spring-mybatis.xml ,包含如下内容:

  • 数据库配置文件 jdbc.properties。包含数据库地址 密码 库名等等。
1
2
复制代码    <!-- 1.配置数据库相关参数properties的属性:${url} -->
<context:property-placeholder location="classpath:jdbc.properties" />
  • 连接池。这里使用 c3p0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码   <!-- 2.数据库连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 配置连接池属性 -->
<property name="driverClass" value="${jdbc.driver}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />

<!-- c3p0连接池的私有属性 -->
<property name="maxPoolSize" value="30" />
<property name="minPoolSize" value="10" />
<!-- 关闭连接后不自动commit -->
<property name="autoCommitOnClose" value="false" />
<!-- 获取连接超时时间 -->
<property name="checkoutTimeout" value="10000" />
<!-- 当获取连接失败重试次数 -->
<property name="acquireRetryAttempts" value="2" />
</bean>
  • 配置SqlSessionFactory对象。专用来获取 sqlSession。
1
2
3
4
5
6
7
8
9
10
11
复制代码    <!-- 3.配置SqlSessionFactory对象 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource" />
<!-- 配置MyBaties全局配置文件:mybatis-config.xml -->
<property name="configLocation" value="classpath:mybatis-config.xml" />
<!-- 扫描entity包 使用别名 -->
<property name="typeAliasesPackage" value="ssm.entity" />
<!-- 扫描sql配置文件:mapper需要的xml文件 -->
<property name="mapperLocations" value="classpath*:mapper/*.xml" />
</bean>

在 resources 下新建 mybatis-config.xml ,此文件也称作 mybatis 的核心配置文件。里面内容为空 暂时

1
2
3
4
5
6
7
复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

</configuration>

新建 entity 包,用来放实体类,也就是 pojo。同时在 sqlSession 工厂中扫描整个实体类包。这样在 mapper 中就可以用 类名做别名,不用写整个类的相对位置路径了。

1
2
3
4
5
6
7
8
复制代码  <select id="queryUserListByUsername" parameterType="String" resultType="User">
SELECT
*
FROM
USER
WHERE
username = #{username}
</select>

比如其中的 User。不然就要写成

1
复制代码ssm.entity.User

对于 sql 配置文件,我这里放在 resources–>mapper–> 下。

这里写图片描述

对于 *mapper.xml 的位置处理有很多中方式。有的放在 mapper 包下。然后在 mapper 下建立两个包,分别放 dao 和 mapper

这里写图片描述

还有分别根据功能建包,一个功能建一个包,其中包含一组 功能文件,分别是 *Dao *Mapper.xml

这里写图片描述

具体怎样做根据实际项目情况来,如果项目系统功能复杂还是建议后面两种,不然到时候会很痛苦(项目多,文件就多,如果第一种方法,则跨度大,屏幕占满 :) )

<property name="mapperLocations" value="classpath*:mapper/*.xml" />
<property name="configLocation" value="classpath:mybatis-config.xml" />
根据自己实际情况填写

  • mapper 动态代理开发,扫描 dao 接口。
1
2
3
4
5
6
7
复制代码   <!-- 4.配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 注入sqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<!-- 给出需要扫描Dao接口包 -->
<property name="basePackage" value="ssm.dao" />
</bean>

<property name="basePackage" value="ssm.dao" /> 根据自己实际情况填写

ok!到这里位置,我们的 spring-mybatis 就配置好了。已经可以通过 mybatis 操作数据库了,现在我们来测试下 这一步出问题没。
先往数据库插入一些数据。这里我提供一份我的 sql 文件,如果各位有需要, 直接运行就可以了。

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
复制代码DROP TABLE IF EXISTS `items`;
CREATE TABLE `items` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL COMMENT '商品名称',
`price` float(10,1) NOT NULL COMMENT '商品定价',
`detail` text COMMENT '商品描述',
`pic` varchar(64) DEFAULT NULL COMMENT '商品图片',
`createtime` datetime NOT NULL COMMENT '生产日期',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of items
-- ----------------------------
INSERT INTO `items` VALUES ('1', '台式机', '3000.0', '该电脑质量非常好!!!!', null, '2016-02-03 13:22:53');
INSERT INTO `items` VALUES ('2', '笔记本', '6000.0', '笔记本性能好,质量好!!!!!', null, '2015-02-09 13:22:57');
INSERT INTO `items` VALUES ('3', '背包', '200.0', '名牌背包,容量大质量好!!!!', null, '2015-02-06 13:23:02');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) NOT NULL COMMENT '用户名称',
`birthday` date DEFAULT NULL COMMENT '生日',
`sex` char(1) DEFAULT NULL COMMENT '性别',
`address` varchar(256) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '王五', null, '2', null);
INSERT INTO `user` VALUES ('10', '张三', '2014-07-10', '1', '北京市');
INSERT INTO `user` VALUES ('16', '张小明', null, '1', '河南郑州');
INSERT INTO `user` VALUES ('22', '陈小明', null, '1', '河南郑州');
INSERT INTO `user` VALUES ('24', '张三丰', null, '1', '河南郑州');
INSERT INTO `user` VALUES ('25', '陈小明', null, '1', '河南郑州');
INSERT INTO `user` VALUES ('26', '王五', null, null, null);

这里写图片描述

这里写图片描述

这是整个项目的结构。
在 entity 下建立 User 实体类,注意字段对齐。
在 dao 下建立 UserDao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码import org.apache.ibatis.annotations.Param;
import ssm.entity.User;
import java.util.List;

/**
* Created by guozhaohui628@gmail.com on 2018/5/7
* Description:
*/
public interface UserDao {

public List<User> queryUserListByUsername(String username);

public List<User> queryUserListByUsername2Sex(@Param("username")String username, @Param("sex") int sex);

}

在 mapper 下新建 UserMappper.xml 用来写 sql 语句。因为测试,所以这里写一个简单的 sql 查询。

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
复制代码<?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="ssm.mapper.UserDao">
<!-- 目的:为dao接口方法提供sql语句配置 -->
<select id="queryUserListByUsername" parameterType="String" resultType="User">
SELECT
*
FROM
USER
WHERE
username = #{username}
</select>

<select id="queryUserListByUsername2Sex" resultType="User">
SELECT
*
FROM
USER
WHERE
username = #{username}
AND sex = #{sex}
</select>
</mapper>

其中有一个地方要注意 <mapper namespace="ssm.mapper.UserDao"> 映射位置 换成自己的,可能我们两不一样。

这里有个小技巧特别要注意,一般开发中,我们都是先在把 sql 写完运行正确才放到这里面来,比如下图中,并且为了美观,复制 sql 语句前都会规范下 sql 语句,避免太乱。

这里写图片描述

ok! 写一个测试类 测试下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import ssm.dao.UserDao;
import ssm.entity.User;

import java.util.List;

/**
* Created by guozhaohui628@gmail.com on 2018/5/7
* Description:
*/
public class TestPratice {
@Test
public void A(){
ApplicationContext ac = new ClassPathXmlApplicationContext("spring-mybatis.xml");
UserDao userDao = ac.getBean(UserDao.class);
List<User> userList = userDao.queryUserListByUsername2Sex("王五",1);
// List<User> userList = userDao.queryUserListByUsername("王五");
System.out.println(userList.toString());
}
}

这里写图片描述

好的,到这一步,说明我们的 mybatis 和 spring 整合没问题。

2.3 第三部分

现在来写 spring-service.xml ,比较简单,就是扫描下 service 类包和配置事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 扫描service包下所有使用注解的类型 -->
<context:component-scan base-package="ssm.service" />

<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource" />
</bean>

<!-- 配置基于注解的声明式事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>

spring + springmvc 整合 建立 spring-web ,里面内容比较少,只是扫描了 controller 用来保证前端控制器 DispatcherServlet 能够找到并进入相关方法中。
同时还配置了试图解析器 ViewResolve, 这样我们跳转视图时直接写 视图名称即可,不用写相对地址路径了。

1
2
3
4
5
6
7
8
9
复制代码    <!-- 3.配置jsp 显示ViewResolver -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/jsp/" />
<property name="suffix" value=".jsp" />
</bean>

<!-- 4.扫描web相关的 controller -->
<context:component-scan base-package="ssm.controller" />

最后就是配置 web.xml。主要作用是 配置 前端控制器 DispatcherServlet 重点是拦截规则处理。这里我们是所有都拦截。其次是 刚才我们 书写的 三个 spring-*.xml 文件在这里配置启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码 <!-- spring mvc servlet-->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/spring-*.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<!-- 此处也可以配置成 *.do *.action形式 -->
<url-pattern>/</url-pattern>
</servlet-mapping>

然后就是日志和编码,对于当前的测试没啥作用,但是还是配置好吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>classpath:log4j.properties</param-value>
</context-param>

<!-- 编码过滤器 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

好的 终于搞完了,现在可以安心的写代码了。现在准备一个这样的 jsp 文件。

这里写图片描述

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
复制代码<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<%@ page isELIgnored="false" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>查询商品列表</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/items/queryitem.action" method="post">
查询条件:
<table width="100%" border=1>
<tr>
<td><input type="submit" value="查询"/></td>
</tr>
</table>
商品列表:
<table width="100%" border=1>
<tr>
<td>商品名称</td>
<td>商品价格</td>
<td>生产日期</td>
<td>商品描述</td>
<td>操作</td>
</tr>
<c:forEach items="${itemList }" var="item">
<tr>
<td>${item.name }</td>
<td>${item.price }</td>
<td>${item.createtime}</td>
<td>${item.detail }</td>
<td><a href="${pageContext.request.contextPath }/itemEdit.action?id=${item.id}">修改</a></td>
</tr>
</c:forEach>
</table>
</form>
</body>

</html>

我们需要查询所有的 item 数据显示在上面。

这里写图片描述

首先写 ItemsDao

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码import org.springframework.stereotype.Repository;
import ssm.entity.Items;

import java.util.List;

/**
* Created by guozhaohui628@gmail.com on 2018/5/8
* Description:
*/
@Repository
public interface ItemsDao {
public List<Items> queryAllItemsList();
}

对应的 ImtesMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码<?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="ssm.dao.ItemsDao">
<!-- 目的:为dao接口方法提供sql语句配置 -->
<select id="queryAllItemsList" resultType="Items">
SELECT
*
FROM
items;
</select>
</mapper>

书写 ItemsService 接口和 其实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码import ssm.entity.Items;

import java.util.List;

/**
* Created by guozhaohui628@gmail.com on 2018/5/8
* Description:
*/
public interface ItemsService {

List<Items> queryAllItemsList();

}
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
复制代码package ssm.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ssm.dao.ItemsDao;
import ssm.entity.Items;

import java.util.List;

/**
* Created by guozhaohui628@gmail.com on 2018/5/8
* Description:
*/
@Service
public class ItemsServiceImpl implements ItemsService {

// 注入Service依赖
@Autowired
private ItemsDao itemsdao;

@Override
public List<Items> queryAllItemsList() {
return itemsdao.queryAllItemsList();
}
}

最后写 ItemsController

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
复制代码import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import ssm.entity.Items;
import ssm.service.ItemsServiceImpl;

import java.util.List;


/**
* Created by guozhaohui628@gmail.com on 2018/5/8
* Description:
*/
@Controller
public class ItemsController {


// 注入Service依赖
@Autowired
private ItemsServiceImpl itemsService;

@RequestMapping(value = "items/queryitem.action")
public ModelAndView itemsList(){
ModelAndView mav = new ModelAndView();
List<Items> itemsList = itemsService.queryAllItemsList();
System.out.println(itemsList.toString());
mav.addObject("itemList", itemsList);
mav.setViewName("itemList");
return mav;
}
}

.jsp中的代码已经写好了,直接用就可以了。
我们要测试 要么直接点击这个地址,要么点击查询

这里写图片描述

这里写图片描述

好的 大功告成,写的比较细 所以比较长。希望能拿帮到你!

3 留步

整合框架期间报错如下几个错误。如果有同样的情况可以参考一二

.JSP 中 EL 表达式不起作用 无效

Cause: java.lang.NoSuchMethodException
还有几个错误,没有记录下来,比如 spring注解方式 idea报could not autowire 等等。

整合期间参考:
这里写链接内容

如果帮到你,欢迎Star支持。源码地址
这里写链接内容

本文转载自: 掘金

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

如何快速成长为技术大牛?阿里资深技术专家的总结亮了!

发表于 2018-05-08

写在前面

不管是开发、测试、运维,每个技术人员心里多多少少都有一个成为技术大牛的梦,毕竟“梦想总是要有的,万一实现了呢”!正是对技术梦的追求,促使我们不断地努力和提升自己。

然而“梦想是美好的,现实却是残酷的”,很多同学在实际工作后就会发现,梦想是成为大牛,但做的事情看起来跟大牛都不沾边,例如,程序员说“天天写业务代码还加班,如何才能成为技术大牛”,测试说“每天都有执行不完的测试用例”,运维说“扛机器接网线敲shell命令,这不是我想要的运维人生”。

我也是一位程序员,所以我希望通过以下基于程序开发的一些例子,帮助大家解决这些困惑。大道理是相通的,测试、运维都可以借鉴。

几个典型的误区

拜大牛为师

有人认为想成为技术大牛最简单直接、快速有效的方式是“拜团队技术大牛为师”,让他们平时给你开小灶,给你分配一些有难度的任务。

我个人是反对这种方法的,主要的原因有几个:

  1. 大牛很忙,不太可能单独给你开小灶,更不可能每天都给你开1个小时的小灶;而且一个团队里面,如果大牛平时经常给你开小灶,难免会引起其他团队成员的疑惑,我个人认为如果团队里的大牛如果真正有心的话,多给团队培训是最好的。然而做过培训的都知道,准备一场培训是很耗费时间的,课件和材料至少2个小时(还不能是碎片时间),讲解1个小时,大牛们一个月做一次培训已经是很高频了。

因为第一个原因,所以一般要找大牛,都是带着问题去请教或者探讨。因为回答或者探讨问题无需太多的时间,更多的是靠经验和积累,这种情况下大牛们都是很乐意的,毕竟影响力是大牛的一个重要指标嘛。然而也要特别注意:如果经常问那些书本或者google能够很容易查到的知识,大牛们也会很不耐烦的,毕竟时间宝贵。经常有网友问我诸如“jvm的-Xmn参数如何配置”这类问题,我都是直接回答“请直接去google”,因为这样的问题实在是太多了,如果自己不去系统学习,每个都要问是非常浪费自己和别人的时间的。

  1. 大牛不多,不太可能每个团队都有技术大牛,只能说团队里面会有比你水平高的人,即使他每天给你开小灶,最终你也只能提升到他的水平;而如果是跨团队的技术大牛,由于工作安排和分配的原因,直接请教和辅导的机会是比较少的,单凭参加几次大牛的培训,是不太可能就成为技术大牛的。

综合上述的几个原因,我认为对于大部分人来说,要想成为技术大牛,首先还是要明白“主要靠自己”这个道理,不要期望有个像武功师傅一样的大牛手把手一步一步地教你。适当的时候可以通过请教大牛或者和大牛探讨来提升自己,但大部分时间还是自己系统性、有针对性的提升。

业务代码一样很牛逼

有人认为写业务代码一样可以很牛逼,理由是业务代码一样可以有各种技巧,例如可以使用封装和抽象使得业务代码更具可扩展性,可以通过和产品多交流以便更好的理解和实现业务,日志记录好了问题定位效率可以提升10倍等等。

业务代码一样有技术含量,这点是肯定的,业务代码中的技术是每个程序员的基础,但只是掌握了这些技巧,并不能成为技术大牛,就像游戏中升级打怪一样,开始打小怪,经验值很高,越到后面经验值越少,打小怪已经不能提升经验值了,这个时候就需要打一些更高级的怪,刷一些有挑战的副本了,没看到哪个游戏只要一直打小怪就能升到顶级的。成为技术大牛的路也是类似的,你要不断的提升自己的水平,然后面临更大的挑战,通过应对这些挑战从而使自己水平更上一级,然后如此往复,最终达到技术大牛甚至业界大牛的境界,写业务代码只是这个打怪升级路上的一个挑战而已,而且我认为是比较初级的一个挑战。

所以我认为:业务代码都写不好的程序员肯定无法成为技术大牛,但只把业务代码写好的程序员也还不能成为技术大牛。

上班太忙没时间自己学习

很多人认为自己没有成为技术大牛并不是自己不聪明,也不是自己不努力,而是中国的这个环境下,技术人员加班都太多了,导致自己没有额外的时间进行学习。

这个理由有一定的客观性,毕竟和欧美相比,我们的加班确实要多一些,但这个因素只是一个需要克服的问题,并不是不可逾越的鸿沟,毕竟我们身边还是有那么多的大牛也是在中国这个环境成长起来的。

我认为有几个误区导致了这种看法的形成:

1)上班做的都是重复工作,要想提升必须自己额外去学习

形成这个误区的主要原因还是在于认为“写业务代码是没有技术含量的”,而我现在上班就是写业务代码,所以我在工作中不能提升。

2)学习需要大段的连续时间

很多人以为要学习就要像学校上课一样,给你一整天时间来上课才算学习,而我们平时加班又比较多,周末累的只想睡懒觉,或者只想去看看电影打打游戏来放松,所以就没有时间学习了。

实际上的做法正好相反:首先我们应该在工作中学习和提升,因为学以致用或者有实例参考,学习的效果是最好的;其次工作后学习不需要大段时间,而是要挤出时间,利用时间碎片来学习。

正确的做法

Do more

做的更多,做的比你主管安排给你的任务更多。

我在HW的时候,负责一个版本的开发,这个版本的工作量大约是2000行左右,但是我除了做完这个功能,还将关联的功能全部掌握清楚了,代码(大约10000行)也全部看了一遍,做完这个版本后,我对这个版本相关的整套业务全部很熟悉了。经过一两次会议后,大家发现我对这块掌握最熟了,接下来就有趣了:产品讨论需求找我、测试有问题也找我、老大对外支撑也找我;后来,不是我负责的功能他们也找我,即使我当时不知道,我也会看代码或者找文档帮他们回答。最后我就成了我这个系统的“专家”了。虽然这个时候我还是做业务的,还是写业务代码,但是我已经对整个业务都很熟悉了。

以上只是一个简单的例子,其实就是想说:要想有机会,首先你得从人群中冒出来,要想冒出来,你就必须做到与众不同,要做到与众不同,你就要做得更多!

怎么做得更多呢?可以从以下几个方面着手:

1)熟悉更多业务,不管是不是你负责的;熟悉更多代码,不管是不是你写的

这样做有很多好处,举几个简单的例子:

需求分析的时候更加准确,能够在需求阶段就识别风险、影响、难点

问题处理的时候更加快速,因为相关的业务和代码都熟悉,能够快速的判断问题可能的原因并进行排查处理

方案设计的时候考虑更加周全,由于有对全局业务的理解,能够设计出更好的方案

2)熟悉端到端

比如说你负责web后台开发,但实际上用户发起一个http请求,要经过很多中间步骤才到你的服务器(例如浏览器缓存、DNS、nginx等),服务器一般又会经过很多处理才到你写的那部分代码(路由、权限等)这整个流程中的很多系统或者步骤,绝大部分人是不可能去参与写代码的,但掌握了这些知识对你的综合水平有很大作用,例如方案设计、线上故障处理这些更加有含金量的技术工作都需要综合技术水平。

“系统性”、“全局性”、“综合性”这些字眼看起来比较虚,但其实都是技术大牛的必备的素质,要达到这样的境界,必须去熟悉更多系统、业务、代码。

3)自学

一般在比较成熟的团队,由于框架或者组件已经进行了大量的封装,写业务代码所用到的技术确实也比较少,但我们要明白“唯一不变的只有变化”,框架有可能要改进,组件可能要替换,或者你换了一家公司,新公司既没有组件也没有框架,要你从头开始来做。这些都是机会,也是挑战,而机会和挑战只会分配给有准备的人,所以这种情况下我们更加需要自学更多东西,因为真正等到要用的时候再来学已经没有时间了。

以java为例,大部分业务代码就是if-else加个数据库操作,但我们完全可以自己学些更多java的知识,例如垃圾回收,调优,网络编程等,这些可能暂时没用,但真要用的时候,不是google一下就可以了,这个时候谁已经掌握了相关知识和技能,机会就是谁的。

以垃圾回收为例,我自己平时就抽时间学习了这些知识,学了1年都没用上,但后来用上了几次,每次都解决了卡死的大问题,而有的同学,写了几年的java代码,对于stop-the-world是什么概念都不知道,更不用说去优化了。

Do better

要知道这个世界上没有完美的东西,你负责的系统和业务,总有不合理和可以改进的地方,这些“不合理”和“可改进”的地方,都是更高级别的怪物,打完后能够增加更多的经验值。识别出这些地方,并且给出解决方案,然后向主管提出,一次不行两次,多提几次,只要有一次落地了,这就是你的机会。

例如:

重复代码太多,是否可以引入设计模式?

系统性能一般,可否进行优化?

目前是单机,如果做成双机是否更好?

版本开发质量不高,是否引入高效的单元测试和集成测试方案?

目前的系统太庞大,是否可以通过重构和解耦改为3个系统?

阿里中间件有一些系统感觉我们也可以用,是否可以引入 ?

只要你去想,其实总能发现可以改进的地方的;如果你觉得系统哪里都没有改进的地方,那就说明你的水平还不够,可以多学习相关技术,多看看业界其它优秀公司怎么做。

我2013年调配到九游,刚开始接手了一个简单的后台系统,每天就是配合前台做数据增删改查,看起来完全没意思,是吧?如果只做这些确实没意思,但我们接手后做了很多事情:

解耦,将一个后台拆分为2个后台,提升可扩展性和稳定性;

双机,将单机改为双机系统,提高可靠性;

优化,将原来一个耗时5小时的接口优化为耗时5分钟

还有其它很多优化,后来我们这个组承担了更多的系统,后来这个小组5个人,负责了6个系统。

Do exercise

在做职业等级沟通的时候,发现有很多同学确实也在尝试Do more、Do better,但在执行的过程中,几乎每个人都遇到同一个问题:光看不用效果很差,怎么办?

例如:

学习了jvm的垃圾回收,但是线上比较少出现FGC导致的卡顿问题,就算出现了,恢复业务也是第一位的,不太可能线上出现问题然后让每个同学都去练一下手,那怎么去实践这些jvm的知识和技能呢?

Netty我也看了,也了解了Reactor的原理,但是我不可能参与Netty开发,怎么去让自己真正掌握Reactor异步模式呢?

看了《高性能MySQL》,但是线上的数据库都是DBA管理的,测试环境的数据库感觉又是随便配置的,我怎么去验证这些技术呢?

框架封装了DAL层,数据库的访问我们都不需要操心,我们怎么去了解分库分表实现?

诸如此类问题还有很多,我这里分享一下个人的经验,其实就是3个词:learning、trying、teaching!

1)Learning

这个是第一阶段,看书、google、看视频、看别人的博客都可以,但要注意一点是“系统化”,特别是一些基础性的东西,例如JVM原理、Java编程、网络编程,HTTP协议等等,这些基础技术不能只通过google或者博客学习,我的做法一般是先完整的看完一本书全面的了解,然后再通过google、视频、博客去有针对性的查找一些有疑问的地方,或者一些技巧。

2)Trying

这个步骤就是解答前面提到的很多同学的疑惑的关键点,形象来说就是“自己动手丰衣足食”,也就是自己去尝试搭建一些模拟环境,自己写一些测试程序。例如:

Jvm垃圾回收:可以自己写一个简单的测试程序,分配内存不释放,然后调整各种jvm启动参数,再运行的过程中使用jstack、jstat等命令查看jvm的堆内存分布和垃圾回收情况。这样的程序写起来很简单,简单一点的就几行,复杂一点的也就几十行。

Reactor原理:自己真正去尝试写一个Reactor模式的Demo,不要以为这个很难,最简单的Reactor模式代码量(包括注释)不超过200行(可以参考Doug Lee的PPT)。自己写完后,再去看看netty怎么做,一对比理解就更加深刻了。

MySQL:既然有线上的配置可以参考,那可以直接让DBA将线上配置发给我们(注意去掉敏感信息),直接学习;然后自己搭建一个MySQL环境,用线上的配置启动;要知道很多同学用了很多年MySQL,但是连个简单的MySQL环境都搭不起来。

框架封装了DAL层:可以自己用JDBC尝试去写一个分库分表的简单实现,然后与框架的实现进行对比,看看差异在哪里。

用浏览器的工具查看HTTP缓存实现,看看不同种类的网站,不同类型的资源,具体是如何控制缓存的;也可以自己用Python写一个简单的HTTP服务器,模拟返回各种HTTP Headers来观察浏览器的反应。

还有很多方法,这里就不一一列举,简单来说,就是要将学到的东西真正试试,才能理解更加深刻,印第安人有一句谚语:I hear and I forget. I see and I remember. I do and I understand ,而且“试试”其实可以比较简单,很多时候我们都可以自己动手做。

当然,如果能够在实际工作中使用,效果会更好,毕竟实际的线上环境和业务复杂度不是我们写个模拟程序就能够模拟的,但这样的机会可遇不可求,大部分情况我们还真的只能靠自己模拟,然后等到真正业务要用的时候,能够信手拈来。

3)Teaching

一般来说,经过Learning和Trying,能掌握70%左右,但要真正掌握,我觉得一定要做到能够跟别人讲清楚。因为在讲的时候,我们既需要将一个知识点系统化,也需要考虑各种细节,这会促使我们进一步思考和学习。同时,讲出来后看或者听的人可以有不同的理解,或者有新的补充,这相当于继续完善了整个知识技能体系。

这样的例子很多,包括我自己写博客的时候经常遇到,本来我觉得自己已经掌握很全面了,但一写就发现很多点没考虑到;组内培训的时候也经常看到,有的同学写了PPT,但是讲的时候,大家一问,或者一讨论,就会发现很多点还没有讲清楚,或者有的点其实是理解错了。写PPT、讲PPT、讨论PPT,这个流程全部走一遍,基本上对一个知识点掌握就比较全面了。

后记

成为技术大牛梦想虽然很美好,但是要付出很多,不管是Do more还是Do better还是Do exercise,都需要花费时间和精力,这个过程中可能很苦逼,也可能很枯燥,这里我想特别强调一下:前面我讲的都是一些方法论的东西,但真正起决定作用的,其实还是我们对技术的热情和兴趣!

我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。

原文:jianshu.com/p/8896f07d9219

作者:李运华

本文转载自: 掘金

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

1…892893894…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%