2019年Java面试-并发容器篇

我将JUC包中的集合类划分为3部分来进行说明。在简单的了解JUC包中集合类的框架之后,后面的章节再逐步对各个类进行介绍。List和SetMapQueue全网唯一一个从0开始帮助Java开发者转做大数据领域的公众号公众号大数据技术与架构或者搜索import_bigdata关注,大数据学习路线最新更新,已经有很多小伙伴加入了List和SetJUC(java.util.concurrent)集合包中的List和Set实现类包括:CopyOnWriteArrayListCopyOnWriteArraySetConcurrentSkipListSetConcurrentSkipListSet稍后在说明Map时再说明,CopyOnWriteArrayList和CopyOnWriteArraySet的框架如下图所示:CopyOnWriteArrayList相当于线程安全的ArrayList,它实现了List接口。CopyOnWriteArrayList是支持高并发的。CopyOnWriteArraySet相当于线程安全的HashSet,它继承于AbstractSet类。CopyOnWriteArraySet内部包含一个CopyOnWriteArrayList对象,它是通过CopyOnWriteArrayList实现的。MapJUC集合包中Map的实现类包括: ConcurrentHashMap和ConcurrentSkipListMap。它们的框架如下图所示:ConcurrentHashMap是线程安全的哈希表(相当于线程安全的HashMap);它继承于AbstractMap类,并且实现ConcurrentMap接口。ConcurrentHashMap是通过“锁分段”来实现的,它支持并发。ConcurrentSkipListMap是线程安全的有序的哈希表(相当于线程安全的TreeMap); 它继承于AbstractMap类,并且实现ConcurrentNavigableMap接口。ConcurrentSkipListMap是通过“跳表”来实现的,它支持并发。ConcurrentSkipListSet是线程安全的有序的集合(相当于线程安全的TreeSet);它继承于AbstractSet,并实现了NavigableSet接口。ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,它也支持并发。QueueJUC集合包中Queue的实现类包括: ArrayBlockingQueue, LinkedBlockingQueue, LinkedBlockingDeque, ConcurrentLinkedQueue和ConcurrentLinkedDeque。它们的框架如下图所示: ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列。LinkedBlockingQueue是单向链表实现的(指定大小)阻塞队列,该队列按 FIFO(先进先出)排序元素。LinkedBlockingDeque是双向链表实现的(指定大小)双向并发阻塞队列,该阻塞队列同时支持FIFO和FILO两种操作方式。ConcurrentLinkedQueue是单向链表实现的无界队列,该队列按 FIFO(先进先出)排序元素。ConcurrentLinkedDeque是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。

February 24, 2019 · 1 min · jiezi

Spring - Java-based configuration: Using @Configuration

@Configuration这是一个类级注解。如下所示,被它注解的类可能包含多个被@Bean注解的方法。Spring容器会调用这些方法,获得你初始化后的对象实例,并把他们注册为容器内的beans。package spring.example@Configurationpublic class MyAppConfig { @bean public SomeBean someBean() { // 实例化并返回,也可进行初始化 return new SomeBeanImpl(); }}同等作用的XML配置会像下面这样:<bean name=“someBean” class=“spring.example.SomeBeanImpl”/>@Configuration类们实际上就是Spring管理的用于创建并注册bean实例的工厂。Spring容器的启动在Java-based的配置方式下,spring容器可以被AnnotationConfigApplicationContext启动,或者,针对Web应用AnnotationConfigWebApplicationContext也行。new AnnotationConfigApplicationContext(MyAppConfig.class);我们也可以指定包含了@Configuration类的有效包名:new AnnotationConfigApplicationContext(“spring.example”);基于上述两个重载方法,我们可以在单个package下放进多个JavaConfig类。使用多个JavaConfig类new AnnotationConfigApplicationContext( AppConfig.class, DataSourceConfig.class );new AnnotationConfigApplicationContext(“example.spring.app”,“example.spring.datasource”);配置类中的依赖注入既然配置类会被Spring容器注册成beans,那意味着,我们可以像使用普通bean那样使用这个配置bean。在以下例子我们要把这个一个配置bean注入给另一个配置bean:@Configurationpublic class AppConfig { // 方式一:注入DataSourceConfig @Autowired private DataSourceConfig dataSourceConfig; @Bean Client clientBean() { return new Client(dataSourceConfig.dataSourceBean()); } public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class, DataSourceConfig.class); context.getBean(Client.class).showData(); } // 方式二:为何要那么麻烦呢?直接注入DataSourceBean不就好了? @Autowired private DataSourceBean dataSourceBean; @Bean Client clientBean() { return new Client(dataSourceBean); }}@Configurationclass DataSourceConfig { @Bean DataSourceBean dataSourceBean() { return new DataSourceBean(); }}class Client { private DataSourceBean dataSourceBean; Client(DataSourceBean dataSourceBean){ this.dataSourceBean = dataSourceBean; } public void showData() { System.out.println(dataSourceBean.getData()); }}class DataSourceBean { public String getData() { return “some data”; }} ...

February 23, 2019 · 1 min · jiezi

某熊的技术之路指北 ☯

某熊的技术之路指北 ☯当我们站在技术之路的原点,未来可能充满了迷茫,也存在着很多不同的可能;我们可能成为 Web/(大)前端/终端工程师、服务端架构工程师、测试/运维/安全工程师等质量保障、可用性保障相关的工程师、大数据/云计算/虚拟化工程师、算法工程师、产品经理等等某个或者某几个角色。某熊的技术之路系列文章/书籍/视频/代码即是笔者蹒跚行进于这条路上的点滴印记,包含了笔者作为程序员的技术视野、知识管理与职业规划,致力于提升开发者的学习能力与实际研发效能。本指北就是对笔者不同领域方面沉淀下的知识仓库的导航与索引,便于读者快速地寻找到自己需要的内容。我们也可以在笔者的个人主页,或者公众号(WIP)中,或者使用 alfred-sg 这样的本地工具进行关键字检索。路漫漫其修远兮,吾正上下而求索,也希望能给所有遇见过笔者痕迹的同学些许帮助,在浩瀚银河间能顺利达到一个又一个彼岸。Just Coder,Travel in Galaxy,欢迎关注某熊的技术之路公众号,让我们一起前行。0.阅读,笔记与编码博观而约取,厚积而薄发。在这个知识爆炸与终身学习/碎片化学习为主的时代,我们面临的问题之一就是如何进行有效学习,不仅能有效平衡广度与深度,并且能真正的积淀下来,提升自己的研发效能。于笔者而言,常常郁结于胸的就是以下三个问题:应该学习什么?这是怎样的一个技术世界?存在着怎样的高峰与路径?如何克服遗忘带来的无效学习?如何不再碎片化地学习?究其根本,也就是需要拓展自己的知识广度,精进自己的知识深度,锤炼自己的编程能力。所谓知识广度,即是为实际问题选择合适的解决方案的能力,广义来说也是眼界与格局的表现。它并不拘泥于某个技术方向或者行业领域,而需要对传统/流行的各类语言、工具、框架、库、服务等有一定的认识;能够明晰各个方案的优劣,并在较高的层次(High Level)描述相关原理。知识广度的拓展与保持需要建立在庞大的阅读量与知识沉淀能力上。Awesome Lists 就为我们准备了精而全的技术开发学习与实践资料索引,去芜存菁,去重留一;譬如其中的 Awesome WebSites 一文就为我们推荐了值得阅读的资讯、博客等站点列表。知识广度的拓展也并非一蹴而就之事,需得循序渐进,从初窥门径,到登堂入室,最后融会贯通,当我们感觉乱花渐欲迷人眼,太多的碎片化知识反而使自己迷失方向之际,就可以前往 Awesome CS Books Warehouse,去深入地阅读学习各个领域的精选书籍、课程等系统化的内容。俗话说,好记性不如烂笔头,当我们阅读的多了,自然也要开始记录;而笔者认为记录的开始就要有自己的知识体系。在自己的知识体系下随看随记、定期整理。唯有建立符合自己认知方式的知识图谱,才能有效地沉淀知识,明晰知识边界并进行不断地探索。上车伊始,笔者即致力于构建自己的 MindMap, IT 技术图谱与知识架构,提供了软件工程通用、前端、后端、DevOps、测试、架构师、人工智能工程师等多领域的知识图谱、学习成长路线与面试必备内容,并在数年来不断维护与刷新。笔者目前选择的是以 MarkDown 格式记录,并且将所有的笔记存放于 Github-文档札记以 Git 方式进行版本管理;编辑器是直接使用的 VSCode,移动端编辑的话也是用的 GitGo/WorkCopy 这样的 Git 应用。这些笔记即是笔者自身技术视野与认知的外化,也类比于外设之于内存,在需要的时候分页加载到脑海中使用,以应对这知识爆炸的时代。其中的典型代表,Awesome CheatSheets,对于日常开发中用到的相关知识的备忘录/清单进行总结, 适合快速掌握或者回顾某个语言/框架/工具的语法或使用要点。Tech Road, 我的技术之路是对于笔者多年学习与认知变迁的总结。先贤有云,知行合一,知是行之始,行是知之成,Linus Torvalds 也曾提到: ‘Talk is cheap. Show me the code.’,在阅读与笔记之后,就是要开始实践编码。所谓编程能力,并不仅仅是编写代码(Write Code)的能力,而是包含了阅读、编写、理解、重构、抽象等多个方面,是所谓的代码管理/掌控。其外在表现之一即是能够随时随地用合适的语言无阻塞地实现某些功能需求,对于常见的语法,接口,算法,设计模式等能够做到心随意动,信手拈来。编程能力是提升研发效能的重要保障,于笔者而言也是毕生应该追求的目标与爱好之一。笔者的编程能力较弱,日常开发,特别是在多语言多框架并用的场景下,往往会需要不断地中断,查找以继续工作,也是令我颇为苦恼。前文重在讨论如何拓宽技术视野、追寻技术的广度,但是需要铭记的是,技术深度才是技术广度的基石,正如中国自古以来常用道术之辩,知其然,也要知其所以然;亦如 Richard Feynman 所述:”What I cannot create, I do not understand.”。所谓知识深度,即是能够对某个方面做到深入了解,并且达到融会贯通,洞若观火,可以随心所欲地加以扩展、优化、创新等改造或变换。这方面则更加的见仁见智,不同的领域与方向对于深度的定义与挖掘方向也是千差万别。我们需要自己去从零开始造些轮子,才能深刻理解使用的框架/库/平台的内部原理,才能在碰到故障时快速地修复;在下文的几乎每个章节中,我们都会提到某些笔者自造的轮子。1.编程语言编程语言是一切的基础,正如 Steve McConnell 在 Code Complete 一书中提及,我们应该 Program into a language 而不是 Program in a language,针对不同的需要选择合适的编程语言来实现,而不是受制于自己所会的语言。在知识图谱中也包含了跨编程语言的公共知识杂谈,笔者与编程语言相关的文章存放在 Programming Language Series | 编程语言语法基础与工程实践仓库中,其涵盖了 C/C++、Go、Java、JavaScript、Python、Rust、Swift 等常见的语言,以及通用的编程语言理论。编程能力锻炼的基础,首要的就是关于数据结构与算法,以及面向对象的设计模式,其对应的代码分别存放在了 coding-snippets, algorithm-snippets, design-pattern-snippets 中。此外,我们还可以从零编写一些类似于 Guava & Lodash 这样的自己的通用工具库,笔者自身是整合在了 Guash 中。在编程语言之上,我们就需要考虑如何去实现真正的软件系统,譬如 软件工程基础 系列中的开发工具/Git 漫谈、软件系统架构、软件质量保障等内容,我们也可以自己去实现一些自己的工具,譬如笔者的 Soogle 是构建自身搜索、外部服务访问能力的工具集合;而 xCompass 是包含个人主页在内的多端阅读能力的源代码仓库。2.Web 与大前端工程师如果您对于 JavaScript 基础语法尚不完全了解,那么建议您首先浏览现代 JavaScript 语法基础与工程实践或者 JavaScript-CheatSheet 以了解基础的 JavaScript 语法及实践应用。如果您想快速地了解 Web 开发实践,或者是想查阅某些清单,那么建议您前往 Awesome-CheatSheets/Web;或者从导论篇开始阅读,它会包含 Web 开发简史与变迁、数据流驱动的界面、模块化与组件化、工具化与工程化、前后端分离与全栈架构、微前端与大前端、运行机制与性能优化等内容。接下来,您可以选择以下章节中感兴趣的模块进行深度阅读:基础篇: 对于 HTML、CSS、DOM 等 Web 开发中涉及的基础知识与理念的总结介绍。工程实践篇: 构建工具,测试,安全,WebAssembly。架构优化篇: 组件化,状态管理,性能优化,PWA。React 篇:近年来前端领域百花齐放,各种技术方案争妍斗艳,各领风骚。本书立足于其中的佼佼者 React,深入浅出的介绍 React, Webpack, ES6, Redux, MobX 等常见前端开发工具与开发库的用法,帮助初学者能够迅速成为一名合格前端工程师。而本书也不仅局限于工具使用的层面,探寻各种技术方案背后蕴含的设计思想与架构模式,从前端工程化的角度讨论前端开发者在进阶过程中需要掌握的工程实践、模块化与组件化、质量保障、性能优化等知识要点。最终帮助开发者在前端开发中能够因地制宜的指定合理方案,以尽可能快的速度实现可信赖的产品。在阅读之外,我们同样需要进行大量的代码实践,不仅仅是熟悉常用的框架,还需要去积累自己的组件、框架等功能库:fe-boilerplates 是笔者对于日常工作中的基于 React/Vue.js 技术栈与实践的收集与沉淀;为了方便不同级别/熟练程度的开发者使用,笔者将模板尽可能地泛化为多个项目,包含了从入门级到生产环境,微前端等多个不同层次/复杂度的模板项目。fractal-components 则是笔者日常工作中总结出来的应用、组件库以及组件开发模式,为了保证其独立性与复用性,笔者以不同的方式实现了组件。Ueact 旨在从零开始实现自定义的组件系统,多调和策略与数据流响应方式,同时能够被渲染/编译到多种组件。Legoble 则承载了自己实现一款可视化的应用构建工具的念想。Pudding 是有关于 Web Automation、多维度记录、回放、优化工具等集合。3.服务端架构工程师这是全栈的时代,我们更多地以业务来划分而非单纯地前后端,Backend Series | 服务端应用程序开发与系统架构/微服务架构与实践承载了笔者在服务端的总结与经验,其包含了服务端应用程序开发基础,深入浅出 Node.js 全栈架构,Spring Boot 5 与 Spring Cloud 微服务实践等内容。Backend-Boilerplates is Boilerplate for Your Server Side(Backend) Application, Java | Spring(Boot, Cloud) | Node.js(Express, Koa, Egg) | Go | Python | DevOps.winter-boot is Another boot for your Java applications like Spring Boot, but Winter is coming.4.测试/运维/安全工程师软件系统的质量保障是服务端运维不可绕过的部分,其包含了软件测试基础以及 DevOps 与 SRE 实战,信息安全与渗透测试必知必会等相关内容。在实践方面,我们还可以参考:Chaos-Scanner 混沌守望者(扫描器),半自动化分布式智能网络空间测绘、管理与安全探测。xe-crawler 是遵循声明式、可监测理念的分布式爬虫,其计划提供 Node.js、Go、Python 多种实现,能够对于静态 Web 页面、动态 Web 页面、关系型数据库、操作系统等异构多源数据进行抓取。5.大数据/云计算/虚拟化工程师前文讨论的更多是应用层的知识,而对于更底层的操作系统、数据库、大数据处理等分布式基础架构相关内容,都存放在了 Distributed-Infrastructure-Series | 深入浅出分布式基础架构系列中,主要包含分布式计算、分布式系统、数据存储、虚拟化、网络、操作系统等几个部分。如上文所述,我们需要在重构轮子中成长:Reinvent-MQ 即是 Multiple home-made Message Queues, LocalMQ(akin RocketMQ), PongoMQ(akin Kafka), etc.Reinvent-DB 即是 Multiple home-made Databases, Godis(akin Redis), HiSQL(akin MySQL), MemDB, DataGo(akin ETL) etc. Understanding DBs by Reinventing It.Focker 是从零开始自定义的类 Docker 简化版容器实现。SparkChain 即是在区块链方面的实验探索的积累。6.算法工程师前文我们讨论过数据结构与算法的相关内容,而在人工智能火热的现在,AIDL-Series | 人工智能与深度学习实战系列包含了数学原理篇、机器学习篇、深度学习篇、自然语言篇、工程实践篇、人工智能与深度学习课程篇等内容。在实践方面,代码主要存放于 Artificial Intelligence & Deep Learning Workbench 中。7.产品经理笔者选择了产品经理作为压轴之篇,也是希望能表述自己关于产品的观点,无论是我们创造的库、框架、应用还是平台,乃至于我们的文章、整理的系列书籍,都当以产品视之,跳出上帝视角,从用户的角度去考量。我们首先可以阅读些产品经理/用户体验方面的书籍。笔者目前积累不多,主要在 Product Series | 产品迷思中,其首先会关注产品经理的基础素养、用户交互体验、文档处理等方面。其次会讨论有关于项目管理、通用的领域能力构建(流程引擎、CRM 等)以及对于经典产品的分析。最后,该系列还会关注于具体的行业观点,譬如电子商务、智能制造等。很多时候,自己动手做些小产品也是有趣的事情,譬如 MushiChat 这样的聊天平台与聊天机器人、IoTable 这样在 IoT 领域的一些探索。 ...

February 23, 2019 · 2 min · jiezi

Spring - Configuration Metadata

metadata:元数据Spring configuration metadata则是告知Spring容器:如何初始化,配置,包裹,和组合应用内特定的对象。Spring从2002年发布第一版到至今最新版,提供了三种方式去进行应用配置:XML-based Configuration: 所有的配置信息存放于多个XML文件中,这也是最冗长繁琐的配置方式。超大型项目们,需要配置超大量的XML文件。想象下就可知道有多难以管理吧?Annotation-based Configuration:Spring 2.5 开始引入基于注解的配置方式,我们仍然需要写XML文件,但现在只需要告知Spring去"component-scan"注解类所在的package即可。Java-based configuration (JavaConfig): 从3.0开始,Spring提供一种纯Java代码的配置方式。我们不再需要写大量的XML文件了。JavaConfig方式为依赖注入提供了一种真正的面向对象机制,意味着我们可以在配置代码中充分地使用Java语言的可重用性,继承性和多态性。开发者完全掌控了应用中bean的初始化和各项依赖注入的关系等等。在这篇文章中我们只关注如JavaConfig。熟识其中一种方式已经足够去理解Spring容器的关键特性了。无论我们使用哪种方式,如上图所示,我们只需要在三处地方使用configuration metadata:Injection Points:这是各项依赖关系必须被注入的地方。注入点一般都是bean Class中的fields/setters/constructors. Spring在bean loading阶段中把相应的对象实例注入到注入点中。Service Providers:这是各项Service的具体实现类,他们的实例对象会被注入到各个bean的Injection Points中。这些Service Providers类本身会在Spring Container中被初始化,并被登记注册为Spring beans,而且他们自身也可以拥有注入点。The Configuration:这里是被@Configuration注解的Java类。同时也是我们定义依赖关系的地方。

February 23, 2019 · 1 min · jiezi

[spring boot] jdbc

1、pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.13</version></dependency>2、application.propertiesspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.url=jdbc:mysql://10.24.73.247:5002/waimai?characterEncoding=utf8spring.datasource.username=spring.datasource.password=3、JdbcTemplate@RestControllerpublic class DBController { @Autowired private JdbcTemplate jdbcTemplate; @GetMapping("/query") public Object load() { String sql = “select * from wm_act_poi_v2 where wm_poi_id = 515”; List<Map<String, Object>> result = jdbcTemplate.queryForList(sql); return result; }}

February 23, 2019 · 1 min · jiezi

Netty源码解析1-Buffer

原文 :GitHub原文: https://github.com/wangzhiwub…更多文章关注:多线程/集合/分布式/Netty/NIO/RPCJava高级特性增强-集合Java高级特性增强-多线程Java高级特性增强-SynchronizedJava高级特性增强-volatileJava高级特性增强-并发集合框架Java高级特性增强-分布式Java高级特性增强-ZookeeperJava高级特性增强-JVMJava高级特性增强-NIORPCzookeeperJVMNIO其他更多上一篇文章我们概要介绍了Netty的原理及结构,下面几篇文章我们开始对Netty的各个模块进行比较详细的分析。Netty的结构最底层是buffer机制,这部分也相对独立,我们就先从buffer讲起。What:buffer简介buffer中文名又叫缓冲区,按照维基百科的解释,是"在数据传输时,在内存里开辟的一块临时保存数据的区域"。它其实是一种化同步为异步的机制,可以解决数据传输的速率不对等以及不稳定的问题。根据这个定义,我们可以知道涉及I/O(特别是I/O写)的地方,基本会有Buffer了。就Java来说,我们非常熟悉的Old I/O–InputStream&OutputStream系列API,基本都是在内部使用到了buffer。Java课程老师就教过,必须调用OutputStream.flush(),才能保证数据写入生效!而NIO中则直接将buffer这个概念封装成了对象,其中最常用的大概是ByteBuffer了。于是使用方式变为了:将数据写入Buffer,flip()一下,然后将数据读出来。于是,buffer的概念更加深入人心了!Netty中的buffer也不例外。不同的是,Netty的buffer专为网络通讯而生,所以它又叫ChannelBuffer(好吧其实没有什么因果关系…)。我们下面就来讲讲Netty中得buffer。当然,关于Netty,我们必须讲讲它的所谓"Zero-Copy-Capable"机制。TCP/IP协议与bufferTCP/IP协议是目前的主流网络协议。它是一个多层协议,最下层是物理层,最上层是应用层(HTTP协议等),而做Java应用开发,一般只接触TCP以上,即传输层和应用层的内容。这也是Netty的主要应用场景。TCP报文有个比较大的特点,就是它传输的时候,会先把应用层的数据项拆开成字节,然后按照自己的传输需要,选择合适数量的字节进行传输。什么叫"自己的传输需要"?首先TCP包有最大长度限制,那么太大的数据项肯定是要拆开的。其次因为TCP以及下层协议会附加一些协议头信息,如果数据项太小,那么可能报文大部分都是没有价值的头信息,这样传输是很不划算的。因此有了收集一定数量的小数据,并打包传输的Nagle算法(这个东东在HTTP协议里会很讨厌,Netty里可以用setOption(“tcpNoDelay”, true)关掉它)。这么说可能太学院派了一点,我们举个例子吧:发送时,我们这样分3次写入(’|‘表示两个buffer的分隔): +—–+—–+—–+ | ABC | DEF | GHI | +—–+—–+—–+接收时,可能变成了这样: +—-+——-+—+—+ | AB | CDEFG | H | I | +—-+——-+—+—+很好懂吧?可是,说了这么多,跟buffer有个什么关系呢?别急,我们来看下面一部分。Buffer中的分层思想我们先回到之前的messageReceived方法: public void messageReceived( ChannelHandlerContext ctx, MessageEvent e) { // Send back the received message to the remote peer. transferredBytes.addAndGet(((ChannelBuffer) e.getMessage()).readableBytes()); e.getChannel().write(e.getMessage()); }这里MessageEvent.getMessage()默认的返回值是一个ChannelBuffer。我们知道,业务中需要的"Message",其实是一条应用层级别的完整消息,而一般的buffer工作在传输层,与"Message"是不能对应上的。那么这个ChannelBuffer是什么呢?来一个官方给的图,我想这个答案就很明显了:这里可以看到,TCP层HTTP报文被分成了两个ChannelBuffer,这两个Buffer对我们上层的逻辑(HTTP处理)是没有意义的。但是两个ChannelBuffer被组合起来,就成为了一个有意义的HTTP报文,这个报文对应的ChannelBuffer,才是能称之为"Message"的东西。这里用到了一个词"Virtual Buffer",也就是所谓的"Zero-Copy-Capable Byte Buffer"了。顿时觉得豁然开朗了有没有!我这里总结一下,如果说NIO的Buffer和Netty的ChannelBuffer最大的区别的话,就是前者仅仅是传输上的Buffer,而后者其实是传输Buffer和抽象后的逻辑Buffer的结合。延伸开来说,NIO仅仅是一个网络传输框架,而Netty是一个网络应用框架,包括网络以及应用的分层结构。当然,在Netty里,默认使用ChannelBuffer表示"Message",不失为一个比较实用的方法,但是MessageEvent.getMessage()是可以存放一个POJO的,这样子抽象程度又高了一些,这个我们在以后讲到ChannelPipeline的时候会说到。Netty中的ChannelBuffer及实现好了,终于来到了代码实现部分。之所以啰嗦了这么多,因为我觉得,关于"Zero-Copy-Capable Rich Byte Buffer",理解为什么需要它,比理解它是怎么实现的,可能要更重要一点。我想可能很多朋友跟我一样,喜欢"顺藤摸瓜"式读代码–找到一个入口,然后顺着查看它的调用,直到理解清楚。很幸运,ChannelBuffers(注意有s!)就是这样一根"藤",它是所有ChannelBuffer实现类的入口,它提供了很多静态的工具方法来创建不同的Buffer,靠“顺藤摸瓜”式读代码方式,大致能把各种ChannelBuffer的实现类摸个遍。先列一下ChannelBuffer相关类图。此外还有WrappedChannelBuffer系列也是继承自AbstractChannelBuffer,图放到了后面。ChannelBuffer中的readerIndex和writerIndex开始以为Netty的ChannelBuffer是对NIO ByteBuffer的一个封装,其实不是的,它是把ByteBuffer重新实现了一遍。以最常用的HeapChannelBuffer为例,其底层也是一个byte[],与ByteBuffer不同的是,它是可以同时进行读和写的,而不需要使用flip()进行读写切换。ChannelBuffer读写的核心代码在AbstactChannelBuffer里,这里通过readerIndex和writerIndex两个整数,分别指向当前读的位置和当前写的位置,并且,readerIndex总是小于writerIndex的。贴两段代码,让大家能看的更明白一点: public void writeByte(int value) { setByte(writerIndex ++, value); } public byte readByte() { if (readerIndex == writerIndex) { throw new IndexOutOfBoundsException(“Readable byte limit exceeded: " + readerIndex); } return getByte(readerIndex ++); } public int writableBytes() { return capacity() - writerIndex; } public int readableBytes() { return writerIndex - readerIndex; }我倒是觉得这样的方式非常自然,比单指针与flip()要更加好理解一些。AbstactChannelBuffer还有两个相应的mark指针markedReaderIndex和markedWriterIndex,跟NIO的原理是一样的,这里不再赘述了。字节序Endianness与HeapChannelBuffer在创建Buffer时,我们注意到了这样一个方法:public static ChannelBuffer buffer(ByteOrder endianness, int capacity);,其中ByteOrder是什么意思呢?这里有个很基础的概念:字节序(ByteOrder/Endianness)。它规定了多余一个字节的数字(int啊long什么的),如何在内存中表示。BIG_ENDIAN(大端序)表示高位在前,整型数12会被存储为0 0 0 12四字节,而LITTLE_ENDIAN则正好相反。可能搞C/C++的程序员对这个会比较熟悉,而Javaer则比较陌生一点,因为Java已经把内存给管理好了。但是在网络编程方面,根据协议的不同,不同的字节序也可能会被用到。目前大部分协议还是采用大端序,可参考RFC1700。了解了这些知识,我们也很容易就知道为什么会有BigEndianHeapChannelBuffer和LittleEndianHeapChannelBuffer了!DynamicChannelBufferDynamicChannelBuffer是一个很方便的Buffer,之所以叫Dynamic是因为它的长度会根据内容的长度来扩充,你可以像使用ArrayList一样,无须关心其容量。实现自动扩容的核心在于ensureWritableBytes方法,算法很简单:在写入前做容量检查,容量不够时,新建一个容量x2的buffer,跟ArrayList的扩容是相同的。贴一段代码吧(为了代码易懂,这里我删掉了一些边界检查,只保留主逻辑): public void writeByte(int value) { ensureWritableBytes(1); super.writeByte(value); } public void ensureWritableBytes(int minWritableBytes) { if (minWritableBytes <= writableBytes()) { return; } int newCapacity = capacity(); int minNewCapacity = writerIndex() + minWritableBytes; while (newCapacity < minNewCapacity) { newCapacity <<= 1; } ChannelBuffer newBuffer = factory().getBuffer(order(), newCapacity); newBuffer.writeBytes(buffer, 0, writerIndex()); buffer = newBuffer; }CompositeChannelBufferCompositeChannelBuffer是由多个ChannelBuffer组合而成的,可以看做一个整体进行读写。这里有一个技巧:CompositeChannelBuffer并不会开辟新的内存并直接复制所有ChannelBuffer内容,而是直接保存了所有ChannelBuffer的引用,并在子ChannelBuffer里进行读写,从而实现了"Zero-Copy-Capable"了。来段简略版的代码吧: public class CompositeChannelBuffer{ //components保存所有内部ChannelBuffer private ChannelBuffer[] components; //indices记录在整个CompositeChannelBuffer中,每个components的起始位置 private int[] indices; //缓存上一次读写的componentId private int lastAccessedComponentId; public byte getByte(int index) { //通过indices中记录的位置索引到对应第几个子Buffer int componentId = componentId(index); return components[componentId].getByte(index - indices[componentId]); } public void setByte(int index, int value) { int componentId = componentId(index); components[componentId].setByte(index - indices[componentId], value); } } 查找componentId的算法再次不作介绍了,大家自己实现起来也不会太难。值得一提的是,基于ChannelBuffer连续读写的特性,使用了顺序查找(而不是二分查找),并且用lastAccessedComponentId来进行缓存。ByteBufferBackedChannelBuffer前面说ChannelBuffer是自己的实现的,其实只说对了一半。ByteBufferBackedChannelBuffer就是封装了NIO ByteBuffer的类,用于实现堆外内存的Buffer(使用NIO的DirectByteBuffer)。当然,其实它也可以放其他的ByteBuffer的实现类。代码实现就不说了,也没啥可说的。WrappedChannelBufferWrappedChannelBuffer都是几个对已有ChannelBuffer进行包装,完成特定功能的类。代码不贴了,实现都比较简单,列一下功能吧。可以看到,关于实现方面,Netty 3.7的buffer相关内容还是比较简单的,也没有太多费脑细胞的地方。而Netty 4.0之后就不同了。4.0,ChannelBuffer改名ByteBuf,成了单独项目buffer,并且为了性能优化,加入了BufferPool之类的机制,已经变得比较复杂了(本质倒没怎么变)。性能优化是个很复杂的事情,研究源码时,建议先避开这些东西,除非你对算法情有独钟。举个例子,Netty4.0里为了优化,将Map换成了Java 8里6000行的ConcurrentHashMapV8,你们感受一下…参考资料:TCP/IP协议 http://zh.wikipedia.org/zh-cn/TCP/IP%E5%8D%8F%E8%AE%AEData_buffer http://en.wikipedia.org/wiki/Data_bufferEndianness http://en.wikipedia.org/wiki/Endianness 请戳GitHub原文: https://github.com/wangzhiwubigdata/God-Of-BigData 关注公众号,内推,面试,资源下载,关注更多大数据技术~ 大数据成神之路预计更新500+篇文章,已经更新60+篇 ...

February 23, 2019 · 2 min · jiezi

领域驱动设计,构建简单的新闻系统,20分钟够吗?

让我们使用领域驱动的方式,构建一个简单的系统。1. 需求新闻系统的需求如下:创建新闻类别;修改新闻类别,只能更改名称;禁用新闻类别,禁用后的类别不能添加新闻;启用新闻类别;根据类别id获取类别信息;指定新闻类别id,创建新闻;更改新闻信息,只能更改标题和内容;禁用新闻;启用新闻;分页查找给定类别的新闻,禁用的新闻不可见。2. 工期估算大家觉得,针对上面需求,大概需要多长时间可以完成,可以先写下来。3. 起航3.1. 项目准备构建项目,使用 http://start.spring.io 或使用模板工程,构建我们的项目(Sprin Boot 项目),在这就不多叙述。3.1.1. 添加依赖首先,添加 gh-ddd-lite 相关依赖和插件。<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-demo</artifactId> <version>1.0.0-SNAPSHOT</version> <parent> <groupId>com.geekhalo</groupId> <artifactId>gh-base-parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <properties> <service.name>demo</service.name> <server.name>gh-${service.name}-service</server.name> <server.version>v1</server.version> <server.description>${service.name} Api</server.description> <servlet.basePath>/${service.name}-api</servlet.basePath> </properties> <dependencies> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-spring</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-codegen</artifactId> <version>1.0.1-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.1-api</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> <configuration> <executable>true</executable> <layout>ZIP</layout> </configuration> </plugin> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> <!–<processor>com.querydsl.apt.QuerydslAnnotationProcessor</processor>–> </configuration> </execution> </executions> </plugin> </plugins> </build></project>3.1.2. 添加配置信息在 application.properties 文件中添加数据库相关配置。spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.url=jdbc:mysql://127.0.0.1:3306/db_test?useUnicode=true&characterEncoding=utf8&useSSL=falsespring.datasource.username=rootspring.datasource.password=spring.application.name=ddd-lite-demoserver.port=8090management.endpoint.beans.enabled=truemanagement.endpoint.conditions.enabled=truemanagement.endpoints.enabled-by-default=falsemanagement.endpoints.web.exposure.include=beans,conditions,env3.1.3. 添加入口类新建 UserApplication 作为应用入口类。@SpringBootApplication@EnableSwagger2public class UserApplication { public static void main(String… args){ SpringApplication.run(UserApplication.class, args); }}使用 SpringBootApplication 和 EnableSwagger2 启用 Spring Boot 和 Swagger 特性。3.2. NewsCategory 建模首先,我们对新闻类型进行建模。3.2.1. 建模 NewsCategory 状态新闻类别状态,用于描述启用、禁用两个状态。在这使用 enum 实现。/** * GenCodeBasedEnumConverter 自动生成 CodeBasedNewsCategoryStatusConverter 类 /@GenCodeBasedEnumConverterpublic enum NewsCategoryStatus implements CodeBasedEnum<NewsCategoryStatus> { ENABLE(1), DISABLE(0); private final int code; NewsCategoryStatus(int code) { this.code = code; } @Override public int getCode() { return code; }}3.2.2. 建模 NewsCategoryNewsCategory 用于描述新闻类别,其中包括状态、名称等。3.2.2.1. 新建 NewsCategory/* * EnableGenForAggregate 自动创建聚合相关的 Base 类 /@EnableGenForAggregate@Data@Entity@Table(name = “tb_news_category”)public class NewsCategory extends JpaAggregate { private String name; @Setter(AccessLevel.PRIVATE) @Convert(converter = CodeBasedNewsCategoryStatusConverter.class) private NewsCategoryStatus status;}3.2.2.2. 自动生成 Base 代码在命令行或ida中执行maven命令,以对项目进行编译,从而触发代码的自动生成。mvn clean compile3.2.2.3. 建模 NewsCategory 创建逻辑我们使用 NewsCategory 的静态工厂,完成其创建逻辑。首先,需要创建 NewsCategoryCreator,作为工程参数。public class NewsCategoryCreator extends BaseNewsCategoryCreator<NewsCategoryCreator>{}其中 BaseNewsCategoryCreator 为框架自动生成的,具体如下:@Datapublic abstract class BaseNewsCategoryCreator<T extends BaseNewsCategoryCreator> { @Setter(AccessLevel.PUBLIC) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private String name; public void accept(NewsCategory target) { target.setName(getName()); }}接下来,需要创建静态工程,并完成 NewsCategory 的初始化。/* * 静态工程,完成 NewsCategory 的创建 * @param creator * @return /public static NewsCategory create(NewsCategoryCreator creator){ NewsCategory category = new NewsCategory(); creator.accept(category); category.init(); return category;}/* * 初始化,默认状态位 ENABLE /private void init() { setStatus(NewsCategoryStatus.ENABLE);}3.2.2.4. 建模 NewsCategory 更新逻辑更新逻辑,只对 name 进行更新操作。首先,创建 NewsCategoryUpdater 作为,更新方法的参数。public class NewsCategoryUpdater extends BaseNewsCategoryUpdater<NewsCategoryUpdater>{}同样,BaseNewsCategoryUpdater 也是框架自动生成,具体如下:@Datapublic abstract class BaseNewsCategoryUpdater<T extends BaseNewsCategoryUpdater> { @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private DataOptional<String> name; public T name(String name) { this.name = DataOptional.of(name); return (T) this; } public T acceptName(Consumer<String> consumer) { if(this.name != null){ consumer.accept(this.name.getValue()); } return (T) this; } public void accept(NewsCategory target) { this.acceptName(target::setName); }}添加 update 方法:/* * 更新 * @param updater /public void update(NewsCategoryUpdater updater){ updater.accept(this);} 3.2.2.5. 建模 NewsCategory 启用逻辑启用,主要是对 status 的操作.代码如下:/* * 启用 /public void enable(){ setStatus(NewsCategoryStatus.ENABLE);}3.2.2.6. 建模 NewsCategory 禁用逻辑禁用,主要是对 status 的操作。代码如下:/* * 禁用 /public void disable(){ setStatus(NewsCategoryStatus.DISABLE);}至此,NewsCategory 的 Command 就建模完成,让我们总体看下 NewsCategory:/* * EnableGenForAggregate 自动创建聚合相关的 Base 类 /@EnableGenForAggregate@Data@Entity@Table(name = “tb_news_category”)public class NewsCategory extends JpaAggregate { private String name; @Setter(AccessLevel.PRIVATE) @Convert(converter = CodeBasedNewsCategoryStatusConverter.class) private NewsCategoryStatus status; private NewsCategory(){ } /* * 静态工程,完成 NewsCategory 的创建 * @param creator * @return / public static NewsCategory create(NewsCategoryCreator creator){ NewsCategory category = new NewsCategory(); creator.accept(category); category.init(); return category; } /* * 更新 * @param updater / public void update(NewsCategoryUpdater updater){ updater.accept(this); } /* * 启用 / public void enable(){ setStatus(NewsCategoryStatus.ENABLE); } /* * 禁用 / public void disable(){ setStatus(NewsCategoryStatus.DISABLE); } /* * 初始化,默认状态位 ENABLE / private void init() { setStatus(NewsCategoryStatus.ENABLE); }}3.2.2.7. 建模 NewsCategory 查找逻辑查找逻辑主要由 NewsCategoryRepository 完成。新建 NewsCategoryRepository,如下:/* * GenApplication 自动将该接口中的方法添加到 BaseNewsCategoryRepository 中 /@GenApplicationpublic interface NewsCategoryRepository extends BaseNewsCategoryRepository{ @Override Optional<NewsCategory> getById(Long aLong);}同样, BaseNewsCategoryRepository 也是自动生成的。interface BaseNewsCategoryRepository extends SpringDataRepositoryAdapter<Long, NewsCategory>, Repository<NewsCategory, Long>, QuerydslPredicateExecutor<NewsCategory> {}领域对象 NewsCategory 不应该暴露到其他层,因此,我们使用 DTO 模式处理数据的返回,新建 NewsCategoryDto,具体如下:public class NewsCategoryDto extends BaseNewsCategoryDto{ public NewsCategoryDto(NewsCategory source) { super(source); }}BaseNewsCategoryDto 为框架自动生成,如下:@Datapublic abstract class BaseNewsCategoryDto extends JpaAggregateVo implements Serializable { @Setter(AccessLevel.PACKAGE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private String name; @Setter(AccessLevel.PACKAGE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “status” ) private NewsCategoryStatus status; protected BaseNewsCategoryDto(NewsCategory source) { super(source); this.setName(source.getName()); this.setStatus(source.getStatus()); }}3.2.3. 构建 NewsCategoryApplication至此,领域的建模工作已经完成,让我们对 Application 进行构建。/* * GenController 自动将该类中的方法,添加到 BaseNewsCategoryController 中 /@GenController(“com.geekhalo.ddd.lite.demo.controller.BaseNewsCategoryController”)public interface NewsCategoryApplication extends BaseNewsCategoryApplication{ @Override NewsCategory create(NewsCategoryCreator creator); @Override void update(Long id, NewsCategoryUpdater updater); @Override void enable(Long id); @Override void disable(Long id); @Override Optional<NewsCategoryDto> getById(Long aLong);}自动生成的 BaseNewsCategoryApplication 如下:public interface BaseNewsCategoryApplication { Optional<NewsCategoryDto> getById(Long aLong); NewsCategory create(NewsCategoryCreator creator); void update(@Description(“主键”) Long id, NewsCategoryUpdater updater); void enable(@Description(“主键”) Long id); void disable(@Description(“主键”) Long id);}得益于我们的 EnableGenForAggregate 和 GenApplication 注解,BaseNewsCategoryApplication 包含我们想要的 Command 和 Query 方法。接口已经准备好了,接下来,处理实现类,具体如下:@Servicepublic class NewsCategoryApplicationImpl extends BaseNewsCategoryApplicationSupport implements NewsCategoryApplication { @Override protected NewsCategoryDto convertNewsCategory(NewsCategory src) { return new NewsCategoryDto(src); }}自动生成的 BaseNewsCategoryApplicationSupport 如下:abstract class BaseNewsCategoryApplicationSupport extends AbstractApplication implements BaseNewsCategoryApplication { @Autowired private DomainEventBus domainEventBus; @Autowired private NewsCategoryRepository newsCategoryRepository; protected BaseNewsCategoryApplicationSupport(Logger logger) { super(logger); } protected BaseNewsCategoryApplicationSupport() { } protected NewsCategoryRepository getNewsCategoryRepository() { return this.newsCategoryRepository; } protected DomainEventBus getDomainEventBus() { return this.domainEventBus; } protected <T> List<T> convertNewsCategoryList(List<NewsCategory> src, Function<NewsCategory, T> converter) { if (CollectionUtils.isEmpty(src)) return Collections.emptyList(); return src.stream().map(converter).collect(Collectors.toList()); } protected <T> Page<T> convvertNewsCategoryPage(Page<NewsCategory> src, Function<NewsCategory, T> converter) { return src.map(converter); } protected abstract NewsCategoryDto convertNewsCategory(NewsCategory src); protected List<NewsCategoryDto> convertNewsCategoryList(List<NewsCategory> src) { return convertNewsCategoryList(src, this::convertNewsCategory); } protected Page<NewsCategoryDto> convvertNewsCategoryPage(Page<NewsCategory> src) { return convvertNewsCategoryPage(src, this::convertNewsCategory); } @Transactional( readOnly = true ) public <T> Optional<T> getById(Long aLong, Function<NewsCategory, T> converter) { Optional<NewsCategory> result = this.getNewsCategoryRepository().getById(aLong); return result.map(converter); } @Transactional( readOnly = true ) public Optional<NewsCategoryDto> getById(Long aLong) { Optional<NewsCategory> result = this.getNewsCategoryRepository().getById(aLong); return result.map(this::convertNewsCategory); } @Transactional public NewsCategory create(NewsCategoryCreator creator) { NewsCategory result = creatorFor(this.getNewsCategoryRepository()) .publishBy(getDomainEventBus()) .instance(() -> NewsCategory.create(creator)) .call(); logger().info(“success to create {} using parm {}",result.getId(), creator); return result; } @Transactional public void update(@Description(“主键”) Long id, NewsCategoryUpdater updater) { NewsCategory result = updaterFor(this.getNewsCategoryRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.update(updater)) .call(); logger().info(“success to update for {} using parm {}”, id, updater); } @Transactional public void enable(@Description(“主键”) Long id) { NewsCategory result = updaterFor(this.getNewsCategoryRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.enable()) .call(); logger().info(“success to enable for {} using parm “, id); } @Transactional public void disable(@Description(“主键”) Long id) { NewsCategory result = updaterFor(this.getNewsCategoryRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.disable()) .call(); logger().info(“success to disable for {} using parm “, id); }}该类中包含我们想要的所有实现。3.2.4. 构建 NewsCategoryControllerNewsInfoApplication 构建完成后,新建 NewsCategoryController 将其暴露出去。新建 NewsCategoryController, 如下:@RequestMapping(“news_category”)@RestControllerpublic class NewsCategoryController extends BaseNewsCategoryController{}是的,核心逻辑都在自动生成的 BaseNewsCategoryController 中:abstract class BaseNewsCategoryController { @Autowired private NewsCategoryApplication application; protected NewsCategoryApplication getApplication() { return this.application; } @ResponseBody @ApiOperation( value = “”, nickname = “create” ) @RequestMapping( value = “/_create”, method = RequestMethod.POST ) public ResultVo<NewsCategory> create(@RequestBody NewsCategoryCreator creator) { return ResultVo.success(this.getApplication().create(creator)); } @ResponseBody @ApiOperation( value = “”, nickname = “update” ) @RequestMapping( value = “{id}/_update”, method = RequestMethod.POST ) public ResultVo<Void> update(@PathVariable(“id”) Long id, @RequestBody NewsCategoryUpdater updater) { this.getApplication().update(id, updater); return ResultVo.success(null); } @ResponseBody @ApiOperation( value = “”, nickname = “enable” ) @RequestMapping( value = “{id}/_enable”, method = RequestMethod.POST ) public ResultVo<Void> enable(@PathVariable(“id”) Long id) { this.getApplication().enable(id); return ResultVo.success(null); } @ResponseBody @ApiOperation( value = “”, nickname = “disable” ) @RequestMapping( value = “{id}/_disable”, method = RequestMethod.POST ) public ResultVo<Void> disable(@PathVariable(“id”) Long id) { this.getApplication().disable(id); return ResultVo.success(null); } @ResponseBody @ApiOperation( value = “”, nickname = “getById” ) @RequestMapping( value = “/{id}”, method = RequestMethod.GET ) public ResultVo<NewsCategoryDto> getById(@PathVariable Long id) { return ResultVo.success(this.getApplication().getById(id).orElse(null)); }}3.2.5. 数据库准备至此,我们的代码就完全准备好了,现在需要准备建表语句。使用 Flyway 作为数据库的版本管理,在 resources/db/migration 新建 V1.002__create_news_category.sql 文件,具体如下:create table tb_news_category( id bigint auto_increment primary key, name varchar(32) null, status tinyint null, create_time bigint not null, update_time bigint not null, version tinyint not null);3.2.6. 测试至此,我们就完成了 NewsCategory 的开发。执行 maven 命令,启动项目:mvn clean spring-boot:run浏览器中输入 http://127.0.0.1:8090/swagger-ui.html , 通过 swagger 查看我们的成果。可以看到如下当然,可以使用 swagger 进行简单测试。3.3. NewsInfo 建模在 NewsCategory 的建模过程中,我们的主要精力放在了 NewsCategory 对象上,其他部分基本都是框架帮我们生成的。既然框架为我们做了那么多工作,为什么还需要我们新建 NewsCategoryApplication 和 NewsCategoryController呢?答案,需要为复杂逻辑预留扩展点。3.3.1. NewsInfo 建模整个过程,和 NewsCategory 基本一致,在此不在重复,只选择差异点进行说明。NewsInfo 最终代码如下:@EnableGenForAggregate@Index(“categoryId”)@Data@Entity@Table(name = “tb_news_info”)public class NewsInfo extends JpaAggregate { @Column(name = “category_id”, updatable = false) private Long categoryId; @Setter(AccessLevel.PRIVATE) @Convert(converter = CodeBasedNewsInfoStatusConverter.class) private NewsInfoStatus status; private String title; private String content; private NewsInfo(){ } /* * GenApplicationIgnore 创建 BaseNewsInfoApplication 时,忽略该方法,因为 Optional<NewsCategory> category 需要通过 逻辑进行获取 * @param category * @param creator * @return / @GenApplicationIgnore public static NewsInfo create(Optional<NewsCategory> category, NewsInfoCreator creator){ // 对 NewsCategory 的存在性和状态进行验证 if (!category.isPresent() || category.get().getStatus() != NewsCategoryStatus.ENABLE){ throw new IllegalArgumentException(); } NewsInfo newsInfo = new NewsInfo(); creator.accept(newsInfo); newsInfo.init(); return newsInfo; } public void update(NewsInfoUpdater updater){ updater.accept(this); } public void enable(){ setStatus(NewsInfoStatus.ENABLE); } public void disable(){ setStatus(NewsInfoStatus.DISABLE); } private void init() { setStatus(NewsInfoStatus.ENABLE); }}3.3.1.1. NewsInfo 创建逻辑建模NewsInfo 的创建逻辑中,需要对 NewsCategory 的存在性和状态进行检查,只有存在并且状态为 ENABLE 才能添加 NewsInfo。具体实现如下:/* * GenApplicationIgnore 创建 BaseNewsInfoApplication 时,忽略该方法,因为 Optional<NewsCategory> category 需要通过 逻辑进行获取 * @param category * @param creator * @return */@GenApplicationIgnorepublic static NewsInfo create(Optional<NewsCategory> category, NewsInfoCreator creator){ // 对 NewsCategory 的存在性和状态进行验证 if (!category.isPresent() || category.get().getStatus() != NewsCategoryStatus.ENABLE){ throw new IllegalArgumentException(); } NewsInfo newsInfo = new NewsInfo(); creator.accept(newsInfo); newsInfo.init(); return newsInfo;}该方法比较复杂,需要我们手工处理。在 NewsInfoApplication 中手工添加创建方法:@GenController(“com.geekhalo.ddd.lite.demo.controller.BaseNewsInfoController”)public interface NewsInfoApplication extends BaseNewsInfoApplication{ // 手工维护方法 NewsInfo create(Long categoryId, NewsInfoCreator creator);}在 NewsInfoApplicationImpl 添加实现:@Autowiredprivate NewsCategoryRepository newsCategoryRepository;@Overridepublic NewsInfo create(Long categoryId, NewsInfoCreator creator) { return creatorFor(getNewsInfoRepository()) .publishBy(getDomainEventBus()) .instance(()-> NewsInfo.create(this.newsCategoryRepository.getById(categoryId), creator)) .call();}其他部分不需要调整。3.3.2. NewsInfo 查找逻辑建模查找逻辑设计两个部分:根据 categoryId 进行分页查找;禁用的 NewsInfo 在查找中不可见。3.3.2.1. Index 注解在 NewsInfo 类上多了一个 @Index(“categoryId”) 注解,该注解会在 BaseNewsInfoRepository 中添加以 categoryId 为维度的查询。interface BaseNewsInfoRepository extends SpringDataRepositoryAdapter<Long, NewsInfo>, Repository<NewsInfo, Long>, QuerydslPredicateExecutor<NewsInfo> { Long countByCategoryId(Long categoryId); default Long countByCategoryId(Long categoryId, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return this.count(booleanBuilder.getValue()); } List<NewsInfo> getByCategoryId(Long categoryId); List<NewsInfo> getByCategoryId(Long categoryId, Sort sort); default List<NewsInfo> getByCategoryId(Long categoryId, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue())); } default List<NewsInfo> getByCategoryId(Long categoryId, Predicate predicate, Sort sort) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue(), sort)); } Page<NewsInfo> findByCategoryId(Long categoryId, Pageable pageable); default Page<NewsInfo> findByCategoryId(Long categoryId, Predicate predicate, Pageable pageable) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return findAll(booleanBuilder.getValue(), pageable); }}这样,并解决了第一个问题。3.3.2.2. 默认方法查看 NewsInfoRepository 类,如下:@GenApplicationpublic interface NewsInfoRepository extends BaseNewsInfoRepository{ default Page<NewsInfo> findValidByCategoryId(Long categoryId, Pageable pageable){ // 查找有效状态 Predicate valid = QNewsInfo.newsInfo.status.eq(NewsInfoStatus.ENABLE); return findByCategoryId(categoryId, valid, pageable); }}通过默认方法将业务概念转为为数据过滤。3.3.3. NewsInfo 数据库准备至此,整个结构与 NewsCategory 再无区别。最后,我们添加数据库文件 V1.003__create_news_info.sql :create table tb_news_info( id bigint auto_increment primary key, category_id bigint not null, status tinyint null, title varchar(64) not null, content text null, create_time bigint not null, update_time bigint not null, version tinyint not null);3.3.4. NewsInfo 测试启动项目,进行简单测试。4. 总结你用了多长时间完成整个系统呢?项目地址见:https://gitee.com/litao851025… ...

February 22, 2019 · 8 min · jiezi

Spring MVC+Stomp+Security+H2 Jetty

这一个什么项目##### 使用技术Spring MVCSpring SecuritySpring webfluxSpring stompJetty 嵌入式运行H2 嵌入式数据库Spring Security OAuth2 ClientActiveMQ实现功能用户使用Stomp 协议发送,接收信息用户账号注册,账号密码登陆认证单用户登陆控制github,google 授权登陆实时接收,发送信息,用户实时在线列表API 接口访问权限控制订阅频道信息权限控制既可以使用模板技术页面,可以使用前端分离的方式,可以自由选择因为这个项目使用Jetty作为嵌入式Servlet 容器,可以像Spring Boot 使用main方法直接运行项目,一句话概括 一个使用Spring MVC项目就像Spring Boot一样运行,一样部署。如果你只想单纯使用Spring MVC构建项目,但是像和Spring Boot直接编译成一个jar运行,又可以忍受没有修改自动编译重启,可以了解这个Demo。在逐步开发过程中,发现自己需求,用户使用,页面样式,做得都不是很好。希望很和牛逼的人合作,一齐完善这个项目,能让它变成可以使用的产品。自己也可以在此不断学习,不断累计新的知识,慢慢变强起来。如果有人想加入我,这个项目里的三个邮箱都是我的账户,随时可以给我email,github为什么选用Spring MVC其实这种Spring Stomp+Spring Security 项目网上有很多,大多数都是用Spring Boot构建,很少有用MVC的。其实这几年很多新出技术demo大多数都是用Spring Boot构建,Spring Boot借助简洁的配置,大量自动化注入深得开发者喜爱,抢夺Spirng MVC市场。但是,再学习前提下,我特别喜欢用Spring MVC,可以深入了解每一个技术细节,每引入一个新东西,都需要了解怎么合并到Spring框架中。我一开始做的时候就直接选择Spring MVC,当我想去网上搜索一些参照例子,发现少得可惜,也踩了不少坑。如果你看下这个项目代码,你会发现,将pom的依赖换成 Spring Boot,不用改任何代码就可以直接运行了。因为你会发现,这个项目也基本上没有任何xml配置,非常简洁,引入Spring Security等框架也只是加一个注解OK。公司构建一个普通Java Web项目都喜欢用Spring Boot,因为构建非常快,配置少,部署方便,但是使用Spring MVC构建也不差什么,Spring Boot有很多特性都是专门为Spring Cloud使用,单纯用来做Web有点浪费了。很多人还停留在,使用Spring MVC 这些框架就是要引入一大堆xml配置文件,但是我跟你说,自从Spring 3.2 推出后,就可以使用Java Config方式了,现在都更新到了Spring 5了,还在用xml方式构建项目。项目路径http://shenyifeng.tk/static/html/jetty-chat.html登录页面 初始化了三个账户 ting6405@gmail.com,aojianshop@gmail.com,shenyifeng0xw@gmail.com ,密码:123456其他的功能我就不展示了,有兴趣可以直接到 http://shenyifeng.tk/static/html/jetty-chat.html 查看代码就在https://github.com/xiaowu6666/spring-stomp-security-webflux-embedded-jetty

February 22, 2019 · 1 min · jiezi

HTTP返回Code常用含义

200: ‘服务器成功返回请求的数据。’, 201: ‘新建或修改数据成功。’, 202: ‘一个请求已经进入后台排队(异步任务)。’, 204: ‘删除数据成功。’, 400: ‘发出的请求有错误,服务器没有进行新建或修改数据的操作。’, 401: ‘用户没有权限(令牌、用户名、密码错误)。’, 403: ‘用户得到授权,但是访问是被禁止的。’, 404: ‘发出的请求针对的是不存在的记录,服务器没有进行操作。’, 406: ‘请求的格式不可得。’, 410: ‘请求的资源被永久删除,且不会再得到的。’, 422: ‘当创建一个对象时,发生一个验证错误。’, 500: ‘服务器发生错误,请检查服务器。’, 502: ‘网关错误。’, 503: ‘服务不可用,服务器暂时过载或维护。’, 504: ‘网关超时。’,

February 22, 2019 · 1 min · jiezi

springMVC学习笔记

springMVC介绍Spring Web MVC是一种基于Java的实现了Web MVC设计模式的请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将web层进行职责解耦,基于请求驱动指的就是使用请求-响应模型。SpringMVC是Spring框架的一个模块,SpringMVC和Spring无需通过中间整合层进行整合。Spring MVC 分离了控制器、模型对象、分派器以及处理程序对象的角色,这种分离让它们更容易进行定制。开发的顺序创建一个路由,且进行测试。创建V层。构建界面。建立C层。测试C层。触发这个路由。建立触发条件,链接、按钮、弹出框建立原型后台开发依照原型,定制api(Application Programming Interface,应用程序编程接口)规范后台开发前后台对接功能性开发定义路由对应的控制器初始化控制器数据绑定至V层功能开发(CRUD)接口开发地址:/xxxx/方法:post开发步骤增加路由注解有CROS(跨域)设置的,要增加CROS设置学习中遇到的问题中文乱码,询问组长,在配置文件添加?characterEncoding=utf8解决了问题。添加的作用是:指定字符的编码、解码格式。本来是想用教程中的findOne(id)这个形式的,结果发现,没有这个可以选择,都是图上的那种方式,后面发现,原因是springboot版本的问题,我用的是2.0以上的版本,2.0以前的都是支持findOne(id)这样的写法。2.0以后就不行了,得换一种写法这种写法就可以。不过findById()后面的需要加其他东西。使用findById(Integer id).orElse(null),这里表示,如果id存在则返回需要查找的信息,如果不存在,这里设置为返回null。

February 22, 2019 · 1 min · jiezi

hibernate实体监听器

上学期学完spring的基本增删改查后,还曾有点小小的得意,感觉自己也算知道不少的编程知识了,但这段时间的项目经验又一次次的教了自己做人,不断出现的新知识让自己目不暇接,知道的越多,未知的越多。也算进入了学习的另一个阶段:知道自己不知道感慨完了,该进入正题了,本周有一个需求,spring sercurity要求在保存密码的时候密码必须加密,学长开始写的时候是在setter密码的时候调用加密方法,不够优雅,就让我把这个方法提出来,换用监听器,当监听的user保存时就自动加密其密码,由于当时只知道拦截器,于是就去找了一篇拦截器的教程,发现写出来感觉相当不合适,就问学长,果然我理解错了意思,是用一个叫实体监听器(Entity Listeners)的东西。Entity Listener顾名思义,它可以监听实体的某些行为,并进行一部分操作。Hibernate支持的回调注解@PrePersistExecuted before the entity manager persist operation is actually executed or cascaded. This call is synchronous with the persist operation.在数据持久化到数据库之前执行@PreRemoveExecuted before the entity manager remove operation is actually executed or cascaded. This call is synchronous with the remove operation.执行数据删除之前调用@PostPersistExecuted after the entity manager persist operation is actually executed or cascaded. This call is invoked after the database INSERT is executed.在执行数据插入之后调用@PostRemoveExecuted after the entity manager remove operation is actually executed or cascaded. This call is synchronous with the remove operation.在执行数据删除之后调用@PreUpdateExecuted before the database UPDATE operation.在执行数据更新之前调用@PostUpdateExecuted after the database UPDATE operation.在执行数据更新之后调用@PostLoadExecuted after an entity has been loaded into the current persistence context or an entity has been refreshed.在数据从数据库加载并且赋值到当前对象后调用用法首先,先定义一个操作用的类,并在其中写下你要用的方法,比如下面的方法就代表在用户持久化和更新的时候执行(只需注解就好,不用继承其他类)。然后在实体上声明,就完工了,相当简单。对于上面的代码,初学者可能会对这一段有疑惑 PasswordEncoder passwordEncoder = ContextConfig.getContext().getBean(PasswordEncoder.class);我开始也不明白,还是学长给我讲解的,这是获取PasswordEncoder的对象,学长给我讲了讲才大概明白了hibernate和spring是两个系统,spring管理的对象,hibernate注入不进来。如果不明白可以看看下面这个图。spring的ioc这篇文章写得挺不错,有兴趣可以看看。总结实体监听器虽然只是一个很简单的功能,但却非常强大与实用,同时了解到hibernate和spring并不是一个东西,以前一直以为hibernate是spring的一个小功能,同时对spring总算有了一丁点清晰的认识,总体状态还算满意,感谢学长们的帮助。 ...

February 22, 2019 · 1 min · jiezi

实战:基于Spring Boot快速开发RESTful风格API接口

写在前面的话这篇文章计划是在过年期间完成的,示例代码都写好了,结果亲戚来我家做客,文章没来得及写。已经很久没有更新文章了,小伙伴们,有没有想我啊。言归正传,下面开始,今天的话题。目标写一套符合规范,并且具有RESTful风格的API接口。假定你已会使用Spring Boot 2.x。你已会使用Gradle构建Spring Boot工程。你已会基于Spring Boot编写API接口。你已会使用接口调试工具。如果你还不会使用Spring Boot写接口,建议先看一下这篇文章 :用Spring Boot开发API接口步骤1、基于Gradle构建Spring Boot示例项目。2、引入JavaLib。3、编写接口代码。4、测试接口。引入JavaLib测试版(SNAPSHOT),都会发布到 JitPack 上,所以,从这里拉取的,都会是最新的,但是需要配置仓库地址。正式版(RELEASE),才会推送到 Maven中央。UserModel我们用UserModel来存放我们的数据,以便存取。我个人比较喜欢用bean的,如果你喜欢用Map,那也是可以的。不过需要注意的是,需要加@JsonInclude(JsonInclude.Include.NON_NULL) ,他的作用是,如果某个字段为空时,在返回的JSON中,则不显示,如果没有,将为 null。完整代码如下:package com.fengwenyi.demojavalibresult.model;import com.fasterxml.jackson.annotation.JsonInclude;import lombok.Data;import lombok.experimental.Accessors;import java.io.Serializable;/** * User Model * @author Wenyi Feng * @since 2019-02-05 /@Data@Accessors(chain = true)@JsonInclude(JsonInclude.Include.NON_NULL)public class UserModel implements Serializable { private static final long serialVersionUID = -835481508750383832L; /* UID / private String uid; /* Name / private String name; /* Age / private Integer age;}编写接口返回码这里我们使用 JavaLib 中result模块为我们提供的方法。只需要调用 BaseCodeMsg.app(Integer, String)即可。这里我们只写几个用作示例,完整代码如下:package com.fengwenyi.demojavalibresult.util;import com.fengwenyi.javalib.result.BaseCodeMsg;/* * 自定义返回码以及描述信息 * @author Wenyi Feng * @since 2019-02-05 /public class CodeMsg { / user error ————————————————————————————————————*/ /** 用户不存在 / public static final BaseCodeMsg ERROR_USER_NOT_EXIST = BaseCodeMsg.app(10001, “User Not Exist”); /* UID不能为空 / public static final BaseCodeMsg ERROR_USER_UID_NOT_NULL = BaseCodeMsg.app(10002, “User UID Must Not null”);}BaseCodeMsg我们看一下源码:package com.fengwenyi.javalib.result;/* * (基类)返回码及描述信息 * @author Wenyi Feng * @since 2019-01-22 /public class BaseCodeMsg { /* 返回码 / private Integer code; /* 返回码描述 / private String msg; /* * 无参数构造方法 / private BaseCodeMsg() {} /* * 构造方法 * @param code * @param msg / private BaseCodeMsg(Integer code, String msg) { this.code = code; this.msg = msg; } public static BaseCodeMsg app(Integer code, String msg) { return new BaseCodeMsg(code, msg); } /* * 返回码填充 * @param args 填充内容 * @return CodeMsgEnum / public BaseCodeMsg fillArgs(Object … args) { this.msg = String.format(this.msg, args); return this; } /* * 获取返回码 * @return 返回码 / public Integer getCode() { return code; } /* * 获取描述信息 * @return 描述信息 / public String getMsg() { return msg; } /* 成功 / public static final BaseCodeMsg SUCCESS = BaseCodeMsg.app(0, “Success”); /* 失败 / public static final BaseCodeMsg ERROR_INIT = BaseCodeMsg.app(-1, “Error”);}成功的标识是:当 code=0 时。另外,我们还为你提供了预留字符串替换的方法。比如你想告诉用户某个字段不合法,那么你可以这样:第一步:在CodeMsg中添加public static final BaseCodeMsg ERROR_PARAM_ILLEGAL = BaseCodeMsg.app(20001, “Request Param Illegal : %s”);第二步:返回 /* * 测试参数错误 * @return {@link Result} / @GetMapping("/test-param-error") public Result testParamError() { return Result.error(CodeMsg.ERROR_PARAM_ILLEGAL.fillArgs(“account”)); }测试结果:编写接口代码接下来,开始编写我们的接口代码。首先指明,我们的接口接收和返回的文档格式。consumes = MediaType.APPLICATION_JSON_UTF8_VALUEproduces = MediaType.APPLICATION_JSON_UTF8_VALUE再使用 JavaLib 中 Result。完整代码如下:package com.fengwenyi.demojavalibresult.controller;import com.fengwenyi.demojavalibresult.model.UserModel;import com.fengwenyi.demojavalibresult.util.CodeMsg;import com.fengwenyi.javalib.result.Result;import org.springframework.http.MediaType;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.;import javax.annotation.PostConstruct;import java.util.ArrayList;import java.util.List;import java.util.UUID;/** * User Controller : 用户操作 * @author Wenyi Feng * @since 2019-02-05 /@RestController@RequestMapping(value = “/user”, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public class UserController { /* 临时存放用户信息 / private List<UserModel> userModelList = new ArrayList<>(); /* * 初始化用户 / @PostConstruct public void init() { for (int i = 0; i < 10; i++) userModelList.add(new UserModel().setUid(UUID.randomUUID().toString()).setName(“u” + i).setAge(10 + i)); } /* * 查询用户列表 * @return {@link Result} / @GetMapping("/list") public Result list() { return Result.success(userModelList); } /* * 添加用户 * @param userModel 这里传JSON字符串 * @return {@link Result} / @PostMapping("/add") public Result add(@RequestBody UserModel userModel) { if (userModel != null) { userModelList.add(userModel.setUid(UUID.randomUUID().toString())); return Result.success(); } return Result.error(); } /* * 根据UID获取用户 * @param uid UID * @return {@link Result} */ @GetMapping("/get/{uid}") public Result getByUid(@PathVariable(“uid”) String uid) { if (StringUtils.isEmpty(uid)) return Result.error(CodeMsg.ERROR_USER_UID_NOT_NULL); for (UserModel userModel : userModelList) if (userModel.getUid().equals(uid)) return Result.success(userModel); return Result.error(CodeMsg.ERROR_USER_NOT_EXIST); }}测试1、启动2、list访问:http://localhost:8080/user/list{ “code”: 0, “msg”: “Success”, “data”: [ { “uid”: “d8e2dfac-b6e8-46c7-9d43-5bb6bf99ce30”, “name”: “u0”, “age”: 10 }, { “uid”: “87001637-9f21-4bc7-b589-bea1b2c795c4”, “name”: “u1”, “age”: 11 }, { “uid”: “5e1398ca-8322-4a68-b0d2-1eb4c1cac9de”, “name”: “u2”, “age”: 12 }, { “uid”: “e6ee5452-4148-4f6d-b820-9cc24e5c91b5”, “name”: “u3”, “age”: 13 }, { “uid”: “3f428e26-57e1-4661-8275-ce3777b5da54”, “name”: “u4”, “age”: 14 }, { “uid”: “b9d994b4-f090-40de-b0f3-e89c613061f2”, “name”: “u5”, “age”: 15 }, { “uid”: “748d1349-5978-4746-b0c1-949eb5613a28”, “name”: “u6”, “age”: 16 }, { “uid”: “abaadb7c-23fb-4297-a531-0c490927f6d5”, “name”: “u7”, “age”: 17 }, { “uid”: “5e5917a1-8674-4367-94c6-6a3fd10a08d6”, “name”: “u8”, “age”: 18 }, { “uid”: “03ed6a83-0cc0-4714-9d0d-f653ebb3a2eb”, “name”: “u9”, “age”: 19 } ]}2、添加数据看一下,数据是什么样子与我们预想的结果一样。获取数据有数据样式:无数据样式:关于冯文议。2017年毕业于阿坝师范学院计算机应用专业。现就职于深圳警圣技术股份有限公司,主要负责服务器接口开发工作。技术方向:Java。 开源软件:JavaLib。后记到这里就结束了,如果在遇到什么问题,或者有不明白的地方,可以通过评论、留言或者私信等方式,告诉我。 ...

February 21, 2019 · 3 min · jiezi

SpringBoot 实战 (八) | 使用 Spring Data JPA 访问 Mysql 数据库

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言如题,今天介绍 Spring Data JPA 的使用。什么是 Spring Data JPA在介绍 Spring Data JPA 之前,首先介绍 Hibernate 。 Hibernate 使用 O/R 映射 (Object-Relation Mapping) 技术实现数据访问, O/R 映射即将领域模型类与数据库的表进行映射,通过程序操作对象而实现表数据操作的能力,让数据访问操作无需关注数据库相关技术。Hibernate 主导了 EJB 3.0 的 JPA 规范, JPA 即 Java Persistence API。JPA 是一个基于 O/R 映射的标准协议(目前最新版本是 JPA 2.1)。所谓规范即只定义标准规制(如注解、接口),不提供实现,软件提供商可以按照标准规范来实现,而使用者只需按照规范中定义的方式来使用,而不用和软件提供商的实现打交道。JPA 的主要实现由 Hibernate 、 EclipseLink 和 OpenJPA 等完成,我们只要使用 JPA 来开发,无论是哪一个开发方式都是一样的。Spring Data JPA 是 Spring Data 的一个子项目,它通过基于 JPA 的 Repository 极大地减少了 JPA 作为数据访问方案的代码量。简而言之,JPA 是一种 ORM 规范,但并未提供 ORM 实现,而 Hibernate 是一个 ORM 框架,它提供了 ORM 实现。准备工作IDEAJDK1.8SpringBoot 2.1.3pom.xml 文件引入的依赖如下:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.nasus</groupId> <artifactId>jpa</artifactId> <version>0.0.1-SNAPSHOT</version> <name>jpa</name> <description>jpa Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!– JPA 依赖 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!– web 依赖 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– mysql 连接类 –> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!– lombok 依赖 –> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!– 单元测试依赖 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>简单说下,加入 JPA 依赖;mysql 连接类用于连接数据;web 启动类,但凡是 web 应用都需要依赖它;lombok 用于简化实体类。不会的看这篇旧文介绍:SpringBoot 实战 (三) | 使用 LomBokapplication.yaml 配置文件spring:# 数据库相关 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true username: root password: 123456# JPA 相关 jpa: hibernate: ddl-auto: update #ddl-auto:设为 create 表示每次都重新建表 show-sql: truerepository (dao) 层package com.nasus.jpa.repository;import com.nasus.jpa.entity.Student;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.data.repository.CrudRepository;import org.springframework.stereotype.Repository;/** * Project Name:springboot_jpa_demo <br/> * Package Name:com.nasus.jpa.repository <br/> * Date:2019/2/19 21:37 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * @author <a href=“turodog@foxmail.com”>nasus</a><br/> /@Repositorypublic interface StudentRepository extends JpaRepository<Student,Integer>, CrudRepository<Student, Integer> {}从上图,可以看出 JpaRepository 继承于 PangingAndSortingRepository 继承于 CrudRepository 。CrudRepository 提供基本的增删改查PagingAndSortingRepository 提供分页和排序方法;JpaRepository 提供 JPA 需要的方法。在使用的时候,可以根据具体需要选中继承哪个接口。使用这些接口的好处有:继承这些接口,可以使Spring找到自定义的数据库操作接口,并生成代理类,后续可以注入到Spring容器中;可以不写相关的sql操作,由代理类生成service 层package com.nasus.jpa.service;import com.nasus.jpa.entity.Student;import java.util.List;/* * Project Name:springboot_jpa_demo <br/> * Package Name:com.nasus.jpa.service <br/> * Date:2019/2/19 21:41 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * @author <a href=“turodog@foxmail.com”>nasus</a><br/> /public interface StudentService { Student save(Student student); Student findStudentById(Integer id); void delete(Integer id); void updateStudent(Student student); List<Student> findStudentList();}实现类:package com.nasus.jpa.service.impl;import com.nasus.jpa.entity.Student;import com.nasus.jpa.repository.StudentRepository;import com.nasus.jpa.service.StudentService;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;/* * Project Name:springboot_jpa_demo <br/> * Package Name:com.nasus.jpa.service.impl <br/> * Date:2019/2/19 21:43 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * @author <a href=“turodog@foxmail.com”>nasus</a><br/> /@Servicepublic class StudentServiceImpl implements StudentService { @Autowired private StudentRepository studentRepository; /* * 保存学生信息 * @param student * @return / @Override public Student save(Student student) { return studentRepository.save(student); } /* * 根据 Id 查询学生信息 * @param id * @return / @Override public Student findStudentById(Integer id) { return studentRepository.findById(id).get(); } /* * 删除学生信息 * @param id / @Override public void delete(Integer id) { Student student = this.findStudentById(id); studentRepository.delete(student); } /* * 更新学生信息 * @param student / @Override public void updateStudent(Student student) { studentRepository.save(student); } /* * 查询学生信息列表 * @return / @Override public List<Student> findStudentList() { return studentRepository.findAll(); }}controller 层构建 restful APIpackage com.nasus.jpa.controller;import com.nasus.jpa.entity.Student;import com.nasus.jpa.service.StudentService;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.DeleteMapping;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.PutMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/* * Project Name:springboot_jpa_demo <br/> * Package Name:com.nasus.jpa.controller <br/> * Date:2019/2/19 21:55 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * @author <a href=“turodog@foxmail.com”>nasus</a><br/> */@RestController@RequestMapping("/student”)public class StudentController { @Autowired private StudentService studentService; @PostMapping("/save”) public Student saveStudent(@RequestBody Student student){ return studentService.save(student); } @GetMapping(”/{id}") public Student findStudentById(@PathVariable(“id”) Integer id){ return studentService.findStudentById(id); } @GetMapping("/list") public List<Student> findStudentList(){ return studentService.findStudentList(); } @DeleteMapping("/{id}") public void deleteStudentById(@PathVariable(“id”) Integer id){ studentService.delete(id); } @PutMapping("/update") public void updateStudent(@RequestBody Student student){ studentService.updateStudent(student); }}测试结果其他接口已通过 postman 测试,无问题。源码下载:https://github.com/turoDog/De…后语以上为 SpringBoot 使用 Spring Data JPA 访问 Mysql 数据库的教程。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点好看,让更多人知道。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

February 20, 2019 · 3 min · jiezi

Java之注解的定义及使用

Java的注解在实际项目中使用得非常的多,特别是在使用了Spring之后。本文会介绍Java注解的语法,以及在Spring中使用注解的例子。注解的语法注解的例子以Junit中的@Test注解为例@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Test { long timeout() default 0L;}可以看到@Test注解上有@Target()和@Retention()两个注解。这种注解了注解的注解,称之为元注解。跟声明了数据的数据,称为元数据是一种意思。之后的注解的格式是修饰符 @interface 注解名 { 注解元素的声明1 注解元素的声明2 }注解的元素声明有两种形式type elementName();type elementName() default value; // 带默认值常见的元注解@Target注解@Target注解用于限制注解能在哪些项上应用,没有加@Target的注解可以应用于任何项上。在java.lang.annotation.ElementType类中可以看到所有@Target接受的项TYPE 在【类、接口、注解】上使用FIELD 在【字段、枚举常量】上使用METHOD 在【方法】上使用PARAMETER 在【参数】上使用CONSTRUCTOR 在【构造器】上使用LOCAL_VARIABLE 在【局部变量】上使用ANNOTATION_TYPE 在【注解】上使用PACKAGE 在【包】上使用TYPE_PARAMETER 在【类型参数】上使用 Java 1.8 引入TYPE_USE 在【任何声明类型的地方】上使用 Java 1.8 引入@Test注解只允许在方法上使用。@Target(ElementType.METHOD)public @interface Test { … }如果要支持多项,则传入多个值。@Target({ElementType.TYPE, ElementType.METHOD})public @interface MyAnnotation { … }此外元注解也是注解,也符合注解的语法,如@Target注解。@Target(ElementType.ANNOTATION_TYPE)表明@Target注解只能使用在注解上。@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public @interface Target { ElementType[] value();}@Retention注解@Retention指定注解应该保留多长时间,默认是RetentionPolicy.CLASS。在java.lang.annotation.RetentionPolicy可看到所有的项SOURCE 不包含在类文件中CLASS 包含在类文件中,不载入虚拟机RUNTIME 包含在类文件中,由虚拟机载入,可以用反射API获取@Test注解会载入到虚拟机,可以通过代码获取@Retention(RetentionPolicy.RUNTIME)public @interface Test { … }@Documented注解主要用于归档工具识别。被注解的元素能被Javadoc或类似的工具文档化。@Inherited注解添加了@Inherited注解的注解,所注解的类的子类也将拥有这个注解注解@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic @interface MyAnnotation { … }父类@MyAnnotation class Parent { … }子类Child会把加在Parent上的@MyAnnotation继承下来class Child extends Parent { … }@Repeatable注解Java 1.8 引入的注解,标识注解是可重复使用的。注解1public @interface MyAnnotations { MyAnnotation[] value(); }注解2@Repeatable(MyAnnotations.class)public @interface MyAnnotation { int value();}有使用@Repeatable()时的使用@MyAnnotation(1)@MyAnnotation(2)@MyAnnotation(3)public class MyTest { … }没使用@Repeatable()时的使用,@MyAnnotation去掉@Repeatable元注解@MyAnnotations({ @MyAnnotation(1), @MyAnnotation(2), @MyAnnotation(3)})public class MyTest { … } 这个注解还是非常有用的,让我们的代码变得简洁不少,Spring的@ComponentScan注解也用到这个元注解。元素的类型支持的元素类型8种基本数据类型(byte,short,char,int,long,float,double,boolean)StringClassenum注解类型数组(所有上边类型的数组)例子枚举类public enum Status { GOOD, BAD}注解1@Target(ElementType.ANNOTATION_TYPE)public @interface MyAnnotation1 { int val();}注解2@Target(ElementType.TYPE)public @interface MyAnnotation2 { boolean boo() default false; Class<?> cla() default Void.class; Status enu() default Status.GOOD; MyAnnotation1 anno() default @MyAnnotation1(val = 1); String[] arr(); }使用时,无默认值的元素必须传值@MyAnnotation2( cla = String.class, enu = Status.BAD, anno = @MyAnnotation1(val = 2), arr = {“a”, “b”})public class MyTest { … }Java内置的注解@Override注解告诉编译器这个是个覆盖父类的方法。如果父类删除了该方法,则子类会报错。@Deprecated注解表示被注解的元素已被弃用。@SuppressWarnings注解告诉编译器忽略警告。@FunctionalInterface注解Java 1.8 引入的注解。该注释会强制编译器javac检查一个接口是否符合函数接口的标准。特别的注解有两种比较特别的注解标记注解 : 注解中没有任何元素,使用时直接是 @XxxAnnotation, 不需要加括号单值注解 : 注解只有一个元素,且名字为value,使用时直接传值,不需要指定元素名@XxxAnnotation(100)利用反射获取注解Java的AnnotatedElement接口中有getAnnotation()等获取注解的方法。而Method,Field,Class,Package等类均实现了这个接口,因此均有获取注解的能力。例子注解@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})public @interface MyAnno { String value(); }被注解的元素@MyAnno(“class”)public class MyClass { @MyAnno(“feild”) private String str; @MyAnno(“method”) public void method() { } }获取注解public class Test { public static void main(String[] args) throws Exception { MyClass obj = new MyClass(); Class<?> clazz = obj.getClass(); // 获取对象上的注解 MyAnno anno = clazz.getAnnotation(MyAnno.class); System.out.println(anno.value()); // 获取属性上的注解 Field field = clazz.getDeclaredField(“str”); anno = field.getAnnotation(MyAnno.class); System.out.println(anno.value()); // 获取方法上的注解 Method method = clazz.getMethod(“method”); anno = method.getAnnotation(MyAnno.class); System.out.println(anno.value()); } }在Spring中使用自定义注解注解本身不会有任何的作用,需要有其他代码或工具的支持才有用。需求设想现有这样的需求,程序需要接收不同的命令CMD,然后根据命令调用不同的处理类Handler。很容易就会想到用Map来存储命令和处理类的映射关系。由于项目可能是多个成员共同开发,不同成员实现各自负责的命令的处理逻辑。因此希望开发成员只关注Handler的实现,不需要主动去Map中注册CMD和Handler的映射。最终效果最终希望看到效果是这样的@CmdMapping(Cmd.LOGIN)public class LoginHandler implements ICmdHandler { @Override public void handle() { System.out.println(“handle login request”); }}@CmdMapping(Cmd.LOGOUT)public class LogoutHandler implements ICmdHandler { @Override public void handle() { System.out.println(“handle logout request”); }}开发人员增加自己的Handler,只需要创建新的类并注上@CmdMapping(Cmd.Xxx)即可。具体做法具体的实现是使用Spring和一个自定义的注解定义@CmdMapping注解@Documented@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Componentpublic @interface CmdMapping { int value(); }@CmdMapping中有一个int类型的元素value,用于指定CMD。这里做成一个单值注解。这里还加了Spring的@Component注解,因此注解了@CmdMapping的类也会被Spring创建实例。然后是CMD接口,存储命令。public interface Cmd { int REGISTER = 1; int LOGIN = 2; int LOGOUT = 3;}之后是处理类接口,现实情况接口会复杂得多,这里简化了。public interface ICmdHandler { void handle(); }上边说过,注解本身是不起作用的,需要其他的支持。下边就是让注解生效的部分了。使用时调用handle()方法即可。@Componentpublic class HandlerDispatcherServlet implements InitializingBean, ApplicationContextAware { private ApplicationContext context; private Map<Integer, ICmdHandler> handlers = new HashMap<>(); public void handle(int cmd) { handlers.get(cmd).handle(); } public void afterPropertiesSet() { String[] beanNames = this.context.getBeanNamesForType(Object.class); for (String beanName : beanNames) { if (ScopedProxyUtils.isScopedTarget(beanName)) { continue; } Class<?> beanType = this.context.getType(beanName); if (beanType != null) { CmdMapping annotation = AnnotatedElementUtils.findMergedAnnotation( beanType, CmdMapping.class); if(annotation != null) { handlers.put(annotation.value(), (ICmdHandler) context.getBean(beanType)); } } } } public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; }}主要工作都是Spring做,这里只是将实例化后的对象put到Map中。测试代码@ComponentScan(“pers.custom.annotation”)public class Main { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class); HandlerDispatcherServlet servlet = context.getBean(HandlerDispatcherServlet.class); servlet.handle(Cmd.REGISTER); servlet.handle(Cmd.LOGIN); servlet.handle(Cmd.LOGOUT); context.close(); }}> 完整项目总结可以看到使用注解能够写出很灵活的代码,注解也特别适合做为使用框架的一种方式。所以学会使用注解还是很有用的,毕竟这对于上手框架或实现自己的框架都是非常重要的知识。 ...

February 19, 2019 · 2 min · jiezi

自学 JAVA 的几点建议

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言许久不见,最近公众号多了很多在校的师弟师妹们。有很多同学都加了我微信问了一些诸如 [如何自学 Java ]的问题,我都一一解答了,这是大家对我的信任,我非常感谢。你们知道我现在的职业是 java web 开发,可你们你们不知道的是我在这个份职业之前做的是 Android 开发,一年前通过自学才转过来 java 岗位。下面就跟你们聊聊我的自学经验以及一些建议,希望能帮助到大家。了解 Java 的前世今生在学习之前,我相信很多人都是网上直接找教程,开始学。但我跟你们不一样,我会先去网上找一些 Java 相关的发展史来了解一下,很多人可能疑惑这有什么用呢?那些发展史又不会帮你提高你的技术。我告诉你们这里的用处可大了,举个栗子,不去了解你又怎么会知道 JDK 11 已经出了,JDK 8 是目前最多人使用的版本,而你此时找到的是 JDK 6 的教程,学习一段时间后,你发现你的代码跟别人不一样,殊不知人家用的 JDK 8。那么这中间你就浪费了许多时间了,所以了解一门语言的前世今生是很有必要的。建立学习大纲很多人不知道怎么学,怎么规划自己的学习路线。对于这种情况我的应对方法是建立学习大纲,比如 java 基础,那你就得想 Java 基础有哪些知识点,然后把它罗列下来做成一个学习大纲或者思维导图。那你在学习之前,你就跟着学习大纲按部就班地学就好了,哪块没攻克的就翻相应的资料重点学习。下面是我整理的简单版的思维导图,给你们做参考,你们的大纲不能像我做的这么简化,你们要逐项展开,比如说Java 基础,应该具体到某一个知识点才行,因为越具体,你学得越清晰。简单说一下,上面我整理的学习大纲分为四个阶段,对于应届生来说,把上面前三阶段掌握了,找到实习工作完全没问题。但如果想更进一步,进 BAT 等公司,可能需要把上面四个阶段都要掌握了。PS:重中之重的是 Java 基础 和 算法,大厂非常青睐算法好的应届生PPS:推荐一款好用的思维导图软件:https://mubu.com/inv/929852PPPS:以上提到的知识点学习教程领取方式见文末不要浪费时间在找资料上自学 Java ,我相信很多人一开始都是煞费苦心找资料的,越多越好,越详细越好,因为这样很有满足感,心里想着只要我每天坚持看几个小时很快看完的。殊不知,不断保存教程的结果就是某度云盘里面躺了一大堆教程,就算是每天不睡觉地看还不一定能看完。自制力好的人,估计在保存后会翻出来看两眼,过几天又忘得一干二净了。自制力不好的人更衰,直接让教程在云盘里面积灰。对于初学者来说,只要找到囊括了 Java 基础的资料就可以开始学了。切忌眼高手低有些问题需然看起来很简单,很弱智,很傻逼。首先,你动手比光看不做敲记忆更牢固;其次,你敲出来可能会有各种各样奇奇怪怪的问题,而你的水平就是在解决这些问题中提高的。所谓大神都是踩各种各样的坑才过来的。善于利用搜索引擎遇到问题,第一时间想到的是网上搜索试试看能不能解决,不能解决再去问人。移动互联网发展到今天,可以毫不夸张的说,你遇到的 99% 的问题都能在网上找到解决办法,剩下的 1% 是因为你还没学会用关键字来搜索。多思考学会思考,养成多思考的习惯。在写代码的过程中多想想人家为什么这样写。举个栗子,为什么 java 类只能单继承,而接口却可以多继承,稍加思考你就会知道答案:如果存在多继承,那个两个父类有两个一模一样的方法怎么办,子类就不知道继承哪一个方法。而接口能多继承的原因是:接口中的方法都是方法名,没有函数体,具体的实现需要靠实现类去实现,一旦实现类实现了方法,那么就不存在多个接口有相同的方法名的出现的问题了,因为函数体都是一样的。熟能生巧很多人困惑,为什么我看视频的时候很懂,过两天就啥都忘了?其实这是正常的,因为你还没有对知识进行实践归纳。在入门 Java 之后,就需要大量的实战来巩固你的基础。所以说忘了不要紧,先按大纲按部就班学,之后再通过实战来巩固基础。项目驱动,不要为了学而学相当一部分初学者都坚持不下去,因为太枯燥了。遇到这种情况,首先你要明确你学习 Java 的目的是什么?我相信很多人最终目的都是–钱。这太正常了,没钱谁愿意做这么费脑力的事啊。但是你是需要通过项目来赚钱的。所以我们学习是要通过项目来驱动的。你要想你学完之后希望鼓捣一个什么东西出来。比如我之前学 Android ,我就是打算弄一个天气预报的APP出来,那有了目标之后,你就开始思考,网上查,这个东西,需要用到什么技术。比如,我当时就是要先会 Javase 才能学安卓,学到了安卓之后又有其他的技术,比如 Android 网络框架,UI框架等等。以项目来驱动自己学习,整个过程将会有趣得多。后语以上就是我对自学Java的几点建议,希望对你们有帮助。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点个赞。另外,关注之后在后台回复 黑马 可免费领取上面学习大纲定制的 Java 学习资料与学习大纲原图。

February 19, 2019 · 1 min · jiezi

SpringBoot 实战 (二) | 第一个 SpringBoot 工程详解

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言哎呦喂,按照以往的惯例今天周六我的安排应该是待在家学学猫叫啥的。但是今年这种日子就可能一去不复返了,没法办法啊。前几天年轻,立下了一周至少更两篇文章的 flag 。废话少说,今天接着前文给你们带来了第一个 SpringBoot 工程的详解。第一个 SpringBoot 工程前文已经说过了 SpringBoot 工程的创建,这里不再赘述,还不会的朋友,请看下面这篇文章。如何使用 IDEA 构建 Spring Boot 工程学过编程的都知道,学习一门新语言的第一个项目肯定是 Hello World 。那这里也不例外,我们先创建一个非常简单的 Hello World 工程。给大家讲解 SpringBoot 的项目目录。创建信息如下:由于本文重点旨在讲解 SpringBoot 的项目目录。所以选择的依赖包非常简单,就选择 Web 足矣。SpringBoot 项目目录详解创建成功之后的 SpringBoot 项目目录如下,以下对各主要目录的作用进行讲解:src 是整个工程的根目录,基本上做 web 开发你的代码大部分都放在这里。其中 main 目录下放置的是你的 Java 代码;resource 目录,顾名思义就是放置配置文件、静态资源( static )以及前端模板( template )。test 目录就是放置你的单元测试代码。target 就是项目编译生成的目录,里面包含代码编译后的 class 文件以及一些静态资源和配置文件。External Libraries 这里放置了,pom.xml 导入的依赖包。其他没提到的目录都是不重要的。由上图项目目录,可以看到有几个文件,这些文件有些是我新建的,有些是项目生成的。下面我会讲解:pom.xml 这个文件是整个项目最重要的文件,它包含了整个项目的依赖包。Maven 会根据这个文件导入相关的我们开发需要的依赖包。代码如下:可以看到 pom.xml 中一共有 4 个依赖,其中只有 Junit 是我手动加入的,用于单元测试。其他的如 Spring Boot 启动父依赖、Spring Boot web依赖 、Spring Boot web test依赖都是创建项目时,勾选 web 选项卡而生成的。这几个依赖包的额作用就是 内嵌 Tomcat 容器,集成各 Spring 组件。比如 如果没有依赖 web 包 ,Spring 的两大核心功能 IOC 和 AOP 将不会生效。<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.nasus</groupId> <artifactId>helloworld</artifactId> <version>0.0.1-SNAPSHOT</version> <name>helloworld</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <!– Spring Boot 启动父依赖 –> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.1.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <dependencies> <!– Spring Boot web依赖 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!– Junit –> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>HelloworldApplication.java 最为重要,它由项目生成,是整个工程的应用启动类 main 函数。代码由项目生成,代码如下:SpringApplication 引导应用,并将 HelloworldApplication 本身作为参数传递给 run 方法。具体 run 方法会启动嵌入式的 Tomcat 并初始化 Spring环境及其各 Spring 组件。需要注意的是,这个类必须放在其他类的上册目录,拿上述目录举个栗子, 若其他HelloWorldController.java 类位于 com.nasus.controller 下。则 HelloworldApplication.java 类必须放置在 com.nasus 下或者 com 下(层级必须大于其他 java 类)。否则启动项目访问会报 Whitelabel Error Page 错误,原因是项目扫描不到 @RestController、@RequestMapping 等注解配置的方法和类。package com.nasus.helloworld;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class HelloworldApplication { public static void main(String[] args) { SpringApplication.run(HelloworldApplication.class, args); }}HelloWorldController 是我手动编写的,代码如下:@RestController 和 @RequestMapping 注解是来自 SpringMVC 的注解,它们不是 SpringBoot 的特定部分。其中 @RestController 注解的作用是:提供实现了 REST API,可以服务 JSON、XML 或者其他。这里是以 String 的形式渲染出结果。 其中 @RestController 注解的作用是:提供路由信息,"/“路径的HTTP Request都会被映射到sayHello方法进行处理。 具体参考,Spring 官方的文档《Spring Framework Document》package com.nasus.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/** * Project Name:helloworld <br/> * Package Name:com.nasus.controller <br/> * Date:2019/1/5 13:59 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * * @author <a href=“turodog@foxmail.com”>nasus</a><br/> * Copyright Notice ========================================================= * This file contains proprietary information of Eastcom Technologies Co. Ltd. * Copying or reproduction without prior written approval is prohibited. * Copyright (c) 2019 ======================================================= /@RestControllerpublic class HelloWorldController { @RequestMapping("/hello”) public String sayHello() { return “Hello,World!”; }}写完 Controller 层的代码,我们就可以启动此项目。点击 IDEA 项目启动按钮,效果如下:好的程序必须配备完善的单元测试。HelloWorldControllerTest.java 文件是由我编写的主要作用就是测试 HelloWorldController.java 中的方法。这里用的是 Junit 依赖包进行单元测试,代码如下:这里的逻辑就是测试 HelloWorldController.java 的 sayHello 方法输出的字符是否是 Hello,World!package com.nasus;import static org.junit.Assert.assertEquals;import com.nasus.controller.HelloWorldController;import org.junit.Test;/* * Project Name:helloworld <br/> * Package Name:com.nasus <br/> * Date:2019/1/5 14:01 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * * @author <a href=“turodog@foxmail.com”>nasus</a><br/> * Copyright Notice ========================================================= * This file contains proprietary information of Eastcom Technologies Co. Ltd. * Copying or reproduction without prior written approval is prohibited. * Copyright (c) 2019 ======================================================= */public class HelloWorldControllerTest { @Test public void testSayHello() { assertEquals(“Hello,World!",new HelloWorldController().sayHello()); }}编写完成之后,可以通过以下按钮启动单元测试类。测试结果如下:可以看到红圈框住的地方,出现这个绿色标志证明单元测试没问题。sayhello 方法的结果是对的。后语我为什么要写这种这么简单的教程?是这样的,我始终认为比我聪明的人有很多,但比我笨的人也不少。在中国有很多你认为众所周知的事,其实有一车人根本不知道,这篇文章哪怕只帮助到一个人,足矣。之后我打算出一个 SpringBoot 系列的教程,敬请关注与指正,本人也是一个小菜鸟在打怪升级中,如本文有不正确的地方,烦请指正。一起学习一起进步。以上就是我对 SpringBoot 工程的理解,希望对你们有帮助。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点好看,让更多人知道。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

February 19, 2019 · 2 min · jiezi

SpringBoot 实战 (四) | 集成 Swagger2 构建强大的 RESTful API 文档

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言快过年了,不知道你们啥时候放年假,忙不忙。反正我是挺闲的,所以有时间写 blog。今天给你们带来 SpringBoot 集成 Swagger2 的教程。什么是 Swagger2Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。为什么使用 Swagger2 ?相信刚开始不熟悉 web 开发的时候,大家都有手写 Api 文档的时候。而手写 Api 文档主要有以下几个痛点:文档需要更新的时候,需要再次发送一份给前端,也就是文档更新交流不及时。接口返回结果不明确。不能直接在线测试接口,通常需要使用工具,比如 postman。接口文档太多,不好管理。这些痛点在前后端分离的大型项目上显得尤为烦躁。而 Swagger2 的出现恰好能个解决这些痛点。因为 Swagger2 有以下功能:文档自动更新,只要生成 Api 的网址没变,基本不需要跟前端沟通。接口返回结果非常明确,包括数据类型,状态码,错误信息等。可以直接在线测试文档,而且还有实例提供给你。只需要一次配置便可使用,之后只要会有一份接口文档,非常易于管理。集成演示首先新建一个 SpringBoot 项目,还不会的参考我这篇旧文—— 如何使用 IDEA 构建 Spring Boot 工程构建时,在选择依赖那一步勾选 Web、LomBok、JPA 和 Mysql 依赖。其中 Mysql 可以不勾,因为我这里用于操作实际的数据库,所以我勾选了。生成 SpringBoot 后的 Pom 文件依赖如下:这里使用的是 2.4.0 的 Swagger2 版本。<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.2.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.nasus</groupId> <artifactId>swagger2</artifactId> <version>0.0.1-SNAPSHOT</version> <name>swagger2</name> <description>Demo project for Swagger2</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.4.0</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.4.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>第二步,在 SpringBoot 启动类(Application)的同级目录新建一个 Swagger 配置类,注意 Swagger2 配置类必须与项目入口类 Application 位于同一级目录,否则生成 Api 文档失败,代码如下:package com.nasus;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import springfox.documentation.builders.ApiInfoBuilder;import springfox.documentation.builders.PathSelectors;import springfox.documentation.builders.RequestHandlerSelectors;import springfox.documentation.service.ApiInfo;import springfox.documentation.service.Contact;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spring.web.plugins.Docket;import springfox.documentation.swagger2.annotations.EnableSwagger2;/** * Project Name:swagger2-demo <br/> * Package Name:com.nasus <br/> * Date:2019/1/22 22:52 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * * @author <a href=“turodog@foxmail.com”>nasus</a><br/> * Copyright Notice ========================================================= * This file contains proprietary information of Eastcom Technologies Co. Ltd. * Copying or reproduction without prior written approval is prohibited. * Copyright (c) 2019 ======================================================= /@Configuration// 启用 Swagger2@EnableSwagger2public class Swagger2 { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) // 文档信息对象 .apiInfo(apiInfo()) .select() // 被注解的包路径 .apis(RequestHandlerSelectors.basePackage(“com.nasus.controller”)) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() // 标题 .title(“springboot 利用 swagger 构建 API 文档”) // Api 文档描述 .description(“简单优雅的 restful 风格,https://blog.csdn.net/turodog/”) .termsOfServiceUrl(“https://blog.csdn.net/turodog/") // 文档作者信息 .contact(new Contact(“陈志远”, “https://github.com/turoDog", “turodog@foxmail.com”)) // 文档版本 .version(“1.0”) .build(); }}第三步,配置被注解的 Controller 类,编写各个接口的请求参数,返回结果,接口描述等等,代码如下:package com.nasus.controller;import com.nasus.entity.Student;import com.nasus.service.StudentService;import io.swagger.annotations.Api;import io.swagger.annotations.ApiImplicitParam;import io.swagger.annotations.ApiImplicitParams;import io.swagger.annotations.ApiOperation;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.DeleteMapping;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.PutMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;import springfox.documentation.annotations.ApiIgnore;/* * Project Name:swagger2-demo <br/> * Package Name:com.nasus.controller <br/> * Date:2019/1/22 22:07 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * * @author <a href=“turodog@foxmail.com”>nasus</a><br/> * Copyright Notice ========================================================= * This file contains proprietary information of Eastcom Technologies Co. Ltd. * Copying or reproduction without prior written approval is prohibited. * Copyright (c) 2019 ======================================================= */@RestController@RequestMapping("/student”)// @Api:修饰整个类,描述Controller的作用@Api(“StudentController Api 接口文档”)public class StudentController { @Autowired private StudentService studentService; // @ApiOperation:描述一个类的一个方法,或者说一个接口 @ApiOperation(value=“获取所有学生列表”, notes=“获取所有学生列表”) @RequestMapping(value={””}, method= RequestMethod.GET) public List<Student> getStudent() { List<Student> list = studentService.findAll(); return list; } @ApiOperation(value=“添加学生信息”, notes=“添加学生信息”) // @ApiImplicitParam:一个请求参数 @ApiImplicitParam(name = “student”, value = “学生信息详细实体”, required = true, dataType = “Student”) @PostMapping("/save”) public Student save(@RequestBody Student student){ return studentService.save(student); } @ApiOperation(value=“获学生信息”, notes=“根据url的id来获取学生详细信息”) @ApiImplicitParam(name = “id”, value = “ID”, required = true, dataType = “Integer”,paramType = “path”) @GetMapping(”/{id}") public Student findById(@PathVariable(“id”) Integer id){ return studentService.findById(id); } @ApiOperation(value=“删除学生”, notes=“根据url的id来指定删除的学生”) @ApiImplicitParam(name = “id”, value = “学生ID”, required = true, dataType = “Integer”,paramType = “path”) @DeleteMapping("/{id}") public String deleteById(@PathVariable(“id”) Integer id){ studentService.delete(id); return “success”; } @ApiOperation(value=“更新学生信息”, notes=“根据url的id来指定更新学生信息”) // @ApiImplicitParams:多个请求参数 @ApiImplicitParams({ @ApiImplicitParam(name = “id”, value = “学生ID”, required = true, dataType = “Integer”,paramType = “path”), @ApiImplicitParam(name = “student”, value = “学生实体student”, required = true, dataType = “Student”) }) @PutMapping(value="/{id}") public String updateStudent(@PathVariable Integer id, @RequestBody Student student) { Student oldStudent = this.findById(id); oldStudent.setId(student.getId()); oldStudent.setName(student.getName()); oldStudent.setAge(student.getAge()); studentService.save(oldStudent); return “success”; } // 使用该注解忽略这个API @ApiIgnore @RequestMapping(value = “/hi”, method = RequestMethod.GET) public String jsonTest() { return " hi you!"; }}第四步,启动项目,访问 http://localhost:8080/swagger-ui.html 地址,结果如下图:项目源代码github图解接口Swagger2 常用注解简介@ApiOperation:用在方法上,说明方法的作用 1.value: 表示接口名称 2.notes: 表示接口详细描述 @ApiImplicitParams:用在方法上包含一组参数说明@ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面 1.paramType:参数位置 2.header 对应注解:@RequestHeader 3.query 对应注解:@RequestParam 4.path 对应注解: @PathVariable 5.body 对应注解: @RequestBody 6.name:参数名 7.dataType:参数类型 8.required:参数是否必须传 9.value:参数的描述 10.defaultValue:参数的默认值@ApiResponses:用于表示一组响应@ApiResponse:用在@ApiResponses中,一般用于表达一个错误的响应信息 1.code:状态码 2.message:返回自定义信息 3.response:抛出异常的类@ApiIgnore: 表示该接口函数不对swagger2开放展示@Api:修饰整个类,描述Controller的作用@ApiParam:单个参数描述@ApiModel:用对象来接收参数@ApiProperty:用对象接收参数时,描述对象的一个字段@ApiIgnore:使用该注解忽略这个API@ApiError :发生错误返回的信息注意事项@ApiImplicitParam 注解下的 paramType 属性,会影响接口的测试,如果设置的属性跟spring 的注解对应不上,会获取不到参数,例如 paramType=path ,函数内却使用@RequestParam 注解,这样,可能会获取不到传递进来的参数,需要按照上面进行对应,将 @RequestParam 注解改为 @PathVariable 才能获取到对应的参数。后语以上就是我对 Swagger2 的理解与使用,以上只是教大家入门 Swagger2 ,想要深入使用还是希望自行查阅官方文档。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点好看,让更多人知道。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

February 19, 2019 · 3 min · jiezi

spring aop 之链式调用

关关雎鸠,在河之洲。窈窕淑女,君子好逑。概述AOP(Aspect Orient Programming),我们一般称为面向方面(切面)编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、日志、缓存等等。 Spring AOP采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,Spring AOP提供了对JDK动态代理的支持以及CGLib的支持。本章我们不关注aop代理类的实现,我简单实现一个指定次序的链式调用。实现链式调用的MethodInterceptor定义拦截器链,MethodInvocation 递归进入下一个拦截器链中。类图如下:MethodInterceptorpublic interface MethodInterceptor { Object invoke(MethodInvocation invocation) throws Throwable;}MethodInvocationpublic interface MethodInvocation { Object proceed() throws Throwable;}AbstractAspectJAdvice抽象类,实现MethodInterceptorpublic abstract class AbstractAspectJAdvice implements MethodInterceptor{ private Method adviceMethod; private Object adviceObject; public AbstractAspectJAdvice(Method adviceMethod, Object adviceObject) { this.adviceMethod = adviceMethod; this.adviceObject = adviceObject; } public Method getAdviceMethod() { return this.adviceMethod; } public void invokeAdviceMethod() throws Throwable { adviceMethod.invoke(adviceObject); }}AspectJBeforeAdvice前置通知public class AspectJBeforeAdvice extends AbstractAspectJAdvice { public AspectJBeforeAdvice(Method method, Object adviceObject) { super(method, adviceObject); } @Override public Object invoke(MethodInvocation invocation) throws Throwable{ this.invokeAdviceMethod(); Object o = invocation.proceed(); return o; }}AspectJAfterReturningAdvice后置通知public class AspectJAfterReturningAdvice extends AbstractAspectJAdvice { public AspectJAfterReturningAdvice(Method method, Object adviceObject) { super(method, adviceObject); } @Override public Object invoke(MethodInvocation invocation) throws Throwable{ Object o = invocation.proceed(); this.invokeAdviceMethod(); return o; }}ReflectiveMethodInvocation实现MethodInvocation,proceed()方法递归实现链式调用。public class ReflectiveMethodInvocation implements MethodInvocation { private final Object targetObject; private final Method targetMethod; private final List<MethodInterceptor> interceptorList; private int currentInterceptorIndex = -1; public ReflectiveMethodInvocation(Object targetObject, Method targetMethod, List<MethodInterceptor> interceptorList) { this.targetObject = targetObject; this.targetMethod = targetMethod; this.interceptorList = interceptorList; } @Override public Object proceed() throws Throwable { if (this.currentInterceptorIndex == this.interceptorList.size() - 1) { return invokeJoinPoint(); } this.currentInterceptorIndex++; MethodInterceptor interceptor = this.interceptorList.get(this.currentInterceptorIndex); return interceptor.invoke(this); } private Object invokeJoinPoint() throws Throwable { return this.targetMethod.invoke(this.targetObject); }}NioCoderService模拟service类public class NioCoderService { public void testAop() { System.out.println(“http://niocoder.com/"); }}TransactionManager模拟通知类public class TransactionManager { public void start() { System.out.println(“start tx”); } public void commit() { System.out.println(“commit tx”); } public void rollback() { System.out.println(“rollback tx”); }}ReflectiveMethodInvocationTestbeforeAdvice->afterReturningAdvice测试类,测试通知public class ReflectiveMethodInvocationTest { private AspectJBeforeAdvice beforeAdvice = null; private AspectJAfterReturningAdvice afterReturningAdvice = null; private NioCoderService nioCoderService; private TransactionManager tx; public void setUp() throws Exception { nioCoderService = new NioCoderService(); tx = new TransactionManager(); beforeAdvice = new AspectJBeforeAdvice(TransactionManager.class.getMethod(“start”), tx); afterReturningAdvice = new AspectJAfterReturningAdvice(TransactionManager.class.getMethod(“commit”), tx); } public void testMethodInvocation() throws Throwable { Method method = NioCoderService.class.getMethod(“testAop”); List<MethodInterceptor> interceptorList = new ArrayList<>(); interceptorList.add(beforeAdvice); interceptorList.add(afterReturningAdvice); ReflectiveMethodInvocation mi = new ReflectiveMethodInvocation(nioCoderService, method, interceptorList); mi.proceed(); } public static void main(String[] args) throws Throwable { ReflectiveMethodInvocationTest reflectiveMethodInvocationTest = new ReflectiveMethodInvocationTest(); reflectiveMethodInvocationTest.setUp(); reflectiveMethodInvocationTest.testMethodInvocation(); }}输出:start txhttp://niocoder.com/commit tx时序图 beforeAdvice->afterReturningAdviceafterReturningAdvice->beforeAdvice修改interceptorList的顺序 public void testMethodInvocation() throws Throwable { Method method = NioCoderService.class.getMethod(“testAop”); List<MethodInterceptor> interceptorList = new ArrayList<>(); interceptorList.add(afterReturningAdvice); interceptorList.add(beforeAdvice); ReflectiveMethodInvocation mi = new ReflectiveMethodInvocation(nioCoderService, method, interceptorList); mi.proceed(); }输出:start txhttp://niocoder.com/commit tx时序图 afterReturningAdvice->beforeAdvice代码下载github:https://github.com/longfeizheng/data-structure-java/blob/master/src/main/java/cn/merryyou/aop代码下载github:https://github.com/longfeizheng/data-structure-java ...

February 19, 2019 · 2 min · jiezi

SpringBoot 实战 (七) | 默认日志配置

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言如题,今天介绍 springboot 默认日志的配置。默认日志 Logback默认情况下,Spring Boot 用 Logback 来记录日志,并用 INFO 级别输出到控制台。如果你在平常项目中用过 Spring Boot,你应该已经注意到很多 INFO 级别的日志了。默认日志长这样:2019-02-18 22:02:14.907 INFO 23384 — [ main] org.hibernate.Version : HHH000412: Hibernate Core {5.3.7.Final}2019-02-18 22:02:14.907 INFO 23384 — [ main] org.hibernate.cfg.Environment : HHH000206: hibernate.properties not found2019-02-18 22:02:15.110 INFO 23384 — [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.0.4.Final}从上面的日志可以看到,日志输出内容元素具体如下:时间日期:精确到毫秒日志级别:ERROR, WARN, INFO, DEBUG or TRACE进程 ID分隔符:— 标识实际日志的开始线程名:方括号括起来(可能会截断控制台输出)Logger 名:通常使用源代码的类名日志内容日志依赖Logback 日志框架依赖于 spring-boot-starter-logging 包,但我们并不需要在 maven 中加入这个依赖,因为 spring-boot-starter其中包含了 spring-boot-starter-logging,该依赖内容就是 Spring Boot 默认的日志框架 logback。控制台输出在 Spring Boot 中默认配置了 ERROR、WARN 和 INFO 级别的日志输出到控制台。我们可以通过两种方式切换至 DEBUG 级别:在运行命令后加入 –debug 标志,如:$ java -jar myapp.jar –debug在 application.properties 中配置 debug=true ,该属性置为 true 的时候,核心 Logger(包含嵌入式容器、hibernate、spring)会输出更多内容,但是你自己应用的日志并不会输出为 DEBUG 级别。多彩输出如果你的终端支持ANSI,设置彩色输出会让日志更具可读性。通过在 application.properties 中设置 spring.output.ansi.enabled 参数来支持。NEVER:禁用 ANSI-colored 输出(默认项)DETECT:会检查终端是否支持 ANSI,是的话就采用彩色输出(推荐项)ALWAYS:总是使用 ANSI-colored 格式输出,若终端不支持的时候,会有很多干扰信息,不推荐使用文件输出Spring Boot默认配置只会输出到控制台,并不会记录到文件中,但是我们通常生产环境使用时都需要以文件方式记录。若要增加文件输出,需要在 application.properties 中配置 logging.file 或 logging.path属性。logging.file,设置文件,可以是绝对路径,也可以是相对路径。如:logging.file=my.loglogging.path,设置目录,会在该目录下创建spring.log文件,并写入日志内容,如:logging.path=/var/log注:二者不能同时使用,如若同时使用,则只有logging.file生效 默认情况下,日志文件的大小达到 10MB 时会切分一次,产生新的日志文件,默认级别为:ERROR、WARN、INFO级别控制在 Spring Boot 中只需要在 application.properties 中进行配置完成日志记录的级别控制。配置格式:logging.level.*=LEVELlogging.level:日志级别控制前缀,*为包名或Logger名LEVEL:选项 TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF举例:logging.level.com.nasus=DEBUG:com.nasus 包下所有 class 以 DEBUG 级别输出logging.level.root=WARN:root日志以 WARN 级别输出自定义日志配置根据不同的日志系统,你可以按如下规则组织配置文件名,就能被正确加载:Logback:logback-spring.xml, logback-spring.groovy, logback.xml, logback.groovyLog4j:log4j-spring.properties, log4j-spring.xml, log4j.properties, log4j.xmlLog4j2:log4j2-spring.xml, log4j2.xmlJDK (Java Util Logging):logging.propertiesSpring Boot 官方推荐优先使用带有 -spring 的文件名作为你的日志配置(如使用 logback-spring.xml,而不是 logback.xml),命名为 logback-spring.xml 的日志配置文件,spring boot 可以为它添加一些 spring boot 特有的配置项(下面会提到)。 默认的命名规则,并且放在 src/main/resources 下面即可如果你即想完全掌控日志配置,但又不想用 logback.xml 作为 Logback 配置的名字,application.yml 可以通过 logging.config 属性指定自定义的名字:logging.config=classpath:logging-config.xml虽然一般并不需要改变配置文件的名字,但是如果你想针对不同运行时 Profile 使用不同的日志配置,这个功能会很有用。 一般不需要这个属性,而是直接在 logback-spring.xml 中使用 springProfile 配置,不需要 logging.config 指定不同环境使用不同配置文件。springProfile 配置在下面介绍。多环境日志输出logback-spring.xml :<configuration> … <!– 测试环境+开发环境. 多个使用逗号隔开. –> <springProfile name=“test,dev”> <logger name=“com.example.demo.controller” level=“DEBUG” additivity=“false”> <appender-ref ref=“consoleLog”/> </logger> </springProfile> <!– 生产环境. –> <springProfile name=“prod”> <logger name=“com.example.demo.controller” level=“INFO” additivity=“false”> <appender-ref ref=“consoleLog”/> </logger> </springProfile></configuration>application.yml 增加环境选择的配置 active: devspring: profiles: active: dev datasource: url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8 username: root password: 123456根据 active 的环境,自动采用上面配置的 springProfile 的 logger 日志。后语以上 SpringBoot 默认日志的配置教程。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点好看,让更多人知道。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

February 19, 2019 · 2 min · jiezi

SpringBoot参数校验

本篇概述 在正常的项目开发中,我们常常需要对程序的参数进行校验来保证程序的安全性。参数校验非常简单,说白了就是对参数进行正确性验证,例如非空验证、范围验证、类型验证等等。校验的方式也有很多种。如果架构设计的比较好的话,可能我们都不需要做任何验证,或者写比较少的代码就可以满足验证的需求。如果架构设计的有缺陷,或者说压根就没有架构的话,那么我们对参数进行验证时,就需要我们写大量相对重复的代码进行验证了。手动参数校验 下面我们还是以上一篇的内容为例,我们首先手动对参数进行校验。下面为Controller源码:package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/userinfo")public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(UserInfoQuery userInfo) { if (StringUtils.isEmpty(userInfo.getUsername())) { return “账号不能为空”; } if (StringUtils.isEmpty(userInfo.getRoleId()) || userInfo.getRoleId() > 100 || userInfo.getRoleId() < 1) { return “权限不能为空,并且范围为[1-99]”; } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; }} 我们只验证了username和roleId参数,分别验证为空验证及范围验证。下面我们测试一下。启动项目后,访问以下地址:http://127.0.0.1:8080/springb… 我们看一下程序的运行结果。 因为我们没有写任何参数,所以参数验证一定是不能通过的。所以就返回的上图中的提示信息。下面我们看一下数据库中的数据,然后访问一下正确的地址,看看能不能成功的返回数据库中的数据。下图为数据库中的数据: 下面我们访问一下正确的参数,然后看一下返回的结果。访问地址:http://127.0.0.1:8080/springb… 访问结果: 我们看上图已经成功的返回数据库中的数据了,这就是简单的参数校验,正是因为简单,所以我们就不做过多的介绍了。下面我们简单分析一下,这样做参数验证好不好。如果我们的项目比较简单,那答案一定是肯定的,因为站在软件设计角度考虑,没必要为了一个简单的功能而设计一个复杂的架构。因为越是复杂的功能,出问题的可能性就越大,程序就越不稳定。但如果站在程序开发角度,那上面的代码一定是有问题的,因为上面的代码根本没办法复用,如果要开发很多这样的项目,要进行参数验证时,那结果一定是代码中有很多相类似的代码,这显然是不合理的。那怎么办呢?那答案就是本篇中的重点内容,也就是SpringBoot对参数的验证,实际上本篇的内容主要是和Spring内容相关和SpringBoot的关系不大。但SpringBoot中基本包括了所有Spring的内容,所以我们还是以SpringBoot项目为例。下面我们看一下,怎么在SpringBoot中的对参数进行校验。ObjectError参数校验 我们首先看一下代码,然后在详细介绍代码中的新知识。下面为接受的参数类的源码。 修改前:package com.jilinwula.springboot.helloworld.query;import lombok.Data;import org.springframework.stereotype.Component;@Component@Datapublic class UserInfoQuery{ private String username; private Long roleId;} 修改后:package com.jilinwula.springboot.helloworld.query;import lombok.Data;import org.springframework.stereotype.Component;import javax.validation.constraints.Max;import javax.validation.constraints.Min;import javax.validation.constraints.NotNull;@Component@Datapublic class UserInfoQuery{ @NotNull(message = “账号不能为空”) private String username; @NotNull(message = “权限不能为空”) @Min(value = 1, message = “权限范围为[1-99]”) @Max(value = 99, message = “权限范围为[1-99]”) private Long roleId;} 我们看代码中唯一的区别就是添加了很多的注解。没错,在SpringBoot项目中进行参数校验时,就是使用这些注解来完成的。并且注解的命名很直观,基本上通过名字就可以知道什么含义。唯一需要注意的就是这些注解的包是javax中的,而不是其它第三方引入的包。这一点要特别注意,因为很多第三方的包,也包含这些同名的注解。下面我们继续看Controller中的改动(备注:有关javax中的校验注解相关的使用说明,我们后续在做介绍)。Controller源码: 改动前:package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/userinfo")public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(UserInfoQuery userInfo) { if (StringUtils.isEmpty(userInfo.getUsername())) { return “账号不能为空”; } if (StringUtils.isEmpty(userInfo.getRoleId()) || userInfo.getRoleId() > 100 || userInfo.getRoleId() < 1) { return “权限不能为空,并且范围为[1-99]”; } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; }} 改动后:package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.validation.BindingResult;import org.springframework.validation.ObjectError;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.validation.Valid;@RestController@RequestMapping("/userinfo")public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(@Valid UserInfoQuery userInfo, BindingResult result) { if (result.hasErrors()) { for (ObjectError error : result.getAllErrors()) { return error.getDefaultMessage(); } } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; }} 我们看代码改动的还是比较大的首先在入参中添加了@Valid注解。该注解就是标识让SpringBoot对请求参数进行验证。也就是和参数类里的注解是对应的。其次我们修改了直接在Controller中进行参数判断的逻辑,将以前的代码修改成了SpringBoot中指定的校验方式。下面我们启动项目,来验证一下上述代码是否能成功的验证参数的正确性。我们访问下面请求地址:http://127.0.0.1:8080/springb… 返回结果: 我们看上图成功的验证了为空的校验,下面我们试一下范围的验证。我们访问下面的请求地址:http://127.0.0.1:8080/springb… 看一下返回结果: 我们看成功的检测到了参数范围不正确。这就是SpringBoot中的参数验证功能。但上面的代码一个问题,就是只是会返回错误的提示信息,而没有提示,是哪个参数不正确。下面我们修改一下代码,来看一下怎么返回是哪个参数不正确。FieldError参数校验package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.validation.Valid;@RestController@RequestMapping("/userinfo")public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(@Valid UserInfoQuery userInfo, BindingResult result) { if (result.hasErrors()) { FieldError error = result.getFieldError(); return error.getField() + “+” + error.getDefaultMessage(); } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; }} 我们将获取ObjectError的类型修改成了FieldError。因为FieldError类型可以获取到验证错误的字段名字,所以我们将ObjectError修改为FieldError。下面我们看一下请求返回的结果。 我们看这回我们就获取到了验证错误的字段名子了。在实际的项目开发中,我们在返回接口数据时,大部分都会采用json格式的方式返回,下面我们简单封装一个返回的类,使上面的验证返回json格式。下面为封装的返回类的源码:package com.jilinwula.springboot.helloworld.utils;import lombok.Data;@Datapublic class Return { private int code; private Object data; private String msg; public static Return error(Object data, String msg) { Return r = new Return(); r.setCode(-1); r.setData(data); r.setMsg(msg); return r; }} Controller修改:package com.jilinwula.springboot.helloworld.controller;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import com.jilinwula.springboot.helloworld.query.UserInfoQuery;import com.jilinwula.springboot.helloworld.utils.Return;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.validation.Valid;@RestController@RequestMapping("/userinfo")public class UserInfoController { @Autowired private UserInfoRepository userInfoRepository; @GetMapping("/query") public Object list(@Valid UserInfoQuery userInfo, BindingResult result) { if (result.hasErrors()) { FieldError error = result.getFieldError(); return Return.error(error.getField(), error.getDefaultMessage()); } UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId()); return userInfoEntity; }} 我们还是启动项目,并访问下面地址看看返回的结果:http://127.0.0.1:8080/springb… 返回结果: 创建切面 这样我们就返回一个简单的json类型的数据了。虽然我们的校验参数的逻辑没有在Controller里面写,但我们还是在Controller里面写了很多和业务无关的代码,并且这些代码还是重复的,这显然是不合理的。我们可以将上述相同的代码的封装起来,然后统一的处理。这样就避免了有很多重复的代码了。那这代码封装到哪里呢?我们可以使用Spring中的切面功能。因为SpringBoot中基本包括了所有Spring中的技术,所以,我们可以放心大胆的在SpringBoot项目中使用Spring中的技术。我们知道在使用切面技术时,我们可以对方法进行前置增强、后置增强、环绕增强等。这样我们就可以利用切面的技术,在方法之前,也就是请求Controller之前,做参数的校验工作,这样就不会对我们的业务代码产生侵入了。下面我们看一下切面的源码然后在做详细说明:package com.jilinwula.springboot.helloworld.aspect;import com.jilinwula.springboot.helloworld.utils.Return;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.stereotype.Component;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;@Slf4j@Aspect@Componentpublic class UserAspect { @Before(“execution(public * com.jilinwula.springboot.helloworld.controller..*(..))”) public void doBefore(JoinPoint joinPoint) { for (Object arg : joinPoint.getArgs()) { if (arg instanceof BindingResult) { BindingResult result = (BindingResult) arg; if (result.hasErrors()) { FieldError error = result.getFieldError(); Return.error(error.getField(), error.getDefaultMessage()); } } } }} 我们看上述的代码中我们添加了一个@Aspect注解,这个就是切面的注解,然后我们在方法中又添加了@Before注解,也就是对目标方法进行前置增强,Spring在请求Controller之前会先请求此方法。所以我们可以将校验参数的代码逻辑写在这个方法中。execution参数为切点函数,也就是目标方法的切入点。切点函数包含一些通配符的语法,下面我们简单介绍一下:匹配任意字符,但它可能匹配上下文中的一个元素.. 匹配任意字符,可以匹配上下文中的多个元素表示按类型匹配指定类的所有类,必须跟在类名后面,也就是会匹配继承或者扩展指定类的所有类,包括指定类.创建异常类 我们通过上述代码知道,Spring中的切面功能是没有返回值的。所以我们在使用切面功能时,是没有办法在切面里面做参数返回的。那我们应该怎么办呢?这时异常就派上用场了。我们知道当程序抛出异常时,如果当前方法没有做try catch处理,那么异常就会一直向上抛出,如果程序也一直没有做处理,那么当前异常就会一直抛出,直到被Java虚拟机捕获。但Java虚拟机也不会对异常进行处理,而是直接抛出异常。这也就是程序不做任何处理抛出异常的根本原因。我们正好可以利用异常的这种特性,返回参数验证的结果。因为在Spring中为我们提供了统一捕获异常的方法,我们可以在这个方法中,将我们的异常信息封装成json格式,这样我们就可以返回统一的jons格式了。所以在上述的切面中我们手动了抛出了一个异常。该异常因为我们没有用任何处理,所以上述异常会被SpringBoot中的统一异常拦截处理。这样当SpringBoot检测到参数不正确时,就会抛出一个异常,然后SpringBoot就会检测到程序抛出的异常,然后返回异常中的信息。下面我们看一下异常类的源码: 异常类:package com.jilinwula.springboot.helloworld.exception;import com.jilinwula.springboot.helloworld.utils.Return;import lombok.Data;@Datapublic class UserInfoException extends RuntimeException { private Return r; public UserInfoException(Return r) { this.r = r; }} Return源码:package com.jilinwula.springboot.helloworld.utils;import com.jilinwula.springboot.helloworld.exception.UserInfoException;import lombok.Data;@Datapublic class Return { private int code; private Object data; private String msg; public static void error(Object data, String msg) { Return r = new Return(); r.setCode(-1); r.setData(data); r.setMsg(msg); throw new UserInfoException(r); } public static Return success() { Return r = new Return(); r.setCode(0); return r; }}SpringBoot统一异常拦截 因为该异常类比较简单,我们就不会过多的介绍了,唯一有一点需要注意的是该异常类继承的是RuntimeException异常类,而不是Exception异常类,原因我们已经在上一篇中介绍了,Spring只会回滚RuntimeException异常类及其子类,而不会回滚Exception异常类的。下面我们看一下Spring中统一拦截异常处理,下面为该类的源码:package com.jilinwula.springboot.helloworld.handler;import com.jilinwula.springboot.helloworld.exception.UserInfoException;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@Slf4j@RestControllerAdvicepublic class UserInfoHandler { /** * 校验错误拦截处理 * * @param e 错误信息集合 * @return 错误信息 */ @ExceptionHandler(UserInfoException.class) public Object handle(UserInfoException e) { return e.getR(); }} 我们在该类添加了@RestControllerAdvice注解。该注解就是为了定义我们统一获取异常拦截的。然后我们又添加了@ExceptionHandler注解,该注解就是用来拦截异常类的注解,并且可以在当前方法中,直接获取到该异常类的对象信息。这样我们直接返回这个异常类的信息就可以了。因为我们在这个自定义异常类中添加了Return参数,所以,我们只要反悔Return对象的信息即可,而不用返回整个异常的信息。下面我们访问一下下面的请求,看看上述代码是否能检测到参数不正确。请求地址:http://127.0.0.1:8080/springb… 返回结果: 这样我们完成了参数校验的功能了,并且这种方式有很大的复用性,即使我们在写新的Controller,也不需要手动的校验参数了,只要我们的请求参数是UserInfoQuery类就可以了。还有一点要注意,所以我们不用手动验证参数了,但我们的请求参数中还是要写BindingResult参数,这一点要特别注意。正则表达式校验注解 下面我们更详细的介绍一下参数验证的注解,我们首先看一下正则校验,我们在实体类中添加一个新属性,然后用正则的的方式,验证该参数的正确性。下面为实体类源码:package com.jilinwula.springboot.helloworld.query;import lombok.Data;import org.springframework.stereotype.Component;import javax.validation.constraints.Max;import javax.validation.constraints.Min;import javax.validation.constraints.NotNull;import javax.validation.constraints.Pattern;@Component@Datapublic class UserInfoQuery{ @NotNull(message = “用户编号不能为空”) @Pattern(regexp = “^[1-10]$",message = “用户编号范围不正确”) private String id; @NotNull(message = “账号不能为空”) private String username; @NotNull(message = “权限不能为空”) @Min(value = 1, message = “权限范围为[1-99]”) @Max(value = 99, message = “权限范围为[1-99]”) private Long roleId;} 下面我们访问以下地址:http://127.0.0.1:8080/springb…http文件请求接口 但这回我们不在浏览器里请求,因为浏览器请求不太方便,并且返回的json格式也没有格式化不方便浏览,除非要装一些浏览器插件才可以。实际上在IDEA中我们可以很方便的请求一下接口地址,并且返回的json内容是自动格式化的。下面我们来看一下怎么在IDEA中发起接口请求。在IDEA中请求一个接口很简单,我们只要创建一个.http类型的文件名字就可以。然后我们可以在该文件中,指定我们接口的请求类型,例如GET或者POST。当我们在文件的开口写GET或者POST时,IDEA会自动有相应的提示。下面我们看一下http文件中的内容。 http.http:GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=阿里巴巴&id=-1 这时标识GET参数的地方,就会出现绿色剪头,但我们点击这个绿色箭头,IDEA就会就会启动请求GET参数后面的接口。下面我们看一下上述的返回结果。GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4&id=-1HTTP/1.1 200 Content-Type: application/json;charset=UTF-8Transfer-Encoding: chunkedDate: Mon, 18 Feb 2019 03:57:29 GMT{ “code”: -1, “data”: “id”, “msg”: “用户编号范围不正确”}Response code: 200; Time: 24ms; Content length: 41 bytes 这就是.http文件类型的返回结果,用该文件请求接口,相比用浏览器来说,要方便的多。因为我们在实体类中使用正则指定参数范围为1-10,所以请求接口时反悔了id参数有错误。下面我们输入一个正确的值在看一下返回结果。 http.http:GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=阿里巴巴&id=1 返回结果: GET <http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4&id=1> HTTP/1.1 200 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Mon, 18 Feb 2019 05:46:49 GMT { “id”: 61, “username”: “阿里巴巴”, “password”: “alibaba”, “nickname”: “阿里巴巴”, “roleId”: 3 } Response code: 200; Time: 25ms; Content length: 77 bytes常见校验注解 我们看已经正确的返回数据库中的数据了。在Spring中,提供了很多种注解来方便我们进行参数校验,下面是比较常见的注解:注解作用@Null参数必须为null @NotNull参数必须不为null @NotBlank参数必须不为null,并且长度必须大于0 @NotEmpty参数必须不为空 @Min参数必须大于等于该值 @Max参数必须小于等于该值 @Size参数必须在指定的范围内 @Past参数必须是一个过期的时间 @Future参数必须是一个未来的时间 @Pattern参数必须满足正则表达式 @Email参数必须为电子邮箱 上述内容就是SpringBoot中的参数校验全部内容,如有不正确的欢迎留言,谢谢。源码地址https://github.com/jilinwula/…原文地址http://jilinwula.com/article/… ...

February 18, 2019 · 3 min · jiezi

Synchronized锁在Spring事务管理下,为啥还线程不安全?

前言只有光头才能变强。文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y大年初二,朋友问了我一个技术的问题(朋友实在是好学,佩服!)该问题来源知乎(synchronized锁问题):https://www.zhihu.com/question/277812143开启10000个线程,每个线程给员工表的money字段【初始值是0】加1,没有使用悲观锁和乐观锁,但是在业务层方法上加了synchronized关键字,问题是代码执行完毕后数据库中的money 字段不是10000,而是小于10000 问题出在哪里?Service层代码:SQL代码(没有加悲观/乐观锁):用1000个线程跑代码:简单来说:多线程跑一个使用synchronized关键字修饰的方法,方法内操作的是数据库,按正常逻辑应该最终的值是1000,但经过多次测试,结果是低于1000。这是为什么呢?一、我的思考既然测试出来的结果是低于1000,那说明这段代码不是线程安全的。不是线程安全的,那问题出现在哪呢?众所周知,synchronized方法能够保证所修饰的代码块、方法保证有序性、原子性、可见性。讲道理,以上的代码跑起来,问题中Service层的increaseMoney()是有序的、原子的、可见的,所以断定跟synchronized应该没关系。(参考我之前写过的synchronize锁笔记:Java锁机制了解一下)既然Java层面上找不到原因,那分析一下数据库层面的吧(因为方法内操作的是数据库)。在increaseMoney()方法前加了@Transcational注解,说明这个方法是带有事务的。事务能保证同组的SQL要么同时成功,要么同时失败。讲道理,如果没有报错的话,应该每个线程都对money值进行+1。从理论上来说,结果应该是1000的才对。(参考我之前写过的Spring事务:一文带你看懂Spring事务!)根据上面的分析,我怀疑是提问者没测试好(hhhh,逃),于是我也跑去测试了一下,发现是以提问者的方式来使用是真的有问题。首先贴一下我的测试代码:@RestControllerpublic class EmployeeController { @Autowired private EmployeeService employeeService; @RequestMapping("/add") public void addEmployee() { for (int i = 0; i < 1000; i++) { new Thread(() -> employeeService.addEmployee()).start(); } }}@Servicepublic class EmployeeService { @Autowired private EmployeeRepository employeeRepository; @Transactional public synchronized void addEmployee() { // 查出ID为8的记录,然后每次将年龄增加一 Employee employee = employeeRepository.getOne(8); System.out.println(employee); Integer age = employee.getAge(); employee.setAge(age + 1); employeeRepository.save(employee); }}简单地打印了每次拿到的employee值,并且拿到了SQL执行的顺序,如下(贴出小部分):从打印的情况我们可以得出:多线程情况下并没有串行执行addEmployee()方法。这就导致对同一个值做重复的修改,所以最终的数值比1000要少。二、图解出现的原因发现并不是同步执行的,于是我就怀疑synchronized关键字和Spring肯定有点冲突。于是根据这两个关键字搜了一下,找到了问题所在。我们知道Spring事务的底层是Spring AOP,而Spring AOP的底层是动态代理技术。跟大家一起回顾一下动态代理: public static void main(String[] args) { // 目标对象 Object target ; Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), Main.class, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 但凡带有@Transcational注解的方法都会被拦截 // 1… 开启事务 method.invoke(target); // 2… 提交事务 return null; } }); }(详细请参考我之前写过的动态代理:给女朋友讲解什么是代理模式)实际上Spring做的处理跟以上的思路是一样的,我们可以看一下TransactionAspectSupport类中invokeWithinTransaction():调用方法前开启事务,调用方法后提交事务在多线程环境下,就可能会出现:方法执行完了(synchronized代码块执行完了),事务还没提交,别的线程可以进入被synchronized修饰的方法,再读取的时候,读到的是还没提交事务的数据,这个数据不是最新的,所以就出现了这个问题。三、解决问题从上面我们可以发现,问题所在是因为@Transcational注解和synchronized一起使用了,加锁的范围没有包括到整个事务。所以我们可以这样做:新建一个名叫SynchronizedService类,让其去调用addEmployee()方法,整个代码如下:@RestControllerpublic class EmployeeController { @Autowired private SynchronizedService synchronizedService ; @RequestMapping("/add") public void addEmployee() { for (int i = 0; i < 1000; i++) { new Thread(() -> synchronizedService.synchronizedAddEmployee()).start(); } }}// 新建的Service类@Servicepublic class SynchronizedService { @Autowired private EmployeeService employeeService ; // 同步 public synchronized void synchronizedAddEmployee() { employeeService.addEmployee(); }}@Servicepublic class EmployeeService { @Autowired private EmployeeRepository employeeRepository; @Transactional public void addEmployee() { // 查出ID为8的记录,然后每次将年龄增加一 Employee employee = employeeRepository.getOne(8); System.out.println(Thread.currentThread().getName() + employee); Integer age = employee.getAge(); employee.setAge(age + 1); employeeRepository.save(employee); }}我们将synchronized锁的范围包含到整个Spring事务上,这就不会出现线程安全的问题了。在测试的时候,我们可以发现1000个线程跑起来比之前要慢得多,当然我们的数据是正确的:最后可以发现的是,虽然说Spring事务用起来我们是非常方便的,但如果不了解一些Spring事务的细节,很多时候出现Bug了就百思不得其解。还是得继续加油努力呀~~~乐于输出干货的Java技术公众号:Java3y。公众号内有200多篇原创技术文章、海量视频资源、精美脑图,不妨来关注一下!觉得我的文章写得不错,不妨点一下赞! ...

February 17, 2019 · 1 min · jiezi

SpringBoot 实战 | 用 JdbcTemplates 访问 Mysql

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言如题,今天介绍 springboot 通过jdbc访问关系型mysql,通过 spring 的 JdbcTemplate 去访问。准备工作SpringBoot 2.xjdk 1.8maven 3.0ideamysql构建 SpringBoot 项目,不会的朋友参考旧文章:如何使用 IDEA 构建 Spring Boot 工程项目目录结构pom 文件引入依赖<dependencies>xml <!– jdbcTemplate 依赖 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!– 开启web: –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– mysql连接类 –> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!– https://mvnrepository.com/artifact/com.alibaba/druid –> <!– druid 连接池–> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.13</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies>application.yaml 配置数据库信息spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true username: 数据库用户名 password: 数据库密码实体类package com.nasus.domain;/** * Project Name:jdbctemplate_demo <br/> * Package Name:com.nasus.domain <br/> * Date:2019/2/3 10:55 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * * @author <a href=“turodog@foxmail.com”>nasus</a><br/> * Copyright Notice ========================================================= * This file contains proprietary information of Eastcom Technologies Co. Ltd. * Copying or reproduction without prior written approval is prohibited. * Copyright (c) 2019 ======================================================= /public class Student { private Integer id; private String name; private Integer age; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return “Student{” + “id=” + id + “, name=’” + name + ‘'’ + “, age=” + age + ‘}’; }}dao 层package com.nasus.dao;import com.nasus.domain.Student;import java.util.List;/* * Project Name:jdbctemplate_demo <br/> * Package Name:com.nasus.dao <br/> * Date:2019/2/3 10:59 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * @author <a href=“turodog@foxmail.com”>nasus</a><br/> /public interface IStudentDao { int add(Student student); int update(Student student); int delete(int id); Student findStudentById(int id); List<Student> findStudentList();}具体实现类:package com.nasus.dao.impl;import com.nasus.dao.IStudentDao;import com.nasus.domain.Student;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.jdbc.core.BeanPropertyRowMapper;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Repository;/* * Project Name:jdbctemplate_demo <br/> * Package Name:com.nasus.dao.impl <br/> * Date:2019/2/3 11:00 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * * @author <a href=“turodog@foxmail.com”>nasus</a><br/> /@Repositorypublic class IStudentDaoImpl implements IStudentDao{ @Autowired private JdbcTemplate jdbcTemplate; @Override public int add(Student student) { return jdbcTemplate.update(“insert into student(name, age) values(?, ?)”, student.getName(),student.getAge()); } @Override public int update(Student student) { return jdbcTemplate.update(“UPDATE student SET NAME=? ,age=? WHERE id=?”, student.getName(),student.getAge(),student.getId()); } @Override public int delete(int id) { return jdbcTemplate.update(“DELETE from TABLE student where id=?",id); } @Override public Student findStudentById(int id) { // BeanPropertyRowMapper 使获取的 List 结果列表的数据库字段和实体类自动对应 List<Student> list = jdbcTemplate.query(“select * from student where id = ?”, new Object[]{id}, new BeanPropertyRowMapper(Student.class)); if(list!=null && list.size()>0){ Student student = list.get(0); return student; }else{ return null; } } @Override public List<Student> findStudentList() { // 使用Spring的JdbcTemplate查询数据库,获取List结果列表,数据库表字段和实体类自动对应,可以使用BeanPropertyRowMapper List<Student> list = jdbcTemplate.query(“select * from student”, new Object[]{}, new BeanPropertyRowMapper(Student.class)); if(list!=null && list.size()>0){ return list; }else{ return null; } }}service 层package com.nasus.service;import com.nasus.domain.Student;import java.util.List;/* * Project Name:jdbctemplate_demo <br/> * Package Name:com.nasus.service <br/> * Date:2019/2/3 11:17 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * * @author <a href=“turodog@foxmail.com”>nasus</a><br/> /public interface IStudentService { int add(Student student); int update(Student student); int delete(int id); Student findStudentById(int id); List<Student> findStudentList();}实现类:package com.nasus.service.impl;import com.nasus.dao.IStudentDao;import com.nasus.domain.Student;import com.nasus.service.IStudentService;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Repository;/* * Project Name:jdbctemplate_demo <br/> * Package Name:com.nasus.service.impl <br/> * Date:2019/2/3 11:18 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * * @author <a href=“turodog@foxmail.com”>nasus</a><br/> * Copyright Notice ========================================================= * This file contains proprietary information of Eastcom Technologies Co. Ltd. * Copying or reproduction without prior written approval is prohibited. * Copyright (c) 2019 ======================================================= /@Repositorypublic class IStudentServiceImpl implements IStudentService { @Autowired private IStudentDao iStudentDao; @Override public int add(Student student) { return iStudentDao.add(student); } @Override public int update(Student student) { return iStudentDao.update(student); } @Override public int delete(int id) { return iStudentDao.delete(id); } @Override public Student findStudentById(int id) { return iStudentDao.findStudentById(id); } @Override public List<Student> findStudentList() { return iStudentDao.findStudentList(); }}controller 构建 restful apipackage com.nasus.controller;import com.nasus.domain.Student;import com.nasus.service.IStudentService;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.DeleteMapping;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.PutMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/* * Project Name:jdbctemplate_demo <br/> * Package Name:com.nasus.controller <br/> * Date:2019/2/3 11:21 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * * @author <a href=“turodog@foxmail.com”>nasus</a><br/> */@RestController@RequestMapping("/student”)public class StudentController { @Autowired private IStudentService iStudentService; @PostMapping("") public int addStudent(@RequestBody Student student){ return iStudentService.add(student); } @PutMapping("/{id}") public String updateStudent(@PathVariable Integer id, @RequestBody Student student){ Student oldStudent = new Student(); oldStudent.setId(id); oldStudent.setName(student.getName()); oldStudent.setAge(student.getAge()); int t = iStudentService.update(oldStudent); if (t == 1){ return student.toString(); }else { return “更新学生信息错误”; } } @GetMapping("/{id}") public Student findStudentById(@PathVariable Integer id){ return iStudentService.findStudentById(id); } @GetMapping("/list") public List<Student> findStudentList(){ return iStudentService.findStudentList(); } @DeleteMapping("/{id}") public int deleteStudentById(@PathVariable Integer id){ return iStudentService.delete(id); }}演示结果其他的 api 测试可以通过 postman 测试。我这里已经全部测试通过,请放心使用。源码下载:https://github.com/turoDog/De…后语以上SpringBoot 用 JdbcTemplates 访问Mysql 的教程。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点好看,让更多人知道。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

February 17, 2019 · 4 min · jiezi

SpringBoot入门系列HelloWorld

根据咱们程序员学习的惯例,学习一门新技术都是从HelloWorld开始的。感觉编程是一件非常富有意义的事情,程序员也是一群可爱的人,渴望被关怀和关注,因为我们总在和世界say Hi.好了进入正题创建项目首先创建一个项目,可看我上一篇文章写得IntelliJ IDEA创建第一个Spring boot项目接下来运行这个项目,你将会看到如下页面提示我们当前没有准确的映射,所以找不到对应的页面也就是404。莫慌,接下来咱们处理一下创建HelloController控制器在项目名/src/main/java/包名下,新建一个config包,包下面创建HelloController@Controllerpublic class HelloController { @RequestMapping(value = “/hello”,method = RequestMethod.GET) @ResponseBody public String hello(){ return “Hello World”; }}注解说明:@Controller: 可让项目扫描自动检测到这个类,处理http请求@ RequestMapping 请求的路由映射,访问的路径就是:http://localhost:8080/hellovalue: 路由名method: 请求方式,GET,POST,PUT,DELETE等重新启动项目访问:http://localhost:8080/hello, 就看到Hello World了看到如上图所示,就表示我们的hello world成功了。目录结构:src/main/java: Java代码的目录src/main/resources: 资源目录src/test/java: 测试代码的目录src/test/resources: 测试资源目录文件说明pom.xml文件父项目<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –></parent>管理Spring Boot应用里面所依赖的版本管理依赖<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>Spring Boot将所有的功能场景都抽取出来,做成一个个的starters(启动器),只需要在项目里面引入这些starter相关场景的所有依赖都会导入进来,要用什么功能就导入什么场景的启动器主程序类,入口类@SpringBootApplication : Spring Boot应用标注在某个类上说明这个类是SpringBoot的主配置类,SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;我的网站:https://wayne214.github.io

February 16, 2019 · 1 min · jiezi

基于Spring Security Role过滤Jackson JSON输出内容

在本文中,我们将展示如何根据Spring Security中定义的用户角色过滤JSON序列化输出。为什么我们需要过滤?让我们考虑一个简单但常见的用例,我们有一个Web应用程序,为不同角色的用户提供服务。例如,这些角色为User和Admin。首先,让我们定义一个要求,即Admin可以完全访问通过公共REST API公开的对象的内部状态。相反,User用户应该只看到一组预定义的对象属性。我们将使用Spring Security框架来防止对Web应用程序资源的未授权访问。让我们定义一个对象,我们将在API中作为REST响应返回数据:class Item { private int id; private String name; private String ownerName; // getters}当然,我们可以为应用程序中的每个角色定义一个单独的数据传输对象类。但是,这种方法会为我们的代码库引入无用的重复或复杂的类层次结构。另一方面,我们可以使用Jackson库的JSON View功能。正如我们将在下一节中看到的那样,它使得自定义JSON表示就像在字段上添加注释一样简单。@JsonView注释Jackson库支持通过使用@JsonView注解标记我们想要包含在JSON表示中的字段来定义多个序列化/反序列化上下文。此注解具有Class类型的必需参数,用于区分上下文。使用@JsonView在我们的类中标记字段时,我们应该记住,默认情况下,序列化上下文包括未明确标记为视图一部分的所有属性。为了覆盖此行为,我们可以禁用DEFAULT_VIEW_INCLUSION映射器功能。首先,让我们定义一个带有一些内部类的View类,我们将它们用作@JsonView注解的参数:class View { public static class User {} public static class Admin extends User {}}接下来,我们将@JsonView注解添加到我们的类中,使ownerName只能访问admin角色:@JsonView(View.User.class)private int id;@JsonView(View.User.class)private String name;@JsonView(View.Admin.class)private String ownerName;如何将@JsonView注解与Spring Security 集成现在,让我们添加一个包含所有角色及其名称的枚举。之后,让我们介绍JSONView和安全角色之间的映射:enum Role { ROLE_USER, ROLE_ADMIN} class View { public static final Map<Role, Class> MAPPING = new HashMap<>(); static { MAPPING.put(Role.ADMIN, Admin.class); MAPPING.put(Role.USER, User.class); } //…}最后,我们来到了整合的中心点。为了绑定JSONView和Spring Security角色,我们需要定义适用于我们应用程序中所有控制器方法的控制器。到目前为止,我们唯一需要做的就是覆盖AbstractMappingJacksonResponseBodyAdvice类的 beforeBodyWriteInternal方法:@RestControllerAdviceclass SecurityJsonViewControllerAdvice extends AbstractMappingJacksonResponseBodyAdvice { @Override protected void beforeBodyWriteInternal( MappingJacksonValue bodyContainer, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { if (SecurityContextHolder.getContext().getAuthentication() != null && SecurityContextHolder.getContext().getAuthentication().getAuthorities() != null) { Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities(); List<Class> jsonViews = authorities.stream() .map(GrantedAuthority::getAuthority) .map(AppConfig.Role::valueOf) .map(View.MAPPING::get) .collect(Collectors.toList()); if (jsonViews.size() == 1) { bodyContainer.setSerializationView(jsonViews.get(0)); return; } throw new IllegalArgumentException(“Ambiguous @JsonView declaration for roles " + authorities.stream() .map(GrantedAuthority::getAuthority).collect(Collectors.joining(”,"))); } }}这样,我们的应用程序的每个响应都将通过这个路由,它将根据我们定义的角色映射找到合适的返回结果。请注意,此方法要求我们在处理具有多个角色的用户时要小心。 ...

February 15, 2019 · 1 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App详解(二)

Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍。Netty+SpringBoot+FastDFS+Html5实现聊天App,项目github链接。本章完整代码本节主要讲解聊天App PigChat中关于用户信息处理,以及文件服务器FastDFS的相关操作。包含以下内容:(1)注册与登录功能(2)文件服务器的配置(3)上传用户头像(4)设置用户昵称(5)用户二维码的生成与上传注册与登录功能自定义一个工具类IMoocJSONResult,是后端响应前端的数据结构。包含下面三个属性: // 响应业务状态 private Integer status; // 响应消息 private String msg; // 响应中的数据 private Object data;提供错误响应与正常响应的方法: public static IMoocJSONResult ok(Object data) { return new IMoocJSONResult(data); } public static IMoocJSONResult ok() { return new IMoocJSONResult(null); } public static IMoocJSONResult errorMsg(String msg) { return new IMoocJSONResult(500, msg, null); } public static IMoocJSONResult errorMap(Object data) { return new IMoocJSONResult(501, “error”, data); } public static IMoocJSONResult errorTokenMsg(String msg) { return new IMoocJSONResult(502, msg, null); } public static IMoocJSONResult errorException(String msg) { return new IMoocJSONResult(555, msg, null); }根据数据库所建的表创建对应的pojo包与mapper包,数据库建表详情创建UserController方法,写入进行注册于登录处理的registOrLogin接口。【0】前端传入Users对象,首先判断前端传入的Users对象是否为空。【1】然后通过userService的queryUsernameIsExist方法根据传入的用户名在数据库中进行查询。【1.1】若该用户存在则进行登录,通过userService的queryUserForLogin方法判断前端传入的用户名与密码试凑匹配,若匹配则登录成功,否则登录失败。【1.2】若该用户不存在则记性注册,根据前端传入的信息构建Users对象,通过userService的saveUser将其保存入数据库中。【2】最后构造UsersVO对象,返回给前端。注意:密码需要使用MD5工具类进行加密后再保存到数据库中。 /** * @Description: 用户注册/登录 / @PostMapping("/registOrLogin") public IMoocJSONResult registOrLogin(@RequestBody Users user) throws Exception { // 0. 判断用户名和密码不能为空 if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) { return IMoocJSONResult.errorMsg(“用户名或密码不能为空…”); } // 1. 判断用户名是否存在,如果存在就登录,如果不存在则注册 boolean usernameIsExist = userService.queryUsernameIsExist(user.getUsername()); Users userResult = null; if (usernameIsExist) { // 1.1 登录 userResult = userService.queryUserForLogin(user.getUsername(), MD5Utils.getMD5Str(user.getPassword())); if (userResult == null) { return IMoocJSONResult.errorMsg(“用户名或密码不正确…”); } } else { // 1.2 注册 user.setNickname(user.getUsername()); user.setFaceImage(""); user.setFaceImageBig(""); user.setPassword(MD5Utils.getMD5Str(user.getPassword())); userResult = userService.saveUser(user); } // 2.构造UsersVO对象 UsersVO userVO = new UsersVO(); BeanUtils.copyProperties(userResult, userVO); return IMoocJSONResult.ok(userVO); }文件服务器的配置在linux中配置好文件服务器FastDFS后,需要在项目中添加如下配置:(1)在Application同目录下创建FastdfsImporterpackage com.imooc;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableMBeanExport;import org.springframework.context.annotation.Import;import org.springframework.jmx.support.RegistrationPolicy;import com.github.tobato.fastdfs.FdfsClientConfig;/* * 导入FastDFS-Client组件 * * @author tobato * /@Configuration@Import(FdfsClientConfig.class)// 解决jmx重复注册bean的问题@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)public class FastdfsImporter { // 导入依赖组件}(2)在application.properties中添加如下配置:fdfs.soTimeout=1501fdfs.connectTimeout=601fdfs.thumbImage.width=80fdfs.thumbImage.height=80# 192.168.1.70为Linux虚拟机的ip地址fdfs.trackerList[0]=192.168.1.70:22122启动服务命令:/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf/usr/bin/fdfs_storaged /etc/fdfs/storage.conf#查看服务启动情况(23000/22122端口)netstat -lnp |grep fdfscd /usr/local/nginx/sbin./nginx上传用户头像在UserController中添加上传用户头像的uploadFaceBase64接口。【1】前端传入UserBO对象,首先获取前端传来的base64字符串,并通过文件工具类FileUtils的base64ToFile方法将其转换成文件对象保存在本地。【2】将文件对象转换成MultipartFile,并通过fastDFSClient的uploadBase64方法将其上传到文件服务器fastDFS中,打印出服务器返回的路径,我们可以通过这个路径访问这张图片。【3】对返回的路径进行切割后得到缩略图的路径。【4】更新数据库中用户头像信息。 /* * @Description: 上传用户头像 / @PostMapping("/uploadFaceBase64") public IMoocJSONResult uploadFaceBase64(@RequestBody UsersBO userBO) throws Exception { // 1. 获取前端传过来的base64字符串, 然后转换为文件对象再上传 String base64Data = userBO.getFaceData(); // 在本地存储图片的路径 String userFacePath = “C:\” + userBO.getUserId() + “userface64.png”; FileUtils.base64ToFile(userFacePath, base64Data); // 2.上传文件到fastdfs MultipartFile faceFile = FileUtils.fileToMultipart(userFacePath); String url = fastDFSClient.uploadBase64(faceFile); System.out.println(url); // “dhawuidhwaiuh3u89u98432.png”// “dhawuidhwaiuh3u89u98432_80x80.png” // 3.获取缩略图的url String thump = “_80x80.”; String arr[] = url.split("\."); String thumpImgUrl = arr[0] + thump + arr[1]; // 4.更新用户头像 Users user = new Users(); user.setId(userBO.getUserId()); user.setFaceImage(thumpImgUrl); user.setFaceImageBig(url); Users result = userService.updateUserInfo(user); return IMoocJSONResult.ok(result); }设置用户昵称在UserController添加设置用户昵称的setNickname接口。 /* * @Description: 设置用户昵称 */ @PostMapping("/setNickname") public IMoocJSONResult setNickname(@RequestBody UsersBO userBO) throws Exception { Users user = new Users(); user.setId(userBO.getUserId()); user.setNickname(userBO.getNickname()); Users result = userService.updateUserInfo(user); return IMoocJSONResult.ok(result); }用户二维码的生成与上传在UserServiceImpl中引入相关工具类与组件 //二维码工具类 @Autowired private QRCodeUtils qrCodeUtils; //上传文件到fsatDFS需要的组件 @Autowired private FastDFSClient fastDFSClient;在UserServiceImpl保存用户信息的saveUser方法中需要为每一个用户生成一个唯一的二维码。【1】通过二维码工具类qrCodeUtils的createQRCode方法为每个用户生成一个唯一的二维码,第一个参数为生成的二维码存储的路径,第二个参数为二维码中保存的信息,然后将文件转成MultipartFile对象,方便上传操作。【2】通过fastDFSClient的uploadQRCode方法将二维码图片上传到文件服务器中。 @Transactional(propagation = Propagation.REQUIRED) @Override public Users saveUser(Users user) { //生成唯一的id String userId = sid.nextShort(); // 1.为每个用户生成一个唯一的二维码 //本地用来存储生成的二维码图片的路径 String qrCodePath = “C://user” + userId + “qrcode.png”; // 扫描二维码后得到的信息:zhuzhu_qrcode:[username] qrCodeUtils.createQRCode(qrCodePath, “zhuzhu_qrcode:” + user.getUsername()); MultipartFile qrCodeFile = FileUtils.fileToMultipart(qrCodePath); //2.上传文件 String qrCodeUrl = “”; try { qrCodeUrl = fastDFSClient.uploadQRCode(qrCodeFile); } catch (IOException e) { e.printStackTrace(); } user.setQrcode(qrCodeUrl); user.setId(userId); userMapper.insert(user); return user; } ...

February 15, 2019 · 2 min · jiezi

[Spring Security 5.2.0] 8.1.3 Authentication

8.1.3 AuthenticationSpring Security can participate in many different authentication environments. While we recommend people use Spring Security for authentication and not integrate with existing Container Managed Authentication, it is nevertheless supported - as is integrating with your own proprietary authentication system.Spring Security可以参与许多不同的身份验证环境。虽然我们建议人们使用Spring Security进行身份验证,而不是与现有的容器管理身份验证集成,但是它仍然受到支持——就像与您自己的专有身份验证系统集成一样。What is authentication in Spring Security?Let’s consider a standard authentication scenario that everyone is familiar with.1, A user is prompted to log in with a username and password.2, The system (successfully) verifies that the password is correct for the username.3, The context information for that user is obtained (their list of roles and so on).4, A security context is established for the user5, The user proceeds, potentially to perform some operation which is potentially protected by an access control mechanism which checks the required permissions for the operation against the current security context information.让我们考虑一个每个人都熟悉的标准身份验证场景。1, 提示用户使用用户名和密码登录。2, 系统(成功)验证用户名的密码是否正确。3, 获取该用户的上下文信息(角色列表等)。4, 为用户建立一个安全上下文5, 用户继续执行某些操作,这些操作可能受到访问控制机制的保护,该机制根据当前安全上下文信息检查操作所需的权限。The first three items constitute the authentication process so we’ll take a look at how these take place within Spring Security.1, The username and password are obtained and combined into an instance of UsernamePasswordAuthenticationToken (an instance of the Authentication interface, which we saw earlier).2, The token is passed to an instance of AuthenticationManager for validation.3, The AuthenticationManager returns a fully populated Authentication instance on successful authentication.4, The security context is established by calling SecurityContextHolder.getContext().setAuthentication(…), passing in the returned authentication object.前三项构成了身份验证过程,因此我们将了解这些在Spring Security中是如何发生的。1, 用户名和密码被获取并组合到UsernamePasswordAuthenticationToken的实例中(Authenticationinterface的实例,我们在前面看到过)。2, 令牌传递给AuthenticationManager的一个实例进行验证。3, AuthenticationManager在身份验证成功时返回一个完整填充的身份验证实例。4, 安全上下文是通过调用securitycontext.getcontext().setauthentication(…),传入返回的身份验证对象来建立的。From that point on, the user is considered to be authenticated. Let’s look at some code as an example.从那时起,用户被认为是经过身份验证的。让我们以一些代码为例。import org.springframework.security.authentication.;import org.springframework.security.core.;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.context.SecurityContextHolder;public class AuthenticationExample {private static AuthenticationManager am = new SampleAuthenticationManager();public static void main(String[] args) throws Exception { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); while(true) { System.out.println(“Please enter your username:”); String name = in.readLine(); System.out.println(“Please enter your password:”); String password = in.readLine(); try { Authentication request = new UsernamePasswordAuthenticationToken(name, password); Authentication result = am.authenticate(request); SecurityContextHolder.getContext().setAuthentication(result); break; } catch(AuthenticationException e) { System.out.println(“Authentication failed: " + e.getMessage()); } } System.out.println(“Successfully authenticated. Security context contains: " + SecurityContextHolder.getContext().getAuthentication());}}class SampleAuthenticationManager implements AuthenticationManager {static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();static { AUTHORITIES.add(new SimpleGrantedAuthority(“ROLE_USER”));}public Authentication authenticate(Authentication auth) throws AuthenticationException { if (auth.getName().equals(auth.getCredentials())) { return new UsernamePasswordAuthenticationToken(auth.getName(), auth.getCredentials(), AUTHORITIES); } throw new BadCredentialsException(“Bad Credentials”);}}Here we have written a little program that asks the user to enter a username and password and performs the above sequence. The AuthenticationManager which we’ve implemented here will authenticate any user whose username and password are the same. It assigns a single role to every user. The output from the above will be something like:在这里,我们编写了一个小程序,要求用户输入用户名和密码并执行上面的顺序。我们在这里实现的AuthenticationManager将对用户名和密码相同的任何用户进行身份验证。它为每个用户分配一个角色。上面的输出将类似于:Please enter your username:bobPlease enter your password:passwordAuthentication failed: Bad CredentialsPlease enter your username:bobPlease enter your password:bobSuccessfully authenticated. Security context contains: \org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: \Principal: bob; Password: [PROTECTED]; \Authenticated: true; Details: null; \Granted Authorities: ROLE_USERNote that you don’t normally need to write any code like this. The process will normally occur internally, in a web authentication filter for example. We’ve just included the code here to show that the question of what actually constitutes authentication in Spring Security has quite a simple answer. A user is authenticated when the SecurityContextHolder contains a fully populated Authentication object.请注意,通常不需要编写这样的代码。这个过程通常发生在内部,例如在web身份验证过滤器中。我们刚刚在这里包含了一些代码,以说明在Spring Security中真正构成身份验证的问题有一个非常简单的答案。当SecurityContextHolder包含一个完全填充的身份验证对象时,对用户进行身份验证。Setting the SecurityContextHolder Contents DirectlyIn fact, Spring Security doesn’t mind how you put the Authentication object inside the SecurityContextHolder. The only critical requirement is that the SecurityContextHolder contains an Authentication which represents a principal before the AbstractSecurityInterceptor (which we’ll see more about later) needs to authorize a user operation.You can (and many users do) write their own filters or MVC controllers to provide interoperability with authentication systems that are not based on Spring Security. For example, you might be using Container-Managed Authentication which makes the current user available from a ThreadLocal or JNDI location. Or you might work for a company that has a legacy proprietary authentication system, which is a corporate “standard” over which you have little control. In situations like this it’s quite easy to get Spring Security to work, and still provide authorization capabilities. All you need to do is write a filter (or equivalent) that reads the third-party user information from a location, build a Spring Security-specific Authentication object, and put it into the SecurityContextHolder. In this case you also need to think about things which are normally taken care of automatically by the built-in authentication infrastructure. For example, you might need to pre-emptively create an HTTP session to cache the context between requests, before you write the response to the client footnote:[It isn’t possible to create a session once the response has been committed.实际上,Spring Security并不介意您如何将身份验证对象放入SecurityContextHolder中。惟一的关键需求是,SecurityContextHolder包含一个身份验证,在AbstractSecurityInterceptor(稍后将详细介绍)需要授权用户操作之前,该身份验证代表一个主体。您可以(许多用户也可以)编写自己的过滤器或MVC控制器,以提供与不基于Spring安全性的身份验证系统的互操作性。例如,您可能正在使用容器管理的身份验证,这使得当前用户可以从ThreadLocal或JNDI位置访问。或者,您可能为一家拥有遗留专有身份验证系统的公司工作,该系统是一个您几乎无法控制的公司“标准”。在这种情况下,很容易让Spring Security工作,并且仍然提供授权功能。您所需要做的就是编写一个过滤器(或等效的过滤器),从一个位置读取第三方用户信息,构建一个Spring特定于安全的身份验证对象,并将其放入SecurityContextHolder中。在这种情况下,您还需要考虑通常由内置身份验证基础设施自动处理的事情。例如,您可能需要先创建一个HTTP会话,以便在请求之间缓存上下文,然后再编写对客户机脚注的响应:[不可能在提交响应之后创建会话。If you’re wondering how the AuthenticationManager is implemented in a real world example, we’ll look at that in the core services chapter.如果您想知道AuthenticationManager在实际示例中是如何实现的,我们将在核心服务一章中对此进行介绍。 ...

February 14, 2019 · 4 min · jiezi

[Spring Security Reference 5.2.0 翻译] 8

Architecture and ImplementationOnce you are familiar with setting up and running some namespace-configuration based applications, you may wish to develop more of an understanding of how the framework actually works behind the namespace facade. Like most software, Spring Security has certain central interfaces, classes and conceptual abstractions that are commonly used throughout the framework. In this part of the reference guide we will look at some of these and see how they work together to support authentication and access-control within Spring Security.8.1 Technical Overview ...

February 14, 2019 · 1 min · jiezi

Java面试 | 001优点有啥?

本博客 猫叔的博客,转载请申明出处前言本系列为猫叔综合整理的Java面试题系列,如有雷同不胜荣幸。001、请你说说Java这门语言的优点?PS:这是看你对Java的理解程序。1、Java是一门面向对象的编程语言,使用它编写出来的程序易读且更为容易。2、“一次编译,到处运行”,由于Java为解释型语言,编译器转换java代码后再由Java虚拟机解释执行,所以java语言可以很好的跨平台执行,具备可移植性。3、java提供了各种内置库,如多线程Thread、网络通信NIO、垃圾回收器(帮助程序员摆脱内存管理)。4、对Web应用程序支持,可以开发分布式类应用。5、自带了很好的安全机制,如:强类型转换、垃圾回收器、异常处理、安全检查等等。6、java是由C++改进重新设计而来的,且去除了C++中部分复杂的习惯。公众号:Java猫说现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。

February 14, 2019 · 1 min · jiezi

SpringBoot下使用定时任务的方式全揭秘

本文旨在用通俗的语言讲述枯燥的知识定时任务作为一种系统调度工具,在一些需要有定时作业的系统中应用广泛,如每逢某个时间点统计数据、在将来某个时刻执行某些动作…定时任务在主流开发语言均提供相应的API供开发者调用,在Java中,实现定时任务有很多种方式,原生的方式实现一个完整定时任务需要由Timer、TimerTask两个类,Timer是定时器类,用来按计划开启后台线程执行指定任务,TimerTask一个抽象类,它的子类代表一个可以被Timer计划的任务。除此之外,还可以用ScheduledExecutorService类或者使用第三方jar库Quartz,其中Quartz是一个优秀的定时任务框架,发展至今已经非常成熟,以致后来其他的定时任务框架的核心思想或底层大多源于Quartz。springboot作为Java的一种开发框架,在springboot项目中实现定时任务不仅可以使用Java提供的原生方式,还可以使用springboot提供的定时任务API,下面,小编把Java原生和springboot所有的实现定时任务的方式做一个整合。文章提纲:1、使用线程2、使用Timer类3、使用ScheduledExecutorService类4、使用Quartz5、使用spring的@Scheduled注解6、cron表达式1. 线程实现利用线程可以设定休眠时间的方式可以实现简单的定时任务逻辑。 public static void main(String[] args){ //定时任务间隔时间 int sleepTime=21000; new Thread(new Runnable() { @Override public void run() { while (true){ try { System.out.println(“Thread方式执行一次定时任务”); //线程休眠规定时间 Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); }2. Timer类Timer类允许调度一个TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行。 public static void main(String[] args){ int sleepTime=21000; TimerTask timerTask = new TimerTask() { @Override public void run() { System.out.println(“Timer方式执行一次定时任务”); } }; new Timer().schedule(timerTask,1,sleepTime); }3. ScheduledExecutorService类ScheduledExecutorService,是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。因此,基于ScheduledExecutorService类的定时任务类,归根到底也是基于线程的调度实现的。 public static void main(String[] args){ int sleepTime=21000; ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); scheduledExecutor.scheduleAtFixedRate( new Runnable() { @Override public void run() { System.out.println(“ScheduledExecutorService方式执行一次定时任务”); } } ,1,sleepTime, TimeUnit.SECONDS); }4. 整合QuartzQuartz是一个完全由Java编写的开源作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制,要理解它的使用方式,需要先理解它的几个核心概念:Job: 表示一个工作,要执行的具体内容。此接口中只有一个方法,如下:void execute(JobExecutionContext context)JobDetail: 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。Trigger: 代表一个调度参数的配置,什么时候去调。Scheduler: 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。有了这些概念之后,我们就可以把Quartz整合到我们的springboot项目中了。引入quartz依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId></dependency>配置@Configurationpublic class QuartzConfig { @Bean public JobDetail quartzDetail(){ return JobBuilder.newJob(QuartzTest.class).withIdentity(“QuartzTest”).storeDurably().build(); } @Bean public SimpleTrigger quartzTrigger(){ SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(10) .repeatForever(); return TriggerBuilder.newTrigger().forJob(quartzDetail()) .withIdentity(“QuartzTest”) .withSchedule(scheduleBuilder) .build(); }}测试public class QuartzTest extends QuartzJobBean { @Override protected void executeInternal(JobExecutionContext jobExecutionContext){ System.out.println(“quartz执行一次定时任务 “); }}5. 使用Scheduled注解@Scheduled是spring为定时任务而生的一个注解,查看注解的源码:@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Repeatable(Schedules.class)public @interface Scheduled {//cron表达式 String cron() default “”;//接收一个java.util.TimeZone#ID。 String zone() default “”;//上一次执行完毕时间点之后多长时间再执行 long fixedDelay() default -1;//支持占位符形式的字符串类型的fixedDelay String fixedDelayString() default “”;//上一次开始执行时间点之后多长时间再执行 long fixedRate() default -1;//支持占位符形式的字符串类型的fixedRateString String fixedRateString() default “”;//第一次延迟多长时间后再执行 long initialDelay() default -1;//支持占位符形式的字符串类型的initialDelay String initialDelayString() default “”;}可以看出:Scheduled注解中的参数用来设置“定时”动作,通常情况下,比较常用的参数是cron(),这意味着我们需要学会一些cron表达式相关的语法,但由于内容较多,篇幅较长,在这里暂不铺开讲解,我们把cron语法相关放到文章最后,在此先讲解如何用Scheduled注解来实现定时任务。开启定时任务支持@SpringBootApplication/** * 开启定时任务支持 /@EnableSchedulingpublic class TestApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(this.getClass()); }}使用@Componentpublic class ScheduledTest { private Logger logger = LoggerFactory.getLogger(ScheduledTest.class); /* * 每15秒执行一次定时任务 / @Scheduled(cron = “0/15 * * * * ? “) public void testCron(){ logger.info(“Scheduled 执行一次定时任务”); }}6. cron表达式cron表达式是一个字符串其语法为:[秒] [分] [小时] [日] [月] [周] [年]其中[年]为非必填项,因此通常cron表达式通常由6或7部分内容组成,内容的取值为数字或者一些cron表达式约定的特殊字符,这些特殊字符称为“通配符”,每一个通配符分别代指一种值。cron表达式可以用这样的表格来表示:顺序取值范围特殊字符串范围秒0~60, - * /分0-60, - * /时0-23, - * /日1-31, - * /月1-12 / JAN-DEC, - * ? / L W周1-7 / SUN-SAT, - * ? / L #年(可省略)1970-2099, - * /其中通配符的解释以及作用如下:通配符代表的值解释所有值如:时字段为,代表每小时都触发?不指定值如:周字段为?,代表表达式不关心是周几-区间如:时字段设置2-5,代表2,3,4,5点钟时都触发,多个值如:时字段设置2,3,5,代表2,3,5点都会触发/递增值如:时字段设置0/2,代表每两个小时触发,时字段设置 2/5,代表从2时开始每隔5小时触发一次L最后值如:日字段设置L,代表本月最后一天W最近工作日如:在日字段设置13W,代表没约13日最近的那个工作日触发一次#序号如:在周字段设置5#2,代表每月的第二个周五示例:每2秒执行一次:0/5 ?每5分钟执行一次:0 0/5 * ?1分、12分、45分执行一次:0 1,12,45 * ?每天23点59分59秒执行一次:59 59 23 ?每月15号凌晨3点执行一次:0 0 3 15 * ?每月最后一天12点执行一次:0 0 12 L * ?觉得本文对你有帮助?请分享给更多人关注「编程无界」,提升装逼技能 ...

February 14, 2019 · 2 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App详解(一)

Netty学习Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍:https://segmentfault.com/a/11…Netty+SpringBoot+FastDFS+Html5实现聊天App,项目github链接:https://github.com/ShimmerPig…本章练习完整代码链接:https://github.com/ShimmerPig…IO编程与NIO编程传统IO编程性能分析IO编程模型在客户端较少的情况下运行良好,但是对于客户端比较多的业务来说,单机服务端可能需要支撑成千上万的连接,IO模型可能就不太合适了。这是因为在传统的IO模型中,每个连接创建成功之后都需要一个线程来维护,每个线程包含一个while死循环,那么1w个连接对应1w个线程,继而1w个while死循环,这就带来如下几个问题:1.线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起。2.线程切换效率低下:单机cpu核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。3.除了以上两个问题,IO编程中,我们看到数据读写是以字节流为单位,效率不高。为了解决这三个问题,JDK在1.4之后提出了NIO。下面简单描述一下NIO是如何解决以上三个问题的。线程资源受限NIO编程模型中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个固定的线程,然后这条连接所有的读写都由这个线程来负责。这个过程的实现归功于NIO模型中selector的作用,一条连接来了之后,现在不创建一个while死循环去监听是否有数据可读了,而是直接把这条连接注册到selector上,然后,通过检查这个selector,就可以批量监测出有数据可读的连接,进而读取数据。线程切换效率低下由于NIO模型中线程数量大大降低,线程切换效率因此也大幅度提高。IO读写以字节为单位NIO解决这个问题的方式是数据读写不再以字节为单位,而是以字节块为单位。IO模型中,每次都是从操作系统底层一个字节一个字节地读取数据,而NIO维护一个缓冲区,每次可以从这个缓冲区里面读取一块的数据。hello netty完整代码链接:https://github.com/ShimmerPig…首先定义一对线程组——主线程bossGroup与从线程workerGroup。bossGroup——用于接受客户端的连接,但是不做任何处理,跟老板一样,不做事。workerGroup——bossGroup会将任务丢给他,让workerGroup去处理。//主线程EventLoopGroup bossGroup = new NioEventLoopGroup();//从线程EventLoopGroup workerGroup = new NioEventLoopGroup();定义服务端的启动类serverBootstrap,需要设置主从线程,NIO的双向通道,与子处理器(用于处理workerGroup),这里的子处理器后面我们会手动创建。// netty服务器的创建, ServerBootstrap 是一个启动类 ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) // 设置主从线程组 .channel(NioServerSocketChannel.class) // 设置nio的双向通道 .childHandler(new HelloServerInitializer()); // 子处理器,用于处理workerGroup启动服务端,绑定8088端口,同时设置启动的方式为同步的,这样我们的Netty就会一直等待,直到该端口启动完毕。ChannelFuture channelFuture = serverBootstrap.bind(8088).sync();监听关闭的通道channel,设置为同步方式。channelFuture.channel().closeFuture().sync();将两个线程优雅地关闭。bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();创建管道channel的子处理器HelloServerInitializer,用于处理workerGroup。HelloServerInitializer里面只重写了initChannel方法,是一个初始化器,channel注册后,会执行里面相应的初始化方法。在initChannel方法中通过SocketChannel获得对应的管道,通过该管道添加相关助手类handler。HttpServerCodec是由netty自己提供的助手类,可以理解为拦截器,当请求到服务端,我们需要做解码,响应到客户端做编码。添加自定义的助手类customHandler,返回"hello netty~“ChannelPipeline pipeline = channel.pipeline();pipeline.addLast(“HttpServerCodec”, new HttpServerCodec());pipeline.addLast(“customHandler”, new CustomHandler());创建自定义的助手类CustomHandler继承SimpleChannelInboundHandler,返回hello netty重写channelRead0方法,首先通过传入的上下文对象ChannelHandlerContext获取channel,若消息类型为http请求,则构建一个内容为"hello netty“的http响应,通过上下文对象的writeAndFlush方法将响应刷到客户端。if (msg instanceof HttpRequest) { // 显示客户端的远程地址 System.out.println(channel.remoteAddress()); // 定义发送的数据消息 ByteBuf content = Unpooled.copiedBuffer(“Hello netty~”, CharsetUtil.UTF_8); // 构建一个http response FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content); // 为响应增加数据类型和长度 response.headers().set(HttpHeaderNames.CONTENT_TYPE, “text/plain”); response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); // 把响应刷到客户端 ctx.writeAndFlush(response);}访问8088端口,返回"hello netty~“netty聊天小练习完整代码链接:https://github.com/ShimmerPig…服务器定义主从线程与服务端的启动类public class WSServer { public static void main(String[] args) throws Exception { EventLoopGroup mainGroup = new NioEventLoopGroup(); EventLoopGroup subGroup = new NioEventLoopGroup(); try { ServerBootstrap server = new ServerBootstrap(); server.group(mainGroup, subGroup) .channel(NioServerSocketChannel.class) .childHandler(new WSServerInitialzer()); ChannelFuture future = server.bind(8088).sync(); future.channel().closeFuture().sync(); } finally { mainGroup.shutdownGracefully(); subGroup.shutdownGracefully(); } } }创建channel的子处理器WSServerInitialzer加入相关的助手类handlerpublic class WSServerInitialzer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); // websocket 基于http协议,所以要有http编解码器 pipeline.addLast(new HttpServerCodec()); // 对写大数据流的支持 pipeline.addLast(new ChunkedWriteHandler()); // 对httpMessage进行聚合,聚合成FullHttpRequest或FullHttpResponse // 几乎在netty中的编程,都会使用到此hanler pipeline.addLast(new HttpObjectAggregator(1024*64)); // ====================== 以上是用于支持http协议 ====================== // ====================== 以下是支持httpWebsocket ====================== /** * websocket 服务器处理的协议,用于指定给客户端连接访问的路由 : /ws * 本handler会帮你处理一些繁重的复杂的事 * 会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳 * 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同 */ pipeline.addLast(new WebSocketServerProtocolHandler("/ws”)); // 自定义的handler pipeline.addLast(new ChatHandler()); }}创建自定义的助手类ChatHandler,用于处理消息。TextWebSocketFrame:在netty中,是用于为websocket专门处理文本的对象,frame是消息的载体。创建管道组ChannelGroup,用于管理所有客户端的管道channel。private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);重写channelRead0方法,通过传入的TextWebSocketFrame获取客户端传入的内容。通过循环的方法对ChannelGroup中所有的channel进行回复。@Overrideprotected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { // 获取客户端传输过来的消息 String content = msg.text(); System.out.println(“接受到的数据:” + content);// for (Channel channel: clients) {// channel.writeAndFlush(// new TextWebSocketFrame(// “[服务器在]” + LocalDateTime.now() // + “接受到消息, 消息为:” + content));// } // 下面这个方法,和上面的for循环,一致 clients.writeAndFlush( new TextWebSocketFrame( “[服务器在]” + LocalDateTime.now() + “接受到消息, 消息为:” + content));}重写handlerAdded方法,当客户端连接服务端之后(打开连接),获取客户端的channle,并且放到ChannelGroup中去进行管理。@Overridepublic void handlerAdded(ChannelHandlerContext ctx) throws Exception { clients.add(ctx.channel());}重写handlerRemoved方法,当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel。@Overridepublic void handlerRemoved(ChannelHandlerContext ctx) throws Exception { // 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel // clients.remove(ctx.channel()); System.out.println(“客户端断开,channle对应的长id为:” + ctx.channel().id().asLongText()); System.out.println(“客户端断开,channle对应的短id为:” + ctx.channel().id().asShortText());}客户端<!DOCTYPE html><html> <head> <meta charset=“utf-8” /> <title></title> </head> <body> <div>发送消息:</div> <input type=“text” id=“msgContent”/> <input type=“button” value=“点我发送” onclick=“CHAT.chat()”/> <div>接受消息:</div> <div id=“receiveMsg” style=“background-color: gainsboro;"></div> <script type=“application/javascript”> window.CHAT = { socket: null, init: function() { if (window.WebSocket) { CHAT.socket = new WebSocket(“ws://192.168.1.4:8088/ws”); CHAT.socket.onopen = function() { console.log(“连接建立成功…”); }, CHAT.socket.onclose = function() { console.log(“连接关闭…”); }, CHAT.socket.onerror = function() { console.log(“发生错误…”); }, CHAT.socket.onmessage = function(e) { console.log(“接受到消息:” + e.data); var receiveMsg = document.getElementById(“receiveMsg”); var html = receiveMsg.innerHTML; receiveMsg.innerHTML = html + “<br/>” + e.data; } } else { alert(“浏览器不支持websocket协议…”); } }, chat: function() { var msg = document.getElementById(“msgContent”); CHAT.socket.send(msg.value); } }; CHAT.init(); </script> </body></html>测试 ...

February 13, 2019 · 2 min · jiezi

最渣的 Spring Boot 文章

spring-boot-starter-parentMaven的用户可以通过继承spring-boot-starter-parent项目来获得一些合理的默认配置。这个parent提供了以下特性:默认使用Java 8使用UTF-8编码一个引用管理的功能,在dependencies里的部分配置可以不用填写version信息,这些version信息会从spring-boot-dependencies里得到继承。识别过来资源过滤(Sensible resource filtering.)识别插件的配置(Sensible plugin configuration (exec plugin, surefire, Git commit ID, shade).)能够识别application.properties和application.yml类型的文件,同时也能支持profile-specific类型的文件(如: application-foo.properties and application-foo.yml,这个功能可以更好的配置不同生产环境下的配置文件)。maven把默认的占位符${…}改为了@..@(这点大家还是看下原文自己理解下吧,我个人用的也比较少 since the default config files accept Spring style placeholders (${…}) the Maven filtering is changed to use @..@ placeholders (you can override that with a Maven property resource.delimiter).)starter启动器包含一些相应的依赖项, 以及自动配置等.Auto-configurationSpring Boot 支持基于Java的配置, 尽管可以将 SpringApplication 与 xml 一起使用, 但是还是建议使用 @Configuration.可以通过 @Import 注解导入其他配置类, 也可以通过 @ImportResource 注解加载XML配置文件.Spring Boot 自动配置尝试根据您添加的jar依赖项自动配置Spring应用程序. 例如, 如果项目中引入 HSQLDB jar, 并且没有手动配置任何数据库连接的bean, 则Spring Boot会自动配置内存数据库.您需要将 @EnableAutoConfiguration 或 @SpringBootApplication 其中一个注解添加到您的 @Configuration 类中, 从而选择进入自动配置.禁用自动配置import org.springframework.boot.autoconfigure.;import org.springframework.boot.autoconfigure.jdbc.;import org.springframework.context.annotation.*;@Configuration@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})public class MyConfiguration {}如果该类不在classpath中, 你可以使用该注解的excludeName属性, 并指定全限定名来达到相同效果. 最后, 你可以通过 spring.autoconfigure.exclude 属性 exclude 多个自动配置项(一个自动配置项集合)@ComponentScanSpringBoot在写启动类的时候如果不使用 @ComponentScan 指明对象扫描范围, 默认指扫描当前启动类所在的包里的对象.@SpringBootApplication@Target(value=TYPE) @Retention(value=RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters={@ComponentScan.Filter(type=CUSTOM,classes=TypeExcludeFilter.class),})public @interface SpringBootApplication使用 @SpringBootApplication 注解相当于使用了下面三个注解.@EnableAutoConfiguration: 启用 Spring Boot 的自动配置.@ComponentScan: 对应用程序所在的包启用 @Component 扫描.@Configuration: 允许在上下文中注册额外的bean或导入其他配置类.ApplicationRunner or CommandLineRunner 区别应用服务启动时,加载一些数据和执行一些应用的初始化动作。如:删除临时文件,清除缓存信息,读取配置文件信息,数据库连接等。1、SpringBoot提供了CommandLineRunner接口。当有该接口多个实现类时,提供了@order注解实现自定义执行顺序,也可以实现Ordered接口来自定义顺序。注意:数字越小,优先级越高,也就是@Order(1)注解的类会在@Order(2)注解的类之前执行。import com.example.studySpringBoot.service.MyMethorClassService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.CommandLineRunner;import org.springframework.core.annotation.Order;import org.springframework.stereotype.Component;@Component@Order(value=1)public class SpringDataInit implements CommandLineRunner { @Autowired private MyMethorClassService myMethorClassService; @Override public void run(String… strings) throws Exception { int result = myMethorClassService.add(8, 56); System.out.println("———-SpringDataInit1———"+result); }}2、SpringBoot提供的ApplicationRunner接口也可以满足该业务场景。不同点:ApplicationRunner中run方法的参数为ApplicationArguments,而CommandLineRunner接口中run方法的参数为String数组。想要更详细地获取命令行参数,那就使用ApplicationRunner接口import com.example.studySpringBoot.service.MyMethorClassService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.ApplicationArguments;import org.springframework.boot.ApplicationRunner;import org.springframework.core.Ordered;import org.springframework.stereotype.Component;@Componentpublic class SpringDataInit3 implements ApplicationRunner,Ordered { @Autowired private MyMethorClassService myMethorClassService; @Override public void run(ApplicationArguments applicationArguments) throws Exception { int result = myMethorClassService.add(10, 82); System.out.println("———-SpringDataInit3———"+result); } @Override public int getOrder() { return 3; }}外部化配置Spring Boot允许你外部化你的配置,这样你就可以在不同的环境中使用相同的应用程序代码,你可以使用 properties 文件、YAML文件、环境变量和命令行参数来外部化配置,属性值可以通过使用 @Value 注解直接注入到你的bean中,通过Spring的 Environment 抽象访问,或者通过 @ConfigurationProperties 绑定到结构化对象。@ConfigurationProperties(“spring.datasource.username”)Spring Boot使用一种非常特殊的 PropertySource 命令, 该命令旨在允许对值进行合理的覆盖, 属性按以下顺序考虑:Devtools全局设置属性在你的主目录( ~/.spring-boot-devtools.properties 当devtools处于激活状态时。测试中的 @TestPropertySource 注解测试中的 @SpringBootTest#properties 注解属性命令行参数来自 SPRING_APPLICATION_JSON(嵌入在环境变量或系统属性中的内联JSON)的属性ServletConfig 初始化参数ServletContext 初始化参数java:comp/env 中的JNDI属性Java系统属性(System.getProperties())操作系统环境变量一个只有random.*属性的RandomValuePropertySource在你的jar包之外的特殊配置文件的应用程序属性(application-{profile}.properties 和YAML 变体)在jar中打包的特殊配置文件的应用程序属性(application-{profile}.properties 和YAML 变体)在你的jar包之外的应用程序属性(application.properties 和YAML 变体)打包在jar中的应用程序属性(application.properties 和YAML 变体)@PropertySource 注解在你的 @Configuration 类上默认属性(通过设置 SpringApplication.setDefaultProperties 指定)访问命令行属性在默认情况下, SpringApplication 会转换任何命令行选项参数 (也就是说,参数从 – 开始, 像 –server.port=9000) 到一个 property, 并将它们添加到Spring Environment 中, 如前所述, 命令行属性总是优先于其他属性源.如果不希望将命令行属性添加到 Environment 中, 你可以使用 SpringApplication.setAddCommandLineProperties(false) 禁用它们.应用程序属性文件SpringApplication 在以下位置从 application.properties 文件加载属性并将它们添加到Spring Environment :当前目录子目录的 /config当前目录类路径下 /config 包类路径的根目录列表按优先顺序排序(在列表中较高的位置定义的属性覆盖在较低位置定义的属性).特殊配置文件的属性我们可能在不同环境下使用不同的配置, 这些配置有可能是在同一个文件中或不同文件中.1.在相同文件中##################################### Determime which configuration be usedspring: profiles: active: “dev” # Mysql connection configuration(share) datasource: platform: “mysql” driverClassName: “com.mysql.cj.jdbc.Driver” max-active: 50 max-idle: 6 min-idle: 2 initial-size: 6 —##################################### for dev environmentspring: profiles: “dev” datasource: # mysql connection user(dev) username: “root” # mysql connection password(dev) password: “r9DjsniiG;>7”—##################################### for product environmentspring: profiles: “product” datasource: # mysql connection user(product) username: “root” # mysql connection password(product) password: “root”—##################################### for test environmentspring: profiles: “test” datasource: # mysql connection user(test) username: “root” # mysql connection password(test) password: “root"这样在配置完相同属性的时, 还可以对不同的环境进行不同的配置.2.多个配置文件.我们可以把特定环境的配置, 放入多个配置文件中, 但是要按照 application-{profile}.properties 格式. 如下图.spring.profiles.active 属性进行设置.我们也可以把配置文件放在 jar 外面, 使用 spring.config.location 属性进行设置.java -jar beetltest-0.0.1-SNAPSHOT.jar -spring.config.location=classpath:/application.properties ...

February 13, 2019 · 2 min · jiezi

ESMap+HBuilder+SpringBoot+FastDFS实现导航导购App

ESMap-Indoor-navigationESMap+HBuilder+SpringBoot+FastDFS实现导航导购Appgithub链接:https://github.com/ShimmerPig…功能介绍(1)搜索定位(2)扫码定位(3)图像识别店铺商标定位(4)导航(5)导购数据库区域表这里的room表示商店中的一个店铺,或者是超市中的一个区域商品表商品类目表后端结构与接口controller包提供如下接口:(1)searchRoom —— 扫描店铺二维码后传给后端id,后端从数据库中查询对应的店铺,并返回给前端(2)searchProduct —— 扫描商品二维码后传给后端id,后端从数据库中查询对应的商品,并返回给前端(3)productCategoryList —— 取商品类目列表(4)productList —— 传入商品类目,查询该类目的所有商品(5)productInfo —— 通过商品的名称查询商品的所有信息测试效果导航模块地图预览[以华发一角为例]导航功能搜索功能扫码定位功能导购模块地图预览商品搜索功能导购功能App更多功能仍在完善…

February 12, 2019 · 1 min · jiezi

《Spring实战》读书笔记——Spring简介

欢迎大家关注我的微信公众号,一起探讨Java相关技术使用Spring的目的Spring是为了解决企业级应用开发的复杂性而创建的,使用Spring可以让简单的JavaBean实现之前只有EJB才能完成的事情。Spring如何降低Java开发的复杂性最小侵入编程通过面向接口和依赖注入实现松耦合基于编程惯例和切面进行声明式编程通过模板减少样板式代码Spring容器在Spring应用中,不再由对象自行创建或管理它们之间的依赖关系;Spring容器负责创建对象、装配对象、配置它们并管理它们的整个生命周期。容器是Spring框架的核心,是依赖注入和切面的基础,其自带了多个容器实现,这多个实现可以归类为两种不同的类型:Bean工厂,提供基本的依赖注入支持ApplicationContext,基于Bean工厂构建,提供应用框架级别的服务ApplicationContext在实际的应用中更广泛、更受欢迎。应用上下文Spring自带了多种类型的应用上下文,其中最常用的几种为:AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring上下文AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文ClassPathXmlApplicationContext:从类路径下的一个或多个xml配置文件中加载Spring上下文FileSystemXmlApplicationContext:从文件系统下的一个或多个xml配置文件中加载Spring上下文XmlWebApplicationContext:从Web应用下的一个或多个xml配置文件中加载Spring上下文Bean的生命周期Spring实例化BeanSpring将值和Bean的引用注入到Bean对应属性中如果Bean实现了BeanNameAware接口,Spring将Bean的id传给setBeanName()方法如果Bean实现了BeanFactoryAware接口,Spring调用setBeanFactor()方法,将BeanFactory实例传入如果Bean实现了ApplicationContextAware接口,Spring调用setApplicationContext()方法,将Bean所在的应用上下文传入如果Bean实现了BeanPostProcessor接口,Spring调用postProcessBeforeInitialization()方法如果Bean实现了InitializingBean接口或使用了init-method声明了初始化方法,Spring调用afterPropertiesSet()方法如果Bean实现了BeanPostProcessor接口,Spring调用postProcessAfterInitialization()方法Bean初始化完毕,可以进行使用,其将一直在应用上下文中驻留,直到该应用上下文销毁如果Bean实现了DesposableBean接口或使用了destroy-method声明了销毁方法,Spring将调用destroy()方法Spring核心模块Spring的模块依据其所属的功能可以划分为6类。Spring核心容器core:Spring核心context:Spring容器beans:Spring的Bean工厂expression:Spring表达式语言context-support:Spring容器辅助支撑Spring AOPaop:面向切面编程aspectJ:切面注解相关数据访问与集成JDBC:数据库访问相关JMS:消息相关ORM:对象关系映射模型相关OXM:XML映射模型相关transaction:事务相关Messaging:消息服务相关Web与远程调用Web MVC:Spring提供的MVC框架Web Servlet:Servlet相关WebSocket:Socket编程相关Instrumentation使用场景有限,暂时不需要了解TestSpring提供的测试框架总结本文从总的方向上对Spring相关的内容进行了介绍,并不涉及任何细节的东西。主要涉及Spring的优势、Spring容器、Spring应用上下文、Spring生命周期、Spring项目的模块化。在接下来的文章中,将会通过示例的方式展示Spring每一项功能的使用。

February 12, 2019 · 1 min · jiezi

Spring AOP(一) AOP基本概念

Spring框架自诞生之日就拯救我等程序员于水火之中,它有两大法宝,一个是IoC控制反转,另一个便是AOP面向切面编程。今日我们就来破一下它的AOP法宝,以便以后也能自由使出一手AOP大法。 AOP全名Aspect-oriented programming面向切面编程大法,它有很多兄弟,分别是经常见的面向对象编程,朴素的面向过程编程和神秘的函数式编程等。所谓AOP的具体解释,以及和OOP的区别不清楚的同学可以自行去了解。 AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理。本文就主要讲解AOP的基本术语,然后用一个例子让大家彻底搞懂这些名词,最后介绍一下AOP的两种代理方式:以AspectJ为代表的静态代理。以Spring AOP为代表的动态代理。基本术语(1)切面(Aspect) 切面是一个横切关注点的模块化,一个切面能够包含同一个类型的不同增强方法,比如说事务处理和日志处理可以理解为两个切面。切面由切入点和通知组成,它既包含了横切逻辑的定义,也包括了切入点的定义。 Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。@Component@Aspectpublic class LogAspect {} 可以简单地认为, 使用 @Aspect 注解的类就是切面(2) 目标对象(Target) 目标对象指将要被增强的对象,即包含主业务逻辑的类对象。或者说是被一个或者多个切面所通知的对象。(3) 连接点(JoinPoint) 程序执行过程中明确的点,如方法的调用或特定的异常被抛出。连接点由两个信息确定:方法(表示程序执行点,即在哪个目标方法)相对点(表示方位,即目标方法的什么位置,比如调用前,后等) 简单来说,连接点就是被拦截到的程序执行点,因为Spring只支持方法类型的连接点,所以在Spring中连接点就是被拦截到的方法。@Before(“pointcut()")public void log(JoinPoint joinPoint) { //这个JoinPoint参数就是连接点}(4) 切入点(PointCut) 切入点是对连接点进行拦截的条件定义。切入点表达式如何和连接点匹配是AOP的核心,Spring缺省使用AspectJ切入点语法。 一般认为,所有的方法都可以认为是连接点,但是我们并不希望在所有的方法上都添加通知,而切入点的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配连接点,给满足规则的连接点添加通知。@Pointcut(“execution(* com.remcarpediem.test.aop.service..(..))")public void pointcut() {} 上边切入点的匹配规则是com.remcarpediem.test.aop.service包下的所有类的所有函数。(5) 通知(Advice) 通知是指拦截到连接点之后要执行的代码,包括了“around”、“before”和“after”等不同类型的通知。Spring AOP框架以拦截器来实现通知模型,并维护一个以连接点为中心的拦截器链。 // @Before说明这是一个前置通知,log函数中是要前置执行的代码,JoinPoint是连接点,@Before(“pointcut()")public void log(JoinPoint joinPoint) { }(6) 织入(Weaving) 织入是将切面和业务逻辑对象连接起来, 并创建通知代理的过程。织入可以在编译时,类加载时和运行时完成。在编译时进行织入就是静态代理,而在运行时进行织入则是动态代理。(7) 增强器(Adviser) Advisor是切面的另外一种实现,能够将通知以更为复杂的方式织入到目标对象中,是将通知包装为更复杂切面的装配器。Advisor由切入点和Advice组成。 Advisor这个概念来自于Spring对AOP的支撑,在AspectJ中是没有等价的概念的。Advisor就像是一个小的自包含的切面,这个切面只有一个通知。切面自身通过一个Bean表示,并且必须实现一个默认接口。// AbstractPointcutAdvisor是默认接口public class LogAdvisor extends AbstractPointcutAdvisor { private Advice advice; // Advice private Pointcut pointcut; // 切入点 @PostConstruct public void init() { // AnnotationMatchingPointcut是依据修饰类和方法的注解进行拦截的切入点。 this.pointcut = new AnnotationMatchingPointcut((Class) null, Log.class); // 通知 this.advice = new LogMethodInterceptor(); }}深入理解 看完了上面的理论部分知识, 我相信还是会有不少朋友感觉AOP 的概念还是很模糊, 对 AOP 的术语理解的还不是很透彻。现在我们就找一个具体的案例来说明一下。 简单来讲,整个 aspect 可以描述为: 满足 pointcut 规则的 joinpoint 会被添加相应的 advice 操作。我们来看下边这个例子。@Component@Aspect // 切面public class LogAspect { private final static Logger LOGGER = LoggerFactory.getLogger(LogAspect.class.getName()); // 切入点,表达式是指com.remcarpediem.test.aop.service // 包下的所有类的所有方法 @Pointcut(“execution( com.remcarpediem.test.aop.service..*(..))”) public void aspect() {} // 通知,在符合aspect切入点的方法前插入如下代码,并且将连接点作为参数传递 @Before(“aspect()”) public void log(JoinPoint joinPoint) { //连接点作为参数传入 if (LOGGER.isInfoEnabled()) { // 获得类名,方法名,参数和参数名称。 Signature signature = joinPoint.getSignature(); String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); Object[] arguments = joinPoint.getArgs(); MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); String[] argumentNames = methodSignature.getParameterNames(); StringBuilder sb = new StringBuilder(className + “.” + methodName + “(”); for (int i = 0; i< arguments.length; i++) { Object argument = arguments[i]; sb.append(argumentNames[i] + “->”); sb.append(argument != null ? argument.toString() : “null “); } sb.append(”)”); LOGGER.info(sb.toString()); } }} 上边这段代码是一个简单的日志相关的切面,依次定义了切入点和通知,而连接点作为log的参数传入进来,进行一定的操作,比如说获取连接点函数的名称,参数等。静态代理模式 所谓静态代理就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强。ApsectJ是静态代理的实现之一,也是最为流行的。静态代理由于在编译时就生成了代理类,效率相比动态代理要高一些。AspectJ可以单独使用,也可以和Spring结合使用。动态代理模式 与静态代理不同,动态代理就是说AOP框架不会去修改编译时生成的字节码,而是在运行时在内存中生成一个AOP代理对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。 Spring AOP中的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。 JDK代理通过反射来处理被代理的类,并且要求被代理类必须实现一个接口。核心类是 InvocationHandler接口 和 Proxy类。 而当目标类没有实现接口时,Spring AOP框架会使用CGLIB来动态代理目标类。 CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。核心类是 MethodInterceptor 接口和Enhancer 类后记 AOP的基础知识都比较枯燥,本人也不擅长概念性的文章,不过下一篇文章就是AOP源码分析了,希望大家可以继续关注。 ...

February 11, 2019 · 2 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App

Netty+SpringBoot+FastDFS+Html5实现聊天Appgithub链接: https://github.com/ShimmerPig…数据库用户表face_image为用户的头像,需要保存小图与大图,qrcode为该用户对应的二维码朋友关系表好友申请表记录发送好友请求申请的数据表聊天记录表sign_flag表示消息的签收状态,为0表示未签收,为1表示已签收项目结构utils包FileUtils 主要提供了一些与文件相关的操作IMoocJSONResult 是自定义的响应给前端的数据结构JsonUtils 提供了一些关于json转换的操作MD5Utils 提供了对字符串进行md5加密的操作QRCodeUtils 提供了一些关于二维码的相关操作FastDFSClient 提供了各种上传文件以及删除文件的操作enums包MsgActionEnum 发送消息的动作的枚举,类似于消息的类型MsgSignFlagEnum 消息签收状态的枚举OperatorFriendRequestTypeEnum 忽略或者通过好友请求的枚举SearchFriendsStatusEnum 添加好友前置状态的枚举pojo包增加了bo包与vo包,分别存放business object与value object / view object;mapper包增加了一个自定义的UsersMapperCustom,在其中自定义了查询好友请求列表,查询好友列表,以及批量签收聊天消息的方法;service包提供如下方法:queryUsernameIsExist 判断用户名是否存在queryUserForLogin 查询用户是否存在saveUser 用户注册updateUserInfo 修改用户记录preconditionSearchFriends 搜索朋友的前置条件queryUserInfoByUsername 根据用户名查询用户对象sendFriendRequest 添加好友请求记录,保存到数据库queryFriendRequestList 查询好友请求deleteFriendRequest 删除好友请求记录passFriendRequest 通过好友请求queryMyFriends 查询好友列表saveMsg 保存聊天消息到数据库updateMsgSigned 批量签收消息getUnReadMsgList 获取未签收消息列表controller包提供如下接口:registOrLogin 用户注册/登录uploadFaceBase64 上传用户头像setNickname 设置用户昵称searchUser 搜索好友addFriendRequest 发送添加好友的请求queryFriendRequests 发送添加好友的请求operFriendRequest 接受方通过或者忽略朋友请求myFriends 查询我的好友列表getUnReadMsgList 用户手机端获取未签收的消息列表测试效果注册与登录个人信息更换头像添加好友二维码添加好友接受好友请求后开始聊天

February 11, 2019 · 1 min · jiezi

Spring Boot 2.1.2 & Spring Cloud Greenwich 升级记录

节前没有新业务代码,正好Greenwich刚发布,于是开始为期四天的框架代码升级。之前的版本是 spring boot 1.5.10 , spring cloud Edgware.SR3依赖升级增加依赖管理插件 apply plugin: ‘io.spring.dependency-management’spring-cloud-starter-eureka → spring-cloud-starter-netflix-eureka-clientspring-cloud-starter-feign → spring-cloud-starter-openfeigngradle版本要求4.4boot : spring-boot-starter-data-jpadelete → deleteByIdfindone → findById这个改动确实大,返回值变成了Optional,合理是合理的,只改的真多。。boot : spring-boot-starter-data-redisJedis → Lettuce还好并没有使用它的autoconfiguration,配置上有一个小坑,Jedis的redis.timeout是表示connection timeout, 而Lettuce是表示command timeout,之前配置成0的,如果set到Lettuce的commandtimeout里面那就要抛异常了。配置:可以在build.gradle中加入,启动时会检查配置是否兼容compile “org.springframework.boot:spring-boot-properties-migrator” 注意:完成迁移后需要删除警告如上图会告知最新的配置格式boot: spring-boot-starter-actuatorendpoint的暴露方式变化,management.endpoints.web.exposure.include = “*” 表示暴露所有endpoints,如果配置了security那么也需要在security的配置中开放访问/actuator路径boot: spring-boot-starter-security自动注入的AuthenticationManager可能会找不到If you want to expose Spring Security’s AuthenticationManager as a bean, override the authenticationManagerBean method on your WebSecurityConfigurerAdapter and annotate it with @Bean.cloud : eureka各个项目在注册中心里面的客户端实例IP显示不正确,需要修改每个项目的bootstarp.yml${spring.cloud.client.ipAddress} → ${spring.cloud.client.ip-address}boot: spring-boot-starter-test:org.mockito.Matchers → org.mockito.ArgumentMatchers 注意build时的warningMock方法时请使用Mocikto.doReturn(…).when(…),不使用when(…).thenReturn(…),否则@spybean的会调用实际方法其他问题版本升级后会有deprecated的类或方法,所以要注意看console中build的warning信息由于spring cloud依赖管理插件强制cuator升级到4.0.1,导致我们使用的elestic-job不能正常工作,只能强行控制版本。dependencyManagement { imports { mavenBom “org.springframework.cloud:spring-cloud-dependencies:${SPRING_CLOUD_VERSION}” } dependencies { dependency ‘org.apache.curator:curator-framework:2.10.0’ dependency ‘org.apache.curator:curator-recipes:2.10.0’ dependency ‘org.apache.curator:curator-client:2.10.0’ }}如果启用出现error,报bean重复,首先确认是不是故意覆盖,如重写spring-boot自带的bean,如是,可以在bootstrap.yml加入spring.main.allow-bean-definition-overriding=trueFeignClient注解增加了contextId属性@FeignClient(value = “foo”, contextId = “fooFeign”)此contextId即表示bean id,所有注入使用时需要@AutowriedFooFeign fooFeign如果不写contextId,当多个class都是@FeignClient(“foo”),即会认为是同一个bean而排除上一条所说的warning ...

February 2, 2019 · 1 min · jiezi

Mybatis批量更新三种方式

Mybatis实现批量更新操作方式一:<update id=“updateBatch” parameterType=“java.util.List”> <foreach collection=“list” item=“item” index=“index” open="" close="" separator=";"> update tableName <set> name=${item.name}, name2=${item.name2} </set> where id = ${item.id} </foreach> </update>但Mybatis映射文件中的sql语句默认是不支持以" ; " 结尾的,也就是不支持多条sql语句的执行。所以需要在连接mysql的url上加 &allowMultiQueries=true 这个才可以执行。方式二:<update id=“updateBatch” parameterType=“java.util.List”> update tableName <trim prefix=“set” suffixOverrides=","> <trim prefix=“c_name =case” suffix=“end,"> <foreach collection=“list” item=“cus”> <if test=“cus.name!=null”> when id=#{cus.id} then #{cus.name} </if> </foreach> </trim> <trim prefix=“c_age =case” suffix=“end,"> <foreach collection=“list” item=“cus”> <if test=“cus.age!=null”> when id=#{cus.id} then #{cus.age} </if> </foreach> </trim> </trim> <where> <foreach collection=“list” separator=“or” item=“cus”> id = #{cus.id} </foreach> </where></update>这种方式貌似效率不高,但是可以实现,而且不用改动mysql连接效率参考文章:https://blog.csdn.net/xu19166…方式三:临时改表sqlSessionFactory的属性,实现批量提交的java,但无法返回受影响数量。public int updateBatch(List<Object> list){ if(list ==null || list.size() <= 0){ return -1; } SqlSessionFactory sqlSessionFactory = SpringContextUtil.getBean(“sqlSessionFactory”); SqlSession sqlSession = null; try { sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH,false); Mapper mapper = sqlSession.getMapper(Mapper.class); int batchCount = 1000;//提交数量,到达这个数量就提交 for (int index = 0; index < list.size(); index++) { Object obj = list.get(index); mapper.updateInfo(obj); if(index != 0 && index%batchCount == 0){ sqlSession.commit(); } } sqlSession.commit(); return 0; }catch (Exception e){ sqlSession.rollback(); return -2; }finally { if(sqlSession != null){ sqlSession.close(); } } }其中 SpringContextUtil 是自己定义的工具类 用来获取spring加载的bean对象,其中getBean() 获得的是想要得到的sqlSessionFactory。Mapper 是自己的更具业务需求的Mapper接口类,Object是对象。总结方式一 需要修改mysql的连接url,让全局支持多sql执行,不太安全方式二 当数据量大的时候 ,效率明显降低方式三 需要自己控制,自己处理,一些隐藏的问题无法发现。附件:SpringContextUtil.java@Componentpublic class SpringContextUtil implements ApplicationContextAware{ private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextUtil.applicationContext = applicationContext; } public static ApplicationContext getApplicationContext(){ return applicationContext; } public static Object getBean(Class T){ try { return applicationContext.getBean(T); }catch (BeansException e){ return null; } } public static Object getBean(String name){ try { return applicationContext.getBean(name); }catch (BeansException e){ return null; } }} ...

February 1, 2019 · 2 min · jiezi

SpringBoot事物管理

本篇概述在上一篇中,我们基本已经将SpringBoot对数据库的操作,都介绍完了。在这一篇中,我们将介绍一下SpringBoot对事物的管理。我们知道在实际的开发中,保证数据的安全性是非常重要的,不能因为异常,或者服务中断等原因,导致脏数据的产生。所以掌握SpringBoot项目的事物管理,尤为的重要。在SpringBoot中对事物的管理非常的方便。我们只需要添加一个注解就可以了,下面我们来详细介绍一下有关SpringBoot事物的功能。创建Service 因为在上一篇中我们已经用测试用例的方式介绍了SpringBoot中的增删改查功能。所以在这一篇的事物管理,我们还将已测试用例为主。唯一不同之处,就是我们需要创建一个Service服务,然后将相关的业务逻辑封装到Service中,来表示该操作是同一个操作。下面我们简单的在Service中只添加一个方法,并且在方法中新增两条数据,并验证该Service是否成功将数据添加到数据库中。下面为Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /** * 保存用户信息 / public void save() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); }} 测试用例:package com.jilinwula.springboot.helloworld;import com.jilinwula.springboot.helloworld.service.UserInfoService;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class)@SpringBootTestpublic class JilinwulaSpringbootHelloworldApplicationTests { @Autowired private UserInfoService userInfoService; @Test public void save() { userInfoService.save(); } @Test public void contextLoads() { }} 下面我们看一下数据库中的数据是否插入成功。 抛出数据库异常 我们看数据成功插入了。现在我们修改一下代码,让插入数据时,第二条数据的数据类型超出范围来模拟程序运行时发生的异常。然后我们看,这样是否影响第一条数据是否能正确的插入数据库。下面为Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / public void save() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东京东京东京东京东京东京东京东京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); }} 为了方便我们测试,我们已经将数据库中的username字段的长度设置为了10。这样当username内容超过10时,第二条就会抛出异常。下面为执行日志:aused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column ‘username’ at row 1 at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3976) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3914) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2530) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2683) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2495) at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1903) at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2124) at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2058) at com.mysql.jdbc.PreparedStatement.executeLargeUpdate(PreparedStatement.java:5158) at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2043) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.tomcat.jdbc.pool.StatementFacade$StatementProxy.invoke(StatementFacade.java:114) at com.sun.proxy.$Proxy90.executeUpdate(Unknown Source) at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204) … 82 more 然后我们现在查一下数据库中的数据,看看第二条数据的异常是否会影响第一条数据的插入。 添加@Transactional事物注解 我们看第一条数据成功的插入,但这明显是错误的,因为正常逻辑是不应该插入成功的。这样会导致脏数据产生,也没办法保证数据的一致性。下面我们看一下在SpringBoot中怎么通过添加事务的方式,解决上面的问题。上面提到过在SpringBoot中使Service支持事物很简单,只要添加一个注解即可,下面我们添加完注解,然后在尝试上面的方式,看看第一条数据还能否添加成功。然后我们在详细介绍该注解的使用。下面为Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / @Transactional public void save() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东京东京东京东京东京东京东京东京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); }} 代码和之前基本一样,只是在方法上新增了一个@Transactional注解,下面我们继续执行测试用例。执行日志:Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column ‘username’ at row 1 at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3976) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3914) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2530) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2683) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2495) at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1903) at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2124) at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2058) at com.mysql.jdbc.PreparedStatement.executeLargeUpdate(PreparedStatement.java:5158) at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2043) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.tomcat.jdbc.pool.StatementFacade$StatementProxy.invoke(StatementFacade.java:114) at com.sun.proxy.$Proxy90.executeUpdate(Unknown Source) at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204) … 92 more 日志还是和之前一样抛出异常,现在我们在查一下数据库中的数据。 发现数据库中已经没有第一条数据的内容了,这就说明了我们的事物添加成功了,在SpringBoot项目中添加事物就是这么简单。手动抛出异常 下面我们来测试一下,手动抛出异常,看看如果不添加@Transactional注解,数据是否能成功插入到数据库中。Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / public void save() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); System.out.println(1 / 0); }} 我们在代码最后写了一个除以0操作,所以执行时一定会发生异常,然后我们看数据能否添加成功。执行日志:java.lang.ArithmeticException: / by zero at com.jilinwula.springboot.helloworld.service.UserInfoService.save(UserInfoService.java:32) at com.jilinwula.springboot.helloworld.JilinwulaSpringbootHelloworldApplicationTests.save(JilinwulaSpringbootHelloworldApplicationTests.java:19) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) 继续查看数据库中的数据。 我们发现这两条数据都插入成功了。我们同样,在方法中添加@Transactional注解,然后继续执行上面的代码在执行一下。Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / @Transactional public void save() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); System.out.println(1 / 0); }} 执行日志:java.lang.ArithmeticException: / by zero at com.jilinwula.springboot.helloworld.service.UserInfoService.save(UserInfoService.java:33) at com.jilinwula.springboot.helloworld.service.UserInfoService$$FastClassBySpringCGLIB$$230fe90e.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:736) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:671) at com.jilinwula.springboot.helloworld.service.UserInfoService$$EnhancerBySpringCGLIB$$33f70012.save(<generated>) at com.jilinwula.springboot.helloworld.JilinwulaSpringbootHelloworldApplicationTests.save(JilinwulaSpringbootHelloworldApplicationTests.java:19) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) 数据库中数据: 我们看数据又没有插入成功,这样就保证了我们事物的一致性。添加try catch 下面我们将上述的代码添加try catch,然后在执行上面的测试用例,查一下结果。Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Slf4j@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / @Transactional public void save() { try { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); System.out.println(1 / 0); } catch (Exception e) { log.info(“保存用户信息异常”, e); } }} 执行日志:2019-01-25 11:21:45.421 INFO 8654 — [ main] c.j.s.h.service.UserInfoService : 保存用户信息异常java.lang.ArithmeticException: / by zero at com.jilinwula.springboot.helloworld.service.UserInfoService.save(UserInfoService.java:36) ~[classes/:na] at com.jilinwula.springboot.helloworld.service.UserInfoService$$FastClassBySpringCGLIB$$230fe90e.invoke(<generated>) [classes/:na] at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) [spring-core-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:736) [spring-aop-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) [spring-aop-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) [spring-tx-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282) [spring-tx-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) [spring-tx-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) [spring-aop-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:671) [spring-aop-4.3.21.RELEASE.jar:4.3.21.RELEASE] at com.jilinwula.springboot.helloworld.service.UserInfoService$$EnhancerBySpringCGLIB$$5284ede6.save(<generated>) [classes/:na] at com.jilinwula.springboot.helloworld.JilinwulaSpringbootHelloworldApplicationTests.save(JilinwulaSpringbootHelloworldApplicationTests.java:19) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_191] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_191] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_191] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_191] at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) [junit-4.12.jar:4.12] at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) [junit-4.12.jar:4.12] at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) [junit-4.12.jar:4.12] at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) [junit-4.12.jar:4.12] at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) [junit-4.12.jar:4.12] at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) [junit-4.12.jar:4.12] at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) [junit-4.12.jar:4.12] at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) [junit-4.12.jar:4.12] at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) [junit-4.12.jar:4.12] at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) [junit-4.12.jar:4.12] at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.junit.runners.ParentRunner.run(ParentRunner.java:363) [junit-4.12.jar:4.12] at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) [spring-test-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.junit.runner.JUnitCore.run(JUnitCore.java:137) [junit-4.12.jar:4.12] at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) [junit-rt.jar:na] at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) [junit-rt.jar:na] at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) [junit-rt.jar:na] at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) [junit-rt.jar:na] 查看数据库中的数据: 我们发现数据成功的插入了,虽然我们添加了@Transactional事物注解,但数据还是添加成功了。这是因为@Transactional注解的处理方式是,检测Service是否发生异常,如果发生异常,则将之前对数据库的操作回滚。上述代码中,我们对异常try catch了,也就是@Transactional注解检测不到异常了,所以该事物也就不会回滚了,所以在Service中添加try catch时要注意,以免事物失效。下面我们手动抛出异常,来验证上面的说法是否正确,也就是看看数据还能否回滚。Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Slf4j@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / @Transactional public void save() throws Exception { try { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); System.out.println(1 / 0); } catch (Exception e) { log.info(“保存用户信息异常”, e); } throw new Exception(); }} 执行日志:java.lang.Exception at com.jilinwula.springboot.helloworld.service.UserInfoService.save(UserInfoService.java:40) at com.jilinwula.springboot.helloworld.service.UserInfoService$$FastClassBySpringCGLIB$$230fe90e.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:736) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:671) at com.jilinwula.springboot.helloworld.service.UserInfoService$$EnhancerBySpringCGLIB$$43d47421.save(<generated>) at com.jilinwula.springboot.helloworld.JilinwulaSpringbootHelloworldApplicationTests.save(JilinwulaSpringbootHelloworldApplicationTests.java:20) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) 查看数据库结果: @Transactional注解的底层实现 我们发现,数据库中居然成功的插入值了,这是为什么呢?上面不是说,在抛出异常时,@Transactional注解是自动检测,是否抛出异常吗?如果抛出了异常就回滚之前对数据库的操作,那为什么我们抛出了异常,而数据没有回滚呢?这是因为@Transactional注解的确会检测是否抛出异常,但并不是检测所有的异常类型,而是指定的异常类型。这里说的指定的异常类型是指RuntimeException类及其它的子类。因为RuntimeException类继承了Exception类,导致Exception类成为了RuntimeException类的父类,所以@Transactional注解并不会检测抛出的异常,所以,上述代码中虽然抛出了异常,但是数据并没有回滚。下面我们继续修改一下Service中的代码,将代码中的异常类修改为RuntimeException,然后在看一下运行结果。下面为Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Slf4j@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / @Transactional public void save() throws RuntimeException { try { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); System.out.println(1 / 0); } catch (Exception e) { log.info(“保存用户信息异常”, e); } throw new RuntimeException(); }} 我们就不看执行的日志了,而是直接查数据库中的结果。 我们看数据没有插入到数据库中,这就说明了,事物添加成功了,数据已经成功的回滚了。在实际的开发中,我们常常需要自定义异常类,来满足我们开发的需求。这时要特别注意,自定义的异常类,一定要继承RuntimeException类,而不能继承Exception类。因为刚刚我们已经验证了,只有继承RuntimeException类,当发生异常时,事物才会回滚。继承Exception类,是不会回滚的。这一点要特别注意。@Transactional注解参数说明 下面我们介绍一下@Transactional注解的参数。因为刚刚我们只是添加了一个@Transactional注解,实际上在@Transactional注解中还包括很多个参数,下面我们详细介绍一下这些参数的作用。 @Transactional注解参数说明:参数作用value指定使用的事务管理器propagation可选的事务传播行为设置isolation可选的事务隔离级别设置readOnly读写或只读事务,默认读写timeout事务超时时间设置rollbackFor导致事务回滚的异常类数组rollbackForClassName导致事务回滚的异常类名字数组noRollbackFor不会导致事务回滚的异常类数组noRollbackForClassName不会导致事务回滚的异常类名字数组 下面我们只介绍一下部分参数,因为大部分参数实际上是和Spring中的注解一样的,有关Spring事物相关的内容,我们将在后续的文章中在做介绍,我们暂时介绍一下rollbackFor参数和noRollbackFor参数。(备注:rollbackForClassName和noRollbackForClassName与rollbackFor和noRollbackFor作用一致,唯一的区别就是前者指定的是异常的类名,后者指定的是类的Class名)。rollbackFor: 指定事物回滚的异常类。因为在上面的测试中我们知道@Transactional事物类只会回滚RuntimeException类及其子类的异常,那么实际的开发中,如果我们就想让抛出Exception异常的类回滚,那应该怎么办呢?这时很简单,只要在@Transactional注解中指定rollbackFor参数即可。该参数指定的是异常类的Class名。下面我们还是修改一下Servcie代码,抛出Exception异常,但我们指定rollbackFor为Exception.class,然后在看一下数据是否能回滚成功。下面为Service源码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Slf4j@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 / @Transactional(rollbackFor = Exception.class) public void save() throws Exception { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); throw new Exception(); }} 按照之前我们的测试结果我们知道,@Transactional注解是不会回滚Exception异常类的,那么现在我们指定了rollbackFor参数,那么结果如何呢?我们看一下数据库中的结果。 我们看数据库中没有任何数据,也就证明了事物添加成功了,数据已经的回滚了。这也就是@Transactional注解中rollbackFor参数的作用,可以指定想要回滚的异常。rollbackForClassName参数和rollbackFor的作用一样,只不过该参数指定的是类的名字,而不是class名。在实际的开发中推荐使用rollbackFor参数,而不是rollbackForClassName参数。因为rollbackFor的参数是类型是Class类型,如果写错了,可以在编译期发现。而rollbackForClassName参数类型是字符串类型,如果写错了,在编译期间是发现不了的。所以推荐使用rollbackFor参数。noRollbackFor: 指定不回滚的异常类。看名字我们就知道该参数是和rollbackFor参数对应的。所以我们就不做过多介绍了,我们直接验证该参数的作用。我们知道@Transactional注解会回滚RuntimeException类及其子类的异常。如果我们将noRollbackFor参数指定RuntimeException类。那么此时事物应该就不会回滚了。下面我们验证一下。下面为Service代码:package com.jilinwula.springboot.helloworld.service;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Slf4j@Servicepublic class UserInfoService { @Autowired private UserInfoRepository userInfoRepository; /* * 保存用户信息 */ @Transactional(noRollbackFor = RuntimeException.class) public void save() throws Exception { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“小米”); userInfoEntity.setPassword(“xiaomi”); userInfoEntity.setNickname(“小米”); userInfoEntity.setRoleId(0L); userInfoRepository.save(userInfoEntity); UserInfoEntity userInfoEntity2 = new UserInfoEntity(); userInfoEntity2.setUsername(“京东”); userInfoEntity2.setPassword(“jingdong”); userInfoEntity2.setNickname(“京东”); userInfoEntity2.setRoleId(0L); userInfoRepository.save(userInfoEntity2); throw new RuntimeException(); }} 我们查看一下数据库中是否成功的插入了数据。 我们看数据库中成功的插入数据了,也就证明了@Transactional注解的noRollbackFor参数成功了,因为正常来说,数据是会回滚的,因为我们抛出的是RuntimeException异常。数据没有回滚也就说明了,参数成功。noRollbackForClassName参数和noRollbackFor参数一样,只是一个指定的是class类型,一个指定的是字符串类型。所以,为了在编译期间发现问题,还是推荐使用noRollbackFor参数。 上述内容就是SpringBoot中的事物管理,如有不正确的欢迎留言,谢谢。项目源码 https://github.com/jilinwula/…原文链接 http://jilinwula.com/article/… ...

January 31, 2019 · 5 min · jiezi

Spring Boot入门篇

前篇 很长时间不写博客了,究其原因则是这几个月工作及生活都发生了很多事情,导致不得分心处理这些。最近难得忙里偷闲,决定还是继续更新吧。毕竟一件事情做久了,如果突然中断,心中难免有些遗憾。由于博客之前更新的内容均是Redis相关的,本打算继续把后续的Redis内容更新出来,但无奈因为这段时间的中断,发现Redis的思路已经断了,所以决定还是很把Redis放一放吧,沉淀一段时间之后,在将后续的内容补充上。虽然这段时间没有更新博客,但在技术角度来说,还是有所收获的,因为公司最近一直在使用Spring Boot,虽然Spring Boot很火,但自己一直没有真正在项目中使用过,正好有这样的机会,并且在使用的过程中遇到了各种各样的问题,我想那就把工作中遇到的种种问题,更新出来吧。所以,接下来,本人将不定期的更新Spring Boot相关的内容。由于Spring Boot实在是太火了,网上有很多相关的资料及书籍。所以,本博客的更新重点,将以实用为主,相关的理论方面的内容,请参考,官方文档,及相关书籍。创建SpringBoot好了,言归正传,我们来学习Spring Boot的第一篇文章,也就是入门篇。我们首先创建一个Spring Boot项目。具体操作如下图所示: 创建Spring Boot的项目和创建Spring的项目不同,在上图中我们不能选择Maven创建项目,而是使用IDEA中Spring Initializr创建Spring Boot项目。因为它会为我们直接生成Spring Boot的项目架构。在Spring Initializr选项中我们看到默认使用了https://start.spring.io>这个…这个域名地址,来生成我们的项目架构。下图就是我们直接访问上述域名来生成项目架构。 因为上图中的配置和IDEA中的Spring Initializr配置基本一样,所以上图中的创建方式,就不做详细介绍了,我们继续介绍Spring Initializr方式的配置。 上图中的选项比较多,下面我们详细介绍一下:Group:同Maven中的Group一样,也就是项目唯一标识Artifact:同Maven中的Artifact一样,通常为项目名Type:项目的Maven类型,我们默认选择就可以Language:项目的开发语言,那结果当然选择Java喽Packaging:打包类型jar或者war,因为SpringBoot可以支持这两种方式启动,所以,这两种选择哪个都可以Java Version:Java的版本号,推荐使用1.8版本Version:项目的版本号Name:项目名,推荐和Artifact一致Description:项目描述Package:项目包的名字 这一步我们选择SpringBoot的版本,及项目的依赖包,这里要注意因为SpringBoot2.0版本和1.0版本相差甚大,所以,暂时推荐使用1.0版本。除此之外,因为创建的是web项目,所以,我还要要添加和web相关的依赖,在这点和Maven创建Spring项目不同,我们只需要选择,一个web的依赖就可以了,SpringBoot会自动把这个web相关的依赖都下载好,这也就是SrpingBoot的优势之一,比较方便。当然如果我们开发一下完整的项目,还是需要很多其它的项目依赖的,这里我们不用着急,暂时只添加web这个就可以,如果需要其它的依赖,我们还是可以修改的。好的我们继续下面操作: 这一步我们只要选择完成则可以了。这样我们的SpingBoot项目就创建好了,下图就是项目架构图: 当项目第一次创建后,右下方,会有上图中的两个提示选项,我们只要选择第二个就可以,这样,当我们修改项目中pom.xml文件添加依赖时,IDEA会自动添加我们的依赖。 启动SpringBoot 上图就是SrpingBoot生成的项目结构图,默认会创建两个类,一个是启动类,一个是测试类。和Spring项目不同,我们不需要配置Tomcat来启动SrpingBoot项目,我们直接使用启动类,即可启动SrpingBoot项目。下面我们尝试启动一下,因为启动类就是一个main方法,所以我们只要直接执行就可以了。因为SrpingBoot项目的默认端口为8080,所以我们启动后可以直接访问8080端口,来验证SrpingBoot是否启动成功。 上图就是我们访问8080端口后的结果。虽然返回的结果报错,但这恰恰说明了我们的项目启动成功了,否则就会报404错误。那为什么会报上面的错误呢?这是因为我们没有写controller,下面我们写一个简单的controller来看一下上面的问题还有没有。下面为controller代码。package com.jilinwula.springboot.helloworld;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/jilinwula")public class JilinwulaController { @RequestMapping("/helloworld") public Object helloWorld() { return “吉林乌拉”; }}启动方式 下面我们访问http://localhost:8080/jilinwu…地址。下图为访问该地址的返回结果。 下面我们看一下SpringBoot的启动方式,上面说过,我们可以不用Tomcat直接启动SpringBoot项目,也就是直接启动main方法,当然我们一样可以使用Tomcat的方式启动SpringBoot项目,我们可以直接将SpringBoot项目项目打包成war放到Tomcat中就可以了。具体操作如下:在SpringBoot项目中的pom.xml中添加如下配置: <packaging>war</packaging> 然后执行以下打包命令: mvn clean install这样在我们的项目中就会生成一个target包里面就会项目的中war包,只要把这个war包放到Tomcat中即可。第二种方式就是直接在项目中使用java -jar 项目名.jar方式启动项目。 还有一种方式就是直接在项目中mvn spring-boot:run命令,也可以正常启动SpringBoot项目。 以上内容就是SpringBoot的入门篇,在下一篇中我们将分享,在SpringBoot中的个性化默认配置。项目源码https://github.com/jilinwula/jilinwula-springboot-helloworld原文链接http://jilinwula.com/article/24336

January 31, 2019 · 1 min · jiezi

SpringBoot个性化配置

在上一篇中我们简单的介绍了SpringBoot项目的创建及其启动方式。在这一篇中我们主要介绍一下SpringBoot项目的个性化配置。因为通过上一篇中知识我们知道SpringBoot项目的默认端口为8080,那如果我要修改这个默认端口,应该怎么改呢?又比如SpringBoot项目在启动时,默认是没有项目名字的,那如果我们想要添加自己喜欢的项目名字又该怎么办呢?这些都在这一篇的内容中。好了,下面我们详细介绍一下怎么修改SpringBoot项目中的默认配置。修改默认端口 在上一篇的SpringBoot项目中我们看到在resources目录中有一个application.properties文件,这个文件就是让我们个性化配置SpringBoot项目参数的,也就是说,在这个文件中按照SpringBoot为我们提供的参数名,就可以直接修改SpringBoot项目的默认参数。下面我们尝试修改SpringBoot项目的默认端口。具体修改如下: 在application.properties文件中添加下面的参数,然后,启动application.properties文件项目即可。server.port=8081 并且如果我们使用IDEA开发工具时,当我们在在application.properties文件中输入参数时,IDEA就会自动为我们提供相关参数提示,这样方便我们修改。也就是如下图所示: 这时我们启动SpringBoot项目并且用8080端口访问项目时,发现已经找不到服务了。 而如果我们用访问8081端口访问项目,则发现服务可以正常访问。这就说明,我们已经成功将SpringBoot项目的默认端口修改为8081端口了。 虽然上面的方式已经成功的修改了SpringBoot项目的默认参数,但在实际的开发中,并不推荐使用application.properties文件的方式修改,因为在SpringBoot项目中有更推荐的方式。也就是使用yml文件的方式。application.yml文件 使用yml文件的方式修改默认参数,也比较简单,也就是把application.properties文件文件修改为application.yml文件即可。唯一不同的方式,就是yml文件有自己独特的语法,和properties文件不同,可以省略很多参数,并且浏览比较直观。下面我们尝试用yml文件的方式,将SpringBoot的端口修改为8082端口。 启动项目后访问刚刚的8081端口,发现项目已经访问不了。 这时我们访问8082端口,发现项目访问又正常了,这就说明我们使用yml的方式修改SpringBoot的默认参数方式成功了。 如果我们访问http://localhost:8082/jilinwu…地址,即可看到SpringBoot接口返回的数据。 修改默认项目名 下面我们还将使用yml的方式配置SpringBoot项目的项目名。具体参数如下:server: port: 8082 context-path: /springboot 我们继续启动项目然后依然访问http://localhost:8082/jilinwu…地址,这时发现接口访问失败。 然后我们访问http://localhost:8082/springb…地址,发现服务又可正常访问了。 获取配置文件中参数 在实际的项目开发中,我们通常会遇到,读取配置文件中的参数,那么在SpringBoot中怎么获取配置文件中的参数呢?下面我们在配置文件中添加如下参数。server: port: 8082 context-path: /springbootemail: username: jilinwula password: 123456 下面我们在Controller中采用如下的方式读取配置文件中的参数。@RestController@RequestMapping("/jilinwula")public class JilinwulaController { @Value("${email.username}") private String username; @Value("${email.password}") private String password; @RequestMapping("/helloworld") public Object helloWorld() { Map<String, Object> map = new HashMap<String, Object>(); map.put(“username”, username); map.put(“password”, password); return map; }} 我们可以直接使用@Value注解来获取配置文件中的参数,并且这个注解不只是在SpringBoot中可以使用,这个注解在Spring的项目中也可以使用。下面我们启动项目,并访问http://localhost:8082/springb…地址,看看是不是可以成功的获取配置文件中的参数。 我们看上图所示,我们成功的获取到了配置文件中的参数。但如果有强迫证的人,对于上面的代码难免有些不满意。因为如果我们要获取配置文件中非常多的参数时,要是按照上面的代码编写,则需要在代码中编写大量的@Value注解,这显然是不合理的。那有没有比较方便的办法呢?答案一定是有的,并且SpringBoot为我们提供了非常方便的方法获取配置文件中的参数。下面我们看一下这种方式。 我们首先要在项目的pom.xml中添加以下依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> <scope>provided</scope> </dependency> 第一个依赖是自动获取配置文件参数的必须依赖,而下面的依赖,则是可以用注解的方式动态生成get和set方法,这样我们在开发时,就不用在写get和set方法了,在实际的项目中比较常用。在使用lombok生成get和set方法时,还要在IDEA中添加相应的lombok插件,否则IDEA会提示到不到get和set方法的警告。 其次我们新创建一下获取配置参数的类,并且添加@ConfigurationProperties注解,该注解会自动将配置文件中的参数注入到类中的属性中(不需要写@Value注解)。并且可以指定prefix参数来指定要获取配置文件中的前缀。具体代码如下:package com.jilinwula.springboot.helloworld;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;@Component@ConfigurationProperties(prefix = “email”)@Datapublic class EmailProperties { private String username; private String password;} 上面中的@Data,注解就是动态生成get和set方法的所以上述的代码是不需要写get和set方法的。下面我们看一下Controller中的代码修改:package com.jilinwula.springboot.helloworld;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;import java.util.Map;@RestController@RequestMapping("/jilinwula")public class JilinwulaController { @Autowired private EmailProperties emailProperties; @RequestMapping("/helloworld") public Object helloWorld() { Map<String, Object> map = new HashMap<String, Object>(); map.put(“username”, emailProperties.getUsername()); map.put(“password”, emailProperties.getPassword()); return map; }} 下面我们启动项目并访问接口看看是否能够成功获取配置文件中的参数。 ) 下面我们介绍一下在SpringBoot中怎么处理不同环境中获取不同的配置参数。下面我们模拟两人环境一个是开发环境,一个是测试环境,我们暂时以不同端口来区分这两个环境的区别。 application-dev.yml:server: port: 8081 context-path: /springbootemail: username: jilinwula password: 123456 application-test.yml:server: port: 8082 context-path: /springbootemail: username: jilinwula password: 654321 application.yml:spring: profiles: active: dev 这样当我们在application.yml文件中的参数设置为dev时,SpringBoot项目在启动时就会读取application-dev.yml中的参数。如果我们将参数设置为test时,则SpringBoot会读取application-test.yml文件中的参数。 下面我们分别启动项目并且访问接口:当参数为dev: 当参数为test: 启动时指定参数 在上一篇中我们已经介绍过了我们可以使用java -jar 项目的名字的方式启动SpringBoot项目。并且,该方式还支持指定SpringBoot参数,例如上面刚刚介绍的指定获取同环境的配置参数。具体命里如下:java -jar jilinwula-springboot-helloworld-0.0.1-SNAPSHOT.jar –spring.profiles.active=dev 我们此时继续访问接口发现还是成功的获取了dev环境中的参数。 上述内容就是SpringBoot个性化配置的内容,如有不正确,或者需要交流的,欢迎留言,谢谢。项目源码:https://github.com/jilinwula/…原文链接:http://jilinwula.com/article/… ...

January 31, 2019 · 1 min · jiezi

SpringBoot数据库操作

本篇概述上一篇中我们已经介绍了在SpringBoot项目中怎么修改默认配置参数,并且我们还掌握了怎么获取配置文件中自定义参数。在这一篇中我们将介绍SpringBoot对数据库的操作。既然是对数据库的操作,那难免有一些配置的参数。例如数据库的连接、数据库账号及数据库密码等。所以掌握上篇中的内容很重要。除此之外,我们还要介绍一下用什么样的技术来操作数据库。操作数据库的技术有很多例如比较常见的JDBC、Mybatis、Hibernate等。在SpringBoot的项目中为我们提供了另外一种操作数据库的技术,也就是JPA。我们可以通过JAP中提供的方式来非常方便的操作数据库。下面我们首先添加SpringBoot对数据库的配置参数。具体参数如下:数据库配置spring: profiles: active: dev datasource: url: jdbc:mysql://localhost:3306/springboot?useSSL=false&characterEncoding=utf8 username: root password: jilinwula driver-class-name: com.mysql.jdbc.Driver jpa: hibernate: ddl-auto: create show-sql: truespring.jpa.hibernate.ddl-auto参数详解 上面的配置参数比较简单,我们就不详细介绍了,我们只介绍spring.jpa.hibernate.ddl-auto参数。该参数的作用是自动操作表结构。且该参数有4种选项,下面我们详细介绍一下这4种的区别。create: 当我们启动SpringBoot项目时,会自动为我们创建与实体类对应的表,不管表中是否有数据。也就是如果指定参数为create时,当项目启动后,该表的数据一定为空。因为该参数的处理方式,是先将表删除后,然后在创建新的表。create-drop: 当我们启动项目时也会我们自动创建表,但当我们的项目运行停止后,它会自动为我们删除表,并且该参数为create一样在启动时也会先把表删除后,然后在创建。update: 每当我们启动项目时,如果表不存在,则会根据实体类自动帮我们创建一张表。如果表存在,则会根据实体类的变化来决定是不是需要更新表,并且不管更不更新表都不会清空原数据。validate: 当我们启动项目时会验证实体类中的属性与数据库中的字段是否匹配,如不匹配则报错。添加相关依赖 如果我们按照上面的方式配置完后,则会发现上面的driver-class-name参数会报红显示,原因是没有找到相关的依赖。并且在SpringBoot的项目中如果想用JPA功能的除了要引入Mysql的依赖外,还要引入Jpa的依赖。具体依赖如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope></dependency> 当我们添加完上面的依赖后发现配置文件中的driver-class-name参数已经不报红显示了,这就表示我们的依赖引入成功了。spring.jpa.hibernate.ddl-auto参数验证 下面我们创建一个实体类,来验证一下,上面的spring.jpa.hibernate.ddl-auto参数是不是上面说的那样。我们首选验证当参数为create时。下面为实体类的代码:package com.jilinwula.springboot.helloworld.entity;import lombok.Data;import javax.persistence.;@Data@Entity@Table(name = “user_info”)public class UserInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password;} 上面实体类中我们指定了几个注解,下面我们详细介绍一下它们的作用:实体类详解@Data: 自动生成GET和SET方法的注解,在上一篇中我们已经介绍过了,该注解可以在类上和属性中添加。如果添加在类上,是表示自动为该实体类的所有属性创建GET和SET方法。如果添加到属性中则表示只会为该属性添加GET和SET方法。这样我们就没必要为代码中生成大量的GET和SET烦恼了。这时可能有人会想到那如果我只想让它帮我生成GET方法或者只想生成SET方法时应该怎么办呢?别着急,既然你想到了,那么开发这个注解的人也想到了,我们只需要将上面的@Data注解修改为相应的@Getter或者@Setter注解即可。它们正好对应的生成GET和SET方法。@Entity: 实体类注解。只有标识该注解的类,JPA才能自动将这个类中的属性和数据库进行映射。@Table: 标识该实体类和数据库中哪个表进行映射。@Id: 标识该自动为主键标识。@GeneratedValue: 标识主键的生成规则。这个在后面的文章中在做详细介绍。 现在我们一切准备就绪了,我们只要启动一下SpringBoot的项目就可以了,看看会不会自动为我们创建一张userinfo的表。(备注:数据库需要我们自己创建)。我们首先确认一下数据库中确实没有userinfo这张表。 下面我们启动一下SpringBoot的项目,看一下数据库中有没有创建新的表。 我们看JPA确实为我们创建了一张和实体类中@Table注解指定的表,并且表中的字段和实体类中的属性一致。下面我们手动向表中添加一条数据,然后重新启动项目,看看项目启动后,这条新增的数据还是否存在。 我们只新增了一条数据,然后重启启动项目后,在看一下数据中的userinfo表,看看该数据还有没有。 我们发现刚刚添加的那条数据已经没有了,这也就恰恰证明了,我们上面所有说当spring.jpa.hibernate.ddl-auto参数为create时,每当项目启动时,都会将原先的表删除,然后在通过实体类重新生成新的表,既然已经是将原先的表都删除了,那曾经添加的数据当然不存在了。如果我们仔细查看SpringBoot项目的启动日志,发现启动日志中已经输出了相应的删除及建表的语句,下面为项目启动的时操作表的日志。Hibernate: drop table if exists user_infoHibernate: create table user_info (id bigint not null auto_increment, password varchar(255), username varchar(255), primary key (id)) 下面我们将spring.jpa.hibernate.ddl-auto参数修改为create-drop来验证一下create-drop参数的特性。我们首先先把表删除掉,以免刚刚的create参数影响create-drop的验证。 我们还是和刚刚一样启动项目后查看数据库中是不是自动为我们创建一张userinfo表。 我们看一样还是自动为我们创建了一个新的表。下面我们将服务执行后,在看一下该表还是不是存在。 我们发现,刚刚创建的表已经自动删除了,这就是create-drop参数的特性。我们查看日志,也可以发现当停止服务时,自动执行的删表语句。下面的日志。Hibernate: drop table if exists user_infoHibernate: create table user_info (id bigint not null auto_increment, password varchar(255), username varchar(255), primary key (id))2019-01-18 16:56:27.033 INFO 6956 — [ main] org.hibernate.tool.hbm2ddl.SchemaExport : HHH000230: Schema export complete2019-01-18 16:56:27.058 INFO 6956 — [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit ‘default'2019-01-18 16:56:27.400 INFO 6956 — [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@21de60b4: startup date [Fri Jan 18 16:56:23 CST 2019]; root of context hierarchy2019-01-18 16:56:27.482 INFO 6956 — [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped “{[/error]}” onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)2019-01-18 16:56:27.483 INFO 6956 — [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped “{[/error],produces=[text/html]}” onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)2019-01-18 16:56:27.511 INFO 6956 — [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]2019-01-18 16:56:27.512 INFO 6956 — [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]2019-01-18 16:56:27.553 INFO 6956 — [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]2019-01-18 16:56:27.888 INFO 6956 — [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup2019-01-18 16:56:27.973 INFO 6956 — [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8081 (http)2019-01-18 16:56:27.978 INFO 6956 — [ main] JilinwulaSpringbootHelloworldApplication : Started JilinwulaSpringbootHelloworldApplication in 5.928 seconds (JVM running for 7.512)2019-01-18 17:00:50.630 INFO 6956 — [ Thread-16] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@21de60b4: startup date [Fri Jan 18 16:56:23 CST 2019]; root of context hierarchy2019-01-18 17:00:50.661 INFO 6956 — [ Thread-16] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown2019-01-18 17:00:50.664 INFO 6956 — [ Thread-16] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit ‘default'2019-01-18 17:00:50.666 INFO 6956 — [ Thread-16] org.hibernate.tool.hbm2ddl.SchemaExport : HHH000227: Running hbm2ddl schema exportHibernate: drop table if exists user_info 在日志中我们发现一共执行了两次删除表的语句,第一次是在启动前,第二次是在服务停止时。 下面我们把spring.jpa.hibernate.ddl-auto参数修改为update来验证update参数的特性。同样我们还是事先要把刚刚生成的表删除掉,因为create-drop参数在停止服务时,已经把刚刚的表删除掉了,所以我们就不用手动删除了,我们直接把spring.jpa.hibernate.ddl-auto参数修改为update,然后直接启动项目。 我们看上图当我们把spring.jpa.hibernate.ddl-auto参数设置为update时,也会自动为我们创建表。并且我们停止服务时,该表依然还存在,并且不会清除数据。下面我们向表中添加一条新数据。然后修改实体类中的结构,看看刚刚新增的数据还存在不存在。 实体类修改如下:package com.jilinwula.springboot.helloworld.entity;import lombok.Data;import javax.persistence.;@Data@Entity@Table(name = “user_info”)public class UserInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private String nickname;} 我们新增了一个nickname属性,然后我们重新启动服务看看这个新的字段会不会自动在表中创建,并且没有更改原先表中的数据。 我们看新的字段已经自动创建成功了,并且没有删除原先表中的数据,这就是update参数的作用,在实际的开发中,把spring.jpa.hibernate.ddl-auto参数设置为update,是比较常见的配置方式。 下面我们验证最后一个参数也就是validate参数。因为之前的操作我们现在userinfo表中一其有3个字段,现在我们将实体类中的nickname字段注释掉,然后我们在启动服务,看一看项目启动是否正常。下面为实体类源码:import javax.persistence.;@Data@Entity@Table(name = “user_info”)public class UserInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password;// private String nickname;} 当我们启动项目时,发现项目是可以正常启动。然后查看数据库中表,我们发现表中的结构没有任何变化。也就是说,我们注释掉数据库中已有的字段然后启动项目时,项目是可以正常启动的。这又是为什么呢?validate参数的作用不就是验证实体类中的属性与数据库中的字段不匹配时抛出异常吗?为什么当我们这么设置时没有抛出异常呢?这是因为validate参数的特性是只有实体类中的属性比数据库中的字段多时才会报错,如实体类中的属性比数据库中字段少,则不会报错。刚刚我们将nickname属性给注释了,但validate是不会更改表结构的,所以数据库中还是会有nickname字段的,这就导致数据中的字段比实体类中的属性多,所以当我们启动项目时是不会抛出异常。但反之,如果我们在实体类中新增一个字段,然后我们在启动项目时,项目就会抛出异常。实体类源码:@Data@Entity@Table(name = “user_info”)public class UserInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private String nickname; private Long roleId;} 我们新增了一个roleId字段,并且该字段在数据库中是没有的,然后我们启动项目。查看日志发现项目已经启动失败了。下面为日志:Caused by: org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing column [role_id] in table [user_info] at org.hibernate.tool.schema.internal.SchemaValidatorImpl.validateTable(SchemaValidatorImpl.java:85) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.tool.schema.internal.SchemaValidatorImpl.doValidation(SchemaValidatorImpl.java:50) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.tool.hbm2ddl.SchemaValidator.validate(SchemaValidator.java:91) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:475) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:444) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:879) ~[hibernate-entitymanager-5.0.12.Final.jar:5.0.12.Final] at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:60) ~[spring-orm-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:360) ~[spring-orm-4.3.21.RELEASE.jar:4.3.21.RELEASE] at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:384) ~[spring-orm-4.3.21.RELEASE.jar:4.3.21.RELEASE] … 20 common frames omitted 下面我们在数据库中将roleId字段手动添加上,然后我们在重新启动项目,在看一下启动时项目还是否报错。 下面我们重新启动项目,然后在看一下日志,发现项目已经成功启动了,这就是validate参数的作用。2019-01-19 17:17:41.160 INFO 1034 — [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup2019-01-19 17:17:41.201 INFO 1034 — [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8081 (http)2019-01-19 17:17:41.204 INFO 1034 — [ main] JilinwulaSpringbootHelloworldApplication : Started JilinwulaSpringbootHelloworldApplication in 2.596 seconds (JVM running for 3.233)实体类注解高级配置 上述内容我们基本已经将spring.jpa.hibernate.ddl-auto参数的的使用介绍完了,下面我们介绍一下实体类中的高级注解。因为我们在上面的测试中我们发现,当我们把spring.jpa.hibernate.ddl-auto参数设置为create时,虽然成功的创建了实体类中指定的表,但是我们发现自动创建的表只是字段和实体类中的属性一致,但例如表中的字段长度、字段的描述、表的索引,这些高级的配置,是需要我们在实体类中添加新的注解,才能设置的。下面我们将实体类中的代码修改一下,添加上面说的注解,并且验证上面注解是否可以正确设置表中的长度、描述及索引。(备注:别忘记将spring.jpa.hibernate.ddl-auto参数设置为create)下面为实体类源码:package com.jilinwula.springboot.helloworld.entity;import lombok.Data;import javax.persistence.;@Data@Entity@Table(name = “user_info”, indexes = @Index(columnList = “username”))public class UserInfoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = “id”, columnDefinition = “bigint(10) comment ‘主键’”) private Long id; @Column(name = “username”, columnDefinition = “varchar(10) not null default ’’ comment ‘账号’”) private String username; @Column(name = “password”, columnDefinition = “varchar(10) not null default ’’ comment ‘密码’”) private String password; @Column(name = “nickname”, columnDefinition = “varchar(10) not null default ’’ comment ‘妮称’”) private String nickname; @Column(name = “role_id”, columnDefinition = “bigint(10) not null default 0 comment ‘角色’”) private Long roleId;} 上面我们介绍过当在类中添加@Entity注解后,JPA会自动将实体类中的属性映射为数据库中表里的字段。但在实际的开发中我们可能会遇到实体类中的属性与数据库中的字段不一致的情况。这时我们就要使用@Column注解了,该注解的参数有很多,我们要掌握2个就可以了。一个参数为name因为JAP在映射属性到数据库时,如果没有指定@Column参数,则默认使用和实体类中的属性一样的名字,如果指定了@Column则使用该注解中的name参数。第二个参数为columnDefinition参数,该参数则是可以直接将我们创建表中的语句写在该参数中,这样我们可以很方便的控制字段的长度及类型。还有一个注解为@indexes。该注解可指定我们指定属性为表中的索引,这里要注意一下如果表中字段名字和实体类中的属性名字不一致,@indexes注解需要指定的是实体类中的属性名,则不是真正表中的字段名。下面我们启动项目,看一下数据库中的表结构是不是和我们实体类中映射的一样。。 我们现在看数据库中的映射,除了创建索引时自动生成的索引名不一样,其它的字段映射类型长度及其描述都和我们实体类中的一致。JpaRepository接口 下面我们介绍一下怎么使用JPA来操作数据库。我们以增删改查为例,分别介绍它们的使用。在使用JAP操作数据库时,我们需要创建一个和实体类相对应的接口,并且让该接口继承JAP中已经提供的JpaRepository(有很多个接口暂时只介绍这一个)接口。这样我们就可以通过这个接口来操作数据库了。下面我们看一下该接口的源码。package com.jilinwula.springboot.helloworld.Repository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.stereotype.Repository;@Repositorypublic interface UserInfoRepository extends JpaRepository<UserInfoEntity, Long> {} 在我们继承JpaRepository接口时需要我们指定两个参数,第一个参数表示我们要操作的实体类是哪一个,第二个参数表示我们实体类中的主键类型,其次我们还需要添加@Repository注解,这样JPA才能操作数据库。下面我们创建一个测试用例,分别介绍数据库中的增删改查。下面为测试用例源码:package com.jilinwula.springboot.helloworld;import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class)@SpringBootTestpublic class JilinwulaSpringbootHelloworldApplicationTests { @Autowired private UserInfoRepository userInfoRepository; @Test public void save() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername(“吉林乌拉”); userInfoEntity.setPassword(“jilinwula”); userInfoEntity.setNickname(“二十四分之七倍根号六”); userInfoEntity.setRoleId(1L); userInfoRepository.save(userInfoEntity); } @Test public void contextLoads() { }}数据库的增删改查 我们暂时只写一个新增的方法,并且我们发现,虽然我们的UserInfoRepository接口中没有写任何方法,但我们居然可以直接调用save方法了。这是因为当我们将UserInfoRepository接口继承JpaRepository接口时,是默认继承了该接口的一些方法,所以这些基本的增删改查操作,是不需要我们写任何代码的。下面我们执行一下测试用例,看看该条数据能否正确的插入到数据库中。 我们看数据已经成功的添加到了数据库中。下面我们在添加一条,方便我们以后测试。 。下面我们编写一下查询语句,看看能否正确查出数据。@Test public void select() { UserInfoEntity userInfoEntity = userInfoRepository.findOne(1L); System.out.println(userInfoEntity); } 我们看同样,我们还是没有写findOne方法,但是我们居然可以直接使用。findOne方法是JPA为我们提供通过主键查询数据的方法,所以该方法的返回值是实体类对象,因为只能返回一条数据。下面我们执行一下该测试用例,看看能否正确查询出数据。UserInfoEntity(id=1, username=吉林乌拉, password=jilinwula, nickname=二十四分之七倍根号六, roleId=1) 我们看已经成功的查询出数据了。这时有人会说,如果我们想查所有的数据应该怎么办呢?别着急,JPA中除了提供了findOne方法,还提供了findAll方法,顾名思义,该方法就是查询所有数据的。既然是所有数据,所以该方法的返回值为List。下面为测试用例源码,及其执行日志。@Testpublic void selectAll() { List<UserInfoEntity> userInfoEntitys = userInfoRepository.findAll(); System.out.println(userInfoEntitys);}[UserInfoEntity(id=1, username=吉林乌拉, password=jilinwula, nickname=二十四分之七倍根号六, roleId=1), UserInfoEntity(id=2, username=阿里巴巴, password=alibaba, nickname=淘宝, roleId=2)] 下面我们介绍一下更新方法,在JPA中更新方法和save方法一样,唯一的区别就是如果我们在实体类中设置了主键,则调用sava方法时,JPA执行的就是更新。如果不设置主键,则JPA执行的就是新增。下面为测试用例源码:@Testpublic void update() { UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setId(1L); userInfoEntity.setUsername(“阿里巴巴”); userInfoEntity.setPassword(“alibaba”); userInfoEntity.setNickname(“淘宝”); userInfoEntity.setRoleId(2L); userInfoRepository.save(userInfoEntity);} 现在我们在查询一下数据库,如果更新语句成功,那么此时数据库中则会有两条一样的数据。 我们看,数据库中的确有两条一模一样的数据了,这就证明了我们刚刚的更新语句成功了。下面我们介绍一下最后一个删除语句,该语句也同样比较简单,因为JPA也同样为我们提供了该方法,下面为测试用例。 @Test public void delete() { userInfoRepository.delete(1L); } 我们在查询一下数据库,看看id为1的数据是否还在数据库中存在。 我们看该数据成功的删除了。这就是JPA对数据库的增删改查的基本操作。当然JPA中还提供了很多复杂的语法,例如级联查询、分页查询等等。这些高级的功能我们在后续的文章中在做详细介绍。这就是本篇的全部内容,如有疑问,欢迎留言,谢谢。项目源码 下面为项目源码:https://github.com/jilinwula/jilinwula-springboot-helloworld3原文链接 下面为项目源码:http://jilinwula.com/article/24338 ...

January 31, 2019 · 4 min · jiezi

一文带你认识Spring事务

前言只有光头才能变强。文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3ySpring事务管理我相信大家都用得很多,但可能仅仅局限于一个@Transactional注解或者在XML中配置事务相关的东西。不管怎么说,日常可能足够我们去用了。但作为程序员,无论是为了面试还是说更好把控自己写的代码,还是应该得多多了解一下Spring事务的一些细节。这里我抛出几个问题,看大家能不能瞬间答得上:如果嵌套调用含有事务的方法,在Spring事务管理中,这属于哪个知识点?我们使用的框架可能是Hibernate/JPA或者是Mybatis,都知道的底层是需要一个session/connection对象来帮我们执行操作的。要保证事务的完整性,我们需要多组数据库操作要使用同一个session/connection对象,而我们又知道Spring IOC所管理的对象默认都是单例的,这为啥我们在使用的时候不会引发线程安全问题呢?内部Spring到底干了什么?人家所说的BPP又是啥东西?Spring事务管理重要接口有哪几个?一、阅读本文需要的基础知识阅读这篇文章的同学我默认大家都对Spring事务相关知识有一定的了解了。(ps:如果不了解点解具体的文章去阅读再回到这里来哦)我们都知道,Spring事务是Spring AOP的最佳实践之一,所以说AOP入门基础知识(简单配置,使用)是需要先知道的。如果想更加全面了解AOP可以看这篇文章:AOP重要知识点(术语介绍、全面使用)。说到AOP就不能不说AOP底层原理:动态代理设计模式。到这里,对AOP已经有一个基础的认识了。于是我们就可以使用XML/注解方式来配置Spring事务管理。在IOC学习中,可以知道的是Spring中Bean的生命周期(引出BPP对象)并且IOC所管理的对象默认都是单例的:单例设计模式,单例对象如果有"状态"(有成员变量),那么多线程访问这个单例对象,可能就造成线程不安全。那么何为线程安全?,解决线程安全有很多方式,但其中有一种:让每一个线程都拥有自己的一个变量:ThreadLocal如果对我以上说的知识点不太了解的话,建议点击蓝字进去学习一番。二、两个不靠谱直觉的例子2.1第一个例子之前朋友问了我一个例子:在Service层抛出Exception,在Controller层捕获,那如果在Service中有异常,那会事务回滚吗?// Service方法 @Transactionalpublic Employee addEmployee() throws Exception { Employee employee = new Employee(“3y”, 23); employeeRepository.save(employee); // 假设这里出了Exception int i = 1 / 0; return employee;}// Controller调用@RequestMapping("/add")public Employee addEmployee() { Employee employee = null; try { employee = employeeService.addEmployee(); } catch (Exception e) { e.printStackTrace(); } return employee;}我第一反应:不会回滚吧。我当时是这样想的:因为Service层已经抛出了异常,由Controller捕获。那是否回滚应该由Controller的catch代码块中逻辑来决定,如果catch代码块没有回滚,那应该是不会回滚。但朋友经过测试说,可以回滚阿。(pappapa打脸)看了一下文档,原来文档有说明:By default checked exceptions do not result in the transactional interceptor marking the transaction for rollback and instances of RuntimeException and its subclasses do结论:如果是编译时异常不会自动回滚,如果是运行时异常,那会自动回滚!2.2第二个例子第二个例子来源于知乎@柳树文章,文末会给出相应的URL我们都知道,带有@Transactional注解所包围的方法就能被Spring事务管理起来,那如果我在当前类下使用一个没有事务的方法去调用一个有事务的方法,那我们这次调用会怎么样?是否会有事务呢?用代码来描述一下:// 没有事务的方法去调用有事务的方法public Employee addEmployee2Controller() throws Exception { return this.addEmployee();}@Transactionalpublic Employee addEmployee() throws Exception { employeeRepository.deleteAll(); Employee employee = new Employee(“3y”, 23); // 模拟异常 int i = 1 / 0; return employee;}我第一直觉是:这跟Spring事务的传播机制有关吧。其实这跟Spring事务的传播机制没有关系,下面我讲述一下:Spring事务管理用的是AOP,AOP底层用的是动态代理。所以如果我们在类或者方法上标注注解@Transactional,那么会生成一个代理对象。接下来我用图来说明一下:显然地,我们拿到的是代理(Proxy)对象,调用addEmployee2Controller()方法,而addEmployee2Controller()方法的逻辑是target.addEmployee(),调用回原始对象(target)的addEmployee()。所以这次的调用压根就没有事务存在,更谈不上说Spring事务传播机制了。原有的数据:测试结果:压根就没有事务的存在2.2.1再延伸一下从上面的测试我们可以发现:如果是在本类中没有事务的方法来调用标注注解@Transactional方法,最后的结论是没有事务的。那如果我将这个标注注解的方法移到别的Service对象上,有没有事务?@Servicepublic class TestService { @Autowired private EmployeeRepository employeeRepository; @Transactional public Employee addEmployee() throws Exception { employeeRepository.deleteAll(); Employee employee = new Employee(“3y”, 23); // 模拟异常 int i = 1 / 0; return employee; }}@Servicepublic class EmployeeService { @Autowired private TestService testService; // 没有事务的方法去调用别的类有事务的方法 public Employee addEmployee2Controller() throws Exception { return testService.addEmployee(); }}测试结果:因为我们用的是代理对象(Proxy)去调用addEmployee()方法,那就当然有事务了。看完这两个例子,有没有觉得3y的直觉是真的水!三、Spring事务传播机制如果嵌套调用含有事务的方法,在Spring事务管理中,这属于哪个知识点?在当前含有事务方法内部调用其他的方法(无论该方法是否含有事务),这就属于Spring事务传播机制的知识点范畴了。Spring事务基于Spring AOP,Spring AOP底层用的动态代理,动态代理有两种方式:基于接口代理(JDK代理)基于接口代理,凡是类的方法非public修饰,或者用了static关键字修饰,那这些方法都不能被Spring AOP增强基于CGLib代理(子类代理)基于子类代理,凡是类的方法使用了private、static、final修饰,那这些方法都不能被Spring AOP增强至于为啥以上的情况不能增强,用你们的脑瓜子想一下就知道了。值得说明的是:那些不能被Spring AOP增强的方法并不是不能在事务环境下工作了。只要它们被外层的事务方法调用了,由于Spring事务管理的传播级别,内部方法也可以工作在外部方法所启动的事务上下文中。至于Spring事务传播机制的几个级别,我在这里就不贴出来了。这里只是再次解释“啥情况才是属于Spring事务传播机制的范畴”。四、多线程问题我们使用的框架可能是Hibernate/JPA或者是Mybatis,都知道的底层是需要一个session/connection对象来帮我们执行操作的。要保证事务的完整性,我们需要多组数据库操作要使用同一个session/connection对象,而我们又知道Spring IOC所管理的对象默认都是单例的,这为啥我们在使用的时候不会引发线程安全问题呢?内部Spring到底干了什么?回想一下当年我们学Mybaits的时候,是怎么编写Session工具类?没错,用的就是ThreadLocal,同样地,Spring也是用的ThreadLocal。以下内容来源《精通 Spring4.x》我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态的“状态性对象”采用ThreadLocal封装,让它们也成为线程安全的“状态性对象”,因此,有状态的Bean就能够以singleton的方式在多线程中工作。我们可以试着点一下进去TransactionSynchronizationManager中看一下:五、啥是BPP?BBP的全称叫做:BeanPostProcessor,一般我们俗称对象后处理器简单来说,通过BeanPostProcessor可以对我们的对象进行“加工处理”。Spring管理Bean(或者说Bean的生命周期)也是一个常考的知识点,我在秋招也重新整理了一下步骤,因为比较重要,所以还是在这里贴一下吧:ResouceLoader加载配置信息BeanDefintionReader解析配置信息,生成一个一个的BeanDefintionBeanDefintion由BeanDefintionRegistry管理起来BeanFactoryPostProcessor对配置信息进行加工(也就是处理配置的信息,一般通过PropertyPlaceholderConfigurer来实现)实例化Bean如果该Bean配置/实现了InstantiationAwareBean,则调用对应的方法使用BeanWarpper来完成对象之间的属性配置(依赖)如果该Bean配置/实现了Aware接口,则调用对应的方法如果该Bean配置了BeanPostProcessor的before方法,则调用如果该Bean配置了init-method或者实现InstantiationBean,则调用对应的方法如果该Bean配置了BeanPostProcessor的after方法,则调用将对象放入到HashMap中最后如果配置了destroy或者DisposableBean的方法,则执行销毁操作其中也有关于BPP图片:5.1为什么特意讲BPP?Spring AOP编程底层通过的是动态代理技术,在调用的时候肯定用的是代理对象。那么Spring是怎么做的呢?我只需要写一个BPP,在postProcessBeforeInitialization或者postProcessAfterInitialization方法中,对对象进行判断,看他需不需要织入切面逻辑,如果需要,那我就根据这个对象,生成一个代理对象,然后返回这个代理对象,那么最终注入容器的,自然就是代理对象了。Spring提供了BeanPostProcessor,就是让我们可以对有需要的对象进行“加工处理”啊!六、认识Spring事务几个重要的接口Spring事务可以分为两种:编程式事务(通过代码的方式来实现事务)声明式事务(通过配置的方式来实现事务)编程式事务在Spring实现相对简单一些,而声明式事务因为封装了大量的东西(一般我们使用简单,里头都非常复杂),所以声明式事务实现要难得多。在编程式事务中有以下几个重要的了接口:TransactionDefinition:定义了Spring兼容的事务属性(比如事务隔离级别、事务传播、事务超时、是否只读状态)TransactionStatus:代表了事务的具体运行状态(获取事务运行状态的信息,也可以通过该接口间接回滚事务等操作)PlatformTransactionManager:事务管理器接口(定义了一组行为,具体实现交由不同的持久化框架来完成—类比JDBC)在声明式事务中,除了TransactionStatus和PlatformTransactionManager接口,还有几个重要的接口:TransactionProxyFactoryBean:生成代理对象TransactionInterceptor:实现对象的拦截TransactionAttrubute:事务配置的数据最后本文主要讲了Spring事务管理一些比较重要的知识点,当然在学习的过程中还看到其他的知识点,如果想要继续学习的同学不妨通过下面给出的参考资料继续阅读。参考资料:那些年,我们一起追的Springhttps://zhuanlan.zhihu.com/p/41961670《精通Spring 4.x 企业应用开发实战》《Spring技术内幕》乐于输出干货的Java技术公众号:Java3y。公众号内有200多篇原创技术文章、海量视频资源、精美脑图,不妨来关注一下!觉得我的文章写得不错,不妨点一下赞! ...

January 31, 2019 · 1 min · jiezi

Spring Boot系列实战文章合集(附源码)

概 述文章开始之前先感叹一番吧。个人从之前的 C语言项目开发转到 Java项目开发来之后开始学着用 Spring Boot做一些后端服务,不得不说 Spring Boot脚手架式的开发真的是十分便利,最近连掉头发现象也好了很多,于是从内心感叹 Java阵营程序员真的比 C阵营程序员工作起来舒服多了,原因就在于Java领域繁荣的生态圈催生了一大批诸如 Spring Boot这样优秀的框架的出现。这段时间也陆陆续续记录了一些有关 Spring Boot应用层开发的点点滴滴,特在此汇聚成文章合集,并 放在了Github上,项目名为 Spring-Boot-In-Action,后续仍然会持续更新。注: 本文首发于 My Personal Blog:CodeSheep·程序羊,欢迎光临 小站数据库/缓存相关Guava Cache本地缓存在 Spring Boot应用中的实践EVCache缓存在 Spring Boot中的实战Spring Boot应用缓存实践之:Ehcache加持Spring Boot集成 MyBatis和 SQL Server实践Elasticsearch搜索引擎在Spring Boot中的实践日志相关Spring Boot日志框架实践应用监控相关利用神器 BTrace 追踪线上 Spring Boot应用运行时信息Spring Boot应用监控实战Spring Boot Admin 2.0开箱体验内部机制相关SpringBoot 中 @SpringBootApplication注解背后的三体结构探秘SpringBoot 应用程序启动过程探秘如何自制一个Spring Boot Starter并推送到远端公服实战经验相关Spring Boot工程集成全局唯一ID生成器 UidGeneratorSpring Boot 工程集成全局唯一ID生成器 VestaSpring Boot优雅编码之:Lombok加持Spring Boot应用 Docker化Spring Boot热部署加持基于Spring Boot实现图片上传/加水印一把梭操作从Spring Boot到 SpringMVC自然语言处理工具包 HanLP在 Spring Boot中的应用Spring Boot应用部署于外置Tomcat容器初探 Kotlin + Spring Boot联合编程【持续更新中……】后 记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊 ...

January 31, 2019 · 1 min · jiezi

如何在Flutter上优雅地序列化一个对象

序列化一个对象才是正经事对象的序列化和反序列化是我们日常编码中一个非常基础的需求,尤其是对一个对象的json encode/decode操作。每一个平台都会有相关的库来帮助开发者方便得进行这两个操作,比如Java平台上赫赫有名的GSON,阿里巴巴开源的fastJson等等。而在flutter上,借助官方提供的JsonCodec,只能对primitive/Map/List这三种类型进行json的encode/decode操作,对于复杂类型,JsonCodec提供了receiver/toEncodable两个函数让使用者手动“打包”和“解包”。显然,JsonCodec提供的功能看起来相当的原始,在闲鱼app中存在着大量复杂对象序列化需求,如果使用这个类,就会出现集体“带薪序列化”的盛况,而且还无法保证正确性。来自官方推荐聪明如Google官方,当然不会坐视不理。json_serializable的出现就是官方给出的推荐,它借助Dart Build System中的build_runner和json_annotation库,来自动生成fromJson/toJson函数内容。(关于使用build_runner生成代码的原理,之前兴往同学的文章已经有所提及)关于如何使用json_serializable网上已经有很多文章了,这里只简单提一些步骤:Step 1 创建一个实体类Step 2 生成代码:来让build runner生成序列化代码。运行完成后文件夹下会出现一个xxx.g.dart文件,这个文件就是生成后的文件。Step 3 代理实现:把fromJson和toJson操作代理给上面生成出来的类我们为什么不用这个实现json_serializable完美实现了需求,但它也有不满足需求的一面:使用起来有些繁琐,多引入了一个类很重要的一点是,大量的使用"as"会给性能和最终产物大小产生不小的影响。实际上闲鱼内部的《flutter编码规范》中,是不建议使用"as"的。(对包大小的影响可以参见三笠同学的文章,同时dart linter也对as的性能影响有所描述)一种正经的方式基于上面的分析,很明显的,需要一种新的方式来解决我们面临的问题,我们暂且叫它,fish-serializable需要实现的功能我们首先来梳理一下,一个序列化库需要用到:获取可序列化对象的所有field以及它们的类型信息能够构造出一个可序列化对象,并对它里面的fields赋值,且类型正确支持自定义类型最好能够解决泛型的问题,这会让使用更加方便最好能够轻松得在不同的序列化/反序列化方式中切换,例如json和protobuf。困难在哪里flutter禁用了dart:mirrors,反射API无法使用,也就无法通过反射的方式new一个instance、扫描class的fields。泛型的问题由于dart不进行类型擦出,可以获取,但泛型嵌套后依然无法解开。Let’s rock无法使用dart:mirrors是个“硬”问题,没有反射的支持,类的内容就是一个黑盒。于是我们在迈出第一步的时候就卡壳了- -!这个时候笔者脑子里闪过了很多画面,白驹过隙,乌飞兔走,啊,不是…是c++,c++作为一种无法使用反射的语言,它是如何实现对象的 序列化/反序列化 操作的呢?一顿搜索猛如虎之后,发现大神们使用创建类对象的回调函数配合宏的方式来实现c++中类似反射这样的操作。这个时候,笔者又想到了曾经朝夕相处的Android(现在已经变成了flutter),Android中的Parcelable序列化协议就是一个很好的参照,它通过writeXXXAPIs将类的数据写入一个中间存储进行序列化,再通过readXXXAPIs进行反序列化,这就解决了我们上面提到的第一个问题,既如何将一个类的“黑盒子”打开。同时,Parcelable协议中还需要使用者提供一个叫做CREATOR的静态内部类,用来在反序列化的时候反射创建一个该类的对象或对象数组,对于没有反射可用的我们来说,用c++的那种回调函数的方式就可以完美解决反序列化中对象创建的问题。于是最终我们的基本设计就是:ValueHolder这是一个数据中转存储的基类,它内部的writeXXX APIs提供展开类内部的fields的能力,而readXXX则用来将ValueHolder中的内容读取赋值给类的fields。readList/readMap/readSerializable函数中的type argument,我们把它作为外部想要解释数据的方式,比如readSerializable<T>(key: ‘object’),表示外部想要把key为object的值解释为T类型。FishSerializableFishSerializable是一个interface,creator是个一个get函数,用来返回一个“创建类对象的回调”,writeTo函数则用来在反序列化的时候放置ValueHoder->fields的代码。JsonSerializer它继承于FishSerializer接口,实现了encode/decode函数,并额外提供encodeToMap和decodeFromMap功能。JsonSerializer类似JsonCodec,直接面向使用者用来json encode/decode以上,我们已经基本做好了一个flutter上支持对象序列化/反序列化操作的库的基本架构设计,对象的序列化过程可以简化为:由于ValueHolder中间存储的存在,我们可以很方便得切换 序列化/反序列器,比如现有的JsonSerializer用来实现json的encode/decode,如果有类似protobuf的需求,我们则可以使用ProtoBufSerializer来将ValueHolder中的内容转换成我们需要的格式。困难是不存在的有了基本的结构设计之后,实现的过程并非一帆风顺。如何匹配类型?为了能支持泛型容器的解析,我们需要类似下面这样的逻辑:List<SerializableObject> list = holder.readList<SerializableObject>(key: ’list’);List<E> readList<E>({String key}){ List<dynamic> list = _read(key);}E _flattenList<E>(List<dynamic> list){ list?.map<E>((dynamic item){ // 比较E是否属于某个类型,然后进行对应类型的转换 });}在Java中,可以使用Class#isAssignableFrom,而在flutter中,我们没有发现类似功能的API提供。而且,如果做下面这个测试,你还会发现一些很有意思的细节:void main() { print(‘int test’); test<int>(1); print(’\r\nint list test’); test<List<int>>(<int>[]); print(’\r\nobject test’); test<A<int>>(A<int>());}void test<T>(T t){ print(T); print(t.runtimeType); print(T == t.runtimeType); print(identical(T, t.runtimeType));}class A<T>{}输出的结果是:可以看到,对于List这样的容器类型,函数的type argument与instance的runtimeType无法比较,当然如果使用t is T,是可以返回正确的值的,但需要构造大量的对象。所以基本上,我们无法进行类型匹配然后做类型转换。如何解析泛型嵌套?接下去就是如何分解泛型容器嵌套的问题,考虑如下场景:Map<String, List<int>> listMap;listMap = holder.readMap<String, List<int>>(key: ’listMap’);readMap中得到的value type是一个List<int>,而我们没有API去切割这个type argument。所以我们采用了一种比较“笨”也相对实用的方式。我们使用字符串切割了type argument,比如:List<int> => <String>[List<int>, List, int]然后在内部展开List或Map的时候,使用字符串匹配的方式匹配类型,在目前的使用中,完美得支持了标准List和Map容器互相嵌套。但目前无法支持标准List和Map之外的其他容器类型。What’s moreIDE插件辅助写过Android的Parcelable的同学应该有种很深刻的体会,Parcelable协议中有大量的“机械”代码需要写,类似设计的fish-serializable也一样。为了不被老板和使用库的同学打死,同时开发了fish-serializable-intelij-plugin来自动生成这些“机械”代码。与json_serializable的对比fish-serializable在使用上配合IDE插件,减少了大量的"as"操作符的使用,同时在步骤上也更加简短方便。相比于json_annotation生成的代码,fish-serializable生成的代码也更具可读性,方便手动修改一些代码实现。fish-serializable可以通过手动接管 序列化/反序列化 过程的方式完美兼容json_annotation等其他方案。目前闲鱼app中已经开始大量使用。开源计划fish-serializable和fish-serializable-intelij-plugin都在开源计划中,相信不久就可以与大家见面,尽请期待~本文作者:闲鱼技术-海潴阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 30, 2019 · 1 min · jiezi

spring cache 实现按照*号删除缓存

spring cache redis的使用过程中,删除缓存只能用具体的key删除,不能使用通配符号,原因是redis不支持del key这种通配符用法,可以通过修改redis源代码实现,但这种方式修改了redis本身代码,后期升级、维护不好操作,具体操作方式可以参见:redis del命令支持正则删除(pattern)git地址:redis-del-with-pattern我们使用改写spring-redis cache实现具体实现方式为:改写:org.springframework.data.redis.cache.RedisCache下的evict方法原为:cacheWriter.remove(name, createAndConvertCacheKey(key)); 改为:cacheWriter.clean(name, createAndConvertCacheKey(key));spring redis最底层是支持了通配符的方式的,但是经过包装后就去掉了具体在项目中的使用实例如:在查询方法上加入缓存: @Override @Cacheable(keyGenerator = “cacheKeyGenerator”) public List query(xx x) throws IllegalAccessException { return xxxx; }其中cacheKeyGenerator生成如com.demo.service.impl.xxServiceImpl-query-99986a删除或更新时: @Override @CacheEvict(key = “targetClass.name+’-*’”) public boolean saveOrUpdate(xx x) { return xxxx; }其中key时spEL表达式,生成 com.demo.service.impl.xxServiceImpl-*的key最终效果是在新增或更新时能删除所有列表的缓存key

January 30, 2019 · 1 min · jiezi

使用Maven配置Spring

这篇文章说明了如何通过Maven配置Spring依赖项。最新的Spring版本可以在Maven Central上找到。Maven中的Spring基本依赖关系Spring的设计是高度模块化的 - 使用Spring的一部分不应该而且不需要另一部分。例如,基本的Spring Context可以没有Persistence或MVC Spring库。让我们先从一个基本Maven配置,将只使用了spring-context依赖:<properties> <org.springframework.version>3.2.8.RELEASE</org.springframework.version> <!– <org.springframework.version>4.0.2.RELEASE</org.springframework.version> –></properties><dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${org.springframework.version}</version> <scope>runtime</scope></dependency>这个依赖项 - spring-context - 定义了实际的Spring Injection Container,并且有少量的依赖项:spring-core,spring-expression,spring-aop和spring-beans。通过支持一些核心Spring技术来扩充容器:Core Spring实用程序,Spring表达式语言(SpEL),面向对象编程支持和JavaBeans机制。注意我们在运行时范围中定义了依赖关系- 这将确保在任何特定于Spring的API上没有编译时依赖性。对于更高级的用例,可以从一些选定的Spring依赖项中删除运行时范围,但是对于更简单的项目,不需要针对Spring进行编译以充分利用该框架。另请注意,从Spring 3.2开始,不需要定义CGLIB依赖项(现在已升级到CGLIB 3.0) - 它已被重新打包(所有net.sf.cglib包现在是org.springframework.cglib)并且直接在内部内联spring-core JAR(有关其他详细信息,请参阅JIRA)。Maven配置Spring Persistence现在让我们看一下Spring Persistence依赖关系 - 主要是spring-orm:<dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${org.springframework.version}</version></dependency>这附带了Hibernate和JPA支持 - 例如HibernateTemplate和JpaTemplate - 以及一些额外的,持久性相关的依赖项:spring-jdbc和spring-tx。JDBC数据访问库定义了Spring JDBC支持以及JdbcTemplate,而spring-tx代表了极其灵活的事务管理抽象。Maven配置Spring MVC要使用Spring Web和Servlet支持,除了上面的核心依赖项外,还需要在pom中包含两个依赖项:<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${org.springframework.version}</version></dependency><dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${org.springframework.version}</version></dependency>spring-web依赖项包含Servlet和Portlet环境的公共web特定实用程序,而spring-webmvc支持Servlet环境的MVC。由于spring-webmvc将spring-web作为依赖项,因此在使用spring-webmvc时不需要明确定义spring-web。使用maven配置Spring Security在使用Maven配置Spring Security文章中深入讨论了Maven配置Spring Security依赖关系。使用Maven配置Spring TestSpring Test Framework可以通过以下依赖项包含在项目中:<dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope></dependency>从Spring 3.2开始,Spring MVC Test项目已经包含在核心测试框架中 - 因此包括spring-test依赖就足够了。使用MilestonesSpring的发布版本托管在Maven Central上。但是,如果项目需要使用Milestones版本,则需要将自定义Spring存储库添加到pom中:<repositories> <repository> <id>repository.springframework.maven.milestone</id> <name>Spring Framework Maven Milestone Repository</name> <url>http://repo.spring.io/milestone/</url> </repository></repositories>已定义了一个此存储库,该项目可以定义依赖项,例如:<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>3.2.0.RC2</version></dependency>使用Snapshots与Milestones类似,Snapshots托管在自定义存储库中:<repositories> <repository> <id>repository.springframework.maven.snapshot</id> <name>Spring Framework Maven Snapshot Repository</name> <url>http://repo.spring.io/snapshot/</url> </repository></repositories>在pom.xml中启用SNAPSHOT存储库后,可以引用以下依赖项:<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>3.3.0.BUILD-SNAPSHOT</version></dependency>对于4.x:<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.0.3.BUILD-SNAPSHOT</version></dependency> ...

January 30, 2019 · 1 min · jiezi

mongoDB原生查询与spring data mongoDB的对象

一、按照in、eq、lte等条件组合查询,同时添加sort和limit1、原生db.message.find( { receiverRoleId: {$in: [1381073, 1381073]}, resourceType:3, sendTime: {$lte: 1523355918300} }) .sort({sendTime: -1 }) .limit(10);2、spring data mongoDBCriteria criteria = Criteria.where(“receiverRoleId”).in(receiverRoleIds) .and(“readState”).is(readState.getIndex()) .and(“sendTime”).lte(sendTime);Query query = Query.query(criteria);query.with(new Sort(Sort.Direction.DESC, “sendTime”));query.limit(count);mongoTemplate.find(query, Notification.class);二、执行update操作,更新单个文档1、原生db.message.update( {_id: 586537, readState: 2}, {$set: {readState: 1}}, {multi: false});2、spring data mongoDBCriteria criteria = Criteria.where("_id").is(id).and(“readState”).is(ReadState.UNREAD.getIndex());Query query = Query.query(criteria);Update update = Update.update(“readState”, ReadState.READ.getIndex());mongoTemplate.updateFirst(query, update, Notification.class);三、通过findAndModify命令更新文档并且返回更新之后的文档(只能作用于单个文档)1、原生db.message.findAndModify({ query: {_id: 586537, readState: 2}, update: {$set: {publishType: 1}}, new: true});2、spring data mongoDBQuery query = Query.query(Criteria.where("_id").is(2).and(“readState”).is(2));Update update = Update.update(“publishType”, 1);Notice updateResult = mongoTemplate.findAndModify( query, update, FindAndModifyOptions.options().returnNew(true), Notice.class);四、聚合操作(根据某一字段group,并且将文档中的某一字段合并到数组中,最后取数组中的第一个元素)1、原生db.message.aggregate([ { $match: {toAffairId : {$in: [590934, 591016]}} }, { $sort: {sendTime: -1} }, { $group: {_id: “$toAffairId”, contents: {$push: “$content”}} }, { $project: {_id: 0, “affaiId”: “$_id”, contents: {$slice: ["$contents", 1]} } }]);2、spring data mongoDBCriteria criteria = Criteria.where(“toAffairId”).in(affairIds);Aggregation aggregation = Aggregation.newAggregation( match(criteria), sort(Sort.Direction.DESC, “sendTime”), group(“toAffairId”).push(“content”).as(“contents”),AggregationResults<MobileDynamicMessageDataModel> results = mongoTemplate.aggregate( aggregation, collectionName, MobileDynamicMessageDataModel.class); ...

January 29, 2019 · 1 min · jiezi

Spring Cloud Alibaba基础教程:支持的几种服务消费方式

通过《Spring Cloud Alibaba基础教程:使用Nacos实现服务注册与发现》一文的学习,我们已经学会如何使用Nacos来实现服务的注册与发现,同时也介绍如何通过LoadBalancerClient接口来获取某个服务的具体实例,并根据实例信息来发起服务接口消费请求。但是这样的做法需要我们手工的去编写服务选取、链接拼接等繁琐的工作,对于开发人员来说非常的不友好。所以接下来,我们再来看看除此之外,还支持哪些其他的服务消费方式。使用RestTemplate在之前的例子中,已经使用过RestTemplate来向服务的某个具体实例发起HTTP请求,但是具体的请求路径是通过拼接完成的,对于开发体验并不好。但是,实际上,在Spring Cloud中对RestTemplate做了增强,只需要稍加配置,就能简化之前的调用方式。比如:@EnableDiscoveryClient@SpringBootApplicationpublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } @Slf4j @RestController static class TestController { @Autowired RestTemplate restTemplate; @GetMapping("/test") public String test() { String result = restTemplate.getForObject(“http://alibaba-nacos-discovery-server/hello?name=didi”, String.class); return “Return : " + result; } } @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }}可以看到,在定义RestTemplate的时候,增加了@LoadBalanced注解,而在真正调用服务接口的时候,原来host部分是通过手工拼接ip和端口的,直接采用服务名的时候来写请求路径即可。在真正调用的时候,Spring Cloud会将请求拦截下来,然后通过负载均衡器选出节点,并替换服务名部分为具体的ip和端口,从而实现基于服务名的负载均衡调用。关于这种方式,可在文末仓库查看完整代码示例。而对于这种方式的实现原理,可以参考我之前写的这篇文章的前半部分:Spring Cloud源码分析(二)Ribbon使用WebClientWebClient是Spring 5中最新引入的,可以将其理解为reactive版的RestTemplate。下面举个具体的例子,它将实现与上面RestTemplate一样的请求调用:@EnableDiscoveryClient@SpringBootApplicationpublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } @Slf4j @RestController static class TestController { @Autowired private WebClient.Builder webClientBuilder; @GetMapping("/test”) public Mono<String> test() { Mono<String> result = webClientBuilder.build() .get() .uri(“http://alibaba-nacos-discovery-server/hello?name=didi”) .retrieve() .bodyToMono(String.class); return result; } } @Bean @LoadBalanced public WebClient.Builder loadBalancedWebClientBuilder() { return WebClient.builder(); }}可以看到,在定义WebClient.Builder的时候,也增加了@LoadBalanced注解,其原理与之前的RestTemplate时一样的。关于WebClient的完整例子也可以通过在文末的仓库中查看。使用Feign上面介绍的RestTemplate和WebClient都是Spring自己封装的工具,下面介绍一个Netflix OSS中的成员,通过它可以更方便的定义和使用服务消费客户端。下面也举一个具体的例子,其实现内容与上面两种方式结果一致:第一步:在pom.xml中增加openfeign的依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId></dependency>第二步:定义Feign客户端和使用Feign客户端:@EnableDiscoveryClient@SpringBootApplication@EnableFeignClientspublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } @Slf4j @RestController static class TestController { @Autowired Client client; @GetMapping("/test") public String test() { String result = client.hello(“didi”); return “Return : " + result; } } @FeignClient(“alibaba-nacos-discovery-server”) interface Client { @GetMapping("/hello”) String hello(@RequestParam(name = “name”) String name); }}这里主要先通过@EnableFeignClients注解开启扫描Spring Cloud Feign客户端的功能;然后又创建一个Feign的客户端接口定义。使用@FeignClient注解来指定这个接口所要调用的服务名称,接口中定义的各个函数使用Spring MVC的注解就可以来绑定服务提供方的REST接口,比如下面就是绑定alibaba-nacos-discovery-server服务的/hello接口的例子。最后,在Controller中,注入了Client接口的实现,并调用hello方法来触发对服务提供方的调用。关于使用Feign的完整例子也可以通过在文末的仓库中查看。深入思考如果之前已经用过Spring Cloud的读者,肯定会这样的感受:不论我用的是RestTempalte也好、还是用的WebClient也好,还是用的Feign也好,似乎跟我用不用Nacos没啥关系?我们在之前介绍Eureka和Consul的时候,也都是用同样的方法来实现服务调用的,不是吗?确实是这样,对于Spring Cloud老手来说,就算我们更换了Nacos作为新的服务注册中心,其实对于我们应用层面的代码是没有影响的。那么为什么Spring Cloud可以带给我们这样的完美编码体验呢?实际上,这完全归功于Spring Cloud Common的封装,由于在服务注册与发现、客户端负载均衡等方面都做了很好的抽象,而上层应用方面依赖的都是这些抽象接口,而非针对某个具体中间件的实现。所以,在Spring Cloud中,我们可以很方便的去切换服务治理方面的中间件。代码示例本文示例读者可以通过查看下面仓库:Github:https://github.com/dyc87112/SpringCloud-Learning/Gitee:https://gitee.com/didispace/SpringCloud-Learning/其中,本文的几种示例可查看下面的几个项目:alibaba-nacos-discovery-server:服务提供者,必须启动alibaba-nacos-discovery-client-resttemplate:使用RestTemplate消费alibaba-nacos-discovery-client-webclient:使用WebClient消费alibaba-nacos-discovery-client-feign:使用Feign消费如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!以下专题教程也许您会有兴趣Spring Boot基础教程Spring Cloud基础教程 ...

January 29, 2019 · 1 min · jiezi

SpringBoot实战 | 配置文件详解

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言如题,今天解析下 SpringBoot 的配置文件。自定义属性加载首先构建 SpringBoot 项目,不会的看这篇旧文 使用 IDEA 构建 Spring Boot 工程。首先在项目根目录下加入以下自定义属性:# 防止读取乱码spring.http.encoding.charset=UTF-8# 项目启动端口server.port=9999# 自定义配置com.nasus.author.name=一个优秀的废人com.nasus.article.title=SpringBoot配置文件详解使用 @value 注解读取配置文件属性:package com.nasus.bean;import lombok.Data;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;/** * Project Name:springboot_properties_demo <br/> * Package Name:com.nasus.properties <br/> * Date:2019/1/28 20:59 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * @author <a href=“turodog@foxmail.com”>nasus</a><br/> /@Data@Componentpublic class PropertiesBean { @Value("${com.nasus.author.name}") private String name; @Value("${com.nasus.article.title}") private String title; @Value("${com.nasus.doing}") private String desc;}之后新建 controller 测试自定义属性加载,代码如下:package com.nasus.controller;import com.nasus.bean.PropertiesBean;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/* * Project Name:springboot_properties_demo <br/> * Package Name:com.nasus.controller <br/> * Date:2019/1/28 21:41 <br/> * <b>Description:</b> TODO: 测试自定义属性加载<br/> * * @author <a href=“turodog@foxmail.com”>nasus</a><br/> */@RestController@RequestMapping("/test")public class TestController { @Autowired private PropertiesBean propertiesBean; @GetMapping("/getInfo") public PropertiesBean getInfo(){ return propertiesBean; }}访问 http://localhost:8080/test/getInfo 查看加载结果:可以看到,加入 @value 注解之后,配置文件的属性都被读取出来了。以前,或许我们还需要专门写一个读取配置文件的工具类才能把属性读取出来,现在有了 Spring ,我们可以直接使用 @value 就能读取了,简直不能太方便。本例源码在这:github 地址参数间的引用配置文件代码如下:# 防止读取乱码spring.http.encoding.charset=UTF-8# 项目启动端口server.port=9999# 自定义配置com.nasus.author.name=一个优秀的废人com.nasus.article.title=SpringBoot配置文件详解com.nasus.doing=${com.nasus.author.name}写文章《${com.nasus.article.title}》可以看到最后一个参数配置使用了前两个的参数配置,测试结果如下:使用随机数有时项目需求,可能我们需要配置一些随机数,比如说为了安全而随机配置的服务器端口,以及登录密钥。这时我们就可以用 SpringBoot 的 random 属性来配置随机数,比如:# 随机字符串com.nasus.article.value=${random.value}# 随机intcom.nasus.article.number=${random.int}# 随机longcom.nasus.article.bignumber=${random.long}# 10以内的随机数com.nasus.article.test1=${random.int(10)}# 10-20的随机数com.nasus.article.test2=${random.int[10,20]}使用多配置文件很多时候我们开发项目都需要很多套环境,比如有测试环境,开发环境以及生产环境。不同的环境就需要使用不同的配置文件,为此我们可以根据这 3 个环境分别新建 以下 3 个配置文件。application-dev.properties:开发环境application-test.properties:测试环境application-prod.properties:生产环境项目中默认的配置文件是 application.properties 。这时我们可以根据自己的环境去使用相应的配置文件,比如说,项目各个环境的端口必须不一样。那我们可以这样配置:application-dev.properties:开发环境server.port=6666application-test.properties:测试环境server.port=7777application-prod.properties:生产环境server.port=8888假如,现在我打包上线,那就必须用生产环境的配置文件了,这时我们可以在 默认的配置文件 application.properties 中加入以下配置即可spring.profiles.active=prod配置数据库SpringBoot 的配置文件有两种格式,一种是 .properties 格式(以上栗子都是用的这种)还有一种用的是 .yaml 格式。以下是用 yaml 方式配置。这两种格式并无好坏之分,纯看个人使用习惯。我就比较喜欢 yaml 格式,因为看起来比较简洁。spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true username: 你的数据库名称 password: 你的数据库密码 jpa: hibernate: ddl-auto: update #ddl-auto:设为update 表示每次都重新建表 show-sql: true注意事项使用 yaml 格式需要注意一点就是 键值对冒号后面,必须空一格。application.properties 配置中文值的时候,读取出来的属性值会出现乱码问题。但是 application.yml 不会出现乱码问题。原因是,Spring Boot 是以 iso-8859 的编码方式读取 application.properties 配置文件。解决第二点,只需加入 spring.http.encoding.charset=UTF-8 配置即可。后语以上就是我对 SpringBoot 配置文件的理解与使用,当然以上只是介绍了一下 SpringBoot 配置文件的几个用法,它的用法还有非常多,想要深入使用还是需要各位多多深入实践。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点好看,让更多人知道。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

January 29, 2019 · 1 min · jiezi

Feign Stub挡板和Mock

背景:在项目开发中,会有调用第三方接口的场景。当开发时,对方不愿意提供测试服务器给我们调用,或者有的接口会按调用次数进行计费。当联调时,第三方的测试服务器也可能会出现不稳定,如果他们的服务挂了,我们就一直等着服务恢复,那么这就相当影响效率了。如果我们在开发时,就定义一个挡板或者mock服务,在发起调用时,不直接调到第三方接口,而是调到我们自己的挡板代码或者mock服务,这样就可以避免这些问题了。优势:挡板代码,不需要侵入业务代码,可以根据入参做一些动态结果返回不需要专门开发一个挡板服务,并且在每次启动客户端都先启动挡板服务可以自由选择使用挡板还是Mock数据Demo详细代码,已经提交到Github,欢迎starDemo地址: https://github.com/Seifon/Fei…一、下面我就以一个第三方SMS短信接口来做演示:首先,我们写一个Feign客户端接口,正常调用第三方接口:1.定义一个SMS短信的Feign客户端接口:import cn.seifon.example.feignstubmock.dto.YunxunSmsReqDto;import cn.seifon.example.feignstubmock.dto.YunxunSmsRespDto;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;/** * @Author: Seifon * @Description: * @Date: Created in 10:24 2019/1/7 /@FeignClient(name = “smsclient”, url = “${sms.url}”, primary = false)public interface YunxunSmsFeign { /* * * @param request * @return {“code”:“0”,“failNum”:“0”,“successNum”:“1”,“msgId”:“19012516213625881”,“time”:“20190125162136”,“errorMsg”:""} * @return {“code”:“107”,“msgId”:"",“time”:“20190125162358”,“errorMsg”:“手机号码格式错误”} / @PostMapping("/msg/variable/json") YunxunSmsRespDto send(@RequestBody YunxunSmsReqDto request);}注意:@FeignClient注解里面的primary属性一定要设置为false,这是为了防止在开启Feign挡板时,出现多个Feign客户端导致启动报错。2.写一个单元测试:import cn.seifon.example.feignstubmock.dto.YunxunSmsReqDto;import cn.seifon.example.feignstubmock.dto.YunxunSmsRespDto;import cn.seifon.example.feignstubmock.feign.YunxunSmsFeign;import com.alibaba.fastjson.JSON;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class)@SpringBootTestpublic class FeignStubMockApplicationTests { @Autowired private YunxunSmsFeign yunxunSmsFeign; @Test public void feignStubMockTest() { YunxunSmsReqDto yunxunSmsReqDto=new YunxunSmsReqDto(); yunxunSmsReqDto.setAccount(“XXXXXXX”); yunxunSmsReqDto.setPassword(“XXXXXXX”); yunxunSmsReqDto.setMsg(“登录验证码:{$var},请不要对非本人透露。”); yunxunSmsReqDto.setParams(“13011112222,123456”); yunxunSmsReqDto.setReport(“true”); YunxunSmsRespDto send = yunxunSmsFeign.send(yunxunSmsReqDto); //打印结果 System.out.println(JSON.toJSON(send)); }}3.1.我们输入一个正确的手机号,拿一个成功的结果:2019-01-28 11:17:56.718 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] —> POST http://smssh1.253.com/msg/variable/json HTTP/1.12019-01-28 11:17:56.719 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] Content-Type: application/json;charset=UTF-82019-01-28 11:17:56.720 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] Content-Length: 1602019-01-28 11:17:56.720 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] 2019-01-28 11:17:56.721 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] {“account”:“XXXXXX”,“password”:“XXXXXXX”,“msg”:“登录验证码:{$var},请不要对非本人透露。”,“params”:“17311112222,123456”,“report”:“true”}2019-01-28 11:17:56.721 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] —> END HTTP (160-byte body)2019-01-28 11:17:56.958 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] <— HTTP/1.1 200 OK (236ms)2019-01-28 11:17:56.960 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] connection: keep-alive2019-01-28 11:17:56.962 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] content-length: 1092019-01-28 11:17:56.963 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] content-type: application/json;charset=UTF-82019-01-28 11:17:56.965 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] date: Mon, 28 Jan 2019 03:17:56 GMT2019-01-28 11:17:56.966 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] 2019-01-28 11:17:56.971 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] {“code”:“0”,“failNum”:“0”,“successNum”:“1”,“msgId”:“19012811175621982”,“time”:“20190128111756”,“errorMsg”:""}2019-01-28 11:17:56.972 DEBUG 6920 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] <— END HTTP (109-byte body){“code”:“0”,“failNum”:“0”,“successNum”:“1”,“msgId”:“19012811175621982”,“time”:“20190128111756”,“errorMsg”:""}此时,我们可以根据日志,看到请求的地址也是第三方的url3.2.我们输入一个错误的手机号,拿一个失败的结果:2019-01-28 11:21:15.300 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] —> POST http://smssh1.253.com/msg/variable/json HTTP/1.12019-01-28 11:21:15.301 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] Content-Type: application/json;charset=UTF-82019-01-28 11:21:15.302 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] Content-Length: 1522019-01-28 11:21:15.302 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] 2019-01-28 11:21:15.303 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] {“account”:“XXXXX”,“password”:“XXXXXXX”,“msg”:“登录验证码:{$var},请不要对非本人透露。”,“params”:“173,123456”,“report”:“true”}2019-01-28 11:21:15.303 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] —> END HTTP (152-byte body)2019-01-28 11:21:15.470 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] <— HTTP/1.1 200 OK (165ms)2019-01-28 11:21:15.471 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] connection: keep-alive2019-01-28 11:21:15.473 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] content-length: 872019-01-28 11:21:15.474 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] content-type: application/json;charset=UTF-82019-01-28 11:21:15.476 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] date: Mon, 28 Jan 2019 03:21:15 GMT2019-01-28 11:21:15.477 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] 2019-01-28 11:21:15.483 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] {“code”:“107”,“msgId”:"",“time”:“20190128112115”,“errorMsg”:“手机号码格式错误”}2019-01-28 11:21:15.484 DEBUG 5288 — [ main] c.s.e.f.feign.YunxunSmsFeign : [YunxunSmsFeign#send] <— END HTTP (87-byte body){“code”:“107”,“msgId”:"",“time”:“20190128112115”,“errorMsg”:“手机号码格式错误”}当我们知道了两种情况下出现的结果,那么我们就可以模拟响应结果啦。小技巧:我们可以先跟对方调接口,把各种响应报文保存下来,方便后面直接mock数据二、接下来进入挡板编写环节:1.编写一个YunxunSmsFeignStub类,并实现YunxunSmsFeign接口:import cn.seifon.example.feignstubmock.dto.YunxunSmsReqDto;import cn.seifon.example.feignstubmock.dto.YunxunSmsRespDto;import cn.seifon.example.feignstubmock.feign.YunxunSmsFeign;import org.apache.commons.lang3.RandomUtils;import org.apache.commons.lang3.StringUtils;import org.apache.commons.lang3.time.DateFormatUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.context.annotation.Primary;import org.springframework.stereotype.Component;import java.util.Date;/* * @Author: Seifon * @Description: * @Date: Created in 10:24 2019/1/7 /@Primary //注意:需要在原Feign接口@FeignClient注解加入primary = false 属性@Component@ConditionalOnProperty(name = “feign-stub.yunxun.sms.mode”, havingValue = “stub”)public class YunxunSmsFeignStub implements YunxunSmsFeign { private static final Logger LOG = LoggerFactory.getLogger(YunxunSmsFeignStub.class); @Override public YunxunSmsRespDto send(YunxunSmsReqDto request) { YunxunSmsRespDto yunxunSmsRespDto = new YunxunSmsRespDto(); //模拟正常响应结果 yunxunSmsRespDto.setCode(“0”); yunxunSmsRespDto.setFailNum(“0”); yunxunSmsRespDto.setSuccessNum(“1”); yunxunSmsRespDto.setMsgId(String.valueOf(RandomUtils.nextLong(19000000000000000L, 19999999999999999L))); yunxunSmsRespDto.setTime(DateFormatUtils.format(new Date(), “yyyyMMddHHmmss”)); yunxunSmsRespDto.setErrorMsg(""); String params = request.getParams(); String[] paramSplit = StringUtils.split(params, “,”); if (paramSplit[0].length() != 11) { //模拟错误响应结果 yunxunSmsRespDto.setCode(“107”); yunxunSmsRespDto.setMsgId(""); yunxunSmsRespDto.setErrorMsg(“手机号码格式错误”); } return yunxunSmsRespDto; }}注意:必须标注@Primary注解,否则启动会报错。@ConditionalOnProperty的作用就是根据application.yaml配置的相关属性,判断是否注入Spring容器2.application.yaml文件,加入下面的配置:sms: url: ‘http://smssh1.253.com’#yunxun:代表第三方系统名称,sms:代表业务名称,mode:代表Stub模式,url:代表mock服务地址feign-stub: yunxun: sms: mode: ‘stub'3.为了区分返回的内容是挡板结果,我们可以写一个AOP切面打印日志:import com.alibaba.fastjson.JSON;import org.apache.commons.lang3.StringUtils;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;/* * @Author: Seifon * @Description: * @Date: Created in 10:24 2019/1/7 /@Aspect@Componentpublic class FeignStubAspect { private static final Logger LOG = LoggerFactory.getLogger(FeignStubAspect.class); @Pointcut(“execution( cn.seifon.example.feignstubmock..stub..(..))”) public void pointCut(){} @Around(“pointCut()”) public Object around(ProceedingJoinPoint pjp){ String name = StringUtils.join(pjp.getTarget().getClass().getName(), “.”, pjp.getSignature().getName()); LOG.info("—–【{}】—- 进入挡板模式… request: 【{}】", name, JSON.toJSON(pjp.getArgs())); try { Object proceed = pjp.proceed(); LOG.info("—–【{}】—- 退出挡板模式… request: 【{}】, response: 【{}】", name, JSON.toJSON(pjp.getArgs()), JSON.toJSON(proceed)); return proceed; } catch (Throwable e) { e.printStackTrace(); } return null; }}4.1.运行之前写的单元测试代码(输入一个正确的手机号):2019-01-28 11:32:51.255 INFO 7488 — [ main] c.s.e.f.aspect.FeignStubAspect : —–【cn.seifon.example.feignstubmock.feign.stub.YunxunSmsFeignStub.send】—- 进入挡板模式… request: 【[{“msg”:“登录验证码:{$var},请不要对非本人透露。”,“password”:“XXXXXXX”,“report”:“true”,“params”:“13011112222,123456”,“account”:“XXXXXXX”}]】2019-01-28 11:32:51.975 INFO 7488 — [ main] c.s.e.f.aspect.FeignStubAspect : —–【cn.seifon.example.feignstubmock.feign.stub.YunxunSmsFeignStub.send】—- 退出挡板模式… request: 【[{“msg”:“登录验证码:{$var},请不要对非本人透露。”,“password”:“XXXXXXX”,“report”:“true”,“params”:“13011112222,123456”,“account”:“XXXXXXX”}]】, response: 【{“code”:“0”,“failNum”:“0”,“successNum”:“1”,“msgId”:“19148964234899564”,“time”:“20190128113251”,“errorMsg”:""}】{“code”:“0”,“failNum”:“0”,“successNum”:“1”,“msgId”:“19148964234899564”,“time”:“20190128113251”,“errorMsg”:""}4.2.运行之前写的单元测试代码(输入一个错误的手机号):2019-01-28 11:35:27.177 INFO 15204 — [ main] c.s.e.f.aspect.FeignStubAspect : —–【cn.seifon.example.feignstubmock.feign.stub.YunxunSmsFeignStub.send】—- 进入挡板模式… request: 【[{“msg”:“登录验证码:{$var},请不要对非本人透露。”,“password”:“XXXXXXX”,“report”:“true”,“params”:“130,123456”,“account”:“XXXXXXX”}]】2019-01-28 11:35:27.900 INFO 15204 — [ main] c.s.e.f.aspect.FeignStubAspect : —–【cn.seifon.example.feignstubmock.feign.stub.YunxunSmsFeignStub.send】—- 退出挡板模式… request: 【[{“msg”:“登录验证码:{$var},请不要对非本人透露。”,“password”:“XXXXXXX”,“report”:“true”,“params”:“130,123456”,“account”:“XXXXXXX”}]】, response: 【{“code”:“107”,“failNum”:“0”,“successNum”:“1”,“msgId”:"",“time”:“20190128113527”,“errorMsg”:“手机号码格式错误”}】{“code”:“107”,“failNum”:“0”,“successNum”:“1”,“msgId”:"",“time”:“20190128113527”,“errorMsg”:“手机号码格式错误”}以上代码就完成了一个stub挡板功能,可有时候,我们已经拿到第三方接口的返回报文,并切不想去写一大段Stub代码。那么这个时候,我们就可以选择下面的Mock方式去完成我们的功能。三、接下来进入Mock环节:1. 首先准备一个mock服务,这里我就用自己比较喜欢的一个mock工具(mock-json-server)给大家演示:1.1 安装nodejs:参看官网:http://nodejs.cn/1.2 安装mock-json-server:npm install -g mock-json-server1.3 新建mock数据文件(命名为:data.json):{ “/msg/variable/json”: { “post”: { “code”:“0”, “failNum”:“0”, “successNum”:“1”, “msgId”:“19012516213625881”, “time”:“20190125162136”, “errorMsg”:"" } }}1.4 运行:mock-json-server {path}/data.json –port=1240{path}替换为存放data.json的绝对路径1.5 如果显示如下结果,就代表mock服务运行成功:JSON Server running at http://localhost:1240/mock-json-server具体使用文档,请参考:https://www.npmjs.com/package…2. 准备工作做好后,接下来,就进入Mock正式环节:2.1 首先,我们定义一个YunxunSmsFeignMock接口,并且继承YunxunSmsFeign接口import cn.seifon.example.feignstubmock.feign.YunxunSmsFeign;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.context.annotation.Primary;import org.springframework.stereotype.Component;/** * @Author: Seifon * @Description: * @Date: Created in 10:24 2019/1/7 /@Primary //注意:需要在原Feign接口@FeignClient注解加入primary = false 属性@Component@ConditionalOnProperty(name = “feign-stub.yunxun.sms.mode”, havingValue = “mock”)@FeignClient(name = “smsclient-mock”, url = “${feign-stub.yunxun.sms.mockUrl}” ,path = “/")public interface YunxunSmsFeignMock extends YunxunSmsFeign {}注意:必须标注@Primary注解,否则启动时会报错。@FeignClient里的name属性不能跟原Feign接口名称相同,如果相同会启动报错。@ConditionalOnProperty的作用就是根据application.yaml配置的相关属性,判断是否注入Spring容器2.2 application.yaml文件,加入下面的配置:sms: url: ‘http://smssh1.253.com’#生产环境请勿添加此配置。mode说明:’’-不开启, ‘mock’-mock模式, ‘stub’-stub模式。url说明:只有mock模式需要配置调试url。fund为第三方机构,repayment是业务名称#yunxun:代表第三方系统名称,sms:代表业务名称,mode:代表挡板模式,url:代表mock服务地址feign-stub: yunxun: sms: mode: ‘mock’ mockUrl: “http://localhost:1240"2.3 为了区分返回的内容是Mock结果,我们可以写一个AOP切面打印日志:import com.alibaba.fastjson.JSON;import org.apache.commons.lang3.StringUtils;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;/* * @Author: Seifon * @Description: * @Date: Created in 10:24 2019/1/7 /@Aspect@Componentpublic class FeignMockAspect { private static final Logger LOG = LoggerFactory.getLogger(FeignMockAspect.class); @Pointcut(“execution( cn.seifon.example.feignstubmock..mock..(..))”) public void pointCut(){} @Around(“pointCut()”) public Object around(ProceedingJoinPoint pjp){ String name = StringUtils.join(pjp.getTarget().getClass().getName(), “.”, pjp.getSignature().getName()); LOG.info(”—–【{}】—- 进入Mock模式… request: 【{}】”, name, JSON.toJSON(pjp.getArgs())); try { Object proceed = pjp.proceed(); LOG.info("—–【{}】—- 退出Mock模式… request: 【{}】, response: 【{}】", name, JSON.toJSON(pjp.getArgs()), JSON.toJSON(proceed)); return proceed; } catch (Throwable e) { e.printStackTrace(); } return null; }}2.4 运行之前的单元测试类,得到如下结果:2019-01-28 16:16:35.567 INFO 8976 — [ main] c.s.e.f.aspect.FeignMockAspect : —–【com.sun.proxy.$Proxy95.send】—- 进入Mock模式… request: 【[{“msg”:“登录验证码:{$var},请不要对非本人透露。”,“password”:“XXXXXXX”,“report”:“true”,“params”:“13011112222,123456”,“account”:“XXXXXXX”}]】2019-01-28 16:16:35.934 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] —> POST http://localhost:1240/msg/variable/json HTTP/1.12019-01-28 16:16:35.935 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] Content-Type: application/json;charset=UTF-82019-01-28 16:16:35.936 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] Content-Length: 1522019-01-28 16:16:35.936 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] 2019-01-28 16:16:35.937 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] {“account”:“XXXXXXX”,“password”:“XXXXXXX”,“msg”:“登录验证码:{$var},请不要对非本人透露。”,“params”:“13011112222,123456”,“report”:“true”}2019-01-28 16:16:35.937 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] —> END HTTP (152-byte body)2019-01-28 16:16:36.021 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] <— HTTP/1.1 200 OK (82ms)2019-01-28 16:16:36.021 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] access-control-allow-origin: *2019-01-28 16:16:36.022 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] connection: keep-alive2019-01-28 16:16:36.023 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] content-length: 1092019-01-28 16:16:36.023 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] content-type: application/json; charset=utf-82019-01-28 16:16:36.024 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] date: Mon, 28 Jan 2019 08:16:36 GMT2019-01-28 16:16:36.024 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] etag: W/“6d-XqhLoZB8r6IRF2Lb6CWoIVVNhIQ"2019-01-28 16:16:36.025 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] x-content-type-options: nosniff2019-01-28 16:16:36.026 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] x-powered-by: Express2019-01-28 16:16:36.027 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] 2019-01-28 16:16:36.030 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] {“code”:“0”,“failNum”:“0”,“successNum”:“1”,“msgId”:“19012516213625881”,“time”:“20190125162136”,“errorMsg”:”"}2019-01-28 16:16:36.030 DEBUG 8976 — [ main] c.s.e.f.feign.mock.YunxunSmsFeignMock : [YunxunSmsFeignMock#send] <— END HTTP (109-byte body)2019-01-28 16:16:36.227 INFO 8976 — [ main] c.s.e.f.aspect.FeignMockAspect : —–【com.sun.proxy.$Proxy95.send】—- 退出Mock模式… request: 【[{“msg”:“登录验证码:{$var},请不要对非本人透露。”,“password”:“XXXXXXX”,“report”:“true”,“params”:“13011112222,123456”,“account”:“XXXXXXX”}]】, response: 【{“code”:“0”,“failNum”:“0”,“successNum”:“1”,“msgId”:“19012516213625881”,“time”:“20190125162136”,“errorMsg”:""}】{“code”:“0”,“failNum”:“0”,“successNum”:“1”,“msgId”:“19012516213625881”,“time”:“20190125162136”,“errorMsg”:""}说明:此时我们根据日志,会发现feign调用的url已经变为我们的Mock服务地址了。同理,如果要返回失败结果,只需要修改data.json文件,再次调用后,即可得到我们想要的结果了。四、结语:如果有什么需要改进的地方,或者不正确的地方,请在评论里面提出并指正。谢谢!Demo详细代码,已经提交到Github,欢迎starDemo地址: https://github.com/Seifon/Fei…项目结构,如图:原文地址:http://www.seifon.cn/2019/01/… ...

January 28, 2019 · 5 min · jiezi

踩坑 Spring Cloud Hystrix 线程池队列配置

背景:有一次在生产环境,突然出现了很多笔还款单被挂起,后来排查原因,发现是内部系统调用时出现了Hystrix调用异常。在开发过程中,因为核心线程数设置的比较大,没有出现这种异常。放到了测试环境,偶尔有出现这种情况,后来在网上查找解决方案,网上的方案是调整maxQueueSize属性就好了,当时调整了一下,确实有所改善。可没想到在生产环境跑了一段时间后却又出现这种了情况,此时我第一想法就是去查看maxQueueSize属性,可是maxQueueSize属性是设置值了。当时就比较纳闷了,为什么maxQueueSize属性不起作用,后来通过查看官方文档发现Hystrix还有一个queueSizeRejectionThreshold属性,这个属性是控制队列最大阈值的,而Hystrix默认只配置了5个,因此就算我们把maxQueueSize的值设置再大,也是不起作用的。两个属性必须同时配置先看一下正确的Hystrix配置姿势。application.yml:hystrix: threadpool: default: coreSize: 200 #并发执行的最大线程数,默认10 maxQueueSize: 1000 #BlockingQueue的最大队列数,默认值-1 queueSizeRejectionThreshold: 800 #即使maxQueueSize没有达到,达到queueSizeRejectionThreshold该值后,请求也会被拒绝,默认值5接下来编写一个测试类,来验证几种错误配置,看看会出现什么情况。测试类代码(A调用方):/** * @Author: XiongFeng * @Description: * @Date: Created in 11:12 2018/6/11 /public class RepaymentHelperTest extends FundApplicationTests { @Autowired RepaymentHelper repaymentHelper; @Autowired private RouterFeign routerFeign; @Test public void hystrixTest() throws InterruptedException { for (int i = 0; i < 135; i++) { new Thread(new Runnable() { @Override public void run() { job(); } }).start(); } Thread.currentThread().join(); } public void job() { String repaymentNo = “xf1002”; String transNo = “T4324324234”; String reqNo = “xf1002”; String begintime = “20180831130030”; String endtime = “20180831130050”; TransRecQueryReqDto transRecQueryReqDto = new TransRecQueryReqDto(); transRecQueryReqDto.setTransNo(transNo); transRecQueryReqDto.setBeginTime(begintime); transRecQueryReqDto.setEndTime(endtime); transRecQueryReqDto.setReqNo(reqNo); Resp<List<TransRecDto>> queryTransRecListResp = routerFeign.queryTransRec(new Req<>(repaymentNo, “2018080200000002”, null, null, transRecQueryReqDto)); System.out.println(String.format(“获取结果为:【%s】”, JsonUtil.toJson(queryTransRecListResp))); }}这个测试类的作用就是创建135个线程,通过RouterFeign类并发请求B服务方,看看请求结果是否出现异常。Feign调用代码:@FeignClient(value = “${core.name}”, fallbackFactory = RouterFeignBackFactory.class, path = “/router”)public interface RouterFeign { /* * 代扣结果查询 * @param transRecQueryReqDtoReq * @return / @PostMapping("/queryTransRec") Resp<List<TransRecDto>> queryTransRec(@RequestBody Req<TransRecQueryReqDto> transRecQueryReqDtoReq);}这个类,就是通过Feign方式去调用B服务方的客户端服务提供方代码(B服务方):/* * @Author: XiongFeng * @Description: * @Date: Created in 16:04 2018/5/24 */@Api(“还款服务”)@RefreshScope@RestController@RequestMapping("/router")public class TestController { private static Logger logger = LoggerFactory.getLogger(TestController.class); // 计数器 private static AtomicInteger count = new AtomicInteger(1); @ApiOperation(value = “代扣结果查询”) @PostMapping("/queryTransRec") Resp<List<TransRecDto>> queryTransRec(@RequestBody Req<TransRecQueryReqDto> transRecQueryReqDtoReq) throws InterruptedException { System.out.println(String.format(“查询支付结果……计数: %s”, count.getAndAdd(1))); Thread.sleep(500); return Resp.success(RespStatus.SUCCESS.getDesc(), null); } 这个类的作用,就是一个服务提供方,计数并返回结果。下面我们看一下几种错误的配置。案例一(将核心线程数调低,最大队列数调大一点,但是队列拒绝阈值设置小一点):hystrix: threadpool: default: coreSize: 10 maxQueueSize: 1000 queueSizeRejectionThreshold: 20此时的结果:左窗口是B服务方,右窗口是A调用方。从结果可以看出,调用135次,成功32次左右,其余线程全部抛异常。案例二(将核心线程数调低,最大队列数调小一点,但是队列拒绝阈值设置大一点):hystrix: threadpool: default: coreSize: 10 maxQueueSize: 15 queueSizeRejectionThreshold: 2000此时的结果:java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@7d6d472b rejected from java.util.concurrent.ThreadPoolExecutor@17f8bcb7[Running, pool size = 3, active threads = 3, queued tasks = 15, completed tasks = 0]左窗口是B服务方,右窗口是A调用方。从结果可以看出,调用135次,成功25次左右,其余线程全部抛异常。。案例三(将核心线程数调低,最大队列数调大一点,但是队列拒绝阈值不设置值):hystrix: threadpool: default: coreSize: 10 maxQueueSize: 1500此时的结果:java.util.concurrent.RejectedExecutionException: Rejected command because thread-pool queueSize is at rejection threshold.左窗口是B服务方,右窗口是A调用方。此时的结果和案例一的情况一样,调用135次,成功47次左右,其余线程全部抛异常。报错跟案例一一样案例四(将核心线程数调低,最大队列数不设值,但是队列拒绝阈值设置的比较大):hystrix: threadpool: default: coreSize: 10 queueSizeRejectionThreshold: 1000此时的结果:java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@23d268ea rejected from java.util.concurrent.ThreadPoolExecutor@66d0e2f4[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0] at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063) at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830) at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379) at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)左窗口是B服务方,右窗口是A调用方。此时的结果和案例二的情况一样,调用135次,成功10次左右,其余线程全部抛异常。报错跟案例二一样下面来看一看正确的配置案例案例一:将核心线程数调低,最大队列数和队列拒绝阈值的值都设置大一点):hystrix: threadpool: default: coreSize: 10 maxQueueSize: 1500 queueSizeRejectionThreshold: 1000此时的结果:左窗口是B服务方,右窗口是A调用方。此时的结果就完全正常了,并发请求了135次,全部成功!结论:官方默认队列阈值只有5个, 如果要调整队列,必须同时修改maxQueueSize和queueSizeRejectionThreshold属性的值,否则都会出现异常!参考文档:Spring Hystrix 官方文档原文地址:http://www.seifon.cn/2018/12/08/踩坑-Spring-Cloud-Hystrix-线程池队列配置/ ...

January 28, 2019 · 2 min · jiezi

Spring单例模式与NIO思考

本人java开发,今天在研究秒杀问题的时候,突然间脑子里就产生了这样的思考。 众所周知,Spring默认是单例模式,那么单例模式有什么优缺点呢? 优点一:创建的对象较少!在Tomcat模型为BIO时,会对每一个请求创建一个线程,如果创建的线程数较大,每一个线程中都有@Autowire这种注解,那么创建的对象就会非常之多。另外,如果对象B注入到对象A的属性中,如果对象A被创建了多个对象,那么对象B也会被创建多个,层层依赖。 当然如果是NIO,就没有单例的必要了,因为它只会用一个线程了来处理数据请求,这也注定NIO不能用来IO密集型操作。 缺点一:由于只有一个对象,如果属于类的成员变量,则会被多次调用,类似于类的静态变量。 缺点二:BIO中,如果你在这个对象中的方法上使用了Synchronized,代表锁住的是该对象。

January 28, 2019 · 1 min · jiezi

猫头鹰的深夜翻译:Spring REST服务异常处理

前言这篇教程主要专注于如何优雅的处理WEB中的异常。虽然我们可以手动的设置ResponseStatus ,但是还有更加优雅的方式将这部分逻辑隔离开来。Spring提供了整个应用层面的异常处理的抽象,并且只是要求您添加一些注释 - 它会处理其他所有内容。下面是一些代码的示例如何手动处理异常下面的代码中, DogController将返回一个ResponseEntity实例,该实例中包含返回的数据和HttpStatus属性如果没有抛出任何异常,则下面的代码将会返回List<Dog>数据作为响应体,以及200作为状态码对于DogsNotFoundException,它返回空的响应体和404状态码对于DogServiceException, 它返回500状态码和空的响应体@RestController@RequestMapping("/dogs")public class DogsController { @Autowired private final DogsService service; @GetMapping public ResponseEntity<List<Dog>> getDogs() { List<Dog> dogs; try { dogs = service.getDogs(); } catch (DogsServiceException ex) { return new ResponseEntity<>(null, null, HttpStatus.INTERNAL_SERVER_ERROR); } catch (DogsNotFoundException ex) { return new ResponseEntity<>(null, null, HttpStatus.NOT_FOUND); } return new ResponseEntity<>(dogs, HttpStatus.OK); }}这种处理异常的方式最大的问题就在于代码的重复。catch部分的代码在很多其它地方也会使用到(比如删除,更新等操作)Controller AdviceSpring提供了一种更好的解决方法,也就是Controller Advice。它将处理异常的代码在应用层面上集中管理。现在我们的的DogsController的代码更加简单清晰了:import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;import static org.springframework.http.HttpStatus.NOT_FOUND;@ControllerAdvicepublic class DogsServiceErrorAdvice { @ExceptionHandler({RuntimeException.class}) public ResponseEntity<String> handleRunTimeException(RuntimeException e) { return error(INTERNAL_SERVER_ERROR, e); } @ExceptionHandler({DogsNotFoundException.class}) public ResponseEntity<String> handleNotFoundException(DogsNotFoundException e) { return error(NOT_FOUND, e); } @ExceptionHandler({DogsServiceException.class}) public ResponseEntity<String> handleDogsServiceException(DogsServiceException e){ return error(INTERNAL_SERVER_ERROR, e); } private ResponseEntity<String> error(HttpStatus status, Exception e) { log.error(“Exception : “, e); return ResponseEntity.status(status).body(e.getMessage()); }}handleRunTimeException:这个方法会处理所有的RuntimeException并返回INTERNAL_SERVER_ERROR状态码handleNotFoundException: 这个方法会处理DogsNotFoundException并返回NOT_FOUND状态码。handleDogsServiceException: 这个方法会处理DogServiceException并返回INTERNAL_SERVER_ERROR状态码这种实现的关键就在于在代码中捕获需检查异常并将其作为RuntimeException抛出。还可以用@ResponseStatus将异常映射成状态码@ControllerAdvicepublic class DogsServiceErrorAdvice { @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler({DogsNotFoundException.class}) public void handle(DogsNotFoundException e) {} @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler({DogsServiceException.class, SQLException.class, NullPointerException.class}) public void handle() {} @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler({DogsServiceValidationException.class}) public void handle(DogsServiceValidationException e) {}}在自定义的异常上添加状态码@ResponseStatus(HttpStatus.NOT_FOUND)public class DogsNotFoundException extends RuntimeException { public DogsNotFoundException(String message) { super(message); }} ...

January 27, 2019 · 1 min · jiezi

Spring Cloud OAuth2 资源服务器CheckToken 源码解析

CheckToken的目的 当用户携带token 请求资源服务器的资源时, OAuth2AuthenticationProcessingFilter 拦截token,进行token 和userdetails 过程,把无状态的token 转化成用户信息。 ## 详解OAuth2AuthenticationManager.authenticate(),filter执行判断的入口当用户携带token 去请求微服务模块,被资源服务器拦截调用RemoteTokenServices.loadAuthentication ,执行所谓的check-token过程。源码如下 CheckToken 处理逻辑很简单,就是调用redisTokenStore 查询token的合法性,及其返回用户的部分信息 (username )继续看 返回给 RemoteTokenServices.loadAuthentication 最后一句tokenConverter.extractAuthentication 解析组装服务端返回的信息最重要的 userTokenConverter.extractAuthentication(map);最重要的一步,是否判断是否有userDetailsService实现,如果有 的话去查根据 返回的username 查询一次全部的用户信息,没有实现直接返回username,这也是很多时候问的为什么只能查询到username 也就是 EnablePigxResourceServer.details true 和false 的区别。那根据的你问题,继续看 UerDetailsServiceImpl.loadUserByUsername 根据用户名去换取用户全部信息。关于pig基于Spring Cloud、oAuth2.0开发基于Vue前后分离的开发平台,支持账号、短信、SSO等多种登录,提供配套视频开发教程。 https://gitee.com/log4j/pig

January 25, 2019 · 1 min · jiezi

Spring5:@Autowired注解、@Resource注解和@Service注解

什么是注解传统的Spring做法是使用.xml文件来对bean进行注入或者是配置aop、事物,这么做有两个缺点:1、如果所有的内容都配置在.xml文件中,那么.xml文件将会十分庞大;如果按需求分开.xml文件,那么.xml文件又会非常多。总之这将导致配置文件的可读性与可维护性变得很低2、在开发中在.java文件和.xml文件之间不断切换,是一件麻烦的事,同时这种思维上的不连贯也会降低开发的效率为了解决这两个问题,Spring引入了注解,通过"@XXX"的方式,让注解与Java Bean紧密结合,既大大减少了配置文件的体积,又增加了Java Bean的可读性与内聚性。本篇文章,讲讲最重要的三个Spring注解,也就是@Autowired、@Resource和@Service,希望能通过有限的篇幅说清楚这三个注解的用法。不使用注解先看一个不使用注解的Spring示例,在这个示例的基础上,改成注解版本的,这样也能看出使用与不使用注解之间的区别,先定义一个老虎:public class Tiger{ private String tigerName = “TigerKing”; public String toString() { return “TigerName:” + tigerName; }}再定义一个猴子:public class Monkey{ private String monkeyName = “MonkeyKing”; public String toString() { return “MonkeyName:” + monkeyName; }}定义一个动物园:public class Zoo{ private Tiger tiger; private Monkey monkey; public void setTiger(Tiger tiger) { this.tiger = tiger; } public void setMonkey(Monkey monkey) { this.monkey = monkey; } public Tiger getTiger() { return tiger; } public Monkey getMonkey() { return monkey; } public String toString() { return tiger + “\n” + monkey; }}spring的配置文件这么写:<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://www.springframework.org/schema/beans" xmlns:context=“http://www.springframework.org/schema/context" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd" default-autowire=“byType”> <bean id=“zoo” class=“com.xrq.bean.Zoo” > <property name=“tiger” ref=“tiger” /> <property name=“monkey” ref=“monkey” /> </bean> <bean id=“tiger” class=“com.xrq.domain.Tiger” /> <bean id=“monkey” class=“com.xrq.domain.Monkey” /> </beans>都很熟悉,权当复习一遍了。@Autowired@Autowired顾名思义,就是自动装配,其作用是为了消除代码Java代码里面的getter/setter与bean属性中的property。当然,getter看个人需求,如果私有属性需要对外提供的话,应当予以保留。因此,引入@Autowired注解,先看一下spring配置文件怎么写:<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://www.springframework.org/schema/beans" xmlns:context=“http://www.springframework.org/schema/context" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd"> <context:component-scan base-package=“com.xrq” /> <bean id=“zoo” class=“com.xrq.bean.Zoo” /> <bean id=“tiger” class=“com.xrq.domain.Tiger” /> <bean id=“monkey” class=“com.xrq.domain.Monkey” /> </beans>注意第10行,使用必须告诉spring一下我要使用注解了,告诉的方式有很多,<context:component-scan base-package=“xxx” />是一种最简单的,spring会自动扫描xxx路径下的注解。看到第12行,原来zoo里面应当注入两个属性tiger、monkey,现在不需要注入了。再看下,Zoo.java也很方便,把getter/setter都可以去掉:public class Zoo{ @Autowired private Tiger tiger; @Autowired private Monkey monkey; public String toString() { return tiger + “\n” + monkey; }}这里@Autowired注解的意思就是,当Spring发现@Autowired注解时,将自动在代码上下文中找到和其匹配(默认是类型匹配)的Bean,并自动注入到相应的地方去。有一个细节性的问题是,假如bean里面有两个property,Zoo.java里面又去掉了属性的getter/setter并使用@Autowired注解标注这两个属性那会怎么样?答案是Spring会按照xml优先的原则去Zoo.java中寻找这两个属性的getter/setter,导致的结果就是初始化bean报错。OK,假设此时我把.xml文件的13行、14行两行给去掉,再运行,会抛出异常:Exception in thread “main” org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘Zoo’: Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.xrq.domain.Tiger com.xrq.bean.Zoo.ttiger; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.xrq.domain.Tiger] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)} at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:334) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1214) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:543) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482) at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:305) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:301) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:196) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:835) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:537) at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139) at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83) at com.xrq.test.MyTest.main(MyTest.java:13)Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.xrq.domain.Tiger com.xrq.bean.Zoo.ttiger; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.xrq.domain.Tiger] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)} at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:571) at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:331) … 13 moreCaused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.xrq.domain.Tiger] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)} at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoSuchBeanDefinitionException(DefaultListableBeanFactory.java:1373) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1119) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1014) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:543) … 15 more 因为,@Autowired注解要去寻找的是一个Bean,Tiger和 Monkey的Bean定义都给去掉了,自然就不是一个Bean了,Spring容器找不到也很好理解。那么,如果属性找不到我不想让Spring容器抛 出异常,而就是显示null,可以吗?可以的,其实异常信息里面也给出了提示了,就是将@Autowired注解的required属性设置为false 即可:public class Zoo{ @Autowired(required = false) private Tiger tiger; @Autowired(required = false) private Monkey monkey; public String toString() { return tiger + “\n” + monkey; }}@Autowired接口注入上面的比较简单,我们只是简单注入一个Java类,那么如果有一个接口,有多个实现,Bean里引用的是接口名,又该怎么做呢?比如有一个Car接口:public interface Car{ public String carName();}两个实现类BMW和Benz:@Servicepublic class BMW implements Car{ public String carName() { return “BMW car”; }}@Servicepublic class Benz implements Car{ public String carName() { return “Benz car”; }}写一个CarFactory,引用Car:@Servicepublic class CarFactory{ @Autowired private Car car; public String toString() { return car.carName(); }}不用说,一定是报错的,Car接口有两个实现类,Spring并不知道应当引用哪个实现类。这种情况通常有两个解决办法:1、删除其中一个实现类,Spring会自动去base-package下寻找Car接口的实现类,发现Car接口只有一个实现类,便会直接引用这个实现类2、实现类就是有多个该怎么办?此时可以使用@Qualifier注解:@Servicepublic class CarFactory{ @Autowired @Qualifier(“BMW”) private Car car; public String toString() { return car.carName(); }}注意@Qualifier注解括号里面的应当是Car接口实现类的类名,我之前试的时候一直以为是bean的名字,所以写了"bMW”,结果一直报错。@Resource把@Resource注解放在@Autowired下面说,是因为它们作用非常相似,这个就简单说了,例子过后点明一下@Resource和@Autowired的区别。先看一下@Resource,直接写Zoo.java了:@Servicepublic class Zoo{ @Resource(name = “tiger”) private Tiger tiger; @Resource(type = Monkey.class) private Monkey monkey; public String toString() { return tiger + “\n” + monkey; }}这是详细一些的用法,说一下@Resource的装配顺序:1、@Resource后面没有任何内容,默认通过name属性去匹配bean,找不到再按type去匹配2、指定了name或者type则根据指定的类型去匹配bean3、指定了name和type则根据指定的name和type去匹配bean,任何一个不匹配都将报错然后,区分一下@Autowired和@Resource两个注解的区别:1、@Autowired默认按照byType方式进行bean匹配,@Resource默认按照byName方式进行bean匹配2、@Autowired是Spring的注解,@Resource是J2EE的注解,这个看一下导入注解的时候这两个注解的包名就一清二楚了Spring属于第三方的,J2EE是Java自己的东西,因此,建议使用@Resource注解,以减少代码和Spring之间的耦合。@Service上面这个例子,还可以继续简化,因为spring的配置文件里面还有12行~14行三个bean,下一步的简化是把这三个bean也给去掉,使得spring配置文件里面只有一个自动扫描的标签,增强Java代码的内聚性并进一步减少配置文件。要继续简化,可以使用@Service。先看一下配置文件,当然是全部删除了:<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://www.springframework.org/schema/beans" xmlns:context=“http://www.springframework.org/schema/context" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd"> <context:component-scan base-package=“com.xrq” /> </beans>是不是感觉很爽?起码我觉得是的。OK,下面以Zoo.java为例,其余的Monkey.java和Tiger.java都一样:@Servicepublic class Zoo{ @Autowired private Tiger ttiger; @Autowired private Monkey mmonkey; public String toString() { return ttiger + “\n” + mmonkey; }}这样,Zoo.java在Spring容器中存在的形式就是"zoo”,即可以通过ApplicationContext的getBean(“zoo”)方法来得到Zoo.java。@Service注解,其实做了两件事情:1、声明Zoo.java是一个bean,这点很重要,因为Zoo.java是一个bean,其他的类才可以使用@Autowired将Zoo作为一个成员变量自动注入2、Zoo.java在bean中的id是"zoo”,即类名且首字母小写如果,我不想用这种形式怎么办,就想让Zoo.java在Spring容器中的名字叫做"Zoo”,可以的:@Service@Scope(“prototype”)public class Zoo{ @Autowired private Monkey monkey; @Autowired private Tiger tiger; public String toString() { return “MonkeyName:” + monkey + “\nTigerName:” + tiger; }}这样,就可以通过ApplicationContext的getBean(“zoo”)方法来得到Zoo.java了。这里我还多加了一个@Scope注解,应该 很好理解。因为Spring默认产生的bean是单例的,假如我不想使用单例怎么办,xml文件里面可以在bean里面配置scope属性。注解也是一 样,配置@Scope即可,默认是"singleton"即单例,“prototype"表示原型即每次都会new一个新的出来。补充细节最后再补充一个我发现的细节。假如animal包下有Tiger、domain包下也有Tiger,它们二者都加了@Service注解,那么在Zoo.java中即使明确表示我要引用的是domain包下的Tiger,程序运行的时候依然会报错。细想,其实这很好理解,两个Tiger都使 用@Service注解标注,意味着两个Bean的名字都是"tiger”,那么我在Zoo.java中自动装配的是哪个Tiger呢?不明确,因 此,Spring容器会抛出BeanDefinitionStoreException异常,Caused by:Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name ‘monkey’ for bean class [com.xrq.domain.Monkey] conflicts with existing, non-compatible bean definition of same name and class [com.xrq.animal.Monkey]转载于:https://www.cnblogs.com/szlbm…作者:IT·达人 ...

January 25, 2019 · 3 min · jiezi

Spring Boot 实战 | 如何使用 IDEA 构建 Spring Boot 工程

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言新年立了个 flag,好好运营这个公众号。具体来说,就是每周要写两篇文章在这个号发表。刚立的 flag 可不能这么快打脸。下面送上本周第一篇。本文我们将介绍嵌入 Intellij IDEA 中的 Spring Initializr 工具,它同Web提供的创建功能一样,可以帮助我们快速的构建出一个基础的Spring Boot工程。什么是 SpringBoot ?SpringBoot 官方有一句话可以概括这个问题。那就是「约定大于配置」。这句话什么意思?相信学过 Spring 的人都知道,我们要手动写一大堆的 xml 文件用于配置,集成项目,才能使这个项目具备 web 的功能。而 SpringBoot 做了那些没有它你也会去做的Spring Bean配置。它使用「约定大于配置」的理念让你的项目快速运行起来。使用 Spring Boot 很容易创建一个独立运行(运行jar,内嵌Servlet容器)、准生产级别的基于 Spring 框架的项目,使用 Spring Boot 你可以不用或者只需要很少的Spring配置。如果说 Spring 是一辆汽车的引擎,那 SpringMVC 就给这辆汽车装上了轮子,而 SpringBoot 的出现就相当于赋予了这辆汽车自动驾驶的功能。如何使用 IDEA 构建 SpringBoot 工程?第一步,当然是安装 Intellij IDEA (傻瓜式教程,请自行百度)。点击菜单栏 File ➤New➤Project ➤ 选择 Spring Initializr 创建界面如下图,可以看到图中 default 指定的 Initializr Service URL 就是 Spring 官方提供的 Spring Initializr 工具地址,一般默认即可,所以这里创建的工程实际上也是基于它的 Web 工具来实现的。点击 next 进入下一步,可以看见这里要我们选择的就是关于工程的一些信息:Group 顾名思义就是你的公司名,一般是填写com.公司名。Artifact groupId 和 artifactId 是maven管理项目包时用作区分的字段,就像是地图上的坐标。这里填写项目名即可。Type 就是构建的项目类型,意思就是你希望你的项目使用什么工具构建,可选 maven 和 gradle 一般选 maven。Language 顾名思义就是你的项目用啥语言开发,可选 Java、Groovy、KotlinPackaging 就是你希望你的项目打成什么形式的包,可选 Jar、War SpringBoot 项目一般选 JarJava Version 意指项目使用的 java 版本,根据你的需要选择。Version 项目的初始版本,默认即可。Name 项目名称。Description 项目描述,默认即可。Package 包名,填完 Group 和 Artifact 后自动生成,默认即可。点击 Next 进入下一步,这一步就是选你的项目依赖包,前文所说的「约定大于配置」就体现在这里。进入选择S pring Boot 版本和依赖管理的窗口。在这里值的我们关注的是,它不仅包含了 Spring Boot Starter POMs 中的各个依赖,还包含了 Spring Cloud 的各种依赖。比如,你需要集成前端模板功能,你就到 Template Engines 选项卡上,勾选你想要访问的前端模板引擎 ,项目需要访问数据库,就到 SQL 选项卡,旋转你项目里使用的数据库类型。选择完成并加以简单的配置,项目就具备了集成前端模板能力与数据库访问能力。这里注意一下,无论你选择哪些依赖包,其中 web 选项卡下的 Web 是必选的。这个包是整个项目的基础。这个包里面集成了 Spring 、WebMvc 、tomcat 以及其他各种基本能力。点击 Next 进入下一步,这一步没啥好说的。就是让你确认自己的项目名以及项目路径。确认无误,点 Finish 完成创建即可。Intellij IDEA 中的 Spring Initializr 是基于官方 Web 实现,但是通过工具来进行调用并直接将结果构建到我们的本地文件系统中,让整个构建流程变得更加顺畅。后语我为什么要写这种这么简单的教程?是这样的,我始终认为比我聪明的人有很多,但比我笨的人也不少。在中国有很多你认为众所周知的事,其实有一车人根本不知道,这篇文章哪怕只帮助到一个人,足矣。之后我打算出一个 SpringBoot 系列的教程,敬请关注与指正,本人也是一个小菜鸟在打怪升级中,如本文有不正确的地方,烦请指正。一起学习一起进步。以上就是使用 IDEA 创建 SpringBoot 的过程,希望对你们有帮助。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点好看,让更多人知道。 ...

January 25, 2019 · 1 min · jiezi

SpringBoot自动配置原理

前言只有光头才能变强。文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y回顾前面Spring的文章(以学习的顺序排好):Spring入门这一篇就够了Spring【依赖注入】就是这么简单Spring【AOP模块】就这么简单Spring【DAO模块】知识要点SpringMVC入门就这么简单SpringMVC【开发Controller】详解SpringMVC【参数绑定、数据回显、文件上传】SpringMVC【校验器、统一处理异常、RESTful、拦截器】SpringBoot就是这么简单SpringData JPA就是这么简单Spring IOC知识点一网打尽!Spring AOP就是这么简单啦外行人都能看懂的SpringCloud,错过了血亏!作为一名Java程序员,就不可能不了解SpringBoot,如果不了解(赶紧学!)一、SpringBoot的自动配置原理不知道大家第一次搭SpringBoot环境的时候,有没有觉得非常简单。无须各种的配置文件,无须各种繁杂的pom坐标,一个main方法,就能run起来了。与其他框架整合也贼方便,使用EnableXXXXX注解就可以搞起来了!所以今天来讲讲SpringBoot是如何实现自动配置的~1.1三个重要的注解我们可以发现,在使用main()启动SpringBoot的时候,只有一个注解@SpringBootApplication我们可以点击进去@SpringBootApplication注解中看看,可以发现有三个注解是比较重要的:@SpringBootConfiguration:我们点进去以后可以发现底层是Configuration注解,说白了就是支持JavaConfig的方式来进行配置(使用Configuration配置类等同于XML文件)。@EnableAutoConfiguration:开启自动配置功能(后文详解)@ComponentScan:这个注解,学过Spring的同学应该对它不会陌生,就是扫描注解,默认是扫描当前类下的package。将@Controller/@Service/@Component/@Repository等注解加载到IOC容器中。所以,Java3yApplication类可以被我们当做是这样的:@SpringBootConfiguration@EnableAutoConfiguration@ComponentScanpublic class Java3yApplication { public static void main(String[] args) { SpringApplication.run(Java3yApplication.class, args); }}1.2重点EnableAutoConfiguration我们知道SpringBoot可以帮我们减少很多的配置,也肯定听过“约定大于配置”这么一句话,那SpringBoot是怎么做的呢?其实靠的就是@EnableAutoConfiguration注解。简单来说,这个注解可以帮助我们自动载入应用程序所需要的所有默认配置。介绍有一句说:if you have tomcat-embedded.jar on your classpath you are likely to want a TomcatServletWebServerFactory如果你的类路径下有tomcat-embedded.jar包,那么你很可能就需要TomcatServletWebServerFactory我们点进去看一下,发现有两个比较重要的注解:@AutoConfigurationPackage:自动配置包@Import:给IOC容器导入组件1.2.1AutoConfigurationPackage网上将这个@AutoConfigurationPackage注解解释成自动配置包,我们也看看@AutoConfigurationPackage里边有什么:我们可以发现,依靠的还是@Import注解,再点进去查看,我们发现重要的就是以下的代码:@Overridepublic void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { register(registry, new PackageImport(metadata).getPackageName());}在默认的情况下就是将:主配置类(@SpringBootApplication)的所在包及其子包里边的组件扫描到Spring容器中。看完这句话,会不会觉得,这不就是ComponentScan的功能吗?这俩不就重复了吗?我开始也有这个疑问,直到我看到文档的这句话:it will be used when scanning for code @Entity classes.It is generally recommended that you place EnableAutoConfiguration (if you’renot using @SpringBootApplication) in a root package so that all sub-packagesand classes can be searched.比如说,你用了Spring Data JPA,可能会在实体类上写@Entity注解。这个@Entity注解由@AutoConfigurationPackage扫描并加载,而我们平时开发用的@Controller/@Service/@Component/@Repository这些注解是由ComponentScan来扫描并加载的。简单理解:这二者扫描的对象是不一样的。1.2.2回到Import我们回到@Import(AutoConfigurationImportSelector.class)这句代码上,再点进去AutoConfigurationImportSelector.class看看具体的实现是什么:我们再进去看一下这些配置信息是从哪里来的(进去getCandidateConfigurations方法):这里包装了一层,我们看到的只是通过SpringFactoriesLoader来加载,还没看到关键信息,继续进去:简单梳理:FACTORIES_RESOURCE_LOCATION的值是META-INF/spring.factoriesSpring启动的时候会扫描所有jar路径下的META-INF/spring.factories,将其文件包装成Properties对象从Properties对象获取到key值为EnableAutoConfiguration的数据,然后添加到容器里边。最后我们会默认加载113个默认的配置类:有兴趣的同学可以去翻一下这些文件以及配置类哦:1.3总结@SpringBootApplication等同于下面三个注解:@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan其中@EnableAutoConfiguration是关键(启用自动配置),内部实际上就去加载META-INF/spring.factories文件的信息,然后筛选出以EnableAutoConfiguration为key的数据,加载到IOC容器中,实现自动配置功能!官网文档参考:https://docs.spring.io/spring-boot/docs/2.2.0.BUILD-SNAPSHOT/reference/html/using-spring-boot.html#using-boot-structuring-your-code英语不好的同学可以像我一样,对照着来看:最后乐于输出干货的Java技术公众号:Java3y。公众号内有200多篇原创技术文章、海量视频资源、精美脑图,不妨来关注一下!觉得我的文章写得不错,不妨点一下赞! ...

January 24, 2019 · 1 min · jiezi

SpringBoot 实战 | 使用 LomBok

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言上周去了开年会,去的地方是温泉度假村。老实说,我是无感的,90% 是因为没中奖(老板太抠,两百人只抽三个奖),10 % 是因为从小泡温泉长大没啥感觉。LomBok 是什么?LomBok 是一个插件,它能通过注解帮我们消除那些必须要写但是重复的代码,比如setter,getter,构造函数之类的方法。试想一个场景,在项目开发中,我们往往需要定义大量的数据库实体或者某些工具 Bean ,每一个 Bean 都需要我们编写 getter 、setter 方法、构造方法以及 toString 等方法。这是一个非常繁琐的过程。程序员准则,做三遍以上的重复事情就必须想办法让它自动化了。所以今天给你们介绍一个牛逼的插件「LomBok」。LomBok 常用注解简介@Data:注解在类上,将类提供的所有属性都添加get、set方法,并添加、equals、canEquals、hashCode、toString方法@Setter:注解在类上,为所有属性添加set方法、注解在属性上为该属性提供set方法@Getter:注解在类上,为所有的属性添加get方法、注解在属性上为该属性提供get方法@NotNull:在参数中使用时,如果调用时传了null值,就会抛出空指针异常@Synchronized 用于方法,可以锁定指定的对象,如果不指定,则默认创建一个对象锁定@Log作用于类,创建一个log属性@Builder:使用builder模式创建对象@NoArgsConstructor:创建一个无参构造函数@AllArgsConstructor:创建一个全参构造函数@ToStirng:创建一个toString方法@Accessors(chain = true)使用链式设置属性,set方法返回的是this对象。@RequiredArgsConstructor:创建对象@UtilityClass:工具类@ExtensionMethod:设置父类@FieldDefaults:设置属性的使用范围,如private、public等,也可以设置属性是否被final修饰。@Cleanup: 关闭流、连接点。@EqualsAndHashCode:重写equals和hashcode方法。@toString:创建toString方法。如何安装?1、直接从 http://plugins.jetbrains.com/ 下载,然后放到IDEA 安装文件下面的 plugins,然后重启 IDEA。2、在 IDEA 的 settings(windows)或者Preferences(mac),下找到 plugins 菜单,点击 Browse repositories,如下图第二步搜索 LomBok 点击 Install (我这里已经安装了,但是有更新。所以显示的是 Update 按钮)重启 IDEA 。代码演示新建 SpringBoot 项目,不会构建的看这篇文章 使用 IDEA 构建 Spring Boot 工程 ,构建时勾选 web 依赖和 Lombok 依赖,完整 pom 如下:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.2.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.nasus</groupId> <artifactId>lombok</artifactId> <version>0.0.1-SNAPSHOT</version> <name>lombok</name> <description>lombok_demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>新建一个 Student 类,这是使用 lombok 的类,代码如下:package com.nasus.entity;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;/** * Project Name:springboot_lomnok_demo <br/> * Package Name:com.nasus.entity <br/> * Date:2019/1/23 0023 14:32 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * * @author <a href=“mailto:chenzy@eastcom-sw.com”>chenzy</a><br/> * Copyright Notice ========================================================= * This file contains proprietary information of Eastcom Technologies Co. Ltd. * Copying or reproduction without prior written approval is prohibited. * Copyright (c) 2019 ======================================================= /@Data //自动生成set/get方法,toString方法,equals方法,hashCode方法,不带参数的构造方法 @AllArgsConstructor //自动生成构造方法 @NoArgsConstructor //自动生成构造方法 public class Student { private String id; private String name; private int age;}新建一个 StudentNoLombok 类,这是没有使用 lombok 的类(此类在项目中无用处,只为与使用 Lombok 的类对比,突出使用 LomBok 类代码的简洁),代码如下:package com.nasus.entity;import java.util.Objects;/* * Project Name:springboot_lomnok_demo <br/> * Package Name:com.nasus.entity <br/> * Date:2019/1/23 0023 14:34 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * * @author <a href=“mailto:chenzy@eastcom-sw.com”>chenzy</a><br/> * Copyright Notice ========================================================= * This file contains proprietary information of Eastcom Technologies Co. Ltd. * Copying or reproduction without prior written approval is prohibited. * Copyright (c) 2019 ======================================================= /public class StudentNoLombok { private String id; private String name; private int age; public StudentNoLombok() { } public StudentNoLombok(String id, String name, int age) { this.id = id; this.name = name; this.age = age; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof StudentNoLombok)) { return false; } StudentNoLombok that = (StudentNoLombok) o; return age == that.age && Objects.equals(id, that.id) && Objects.equals(name, that.name); } @Override public int hashCode() { return Objects.hash(id, name, age); } @Override public String toString() { return “StudentNoLombok{” + “id=’” + id + ‘'’ + “, name=’” + name + ‘'’ + “, age=” + age + ‘}’; }}从上面两个类对比可以看出,使用 LomBok 插件的类比不使用简洁美观得多。新建 StudentController 类,代码如下:package com.nasus.controller;import com.nasus.entity.Student;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/* * Project Name:springboot_lomnok_demo <br/> * Package Name:com.nasus.controller <br/> * Date:2019/1/23 0023 14:37 <br/> * <b>Description:</b> TODO: 描述该类的作用 <br/> * * @author <a href=“mailto:chenzy@eastcom-sw.com”>chenzy</a><br/> * Copyright Notice ========================================================= * This file contains proprietary information of Eastcom Technologies Co. Ltd. * Copying or reproduction without prior written approval is prohibited. * Copyright (c) 2019 ======================================================= */@RestController@RequestMapping("/student”)public class StudentController { @GetMapping("/testLombok”) public Student getStudent(){ Student student = new Student(); student.setId(“6666666666”); student.setAge(24); student.setName(“陈志远”); System.out.println(student.toString()); return student; }}启动项目,访问地址 http://localhost:8080/student/testLombok 浏览器输出如下:控制台输出如下:由上面两个输出结果可以看出,LomBok 插件起作用了。即使 Student 没有重写 toString 方法,依然可以调用,而且结果和重写了 toString 方法一样。当项目很庞大的时候,往往基础的 Bean 也非常多。建议看到这篇文章的你可以在项目中把 LomBok 用起来。最后奉上 项目完整代码后语以上就是我对 LomBok 插件的理解与使用,希望对你们有帮助。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点好看,让更多人知道。 ...

January 24, 2019 · 3 min · jiezi

Spring Cloud Alibaba基础教程:Nacos 生产级版本 0.8.0

昨晚Nacos社区发布了第一个生产级版本:0.8.0。由于该版本除了Bug修复之外,还提供了几个生产管理非常重要的特性,所以觉得还是有必要写一篇讲讲这次升级,在后续的文章中也都将以0.8.0版本为基础。升级的理由如Nacos官方的发布文档中描述的那样,本版本将支持非常重要的三个特性:第一,用户登录。在过去版本的Nacos中,用户是可以直接访问Nacos的页面的,我们需要通过网络或者代理手段来增加这样的安全性控制,在该版本后就不需要了。第二:Prometheus的支持。对于一个基础中间件来说,完善的监控指标输出在生产环境是必须的,通过在/prometheus端点上暴露监控指标,以保障Nacos集群的正常服务。第三:Namespace的支持。服务发现的功能将支持Namespace的隔离,可以方便的在一套Nacos集群下,实现多环境服务发现的隔离等。发布清单可见文末参考资料。这些重要功能的具体使用,后续继续连载,敬请期待!安装与使用如果之前有看过《Spring Cloud Alibaba基础教程:使用Nacos实现服务注册与发现》的话,只需要将Nacos安装部分把安装包替换成 0.8.0 版本即可。下载地址:https://github.com/alibaba/na…下载完成之后,解压。根据不同平台,执行不同命令,启动单机版Nacos服务:Linux/Unix/Mac:sh startup.sh -m standaloneWindows:cmd startup.cmd -m standalonestartup.sh脚本位于Nacos解压后的bin目录下。启动完成之后,访问:http://127.0.0.1:8848/nacos/,可以进入Nacos的登录页面,具体如下;evernotecid://1F3482F0-828B-4B97-918F-4856C2E684DF/appyinxiangcom/113183/ENResource/p2004默认情况下,用户名与密码都为nacos,登录后进入控制台如下:evernotecid://1F3482F0-828B-4B97-918F-4856C2E684DF/appyinxiangcom/113183/ENResource/p2005对于应用端,不需要做任何改动,就能够适配新版本。如果还没有对接过Nacos,那么看看这篇吧:《Spring Cloud Alibaba基础教程:使用Nacos实现服务注册与发现》参考资料Nacos官方文档0.8.0版本说明

January 24, 2019 · 1 min · jiezi

spring: loadBeanDefinitions 时序图

plantuml code@startuml"xmlBeanFactory: XmlBeanFactory" -> “reader:XmlBeanDefinitionReader” : 1: loadBeanDefinitions(resource)activate “xmlBeanFactory: XmlBeanFactory"activate “reader:XmlBeanDefinitionReader"“reader:XmlBeanDefinitionReader” -> “encodedResource:EncodedResource” : 1.1: new EncodedResource(resource)activate “encodedResource:EncodedResource"“encodedResource:EncodedResource” –> “reader:XmlBeanDefinitionReader” : 1.2: encodedResource:EncodedResourcedeactivate “encodedResource:EncodedResource"“reader:XmlBeanDefinitionReader” -> “reader:XmlBeanDefinitionReader”: 1.3: loadBeanDefinitions(encodedResource)activate “reader:XmlBeanDefinitionReader"“reader:XmlBeanDefinitionReader” -> “encodedResource:EncodedResource”: 1.3.1: getResource()activate “encodedResource:EncodedResource"“encodedResource:EncodedResource” –> “reader:XmlBeanDefinitionReader” : 1.3.2: resource:Resourcedeactivate “encodedResource:EncodedResource"“reader:XmlBeanDefinitionReader” -> “resource:Resource” : 1.3.3: getInputStream()activate “resource:Resource"“resource:Resource” –> “reader:XmlBeanDefinitionReader”: inputStream: InputStreamdeactivate “resource:Resource"“reader:XmlBeanDefinitionReader” -> “inputSource: InputSource”: 1.3.5: new InputStream(inputSource)activate “inputSource: InputSource"“inputSource: InputSource” –> “reader:XmlBeanDefinitionReader”: 1.3.6: inputSource: InputSourcedeactivate “inputSource: InputSource"“reader:XmlBeanDefinitionReader” -> “reader:XmlBeanDefinitionReader”: 1.3.7: loadBeanDefinitions((inputSource, encodedResource.getResource()))activate “reader:XmlBeanDefinitionReader"“reader:XmlBeanDefinitionReader” –> “reader:XmlBeanDefinitionReader”: 1.3.3: loadedBeanDefinitionNum:intdeactivate “reader:XmlBeanDefinitionReader"“reader:XmlBeanDefinitionReader” –> “reader:XmlBeanDefinitionReader”: 1.3.4: loadedBeanDefinitionNum:intdeactivate “reader:XmlBeanDefinitionReader"“reader:XmlBeanDefinitionReader” –> “xmlBeanFactory: XmlBeanFactory”: 1.3.4: loadedBeanDefinitionNum:intdeactivate “reader:XmlBeanDefinitionReader"deactivate “xmlBeanFactory: XmlBeanFactory”@endumlimg ...

January 23, 2019 · 1 min · jiezi

hibernate和jdbc的渊源

简单介绍jdbc我们学习Java数据库操作时,一般会设计到jdbc的操作,这是一位程序员最基本的素养。jdbc以其优美的代码和高性能,将瞬时态的javabean对象转化为持久态的SQL数据。但是,每次SQL操作都需要建立和关闭连接,这势必会消耗大量的资源开销;如果我们自行创建连接池,假如每个项目都这样做,势必搞死的了。同时,我们将SQL语句预编译在PreparedStatement中,这个类可以使用占位符,避免SQL注入,当然,后面说到的hibernate的占位符的原理也是这样,同时,mybatis的#{}占位符原理也是如此。预编译的语句是原生的SQL语句,比如更新语句:private static int update(Student student) { Connection conn = getConn(); int i = 0; String sql = “update students set Age=’” + student.getAge() + “’ where Name=’” + student.getName() + “’”; PreparedStatement pstmt; try { pstmt = (PreparedStatement) conn.prepareStatement(sql); i = pstmt.executeUpdate(); System.out.println(“resutl: " + i); pstmt.close(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } return i;}上面的sql语句没有使用占位符,如果我们少了varchar类型的单引号,就会保存失败。在这种情况下,如果写了很多句代码,最后因为一个单引号,导致代码失败,对于程序员来说,无疑是很伤自信心的事。如果涉及到了事务,那么就会非常的麻烦,一旦一个原子操作的语句出现错误,那么事务就不会提交,自信心就会跌倒低谷。然而,这仅仅是更新语句,如果是多表联合查询语句,那么就要写很多代码了。具体的jdbc的操作,可以参考这篇文章:jdbc操作。因而,我们在肯定它的优点时,也不应该规避他的缺点。随着工业化步伐的推进,每个数据库往往涉及到很多表,每张表有可能会关联到其他表,如果我们还是按照jdbc的方式操作,这无疑是增加了开发效率。所以,有人厌倦这种复杂繁琐、效率低下的操作,于是,写出了著名的hibernate框架,封装了底层的jdbc操作,以下是jdbc的优缺点:由上图可以看见,jdbc不适合公司的开发,公司毕竟以最少的开发成本来创造更多的利益。这就出现了痛点,商机伴随着痛点的出现。因而,应世而生了hibernate这个框架。即便没有hibernate的框架,也会有其他框架生成。hibernate的底层封装了jdbc,比如说jdbc为了防止sql注入,一般会有占位符,hibernate也会有响应的占位符。hibernate是orm(object relational mapping)的一种,即对象关系映射。什么对象关系映射通俗地来说,对象在pojo中可以指Javabean,关系可以指MySQL的关系型数据库的表字段与javabean对象属性的关系。映射可以用我们高中所学的函数映射,即Javabean顺时态的对象映射到数据库的持久态的数据对象。我们都知道javabean在内存中的数据是瞬时状态或游离状态,就像是宇宙星空中的一颗颗行星,除非行星被其他行星所吸引,才有可能不成为游离的行星。瞬时状态(游离状态)的javabean对象的生命周期随着进程的关闭或者方法的结束而结束。如果当前javabean对象与gcRoots没有直接或间接的关系,其有可能会被gc回收。我们就没办法长久地存储数据,这是一个非常头疼的问题。假如我们使用文件来存储数据,但是文件操作起来非常麻烦,而且数据格式不是很整洁,不利于后期的维护等。因而,横空出世了数据库。我们可以使用数据库存储数据,数据库中的数据才是持久数据(数据库的持久性),除非人为的删除。这里有个问题——怎么将瞬时状态(游离状态)的数据转化为持久状态的数据,肯定需要一个连接Javabean和数据库的桥梁,于是乎就有了上面的jdbc。单独来说mysql,mysql是一个远程服务器。我们在向mysql传输数据时,并不是对象的方式传输,而是以字节码的方式传输数据。为了保证数据准确的传输,我们一般会序列化当前对象,用序列号标志这个唯一的对象。如果,我们不想存储某个属性,它是有数据库中的数据拼接而成的,我们大可不用序列化这个属性,可以使用Transient来修饰。比如下面的获取图片的路径,其实就是服务器图片的文件夹地址和图片的名称拼接而成的。当然,你想存储这个属性,也可以存储这个属性。我们有时候图片的路由的字节很长,这样会占用MySQL的内存。因而,学会取舍,未尝不是一个明智之举。@Entity@Table(name = “core_picture”)public class Picture extends BaseTenantObj { 。。。。 @Transient public String getLocaleUrl() { return relativeFolder.endsWith(”/") ? relativeFolder + name : relativeFolder + “/” + name; } 。。。。}网上流传盛广的对象关系映射的框架(orm)有hibernate、mybatis等。重点说说hibernate和mybatis吧。hibernate是墨尔本的一位厌倦重复的javabean的程序员编写而成的,mybatis是appache旗下的一个子产品,其都是封装了jdbc底层操作的orm框架。但hibernate更注重javabean与数据表之间的关系,比如我们可以使用注解生成数据表,也可以通过注解的方式设置字段的类型、注释等。他将javabean分成了游离态、顺时态、持久态等,hibernate根据这三种状态来触及javabean对象的数据。而mybatis更多的是注重SQL语句的书写,也就是说主要是pojo与SQL语句的数据交互,对此,Hibernate对查询对象有着良好的管理机制,用户无需关心SQL。一旦SQL语句的移动,有可能会影响字段的不对应,因而,mybatis移植性没有hibernate好。mybatis接触的是底层SQL数据的书写,hibernate根据javabean的参数来生成SQL语句,再将SQL查询的结果封装成pojo,因而,mybatis的性能相来说优于hibernate,但这也不是绝对的。性能还要根据你的表的设计结构、SQL语句的封装、网络、带宽等等。我只是抛砖引玉,它们具体的区别,可以参考这篇文档。mybatis和hibernate的优缺点。hibernate和mybatis之间的区别,也是很多公司提问面试者的问题。但是真正熟知他们区别的人,一般是技术选型的架构师。如果你只是负责开发,不需要了解它们的区别,因为他们都封装了jdbc。所以,你不论使用谁,都还是比较容易的。然而,很多公司的HR提问这种问题很死板,HR并不懂技术,他们只是照本宣科的提问。如果你照本宣科的回答,它们觉着你很厉害。但是,如果是一个懂技术的人提问你,如果你只是临时背了它们的区别,而没有相应的工作经验,他们会问的让你手足无措。hibernate的讲解因为我们公司使用的是hibernate,我在这里简单地介绍下hibernate。但相对于jdbc来说,hibernate框架还是比较重的。为什么说他重,因为它集成了太多的东西,看如下的hibernate架构图:你会发现最上层使我们的java应用程序的开始,比如web的Tomcat服务器的启动,比如main方法的启动等。紧接着就是需要(needing)持久化的对象,这里为什么说是需要,而不是持久化的对象。只有保存到文件、数据库中的数据才是持久化的想通过hibernate,我们可以毫不费力的将瞬时状态的数据转化为持久状态的数据,下面便是hibernate的内部操作数据。其一般是这样的流程:我个人画的这个地址的图片如果你是用过jdbc连接数据库的话,我们一般是这样写的:package com.zby.jdbc.config;import com.zby.util.exception.TableException;import org.apache.commons.lang3.StringUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.io.IOException;import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLException;import java.util.Properties;/** * Created By zby on 22:12 2019/1/5 /public class InitJdbcFactory { private static Properties properties = new Properties(); private static Logger logger = LoggerFactory.getLogger(InitJdbcFactory.class); static { try { //因为使用的类加载器获取配置文件,因而,配置文件需要放在classpath下面, // 方能读到数据 properties.load(Thread.currentThread().getContextClassLoader(). getResourceAsStream("./jdbc.properties")); } catch (IOException e) { logger.info(“初始化jdbc失败”); e.printStackTrace(); } } public static Connection createConnection() { String drivers = properties.getProperty(“jdbc.driver”); if (StringUtils.isBlank(drivers)) { drivers = “com.mysql.jdbc.Driver”; } String url = properties.getProperty(“jdbc.url”); String username = properties.getProperty(“jdbc.username”); String password = properties.getProperty(“jdbc.password”); try { Class.forName(drivers); return DriverManager.getConnection(url, username, password); } catch (ClassNotFoundException e) { logger.error(InitColTable.class.getName() + “:连接数据库的找不到驱动类”); throw new TableException(InitColTable.class.getName() + “: 连接数据库的找不到驱动类”, e); } catch (SQLException e) { logger.error(InitColTable.class.getName() + “:连接数据库的sql异常”); throw new TableException(InitColTable.class.getName() + “连接数据库的sql异常”, e); } }}hibernate一般这样连接数据库:public class HibernateUtils { private static SessionFactory sf; //静态初始化 static{ //【1】加载配置文件 Configuration conf = new Configuration().configure(); //【2】 根据Configuration 配置信息创建 SessionFactory sf = conf.buildSessionFactory(); //如果这里使用了hook虚拟机,需要关闭hook虚拟机 Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { System.out.println(“虚拟机关闭!释放资源”); sf.close(); } })); } /* * 采用openSession创建一个与数据库的连接会话,但这种方式需要手动关闭与数据库的session连接(会话), * 如果不关闭,则当前session占用数据库的资源无法释放,最后导致系统崩溃。 * / public static org.hibernate.Session openSession(){ //【3】 获得session Session session = sf.openSession(); return session; } / * 这种方式连接数据库,当提交事务时,会自动关闭当前会话; * 同时,创建session连接时,autoCloseSessionEnabled和flushBeforeCompletionEnabled都为true, * 并且session会同sessionFactory组成一个map以sessionFactory为主键绑定到当前线程。 * 采用getCurrentSession()需要在Hibernate.cfg.xml配置文件中加入如下配置: 如果是本地事物,及JDBC一个数据库: <propety name=”Hibernate.current_session_context_class”>thread</propety> 如果是全局事物,及jta事物、多个数据库资源或事物资源: <propety name=”Hibernate.current_session_context_class”>jta</propety> 使用spring的getHiberanteTemplate 就不需要考虑事务管理和session关闭的问题: * / public static org.hibernate.Session getCurrentSession(){ //【3】 获得session Session session = sf.getCurrentSession(); return session; }}mybatis的配置文件:public class DBTools { public static SqlSessionFactory sessionFactory; static{ try { //使用MyBatis提供的Resources类加载mybatis的配置文件 Reader reader = Resources.getResourceAsReader(“mybatis.cfg.xml”); //构建sqlSession的工厂 sessionFactory = new SqlSessionFactoryBuilder().build(reader); } catch (Exception e) { e.printStackTrace(); } } //创建能执行映射文件中sql的sqlSession public static SqlSession getSession(){ return sessionFactory.openSession(); }}hibernate、mybatis、jdbc创建于数据库的连接方式虽然不同,但最红都是为了将顺时态的数据写入到数据库中的,但这里主要说的是hibernate。但是hibernate已经封装了这些属性,我们可以在configuration在配置驱动类、用户名、用户密码等。再通过sessionFactory创建session会话,也就是加载Connection的物理连接,创建sql的事务,然后执行一系列的事务操作,如果事务全部成功即可成功,但反有一个失败都会失败。jdbc是最基础的操作,但是,万丈高楼平地起,只有基础打牢,才能走的更远。因为hibernate封装了这些基础,我们操作数据库不用考虑底层如何实现的,因而,从某种程度上来说,hibernate还是比较重的。hibernate为什么会重?比如我们执行插入语句,可以使用save、saveOrUpdate,merge等方法。需要将实体bean通过反射转化为mysql的识别的SQL语句,同时,查询虽然用到了反射,但是最后转化出来的还是object的根对象,这时需要将根对象转化为当前对象,返回给客户端。虽然很笨重,但是文件配置好了,可以大大地提高开发效率。毕竟现在的服务器的性能都比较好,公司追求的是高效率的开发,而往往不那么看重性能,除非用户提出性能的问题。说说merge和saveOrUpdatemerge方法与saveOrUpdate从功能上类似,但他们仍有区别。现在有这样一种情况:我们先通过session的get方法得到一个对象u,然后关掉session,再打开一个session并执行saveOrUpdate(u)。此时我们可以看到抛出异常:Exception in thread “main” org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session,即在session缓存中不允许有两个id相同的对象。不过若使用merge方法则不会异常,其实从merge的中文意思(合并)我们就可以理解了。我们重点说说merge。merge方法产生的效果和saveOrUpdate方法相似。这是hibernate的原码: void saveOrUpdate(Object var1); void saveOrUpdate(String var1, Object var2); public Object merge(Object object); public Object merge(String var1, Object var2);前者不用返回任何数据,后者返回的是持久化的对象。如果根据hibernate的三种状态,比如顺时态、持久态、游离态来说明这个问题,会比较难以理解,现在,根据参数有无id或id是否已经存在来理解merge,而且从执行他们两个方法而产生的sql语句来看是一样的。参数实例对象没有提供id或提供的id在数据库找不到对应的行数据,这时merger将执行插入操作吗,产的SQL语句如下: Hibernate: select max(uid) from user Hibernate: insert into hibernate1.user (name, age, uid) values (?, ?, ?)一般情况下,我们新new一个对象,或者从前端向后端传输javabean序列化的对象时,都不会存在当前对象的id,如果使用merge的话,就会向数据库中插入一条数据。参数实例对象的id在数据库中已经存在,此时又有两种情况(1)如果对象有改动,则执行更新操作,产生sql语句有: Hibernate: select user0_.uid as uid0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from hibernate1.user user0_ where user0_.uid=? Hibernate: update hibernate1.user set name=?, age=? where uid=?(2)如果对象未改动,则执行查询操作,产生的语句有: Hibernate: select user0_.uid as uid0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from hibernate1.user user0_ where user0_.uid=?以上三种是什么情况呢?如果我们保存用户时,数据库中肯定不存在即将添加的用户,也就是说,我们的保存用户就是向数据库中添加用户。但是,其也会跟着某些属性, 比如说用户需要头像,这是多对一的关系,一个用户可能多个对象,然而,头像的关联的id不是放在用户表中的,而是放在用户扩张表中的,这便用到了切分表的概念。题外话,我们有时会用到快照表,比如商品快照等,也许,我们购买商品时,商品是一个价格,但随后商品的价格变了,我们需要退商品时,就不应该用到商品改变后的价格了,而是商品改变前的价格。扩展表存放用户额外的信息,也就是用户非必须的信息,比如说昵称,性别,真实姓名,头像等。 因而,头像是图片类型,使用hibernate的注解方式,创建用户表、图片表、用户扩展表。如下所示(部分重要信息已省略) //用户头像 @Entity @Table(name = “core_user”) public class User extends BaseTenantConfObj { / * 扩展表 * / @OneToOne(mappedBy = “user”, fetch = FetchType.LAZY, cascade = CascadeType.ALL) private UserExt userExt; } //用户扩展表的头像属性 @Entity @Table(name = “core_user_ext”) public class UserExt implements Serializable { /* * 头像 / @ManyToOne @JoinColumn(name = “head_logo”) private Picture headLogo; } //图片表 @Entity @Table(name = “core_picture”) public class Picture extends BaseTenantObj { private static Logger logger = LoggerFactory.getLogger(Picture.class); 。。。。。。 //图片存放在第三方的相对url。 @Column(name = “remote_relative_url”, length = 300) private String remoteRelativeUrl; // 图片大小 @Column(length = 8) private Integer size; /* * 图片所属类型 * user_logo:用户头像 / @Column(name = “host_type”, length = 58) private String hostType; //照片描述 @Column(name = “description”, length = 255) private String description; } 前端代码是://这里使用到了vue.js的代码,v-model数据的双向绑定,前端的HTML代码<tr> <td class=“v-n-top”>头像:</td> <td> <div class=“clearfix”> <input type=“hidden” name=“headLogo.id” v-model=“pageData.userInfo.logo.id”/> <img class=“img-user-head fl” :src="(pageData.userInfo&&pageData.userInfo.logo&&pageData.userInfo.logo.path) ? (constant.imgPre + pageData.userInfo.logo.path) : ‘img/user-head-default.png’"> <div class=“img-btn-group”> <button cflag=“upImg” type=“button” class=“btn btn-sm btn-up”>点击上传</button> <button cflag=“delImg” type=“button” class=“btn btn-white btn-sm btn-del”>删除</button> </div> </div> <p class=“img-standard”>推荐尺寸800800;支持.jpg, .jpeg, .bmp, .png类型文件,1M以内</p> </td></tr>//这里用到了js代码,这里用到了js的属性方法upImg: function(me) { Utils.asyncImg({ fn: function(data) { vm.pageData.userInfo.logo = { path: data.remoteRelativeUrl, id: data.id }; } });},上传头像是异步提交,如果用户上传了头像,我们在提交用户信息时,通过“headLogo.id”可以获取当前头像的持久化的图片对象,hibernate首先会根据属性headLogo找到图片表,根据当前头像的id找到图片表中对应的行数据,为什么可以根据id来获取行数据?-图片表的表结构信息从这张图片可以看出,图片采用默认的存储引擎,也就是InnoDB存储引擎,而不是myiSam的存储引擎。我们都知道这两种存储引擎的区别,如果不知道的话,可以参这篇文章MySQL中MyISAM和InnoDB的索引方式以及区别与选择。innodb采用BTREE树的数据结构方式存储,它有0到1直接前继和0到n个直接后继,这是什么意思呢?一棵树当前叶子节点的直接父节点只有一个,但其儿子节点可以一个都没有,也可以有1个、2个、3个……,如果mysql采用的多对一的方式存储的话,你就会发现某条外键下有许多行数据,比如如下的这张表这张表记录的是项目的完成情况,一般有预约阶段,合同已签,合同完成等等。你会发现project_id=163的行数据不止一条,我们通过查询语句:SELECT zpp.* from zq_project_process zpp WHERE zpp.is_deleted = 0 AND zpp.project_id=163,查找速度非常快。为什么这么快呢,因为我刚开始说的innodb采用的BTREE树结构存储,其数据是放在当前索引下,什么意思?innodb的存储引擎是以索引作为当前节点值,比如说银行卡表的有个主键索引,备注,如果我们没有创建任何索引,如果采用的innodb的数据引擎,其内部会创建一个默认的行索引,这就像我们在创建javabean对象时,没有创建构造器,其内部会自动创建一个构造器的道理是一样的。其数据是怎么存储的呢,如下图所示:mysql银行卡数据其内部存储数据其所对应的行数据是放在当前索引下的,因而,我们取数据不是取表中中的数据,而是取当前主键索引下的数据。项目进程表如同银行卡的主键索引,只不过其有三个索引,分别是主键索引和两个外键索引,如图所示的索引:索引名是hibernate自动生成的一个名字,索引是项目id、类型两个索引。因为我们不是从表中取数据,而是从当前索引的节点下取数据,所以速度当然快了。索引有主键索引、外键索引、联合索引等,但一般情况下,主键索引和外键索引使用频率比较高。同时,innodb存储引擎的支持事务操作,这是非常重要,我们操作数据库,一般都是设计事务的操作,这也mysql默认的存储引擎是innodb。我们通过主键获取图片的行数据,就像通过主键获取银行卡的行数据。这也是上面所说的,根据是否有id来确定是插入还是更新数据。通过图片主键id获取该行数据后,hibernate会在堆中创建一个picture对象。用户扩展表的headLogo属性指向这个图片对象的首地址,从而创建一个持久化的图片对象。前台异步提交头像时,如果是编辑头像,hibernate会觉擦到当前对象的属性发生了改变,于是,在提交事务时将修改后的游离态的类保存到数据库中。如果我们保存或修改用户时,我们保存的就是持久化的对象,其内部会自动存储持久化头像的id。这是hibernate底层所做的,我们不需要关心。再举一个hibernate事务提交的例子:我们在支付当中搞得提现事务时,调用第三方支付的SDK时,第三方一般会用我们到订单号,比如我们调用连连支付这个第三方支付的SDK的payRequestBean的实体类:/** * Created By zby on 11:00 2018/12/11 * 发送到连连支付的body内容 /@Data@AllArgsConstructor@NoArgsConstructorpublic class PaymentRequestBean extends BaseRequestBean { /* * 版本号 / @NonNull private String api_version; /* * 银行账户 / @NonNull private String card_no; /* * 对私 / @NonNull private String flag_card; /* * 回调接口 / @NonNull private String notify_url; /* * 商户订单号 / @NonNull private String no_order; /* * 商户订单时间,时间格式为 YYYYMMddHHmmss / @NonNull private String dt_order; /* * 交易金额 / @NonNull public String money_order; /* * 收款方姓名 即账户名 / @NonNull private String acct_name; /* * 收款银行姓名 / private String bank_name; /* * 订单描述 ,代币类型 + 支付 / @NonNull private String info_order; /* * 收款备注 / private String memo; /* * 支行名称 */ private String brabank_name;}商户订单号是必传的,且这个订单号是我们这边提供的,这就有一个问题了,怎么避免订单号不重复呢?我们可以在提现记录表事先存储一个订单号,订单号的规则如下:“WD” +系统时间+ 当前提现记录的id,这个id怎么拿到呢?既然底层使用的是merge方法,我们事先不创建订单号,先保存这个记录,其返回的是已经创建好的持久化的对象,该持久化的对象肯定有提现主键的id。我们拿到该持久化对象的主键id,便可以封装订单号,再次保存这个持久化的对象,其内部会执行类似以下的操作:Hibernate: select user0_.uid as uid0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from hibernate1.user user0_ where user0_.uid=? Hibernate: update hibernate1.user set name=?, age=? where uid=?代码如下: withdraw.setWithdrawStatus(WITHDRAW_STATUS_WAIT_PAY); withdraw.setApplyTime(currentTime); withdraw.setExchangeHasThisMember(hasThisMember ? YES : NO); withdraw = withdrawDao.save(withdraw); withdraw.setOrderNo(“WD” + DateUtil.ISO_DATETIME_FORMAT_NONE.format(currentTime) + withdraw.getId()); withdrawDao.save(withdraw);不管哪种情况,merge的返回值都是一个持久化的实例对象,但对于参数而言不会改变它的状态。 ...

January 23, 2019 · 4 min · jiezi

Spring Data JPA REST Query Criteria

案例概述在本系列的第一篇文章中,我们将探索一种用于REST API的简单查询语言。我们将充分利用Spring作为REST API,并将JPA 2标准用于持久性方面。为什么使用查询语言?因为 - 对于任何复杂的API - 通过非常简单的字段搜索/过滤资源是不够的。查询语言更灵活,允许您精确过滤所需的资源。User Entity首先 - 让我们提出我们将用于过滤器/搜索API的简单实体 - 一个基本用户:@Entitypublic class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age;}使用CriteriaBuilder进行过滤现在 - 让我们深入研究问题 - 持久层中的查询。构建查询抽象是一个平衡问题。一方面我们需要很大的灵活性,另一方面我们需要保持复杂性可管理性。高级别,功能很简单 - 你传递一些约束,你会得到一些结果。让我们看看它是如何工作的:@Repositorypublic class UserDAO implements IUserDAO { @PersistenceContext private EntityManager entityManager; @Override public List<User> searchUser(List<SearchCriteria> params) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = builder.createQuery(User.class); Root r = query.from(User.class); Predicate predicate = builder.conjunction(); for (SearchCriteria param : params) { if (param.getOperation().equalsIgnoreCase(">")) { predicate = builder.and(predicate, builder.greaterThanOrEqualTo(r.get(param.getKey()), param.getValue().toString())); } else if (param.getOperation().equalsIgnoreCase("<")) { predicate = builder.and(predicate, builder.lessThanOrEqualTo(r.get(param.getKey()), param.getValue().toString())); } else if (param.getOperation().equalsIgnoreCase(":")) { if (r.get(param.getKey()).getJavaType() == String.class) { predicate = builder.and(predicate, builder.like(r.get(param.getKey()), "%" + param.getValue() + “%”)); } else { predicate = builder.and(predicate, builder.equal(r.get(param.getKey()), param.getValue())); } } } query.where(predicate); List<User> result = entityManager.createQuery(query).getResultList(); return result; } @Override public void save(User entity) { entityManager.persist(entity); }}如您所见,searchUser API获取非常简单的约束列表,根据这些约束组成查询,执行搜索并返回结果。约束类也很简单:public class SearchCriteria { private String key; private String operation; private Object value;}该SearchCriteria实现持有我们的查询参数:key:用于保存字段名称 - 例如:firstName,age,…等。operation:用于保持操作 - 例如:Equality,less,…等。value:用于保存字段值 - 例如:john,25,…等。测试搜索查询现在 - 让我们测试我们的搜索机制,以确保它可用。首先 - 让我们通过添加两个用户来初始化我们的数据库以进行测试 - 如下例所示:@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes = { PersistenceConfig.class })@Transactional@TransactionConfigurationpublic class JPACriteriaQueryTest { @Autowired private IUserDAO userApi; private User userJohn; private User userTom; @Before public void init() { userJohn = new User(); userJohn.setFirstName(“John”); userJohn.setLastName(“Doe”); userJohn.setEmail(“john@doe.com”); userJohn.setAge(22); userApi.save(userJohn); userTom = new User(); userTom.setFirstName(“Tom”); userTom.setLastName(“Doe”); userTom.setEmail(“tom@doe.com”); userTom.setAge(26); userApi.save(userTom); }}现在,让我们得到一个具有特定firstName和lastName的用户 - 如下例所示:@Testpublic void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria(“firstName”, “:”, “John”)); params.add(new SearchCriteria(“lastName”, “:”, “Doe”)); List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results)));}接下来,让我们得到一个具有相同lastName的用户列表:@Testpublic void givenLast_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria(“lastName”, “:”, “Doe”)); List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, isIn(results));}接下来,让age大于或等于25的用户:@Testpublic void givenLastAndAge_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria(“lastName”, “:”, “Doe”)); params.add(new SearchCriteria(“age”, “>”, “25”)); List<User> results = userApi.searchUser(params); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results)));}接下来,让我们搜索实际不存在的用户:@Testpublic void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria(“firstName”, “:”, “Adam”)); params.add(new SearchCriteria(“lastName”, “:”, “Fox”)); List<User> results = userApi.searchUser(params); assertThat(userJohn, not(isIn(results))); assertThat(userTom, not(isIn(results)));}最后,让我们搜索仅给出部分firstName的用户:@Testpublic void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria(“firstName”, “:”, “jo”)); List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results)));}UserController最后,让我们现在将这种灵活搜索的持久性支持连接到我们的REST API。我们将设置一个简单的UserController - 使用findAll()使用“search”传递整个搜索/过滤器表达式:@Controllerpublic class UserController { @Autowired private IUserDao api; @RequestMapping(method = RequestMethod.GET, value = “/users”) @ResponseBody public List<User> findAll(@RequestParam(value = “search”, required = false) String search) { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); if (search != null) { Pattern pattern = Pattern.compile("(\w+?)(:|<|>)(\w+?),"); Matcher matcher = pattern.matcher(search + “,”); while (matcher.find()) { params.add(new SearchCriteria(matcher.group(1), matcher.group(2), matcher.group(3))); } } return api.searchUser(params); }}请注意我们如何简单地从搜索表达式中创建搜索条件对象。我们现在正处于开始使用API并确保一切正常工作的地步:http://localhost:8080/users?search=lastName:doe,age>25这是它的回应:[{ “id”:2, “firstName”:“tom”, “lastName”:“doe”, “email”:“tom@doe.com”, “age”:26}]案例结论这个简单而强大的实现支持对REST API进行相当多的智能过滤。是的—它仍然很粗糙,可以改进(下一篇文章将对此进行改进)—但它是在api上实现这种过滤功能的坚实起点。 ...

January 23, 2019 · 2 min · jiezi

Spring Data JPA REST Query QueryDSL

案例概述在本教程中,我们将研究使用Spring Data JPA和Querydsl为REST API构建查询语言。在本系列的前两篇文章中,我们使用JPA Criteria和Spring Data JPA规范构建了相同的搜索/过滤功能。那么 - 为什么要使用查询语言?因为 - 对于任何复杂的API来说 - 通过非常简单的字段搜索/过滤资源是不够的。查询语言更灵活,允许您精确过滤所需的资源。Querydsl配置首先 - 让我们看看如何配置我们的项目以使用Querydsl。我们需要将以下依赖项添加到pom.xml:<dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>4.1.4</version> </dependency><dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>4.1.4</version> </dependency>我们还需要配置APT - Annotation处理工具 - 插件如下:<plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions></plugin>MyUser Entity接下来 - 让我们看一下我们将在Search API中使用的“MyUser”实体:@Entitypublic class MyUser { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age;}使用PathBuilder自定义Predicate现在 - 让我们根据一些任意约束创建一个自定义Predicate。我们在这里使用PathBuilder而不是自动生成的Q类型,因为我们需要动态创建路径以获得更抽象的用法:public class MyUserPredicate { private SearchCriteria criteria; public BooleanExpression getPredicate() { PathBuilder<MyUser> entityPath = new PathBuilder<>(MyUser.class, “user”); if (isNumeric(criteria.getValue().toString())) { NumberPath<Integer> path = entityPath.getNumber(criteria.getKey(), Integer.class); int value = Integer.parseInt(criteria.getValue().toString()); switch (criteria.getOperation()) { case “:”: return path.eq(value); case “>”: return path.goe(value); case “<”: return path.loe(value); } } else { StringPath path = entityPath.getString(criteria.getKey()); if (criteria.getOperation().equalsIgnoreCase(":")) { return path.containsIgnoreCase(criteria.getValue().toString()); } } return null; }}请注意Predicate的实现是通常如何处理多种类型的操作。这是因为查询语言根据定义是一种开放式语言,您可以使用任何支持的操作对任何字段进行过滤。为了表示这种开放式过滤标准,我们使用了一个简单但非常灵活的实现 - SearchCriteria:public class SearchCriteria { private String key; private String operation; private Object value;}key:用于保存字段名称 - 例如:firstName,age,…等。operation:用于保持操作 - 例如:Equality,less,…等。value:用于保存字段值 - 例如:john,25,…等。MyUserRepository现在 - 让我们来看看我们的MyUserRepository。我们需要MyUserRepository来扩展QueryDslPredicateExecutor,以便我们以后可以使用Predicates来过滤搜索结果:public interface MyUserRepository extends JpaRepository<MyUser, Long>, QueryDslPredicateExecutor<MyUser>, QuerydslBinderCustomizer<QMyUser> { @Override default public void customize( QuerydslBindings bindings, QMyUser root) { bindings.bind(String.class) .first((SingleValueBinding<StringPath, String>) StringExpression::containsIgnoreCase); bindings.excluding(root.email); }}结合Predicates接下来让我们看看组合Predicates在结果过滤中使用多个约束。在以下示例中 - 我们使用构建器 - MyUserPredicatesBuilder - 来组合Predicates:public class MyUserPredicatesBuilder { private List<SearchCriteria> params; public MyUserPredicatesBuilder() { params = new ArrayList<>(); } public MyUserPredicatesBuilder with( String key, String operation, Object value) { params.add(new SearchCriteria(key, operation, value)); return this; } public BooleanExpression build() { if (params.size() == 0) { return null; } List<BooleanExpression> predicates = new ArrayList<>(); MyUserPredicate predicate; for (SearchCriteria param : params) { predicate = new MyUserPredicate(param); BooleanExpression exp = predicate.getPredicate(); if (exp != null) { predicates.add(exp); } } BooleanExpression result = predicates.get(0); for (int i = 1; i < predicates.size(); i++) { result = result.and(predicates.get(i)); } return result; }}测试搜索查询接下来 - 让我们测试一下我们的Search API。我们将首先使用少数用户初始化数据库 - 准备好这些数据并进行测试:@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes = { PersistenceConfig.class })@Transactional@Rollbackpublic class JPAQuerydslIntegrationTest { @Autowired private MyUserRepository repo; private MyUser userJohn; private MyUser userTom; @Before public void init() { userJohn = new MyUser(); userJohn.setFirstName(“John”); userJohn.setLastName(“Doe”); userJohn.setEmail(“john@doe.com”); userJohn.setAge(22); repo.save(userJohn); userTom = new MyUser(); userTom.setFirstName(“Tom”); userTom.setLastName(“Doe”); userTom.setEmail(“tom@doe.com”); userTom.setAge(26); repo.save(userTom); }}接下来,让我们看看如何查找具有给定姓氏的用户:@Testpublic void givenLast_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with(“lastName”, “:”, “Doe”); Iterable<MyUser> results = repo.findAll(builder.build()); assertThat(results, containsInAnyOrder(userJohn, userTom));}现在,让我们看看如何找到具有名字和姓氏的用户:@Testpublic void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder() .with(“firstName”, “:”, “John”).with(“lastName”, “:”, “Doe”); Iterable<MyUser> results = repo.findAll(builder.build()); assertThat(results, contains(userJohn)); assertThat(results, not(contains(userTom)));}接下来,让我们看看如何找到具有姓氏和最小年龄的用户@Testpublic void givenLastAndAge_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder() .with(“lastName”, “:”, “Doe”).with(“age”, “>”, “25”); Iterable<MyUser> results = repo.findAll(builder.build()); assertThat(results, contains(userTom)); assertThat(results, not(contains(userJohn)));}接下来,让我们搜索实际不存在的用户:@Testpublic void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder() .with(“firstName”, “:”, “Adam”).with(“lastName”, “:”, “Fox”); Iterable<MyUser> results = repo.findAll(builder.build()); assertThat(results, emptyIterable());}最后 - 让我们看看如何找到仅给出名字的一部分的MyUser - 如下例所示:@Testpublic void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with(“firstName”, “:”, “jo”); Iterable<MyUser> results = repo.findAll(builder.build()); assertThat(results, contains(userJohn)); assertThat(results, not(contains(userTom)));}UserController最后,让我们将所有内容放在一起并构建REST API。我们定义了一个UserController,它定义了一个带有“search”参数的简单方法findAll()来传递查询字符串:@Controllerpublic class UserController { @Autowired private MyUserRepository myUserRepository; @RequestMapping(method = RequestMethod.GET, value = “/myusers”) @ResponseBody public Iterable<MyUser> search(@RequestParam(value = “search”) String search) { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder(); if (search != null) { Pattern pattern = Pattern.compile("(\w+?)(:|<|>)(\w+?),"); Matcher matcher = pattern.matcher(search + “,”); while (matcher.find()) { builder.with(matcher.group(1), matcher.group(2), matcher.group(3)); } } BooleanExpression exp = builder.build(); return myUserRepository.findAll(exp); }}这是一个快速测试URL示例:http://localhost:8080/myusers?search=lastName:doe,age>25回应:[{ “id”:2, “firstName”:“tom”, “lastName”:“doe”, “email”:“tom@doe.com”, “age”:26}]案例结论第三篇文章介绍了为REST API构建查询语言的第一步,充分利用了Querydsl库。 ...

January 23, 2019 · 3 min · jiezi

在Spring 5中调试Reactive Streams

案例概述调试Reactive Streams可能是我们开始使用这些数据结构后必须面对的主要挑战之一。考虑到Reactive Streams在过去几年中越来越受欢迎,了解我们如何有效地执行此任务是个好主意。让我们首先使用Reactive Streams设置项目,看看为什么这通常很麻烦。带有错误的场景我们想要模拟一个实际情况,其中运行了几个异步进程,并且我们在代码中引入了一些最终会触发异常的缺陷。为了理解全局,我们将提到我们的应用程序将使用和处理简单Foo对象流,这些对象只包含id、formattedName和quantity字段。分析日志输出现在,让我们检查一个片段以及当出现未处理的错误时它生成的输出:public void processFoo(Flux<Foo> flux) { flux = FooNameHelper.concatFooName(flux); flux = FooNameHelper.substringFooName(flux); flux = FooReporter.reportResult(flux); flux.subscribe();} public void processFooInAnotherScenario(Flux<Foo> flux) { flux = FooNameHelper.substringFooName(flux); flux = FooQuantityHelper.divideFooQuantity(flux); flux.subscribe();}运行我们的应用程序几秒钟后,我们会看到它会不时记录异常。仔细查看其中一个错误,我们会发现类似于此的内容:Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: 15 at j.l.String.substring(String.java:1963) at com.baeldung.debugging.consumer.service.FooNameHelper .lambda$1(FooNameHelper.java:38) at r.c.p.FluxMap$MapSubscriber.onNext(FluxMap.java:100) at r.c.p.FluxMap$MapSubscriber.onNext(FluxMap.java:114) at r.c.p.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:275) at r.c.p.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:849) at r.c.p.Operators$MonoSubscriber.complete(Operators.java:1476) at r.c.p.MonoDelayUntil$DelayUntilCoordinator.signal(MonoDelayUntil.java:211) at r.c.p.MonoDelayUntil$DelayUntilTrigger.onComplete(MonoDelayUntil.java:290) at r.c.p.MonoDelay$MonoDelayRunnable.run(MonoDelay.java:118) at r.c.s.SchedulerTask.call(SchedulerTask.java:50) at r.c.s.SchedulerTask.call(SchedulerTask.java:27) at j.u.c.FutureTask.run(FutureTask.java:266) at j.u.c.ScheduledThreadPoolExecutor$ScheduledFutureTask .access$201(ScheduledThreadPoolExecutor.java:180) at j.u.c.ScheduledThreadPoolExecutor$ScheduledFutureTask .run(ScheduledThreadPoolExecutor.java:293) at j.u.c.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at j.u.c.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at j.l.Thread.run(Thread.java:748)基于根本原因,并注意到堆栈跟踪中提到的FooNameHelper类,我们可以想象在某些情况下,我们的Foo对象正在使用比预期更短的formattedName 值进行处理。当然,这只是一个简化的案例,解决方案似乎相当明显。但是让我们假设这是一个真实案例场景,如果没有一些上下文信息,异常本身并不能帮助我们解决问题。异常是作为processFoo或processFooInAnotherScenario方法的一部分触发的吗?在到达此阶段之前,其他前面的步骤是否影响了formattedName字段?日志条目无法帮助我们找出这些问题。更糟糕的是,有时甚至不会从我们的功能中抛出异常。例如,假设我们依赖反应式存储库来保存我们的Foo对象。如果此时错误上升,我们甚至可能不知道从哪里开始调试代码。我们需要工具来有效地调试反应流。使用调试会话确定我们的应用程序正在发生什么的一个选项是使用我们喜欢的IDE启动调试会话。我们必须设置几个条件断点,并在流中的每个步骤执行时分析数据流。实际上,当我们有大量的被动进程在运行和共享资源时,这可能是一项繁琐的任务。此外,在许多情况下,出于安全原因,我们无法启动调试会话。使用doOnError方法或使用订阅参数记录信息有时,我们可以通过提供Consumer作为subscribe方法的第二个参数来添加有用的上下文信息:public void processFoo(Flux<Foo> flux) { // … flux.subscribe(foo -> { logger.debug(“Finished processing Foo with Id {}”, foo.getId()); }, error -> { logger.error( “The following error happened on processFoo method!”, error); });}注意:值得一提的是,如果我们不需要对subscribe方法进行进一步处理,我们可以在发布者上链接doOnError函数:flux.doOnError(error -> { logger.error(“The following error happened on processFoo method!”, error);}).subscribe();现在我们将对错误的来源提供一些指导,即使我们仍然没有太多关于生成异常的实际元素的信息。激活Reactor的全局调试配置Reactor库提供了一个hook类,它允许我们配置Flux和Mono操作符的行为。通过添加以下语句,我们的应用程序将检测对发布者方法的调用,包装运算符的构造,并捕获堆栈跟踪:Hooks.onOperatorDebug();这样就可以默认启用Thymeleaf - 无需额外配置。调试模式激活后,我们的异常日志将包含一些有用的信息:16:06:35.334 [parallel-1] ERROR c.b.d.consumer.service.FooService - The following error happened on processFoo method!java.lang.StringIndexOutOfBoundsException: String index out of range: 15 at j.l.String.substring(String.java:1963) at c.d.b.c.s.FooNameHelper.lambda$1(FooNameHelper.java:38) … at j.l.Thread.run(Thread.java:748) Suppressed: r.c.p.FluxOnAssembly$OnAssemblyException: Assembly trace from producer [reactor.core.publisher.FluxMapFuseable] : reactor.core.publisher.Flux.map(Flux.java:5653) c.d.b.c.s.FooNameHelper.substringFooName(FooNameHelper.java:32) c.d.b.c.s.FooService.processFoo(FooService.java:24) c.d.b.c.c.ChronJobs.consumeInfiniteFlux(ChronJobs.java:46) o.s.s.s.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) o.s.s.s.DelegatingErrorHandlingRunnable .run(DelegatingErrorHandlingRunnable.java:54) o.u.c.Executors$RunnableAdapter.call(Executors.java:511) o.u.c.FutureTask.runAndReset(FutureTask.java:308)Error has been observed by the following operator(s): | Flux.map ⇢ c.d.b.c.s.FooNameHelper .substringFooName(FooNameHelper.java:32) | Flux.map ⇢ c.d.b.c.s.FooReporter.reportResult(FooReporter.java:15)我们可以看到,第一部分保持相对相同,但以下部分提供了有关以下内容的信息:发布者的程序集跟踪 - 这里我们可以确认错误最初是在processFoo方法中生成的。在第一次触发错误之后观察到错误的运算符,以及链接它们的用户类。注意:在这个例子中,主要是为了清楚地看到这一点,我们在不同的类上添加操作。我们可以随时打开或关闭调试模式,但它不会影响已经实例化的Flux和Mono对象。在不同的线程上执行运算符要记住的另一个方面是即使在流上运行不同的线程,也会正确生成程序集跟踪。我们来看看下面的例子:public void processFoo(Flux<Foo> flux) { flux = flux.publishOn(Schedulers.newSingle(“foo-thread”)); // … flux = flux.publishOn(Schedulers.newSingle(“bar-thread”)); flux = FooReporter.reportResult(flux); flux.subscribeOn(Schedulers.newSingle(“starter-thread”)) .subscribe();}现在,如果我们检查日志,我们会理解在这种情况下,第一部分可能会稍微改变,但最后两部分保持相同。第一部分是线程堆栈跟踪,因此它只显示特定线程执行的操作。正如我们所看到的,当我们调试应用程序时,这不是最重要的部分,因此这种更改是可以接受的。在单个进程上激活调试输出在每个单一的反应过程中检测和生成堆栈跟踪都是昂贵的。因此,我们应该只在关键情况下实施前一种方法。无论如何,Reactor提供了一种在单个关键进程上启用调试模式的方法,这样可以减少内存消耗。我们指的是检查点操作员:public void processFoo(Flux<Foo> flux) { // … flux = flux.checkpoint(“Observed error on processFoo”, true); flux.subscribe();}请注意,以这种方式,将在检查点阶段记录程序集跟踪:Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: 15 …Assembly trace from producer [reactor.core.publisher.FluxMap], described as [Observed error on processFoo] : r.c.p.Flux.checkpoint(Flux.java:3096) c.b.d.c.s.FooService.processFoo(FooService.java:26) c.b.d.c.c.ChronJobs.consumeInfiniteFlux(ChronJobs.java:46) o.s.s.s.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) o.s.s.s.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) j.u.c.Executors$RunnableAdapter.call(Executors.java:511) j.u.c.FutureTask.runAndReset(FutureTask.java:308)Error has been observed by the following operator(s): |_ Flux.checkpoint ⇢ c.b.d.c.s.FooService.processFoo(FooService.java:26)我们应该在反应链的末尾实施检查点方法。否则,操作员将无法观察下游发生的错误。另外,请注意,库提供了重载方法。我们可以避免:如果我们使用no-args选项,则指定观察到的错误的描述通过仅提供自定义描述来生成填充堆栈跟踪(这是最昂贵的操作)记录元素序列最后,Reactor发布商提供了一种在某些情况下可能会派上用场的方法。通过在我们的反应链中调用log方法,应用程序将使用它在该阶段具有的状态记录流中的每个元素。让我们在我们的例子中尝试一下:public void processFoo(Flux<Foo> flux) { flux = FooNameHelper.concatFooName(flux); flux = FooNameHelper.substringFooName(flux); flux = flux.log(); flux = FooReporter.reportResult(flux); flux = flux.doOnError(error -> { logger.error(“The following error happened on processFoo method!”, error); }); flux.subscribe();}并检查日志:INFO reactor.Flux.Map.1 - onSubscribe(FluxMap.MapSubscriber)INFO reactor.Flux.Map.1 - request(unbounded)INFO reactor.Flux.Map.1 - onNext(Foo(id=0, formattedName=theFo, quantity=8))INFO reactor.Flux.Map.1 - onNext(Foo(id=1, formattedName=theFo, quantity=3))INFO reactor.Flux.Map.1 - onNext(Foo(id=2, formattedName=theFo, quantity=5))INFO reactor.Flux.Map.1 - onNext(Foo(id=3, formattedName=theFo, quantity=6))INFO reactor.Flux.Map.1 - onNext(Foo(id=4, formattedName=theFo, quantity=6))INFO reactor.Flux.Map.1 - cancel()ERROR c.b.d.consumer.service.FooService - The following error happened on processFoo method!…我们可以在此阶段轻松查看每个Foo对象的状态,以及在异常发生时框架如何取消流。当然,这种方法也很昂贵,我们必须适度使用它。案例结论如果我们不知道正确调试应用程序的工具和机制,我们可能会花费大量时间和精力来解决问题。如果我们不习惯处理被动和异步数据结构,那么尤其如此,我们需要额外的帮助来弄清楚事情是如何工作的。 ...

January 23, 2019 · 2 min · jiezi

长连接的心跳及重连设计

前言说道“心跳”这个词大家都不陌生,当然不是指男女之间的心跳,而是和长连接相关的。顾名思义就是证明是否还活着的依据。什么场景下需要心跳呢?目前我们接触到的大多是一些基于长连接的应用需要心跳来“保活”。由于在长连接的场景下,客户端和服务端并不是一直处于通信状态,如果双方长期没有沟通则双方都不清楚对方目前的状态;所以需要发送一段很小的报文告诉对方“我还活着”。同时还有另外几个目的:服务端检测到某个客户端迟迟没有心跳过来可以主动关闭通道,让它下线。客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。正好借着在 cim有这样两个需求来聊一聊。心跳实现方式心跳其实有两种实现方式:TCP 协议实现(keepalive 机制)。应用层自己实现。由于 TCP 协议过于底层,对于开发者来说维护性、灵活度都比较差同时还依赖于操作系统。所以我们这里所讨论的都是应用层的实现。如上图所示,在应用层通常是由客户端发送一个心跳包 ping 到服务端,服务端收到后响应一个 pong 表明双方都活得好好的。一旦其中一端延迟 N 个时间窗口没有收到消息则进行不同的处理。客户端自动重连先拿客户端来说吧,每隔一段时间客户端向服务端发送一个心跳包,同时收到服务端的响应。常规的实现应当是:开启一个定时任务,定期发送心跳包。收到服务端响应后更新本地时间。再有一个定时任务定期检测这个“本地时间”是否超过阈值。超过后则认为服务端出现故障,需要重连。这样确实也能实现心跳,但并不友好。在正常的客户端和服务端通信的情况下,定时任务依然会发送心跳包;这样就显得没有意义,有些多余。所以理想的情况应当是客户端收到的写消息空闲时才发送这个心跳包去确认服务端是否健在。好消息是 Netty 已经为我们考虑到了这点,自带了一个开箱即用的 IdleStateHandler 专门用于心跳处理。来看看 cim 中的实现:在 pipeline 中加入了一个 10秒没有收到写消息的 IdleStateHandler,到时他会回调 ChannelInboundHandler 中的 userEventTriggered 方法。所以一旦写超时就立马向服务端发送一个心跳(做的更完善应当在心跳发送失败后有一定的重试次数);这样也就只有在空闲时候才会发送心跳包。但一旦间隔许久没有收到服务端响应进行重连的逻辑应当写在哪里呢?先来看这个示例:当收到服务端响应的 pong 消息时,就在当前 Channel 上记录一个时间,也就是说后续可以在定时任务中取出这个时间和当前时间的差额来判断是否超过阈值。超过则重连。同时在每次心跳时候都用当前时间和之前服务端响应绑定到 Channel 上的时间相减判断是否需要重连即可。也就是 heartBeatHandler.process(ctx); 的执行逻辑。伪代码如下:@Overridepublic void process(ChannelHandlerContext ctx) throws Exception { long heartBeatTime = appConfiguration.getHeartBeatTime() * 1000; Long lastReadTime = NettyAttrUtil.getReaderTime(ctx.channel()); long now = System.currentTimeMillis(); if (lastReadTime != null && now - lastReadTime > heartBeatTime){ reconnect(); }}IdleStateHandler 误区一切看起来也没毛病,但实际上却没有这样实现重连逻辑。最主要的问题还是对 IdleStateHandler 理解有误。我们假设下面的场景:客户端通过登录连上了服务端并保持长连接,一切正常的情况下双方各发心跳包保持连接。这时服务端突入出现 down 机,那么理想情况下应当是客户端迟迟没有收到服务端的响应从而 userEventTriggered 执行定时任务。判断当前时间 - UpdateWriteTime > 阈值 时进行重连。但却事与愿违,并不会执行 2、3两步。因为一旦服务端 down 机、或者是与客户端的网络断开则会回调客户端的 channelInactive 事件。IdleStateHandler 作为一个 ChannelInbound 也重写了 channelInactive() 方法。这里的 destroy() 方法会把之前开启的定时任务都给取消掉。所以就不会再有任何的定时任务执行了,也就不会有机会执行这个重连业务。靠谱实现因此我们得有一个单独的线程来判断是否需要重连,不依赖于 IdleStateHandler。于是 cim 在客户端感知到网络断开时就会开启一个定时任务:之所以不在客户端启动就开启,是为了节省一点线程消耗。网络问题虽然不可避免,但在需要的时候开启更能节省资源。在这个任务重其实就是执行了重连,限于篇幅具体代码就不贴了,感兴趣的可以自行查阅。同时来验证一下效果。启动两个服务端,再启动客户端连接上一台并保持长连接。这时突然手动关闭一台服务,客户端可以自动重连到可用的那台服务节点。启动客户端后服务端也能收到正常的 ping 消息。利用 :info 命令查看当前客户端的链接状态发现连的是 9000端口。:info 是一个新增命令,可以查看一些客户端信息。这时我关掉连接上的这台节点。kill -9 2142这时客户端会自动重连到可用的那台节点。这个节点也收到了上线日志以及心跳包。服务端自动剔除离线客户端现在来看看服务端,它要实现的效果就是延迟 N 秒没有收到客户端的 ping 包则认为客户端下线了,在 cim 的场景下就需要把他踢掉置于离线状态。消息发送误区这里依然有一个误区,在调用 ctx.writeAndFlush() 发送消息获取回调时。其中是 isSuccess 并不能作为消息发送成功与否的标准。也就是说即便是客户端直接断网,服务端这里发送消息后拿到的 success 依旧是 true。这是因为这里的 success 只是告知我们消息写入了 TCP 缓冲区成功了而已。和我之前有着一样错误理解的不在少数,这是 Netty 官方给的回复。相关 issue:https://github.com/netty/netty/issues/4915同时感谢 95老徐以及闪电侠的一起排查。所以我们不能依据此来关闭客户端的连接,而是要像上文一样判断 Channel 上绑定的时间与当前时间只差是否超过了阈值。以上则是 cim 服务端的实现,逻辑和开头说的一致,也和 Dubbo 的心跳机制有些类似。于是来做个试验:正常通信的客户端和服务端,当我把客户端直接断网时,服务端会自动剔除客户端。总结这样就实现了文初的两个要求。服务端检测到某个客户端迟迟没有心跳过来可以主动关闭通道,让它下线。客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。同时也踩了两个误区,坑一个人踩就可以了,希望看过本文的都有所收获避免踩坑。本文所有相关代码都在此处,感兴趣的可以自行查看:https://github.com/crossoverJie/cim如果本文对你有所帮助还请不吝转发。 ...

January 23, 2019 · 1 min · jiezi

Spring 指南(调度任务)

调度任务本指南将指导你完成使用Spring调度任务的步骤。将要构建什么你将构建一个应用程序,使用Spring的@Scheduled注解每五秒打印一次当前时间。需要什么大约15分钟最喜欢的文本编辑器或IDEJDK 1.8或更高版本Gradle 4+或Maven 3.2+你还可以将代码直接导入IDE:Spring Tool Suite(STS)IntelliJ IDEA如何完成本指南请执行以下操作:下载并解压缩本指南的源存储库,或使用Git克隆它:git clone https://github.com/spring-guides/gs-scheduling-tasks.git进入gs-scheduling-tasks/initial完成后,你可以根据gs-scheduling-tasks/complete中的代码检查结果。创建调度任务现在你已经设置了项目,可以创建调度任务。src/main/java/hello/ScheduledTasks.javapackage hello;import java.text.SimpleDateFormat;import java.util.Date;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;@Componentpublic class ScheduledTasks { private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class); private static final SimpleDateFormat dateFormat = new SimpleDateFormat(“HH:mm:ss”); @Scheduled(fixedRate = 5000) public void reportCurrentTime() { log.info(“The time is now {}”, dateFormat.format(new Date())); }}Scheduled注解定义特定方法何时运行,注意:此示例使用fixedRate,它指定从每次调用的开始时间计算的方法调用之间的间隔。还有其他选项,例如fixedDelay,它指定从完成任务计算的调用之间的间隔,你还可以使用@Scheduled(cron=". . .")表达式进行更复杂的任务调度。启用调度虽然调度任务可以嵌入到Web应用程序和WAR文件中,但下面演示的更简单的方法创建了一个独立的应用程序,将所有内容打包在一个可执行的JAR文件中,由main()方法驱动。src/main/java/hello/Application.javapackage hello;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.scheduling.annotation.EnableScheduling;@SpringBootApplication@EnableSchedulingpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class); }}@SpringBootApplication是一个方便的注解,添加了以下所有内容:@Configuration将类标记为应用程序上下文的bean定义源。@EnableAutoConfiguration告诉Spring Boot根据类路径设置、其他bean和各种属性设置开始添加bean。通常你会为Spring MVC应用添加@EnableWebMvc,但Spring Boot会在类路径上看到spring-webmvc时自动添加它,这会将应用程序标记为Web应用程序并激活关键行为,例如设置DispatcherServlet。@ComponentScan告诉Spring在hello包中查找其他组件、配置和服务,允许它找到控制器。main()方法使用Spring Boot的SpringApplication.run()方法来启动应用程序,你是否注意到没有一行XML?也没有web.xml文件,此Web应用程序是100%纯Java,你无需处理配置任何管道或基础结构。@EnableScheduling确保创建后台任务执行程序,没有它,就没有任何调度。构建可执行的JAR你可以使用Gradle或Maven从命令行运行该应用程序,或者,你可以构建一个包含所有必需依赖项、类和资源的可执行JAR文件,并运行它,这使得在整个开发生命周期中、跨不同环境等将服务作为应用程序发布、版本和部署变得容易。如果你使用的是Gradle,则可以使用./gradlew bootRun运行该应用程序,或者你可以使用./gradlew build构建JAR文件,然后你可以运行JAR文件:java -jar build/libs/gs-scheduling-tasks-0.1.0.jar如果你使用的是Maven,则可以使用./mvnw spring-boot:run运行该应用程序,或者你可以使用./mvnw clean package构建JAR文件,然后你可以运行JAR文件:java -jar target/gs-scheduling-tasks-0.1.0.jar上面的过程将创建一个可运行的JAR,你也可以选择构建经典WAR文件。显示日志输出,你可以从日志中看到它在后台线程上,你应该看到你的调度任务每5秒出发一次:[…]2016-08-25 13:10:00.143 INFO 31565 — [pool-1-thread-1] hello.ScheduledTasks : The time is now 13:10:002016-08-25 13:10:05.143 INFO 31565 — [pool-1-thread-1] hello.ScheduledTasks : The time is now 13:10:052016-08-25 13:10:10.143 INFO 31565 — [pool-1-thread-1] hello.ScheduledTasks : The time is now 13:10:102016-08-25 13:10:15.143 INFO 31565 — [pool-1-thread-1] hello.ScheduledTasks : The time is now 13:10:15 ...

January 22, 2019 · 1 min · jiezi

Spring 指南(了解视图模板)

了解视图模板模型—视图—控制器(MVC)软件设计模式是用于在软件应用程序内分离关注点的方法,原则上,应用程序逻辑或控制器与用于向用户或视图层显示信息的技术分离,该模型是控制器和视图层之间的通信工具。在应用程序内,视图层可以使用一种或多种不同的技术来渲染视图,Spring基于Web的应用程序支持各种视图选项,通常称为视图模板,这些技术被描述为“模板”,因为它们提供了一种标记语言,用于在服务器端渲染期间公开视图中的模型属性。视图模板库以下视图模板库与Spring兼容:JSP/JSTLThymeleafTilesFreemarkerVelocityJSP和Thymeleaf比较以下示例说明了如何使用JSP和Thymeleaf模板渲染相同的内容。JSP请注意此示例中的JSTL(JavaServer网页标准标签库)表达式。<c:url var=“hotelsUrl” value="/hotels"/><form:form modelAttribute=“searchCriteria” action="${hotelsUrl}" method=“get” cssClass=“inline”> <span class=“errors span-18”> <form:errors path=""/> </span> <fieldset> <div class=“span-8”> <label for=“searchString”>SeaString:</label> <form:input id=“searchString” path=“searchString”/> </div> … </fieldset></form:form>Thymeleaf在此示例中,标记与标准HTML集成。<form action="#" th:object="${searchCriteria}" th:action="@{/hotels}" method=“get” class=“inline”> <ul th:if="${#fields.hasErrors(’’)}" class=“errors span-18”> <li th:each=“err : ${#fields.errors(’’)}” th:text="${err}">Input is incorrect</li> </ul> <fieldset> <div class=“span-8”> <label for=“searchString”>Search String:</label> <input type=“text” id=“searchString” th:field="{searchString}" /> </div> … </fieldset></form>

January 22, 2019 · 1 min · jiezi

Spring 指南(构建RESTful Web服务)

构建RESTful Web服务本指南将引导你完成使用Spring创建“hello world” RESTful Web服务的过程。将要构建什么你将构建一个接受HTTP GET请求的服务:http://localhost:8080/greeting并使用JSON响应表示问候语:{“id”:1,“content”:“Hello, World!"}你可以使用查询字符串中的可选name参数自定义问候语:http://localhost:8080/greeting?name=Username参数值将覆盖默认值“World”并反映在响应中:{“id”:1,“content”:“Hello, User!"}需要什么大约15分钟最喜欢的文本编辑器或IDEJDK 1.8或更高版本Gradle 4+或Maven 3.2+你还可以将代码直接导入IDE:Spring Tool Suite(STS)IntelliJ IDEA如何完成本指南请执行以下操作:下载并解压缩本指南的源存储库,或使用Git克隆它:git clone https://github.com/spring-guides/gs-rest-service.git进入gs-rest-service/initial完成后,你可以根据gs-rest-service/complete中的代码检查结果。创建资源表示类现在你已经设置了项目和构建系统,你可以创建Web服务。通过考虑服务交互来开始这个过程。该服务将处理/greeting的GET请求,可选地在查询字符串中使用name参数,GET请求应返回200 OK响应,其中JSON位于表示问候语的正文中,它应该看起来像这样:{ “id”: 1, “content”: “Hello, World!"}id字段是问候语的唯一标识符,content是问候语的文本表示。要为问候语表示建模,需要创建一个资源表示类,提供一个普通的java对象,其中包含id和content数据的字段、构造函数和访问器:src/main/java/hello/Greeting.javapackage hello;public class Greeting { private final long id; private final String content; public Greeting(long id, String content) { this.id = id; this.content = content; } public long getId() { return id; } public String getContent() { return content; }}正如你在下面的步骤中看到的那样,Spring使用Jackson JSON库自动将Greeting类型的实例编组为JSON。接下来,你将创建将为这些问候语提供服务的资源控制器。创建资源控制器在Spring构建RESTful Web服务的方法中,HTTP请求由控制器处理,这些组件可以通过@RestController注解轻松被识别,下面的GreetingController通过返回Greeting类的新实例来处理/greeting的GET请求:src/main/java/hello/GreetingController.javapackage hello;import java.util.concurrent.atomic.AtomicLong;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class GreetingController { private static final String template = “Hello, %s!”; private final AtomicLong counter = new AtomicLong(); @RequestMapping("/greeting”) public Greeting greeting(@RequestParam(value=“name”, defaultValue=“World”) String name) { return new Greeting(counter.incrementAndGet(), String.format(template, name)); }}这个控制器简洁并简单,但它的内部有很多东西,让我们一步一步地分解它。@RequestMapping注解确保对/greeting的HTTP请求映射到greeting()方法。上面的示例未指定GET与PUT、POST等,因为@RequestMapping默认映射所有HTTP操作,使用@RequestMapping(method=GET)缩小此映射范围。@RequestParam将查询字符串参数name的值绑定到greeting()方法的name参数中,如果请求中不存在name参数,则使用“World”的defaultValue。方法体的实现基于counter的下一个值和使用问候语template格式化给定name创建并返回具有id和content属性的新Greeting对象。传统MVC控制器和上面的RESTful Web服务控制器之间的关键区别在于创建HTTP响应体的方式,这个RESTful Web服务控制器只是填充并返回一个Greeting对象,而不是依赖于视图技术来执行问候数据到HTML的服务器端渲染,对象数据将作为JSON直接写入HTTP响应。此代码使用Spring 4的新@RestController注解,它将类标记为控制器,其中每个方法都返回一个域对象而不是一个视图,这是@Controller和@ResponseBody汇总在一起的简写。Greeting对象必须转换为JSON,由于Spring的HTTP消息转换器支持,你无需手动执行此转换,因为Jackson 2在类路径上,所以会自动选择Spring的MappingJackson2HttpMessageConverter将Greeting实例转换为JSON。使应用程序可执行虽然可以将此服务打包为传统的WAR文件以便部署到外部应用程序服务器,但下面演示的更简单的方法创建了一个独立的应用程序,将所有内容打包在一个可执行的JAR文件中,由Java的main()方法驱动,在此过程中,你使用Spring的支持将Tomcat servlet容器嵌入为HTTP运行时,而不是部署到外部实例。src/main/java/hello/Application.javapackage hello;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}@SpringBootApplication是一个方便的注解,添加了以下所有内容:@Configuration将类标记为应用程序上下文的bean定义源。@EnableAutoConfiguration告诉Spring Boot根据类路径设置、其他bean和各种属性设置开始添加bean。通常你会为Spring MVC应用添加@EnableWebMvc,但Spring Boot会在类路径上看到spring-webmvc时自动添加它,这会将应用程序标记为Web应用程序并激活关键行为,例如设置DispatcherServlet。@ComponentScan告诉Spring在hello包中查找其他组件、配置和服务,允许它找到控制器。main()方法使用Spring Boot的SpringApplication.run()方法来启动应用程序,你是否注意到没有一行XML?也没有web.xml文件,此Web应用程序是100%纯Java,你无需处理配置任何管道或基础结构。构建可执行的JAR你可以使用Gradle或Maven从命令行运行该应用程序,或者,你可以构建一个包含所有必需依赖项、类和资源的可执行JAR文件,并运行它,这使得在整个开发生命周期中、跨不同环境等将服务作为应用程序发布、版本和部署变得容易。如果你使用的是Gradle,则可以使用./gradlew bootRun运行该应用程序,或者你可以使用./gradlew build构建JAR文件,然后你可以运行JAR文件:java -jar build/libs/gs-rest-service-0.1.0.jar如果你使用的是Maven,则可以使用./mvnw spring-boot:run运行该应用程序,或者你可以使用./mvnw clean package构建JAR文件,然后你可以运行JAR文件:java -jar target/gs-rest-service-0.1.0.jar上面的过程将创建一个可运行的JAR,你也可以选择构建经典WAR文件。显示日志输出,该服务应在几秒内启动并运行。测试服务现在服务已启动,请访问http://localhost:8080/greeting,你将看到:{“id”:1,“content”:“Hello, World!"}使用http://localhost:8080/greeting?name=User提供name查询字符串参数,注意content属性的值是如何从“Hello, World!”改变为“Hello, User!”:{“id”:2,“content”:“Hello, User!"}此更改表明GreetingController中的@RequestParam正在按预期工作,name参数的默认值为“World”,但始终可以通过查询字符串显式覆盖。另请注意id属性如何从1更改为2,这证明你正在针对相同的GreetingController实例跨多个请求,并且其counter字段在每次调用时按预期递增。 ...

January 22, 2019 · 1 min · jiezi

关于开源分布式事务中间件Fescar,我们总结了开发者关心的13个问题

开源分布式事务中间件 Fescar 自1月10日上线v0.1版本以来,受到了开发者们的极大关注(watch249,star3005,fork649,社区讨论的issue58,数据统计于1月17日14:00),可见,天下苦分布式事务久矣。为此,我们收集了大家在社区(Github)和社群(钉钉群&微信群)关注的核心问题,总结如下,并给出回复。Q1:Fescar 的发展经历了哪些历程?和阿里云全局事务服务GTS之间是什么关系?A1:阿里巴巴是国内最早一批进行应用分布式(微服务化)改造的企业,所以很早就遇到微服务架构下的分布式事务问题。2014 年阿里巴巴中间件团队发布TXC(Taobao Transaction Constructor),为集团内应用提供分布式事务服务。2016 年TXC 经过产品化改造,以GTS(Global TransactionService)的身份上线阿里云,成为当时业界唯一一款云上分布式事务产品,以阿里云公有云或专有云解决方案的形式,交付给众多外部客户。2019 年基于 TXC 和 GTS 的技术积累,阿里巴巴中间件团队发起了开源项目Fescar(Fast & EaSy Commit And Rollback, FESCAR),和社区一起建设这个分布式事务解决方案。TXC/GTS/Fescar一脉相承,为解决微服务架构下的分布式事务问题交出了一份与众不同的答卷。Q2:Fescar 有哪些适用场景?A2:Fescar 的愿景是让分布式事务的使用像现在本地事务的使用一样简单、高效,最终的目标是希望可以适用于所有的分布式事务场景。目前,核心的 AT 模式适用于构建于支持本地 ACID 事务的关系型数据库。非关系型数据库类资源的管理,通过 MT 模式来支持。AT 模式与 MT 模式完全兼容,所以可以在同一个分布式事务中,同时管理两类资源。Q3:目前有已经有一些其他的分布式事务开源方案,Fescar 和他们之间有哪些区别?和JTA支持分布式事务有哪些区别?A3:既有的分布式事务解决方案按照对业务侵入性分为两类,即:对业务无侵入的和对业务有侵入的。业务无侵入的方案既有的主流分布式事务解决方案中,对业务无侵入的只有基于 XA 的方案(注:问题中提到的 JTA 是XA 方案的 Java 版本),但应用XA 方案存在 3 个方面的问题:1、要求数据库提供对 XA 的支持。如果遇到不支持 XA(或支持得不好,比如 MySQL 5.7 以前的版本)的数据库,则不能使用。2、受协议本身的约束,事务资源(数据记录、数据库连接)的锁定周期长。长周期的资源锁定从业务层面来看,往往是不必要的,而因为事务资源的管理器是数据库本身,应用层无法插手。这样形成的局面就是,基于 XA 的应用往往性能会比较差,而且很难优化。3、已经落地的基于 XA 的分布式解决方案,都依托于重量级的应用服务器(Tuxedo/WebLogic/WebSphere 等),这是不适用于微服务架构的。侵入业务的方案实际上,最初分布式事务只有 XA 这个唯一方案。XA 是完备的,但在实践过程中,由于种种原因(包含但不限于上面提到的3 点)往往不得不放弃,转而从业务层面着手来解决分布式事务问题。比如:基于可靠消息的最终一致性方案TCCSaga都属于这一类。这些方案的具体机制在这里不做展开,网上这方面的论述文章非常多。总之,这些方案都要求在应用的业务层面把分布式事务技术约束考虑到设计中,通常每一个服务都需要设计实现正向和反向的幂等接口。这样的设计约束,往往会导致很高的研发和维护成本。不可否认,侵入业务的分布式事务方案都经过大量实践验证,能有效解决问题,在各行种业的业务应用系统中起着重要作用。但回到原点来思考,这些方案的采用实际上都是迫于无奈。回到问题:与基于消息的最终一致、TCC、Saga等业务逻辑侵入方案的不同在于,Fescar 的设计初衷就是保持对业务的非侵入性,不要求业务层面按照分布式事务的特定场景来设计正向和反向的两套(甚至多套)业务逻辑。这方面的差别就不展开了。与 XA 的区别在于,设计了一套不同与 XA 的两阶段协议,在保持对业务不侵入的前提下,保证良好的性能,也避免了对底层数据库协议支持的要求。可以看作是一套轻量级的XA 机制。具体的差别如下:架构层次XA方案的 RM 实际上是在数据库层,RM本质上就是数据库自身(通过提供支持 XA 的驱动程序来供应用使用)。而 Fescar 的RM 是以二方包的形式作为中间件层部署在应用程序这一侧的,不依赖与数据库本身对协议的支持,当然也不需要数据库支持XA 协议。这点对于微服务化的架构来说是非常重要的:应用层不需要为本地事务和分布式事务两类不同场景来适配两套不同的数据库驱动。这个设计,剥离了分布式事务方案对数据库在协议支持上的要求。两阶段提交先来看一下 XA 的2PC 过程。无论 Phase2 的决议是commit 还是 rollback,事务性资源的锁都要保持到Phase2 完成才释放。再看 Fescar 的2PC 过程。分支事务中数据的 本地锁 由本地事务管理,在分支事务 Phase1 结束时释放。同时,随着本地事务结束,连接 也得以释放。分支事务中数据的 全局锁 在事务协调器侧管理,在决议 Phase2 全局提交时,全局锁马上可以释放。只有在决议全局回滚的情况下,全局锁 才被持有至分支的 Phase2 结束。这个设计,极大地减少了分支事务对资源(数据和连接)的锁定时间,给整体并发和吞吐的提升提供了基础。Q4:Fescar 支持 Dubbo 的哪些版本?A4:所有版本。Q5:Fescar 支持 Spring Cloud么?A5:Fescar 与微服务框架的接口点在于,需要把事务的唯一标识 XID(一个字符串)通过微服务框架的服务调用间调用的机制中,透明地传递,并通过 Fescar 的 API 来绑定(或解绑)到应用的线程上下文中。(机制可以参考内置的对 Dubbo 支持的实现 com.alibaba.fescar.dubbo.TransactionPropagationFilter)所以,本质上这个问题不是支不支持 Spring Cloud,而是如何支持 Spring Cloud 中选用的服务调用机制。目前正在和 Spring Cloud Alibaba 的同学合作,准备在v0.5.x版本(或更早)发布对 Spring Cloud默认的支持。同时,非常欢迎社区的朋友参与进来,贡献包括 Spring Cloud 在内的各类微服务框架的支持。Q6:Fescar 是否支持本地跨库多数据源?除了关系型数据库,是否还支持NoSQL数据库?A6:本地跨多数据源同样是支持的,在 Fescar 的架构中,同一个服务中的多个数据源与跨服务的多个数据源,没有本质区别。AT 模式目前仅限于对关系型数据库的支持(本身具备ACID 事务支持),后面会发布出来的 MT 模式可以支持 NoSQL 这类本身不具备本地事务支持的资源。Q7:Fescar 现在开源的是AT模式,MT模式暂时不支持,什么时候会开源?A7:当前 0.1.0 版本只是把 Fescar 最核心的 AT 模式的最小集发布出来,一方面是按开源的规划和架构的重构进展,另一方面也是希望通过最小集版本,让用户和开发者社区更容易理解到我们核心的设计思路,让更多人比较容易地参与进来建设,而不是完全由阿里巴巴主导,仅仅把我们的整套方案开源出来给大家用而已。阿里巴巴在分布式事务上的技术积累,我们会通过 Fescar 项目毫无保留地贡献给社区,所有功能特性都会按规划和社区的反馈陆续开源出来。MT 按初步的计划,会在 0.5.x 版本发布。Q8:Fescar 什么时候提供HA cluster,单节点的server的瓶颈如何处理?A8:按初步的计划,HA Cluster 会在 0.5.x 版本发布,解决单机部署的单点问题。Q9:因网络中断、网张闪断、节点宕机和超时等引起的异常,Fescar会提供相应的补偿措施么?A9:这些异常情况的处理是分布式事务解决方案的基本要求,Fescar 同样也是提供了整套方案来处理各类异常场景。这方面的具体机制会在 HA Cluster 版本发布时,给出全面的分析介绍。Q10:Fescar框架中,如何监控分布式事务?A10:监控是非常重要的一块儿内容。TXC 和 GTS 的监控在阿里巴巴内部使用了很多基础设施的辅助。而在开源版本中,我们还没有一个现成的监控方案。大体上,监控的基础是两个方面:一方面是日志,通过日志的采集和处理,可以形成一个完整的事务链路,这些数据对于业务层面的分析和调优是重要的参考依据。另一方面是 API,Fescar 会提供一套管控 API,用于对运行时事务的管理。我们后续会把这两方面的数据格式、部署形态及接口整理出来,希望和社区来共建监控这个重要的方面。Q11:Fescar 的roadmap 有了么?A11:目前最新的roadmap如下:v0.1.0微服务框架支持: Dubbo数据库支持: MySQL基于 Spring AOP 的 Annotation事务协调器: 单机版本v0.5.x微服务框架支持: Spring CloudMT 模式支持 TCC 模式事务的适配动态配置和服务发现事务协调器: 高可用集群版本v0.8.xMetrics控制台: 监控/部署/升级/扩缩容v1.0.0General Availability: 生产环境适用v1.5.x数据库支持: Oracle/PostgreSQL/OceanBase不依赖 Spring AOP 的 Annotation热点数据的优化处理机制RocketMQ 事务消息纳入全局事务管理NoSQL 纳入全局事务管理的适配机制支持 HBase支持 Redisv2.0.0支持 XA当然,项目迭代演进的过程,我们最重视的是社区的声音,路线图会和社区充分交流及时进行调整。Q12:Fescar 官网什么时候上线?A12:Fescar 官方域名已经注册,官网将采用静态开源站点搭建工具Docsite「传送门」进行搭建,logo 已经设计并将于近期公布。Q13:如何加入 Fescar 社区,进行贡献,已经摩拳擦掌了。A13:我们非常欢迎大家通过各种形式参与到我们项目的建设中,包括但不限于:架构设计模块设计代码实现Bug FixDemo样例文档、网站和翻译具体的参与方法可以参见我们项目中的CONTRIBUTING 指引,或与 @eternaltingting@163.com 联系。实际上,我们并不拘泥于贡献的形式,开发者提出的每一个 issue,无论是Bug Report、改进建议或者甚至是问题咨询都代表着对项目的关注和帮助。希望 Fescar 项目和社区一起健康成长,成为分布式事务领域一个优秀的解决方案。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 22, 2019 · 1 min · jiezi

Spring 指南(了解Git)

了解GitGit是一个免费开源的分布式版本控制系统(DVCS),它旨在快速、高效地处理任何规模和范围的项目。Git是由Linux创建者Linus Torvalds发明的,用于支持庞大的、不同的Linux开发人员群体,但Git的受欢迎程度与http://github.com关系更紧密,Git已存在多年,但直到GitHub受欢迎程度激增之后,才在Linux社区之外获得广泛认可。GitHub允许你免费托管开源项目,它还提供简单的钩子和友好的用户体验,使Git更容易使用。Mac Homebrew等其他项目也对Git投入很深,Homebrew允许你在Mac上安装开源软件包,构建和管理这些公式的工具利用了Git,用于差异工具、制作补丁、管理资源,以及通过拉取请求提交新的和更新的包。Git与其他DVCS另外两个最受欢迎的DVCS选择是Mercurial和Bazaar,Mercurial有命令行工具hg(以汞的化学符号命名),而Bazaar的命令行工具是bzr。Mercurial与许多开源项目相关联,Ubuntu Linux背后的Canonical公司使用Bazaar,开发人员通常需要熟悉Git、Mercurial和Bazaar。开发人员使用的DVCS通常由开发人员参与的组织决定,而不是由给定的一组功能决定,Git、Mercurial和Bazaar都具有基本功能,例如分支、标签、合并以及不依赖于中央服务器,目前使用Git的开发人员可能会在一年后在Mercurial管理的另一个项目开始工作。关键的挑战是了解每个工具的命令和语言的差异,例如,Mercurial中的hg revert意味着回滚当前更改,恢复为正式版本。git revert意味着添加一个反转先前编辑的新提交,Git有一个命令来支持像Mercurial这样的更改,但它有一个不同的名称。Git与非分布式SCM几个非分布式源代码管理系统(SCM)早于DVCS并仍在使用中:SubversionCVS(并发版本系统)许多专有版本控制系统目前仍在大量使用,例如:Rational ClearCasePerforceVisual SourceSafe这些专有产品通常与其他软件开发工具捆绑在一起,因此在某些软件开发商店中根深蒂固。与DVCS模型相反,这些系统中的关键因素是它们依赖中央服务器来保存跟踪版本和分支所涉及的所有关键数据,从本质上讲,开发人员在家中进行多次提交,与此中央服务器断开连接,然后将其添加到服务器,这不是内置功能,其中一些系统增加了支持此类功能的特性,但它并不是其本质的核心部分。为了说明DVCS和非DVCS的SCM系统之间的区别,请考虑两个人如何分别独立工作,在使用DVCS时如何在某个遥远的地方会面,比如在游轮旅行中,并共享提交。共享提交将具有与中央服务器上相同的权限,使用非分布式SCM时,这两个人只能共享代码差异,而不能提交,为了使提交成为正式提交,必须在到家并访问中央服务器时将提交发布到中央服务器。DVCS的固有优势像Git这样的工具的内置优势在于每个拥有副本的人都拥有重建项目所需的一切,如果中央服务器崩溃且所有数据都丢失,则可以将任何远程副本指定为正式副本,因为它将具有足够的信息以继续,如果开发人员没有最新的提交,则会出现唯一的差异。上一篇:了解REST

January 22, 2019 · 1 min · jiezi

Spring 指南(了解REST)

了解RESTREST(Representational State Transfer)由Roy Fielding于2000年在他的博士论文中引入和定义,REST是用于设计分布式系统的架构风格,它不是标准,而是一组约束,例如无状态,具有客户端/服务器关系和统一接口,REST与HTTP并不严格相关,但最常与它相关联。REST原则资源公开易于理解的目录结构URI。表示传输JSON或XML以表示数据对象和属性。消息显式使用HTTP方法(例如,GET、POST、PUT和DELETE)。无状态交互在请求之间不在服务器上存储客户端上下文,状态依赖性限制和限制可伸缩性,客户端保持会话状态。HTTP方法使用HTTP方法将CRUD(创建、检索、更新、删除)操作映射到HTTP请求。GET检索信息,GET请求必须是安全且幂等的,这意味着无论使用相同参数重复多少次,结果都是相同的,它们可能有副作用,但用户不期望它们,因此它们对系统的操作不是至关重要的,请求也可以是部分的或有条件的。检索ID为1的地址:GET /addresses/1POST请求URI上的资源对提供的实体执行某些操作,POST通常用于创建新实体,但也可用于更新实体。创建一个新地址:POST /addressesPUT将实体存储在URI中,PUT可以创建新实体或更新现有实体,PUT请求是幂等的,幂等性是PUT与POST请求的期望之间的主要区别。修改ID为1的地址:PUT /addresses/1注意:PUT替换现有实体,如果仅提供数据元素的子集,则其余数据元素将替换为空或null。PATCH仅更新URI上实体的指定字段,PATCH请求既不安全也不是幂等(RFC 5789),这是因为PATCH操作无法确保整个资源已更新。PATCH /addresses/1DELETE请求删除资源,但是,不必立即删除资源,它可能是异步或长时间运行的请求。删除ID为1的地址:DELETE /addresses/1HTTP状态码状态码表明HTTP请求的结果。1XX — 信息2XX — 成功3XX — 重定向4XX — 客户端错误5XX — 服务器错误媒体类型Accept和Content-Type HTTP头可用于描述HTTP请求中发送或请求的内容,如果客户端正在请求JSON格式的响应,则可以将Accept设置为application/json,相反,在发送数据时,将Content-Type设置为application/xml会告诉客户端请求中发送的数据是XML。

January 21, 2019 · 1 min · jiezi

Spring 指南(目录)

Spring 指南无论你正在构建什么,这些指南都旨在让你尽快提高工作效率 — 使用Spring团队推荐的最新Spring项目版本和技术。入门指南这些指南旨在在15-30分钟内完成,为使用Spring构建任何开发任务的“Hello World”提供了快速、实际的说明,在大多数情况下,唯一的前提条件是JDK和文本编辑器。构建RESTful Web服务了解如何使用Spring创建RESTful Web服务。调度任务了解如何使用Spring调度任务消费RESTful Web服务了解如何使用Spring的RestTemplate检索web页面数据。使用Gradle构建Java项目了解如何使用Gradle构建Java项目。使用Maven构建Java项目了解如何使用Maven构建Java项目。使用JDBC和Spring访问关系数据了解如何使用Spring访问关系数据。上传文件了解如何构建一个接受多部分文件上载的Spring应用程序。使用LDAP验证用户了解如何使用LDAP保护应用程序。使用Redis进行消息传递了解如何将Redis用作消息代理。使用RabbitMQ进行消息传递了解如何使用Spring和RabbitMQ创建简单的发布—订阅应用程序。使用Neo4j访问数据了解如何在Neo4j的NoSQL数据存储中持久化对象和关系。验证表单输入了解如何使用Spring执行表单验证。使用Spring Boot Actuator构建RESTful Web服务了解如何使用Spring Boot Actuator创建RESTful Web服务。使用JMS进行消息传递了解如何使用JMS代理发布和订阅消息。创建批处理服务了解如何创建基础的批处理驱动解决方案。保护Web应用程序了解如何使用Spring Security保护你的Web应用程序。构建超媒体驱动的RESTful Web服务了解如何使用Spring创建超媒体驱动的RESTful Web服务。访问Pivotal GemFire中的数据了解如何使用Gemfire的数据结构构建应用程序。集成数据了解如何构建一个使用Spring Integration获取数据、处理数据并将其写入文件的应用程序。使用MongoDB访问数据了解如何在MongoDB中持久化数据。使用Spring MVC提供Web内容了解如何使用Spring MVC和Thymeleaf创建web页面。将Spring Boot JAR应用程序转换为WAR了解如何将基于Spring Boot JAR的应用程序转换为WAR文件。创建异步方法了解如何创建异步服务方法。处理表格提交了解如何使用Spring创建和提交Web表单。使用Spring Boot构建应用程序了解如何使用最少的配置构建应用程序。使用WebSocket构建交互式Web应用程序了解如何通过WebSocket在浏览器和服务器之间发送和接收消息。使用STS制作入门指南了解如何使用Spring Tool Suite(STS)导入入门指南。使用AngularJS消费RESTful Web服务了解如何使用AngularJS检索web页面数据。使用rest.js消费RESTful Web服务了解如何使用rest.js检索web页面数据。使用jQuery消费RESTful Web服务学习如何使用jQuery检索web页面数据。为RESTful Web服务启用跨源请求了解如何使用Spring创建支持跨源资源共享(CORS)的RESTful Web服务。消费SOAP Web服务了解如何创建消费基于WSDL的服务的客户端。使用REST访问JPA数据了解如何使用Spring data REST使用RESTful、基于超媒体的数据持久性。使用REST访问Neo4j数据了解如何使用Spring data REST使用RESTful、基于超媒体的数据持久性。使用REST访问MongoDB数据了解如何使用Spring data REST使用RESTful、基于超媒体的数据持久性。使用REST访问Pivotal GemFire中的数据了解如何使用Spring data REST使用RESTful、基于超媒体的数据持久性。生成SOAP Web服务了解如何使用Spring创建基于SOAP的Web服务。使用Spring缓存数据了解如何使用Spring在内存中缓存数据。从STS部署到Cloud Foundry了解如何从STS将Spring应用程序部署到Cloud Foundry。Spring Boot使用Docker了解如何使用Maven或Gradle从Spring Boot应用程序创建Docker容器。使用IntelliJ IDEA制作入门指南了解如何使用IntelliJ IDEA制作入门指南。使用Vaadin创建CRUD UI使用Vaadin和Spring Data JPA构建动态UI。服务注册与发现了解如何使用Eureka注册和查找服务。集中配置了解如何从外部集中式源管理应用程序设置。路由和过滤了解如何使用Netflix Zuul将请求路由和过滤到微服务。断路器了解如何使用Hystrix优雅地降级服务。使用Ribbon和Spring Cloud进行客户端负载均衡动态支持即将上线和下线的服务,而不会中断客户端。测试Web层了解如何测试Spring Boot应用程序和MVC控制器。使用MySQL访问数据了解如何在MySQL上设置和管理用户帐户,以及如何配置Spring Boot以在运行时连接到它。创建多模块项目了解如何构建库并将其打包以供Spring Boot应用程序使用。使用Restdocs创建API文档了解如何使用Spring Restdocs为HTTP端点生成文档。使用Google Cloud Pub/Sub进行消息传递了解如何使用Spring Integration通道适配器和Google Cloud Pub/Sub交换消息。构建反应式RESTful Web服务了解如何使用Reactive Spring创建RESTful Web服务。费者驱动合约了解如何使用合同存根并从另一个Spring应用程序中使用该合同。访问Vault了解如何使用Spring Vault从HashiCorp Vault加载机密。Vault配置了解如何在HashiCorp Vault中存储和检索应用程序配置详细信息。使用Redis反应式访问数据了解如何与Redis和Spring Data进行反应性交互。将Spring Boot应用程序部署到Azure了解如何将Spring Boot应用程序部署到Azure。构建网关了解如何配置网关。专题指南设计为在一小时或更短的时间内阅读和理解,提供比入门指南更广泛或主观的内容。Spring安全架构Spring Security的主题指南,这些位如何组合以及它们如何与Spring Boot交互。Spring Boot Docker使用Docker的主题指南以及如何为Spring Boot应用程序创建容器镜像。教程这些指南旨在在2-3小时内完成,为企业应用程序开发主题提供更深入的上下文探索,让你随时准备实施真实的解决方案。使用Spring构建REST服务了解如何使用Spring轻松构建RESTful服务。Spring Security和Angular有关如何将Spring Security与具有各种后端体系结构的单页面应用程序一起使用的教程,范围从简单的单一服务器到具有OAuth2身份验证的API网关。React.js和Spring Data REST基于Greg Turnquist的5部分博客系列的教程。Spring Boot和OAuth2使用Facebook和Github进行“社交”登录和单点登录的教程。使用Spring Boot和Kotlin构建Web应用程序了解如何使用Spring、Kotlin、Junit 5和JPA轻松构建和测试Web应用程序。 ...

January 21, 2019 · 1 min · jiezi

微服务架构设计基础之领域驱动设计

DDD早于微服务「出道」十年,这两个「忘年交」的软件设计哲学是如何相爱相杀的?背景微服务现在可以说是软件研发领域无人不提的话题,然而业界流行的对比多数都是所谓的Monolithic(单体应用),而大量的系统在十几年前都已经是以SOA(面向服务架构)为基础的分布式系统了,那么微服务作为新的架构标准与SOA有什么差异点呢?其本质区别在于设计原理,微服务是去中心化设计,SOA是「集成」形成中心设计;另外,笔者认为以下几点并不是微服务和SOA的区别点:CI/CD:持续集成、持续部署本身与敏捷、DevOps是交织在一起的,CICD更倾向于软件工程的领域,与微服务无关;基于容器还是虚拟机:Docker、虚拟机、物理机等是物理介质的一种实现方式,与微服务无关;微服务周边生态:比如日志平台、调用链系统?更多的是研发本身对于效率提高的自驱力,而与使用何种架构方式无关;通讯协议:微服务的推荐通讯协议是RESTful,而传统的SOA是SOAP。不过基于轻量级的RPC框架Dubbo、Thrift、gRPC来实现微服务也很多;在Spring Cloud中也有Feign框架将标准RESTful转为代码的API这种仿RPC的行为,这些通讯协议不是区分微服务架构和SOA架构的核心差别;当然,软件工程(DevOps)、基础设施(容器化)、软件开发模式(敏捷开发)的变革有利的推进了微服务架构的大行其道。而微服务架构是一种架构风格、架构理念,其中的「微」更体现了它的精髓在切分。在实际微服务的落地过程中证明,如果切分是错误的,你得不到微服务承诺的「低耦合、自治、易维护」之类的优势,并且还会比单体架构拥有更多的麻烦。那么如何切分呢?其实并不是一些新的方法论,而是都提出很多年的架构设计方法,也称它们为微服务设计基础或架构模型:领域驱动设计和立方体模型。领域驱动设计2004年,Eric Evans 发表了Domain Driven Design(领域驱动设计,DDD)。领域驱动设计已经问世十几年,从Eric Evans出版的著作「领域驱动设计」一书中对领域驱动做了开创性的理论阐述,在软件设计领域中,DDD可以称得上是步入暮年时期了。遗憾的是,国外软件圈享有盛誉并行之有效的设计方法学,国内大多数的技术人员却并不了解,也未曾运用到项目实践中。直到行业内吹起微服务的热风,人们似乎才重新发现了领域驱动设计的价值,并不是微服务拯救了领域驱动设计,是因为领域驱动设计一直在顽强的成长,其设计开放的设计方法体系,虽然从来不曾在国内大行其道,但却发挥着巨大的价值。表面上看确实是因为微服务,领域驱动设计才又开始出现在大众视野里。领域驱动设计的意义当然,领域驱动设计并非「银弹」,不是能解决所有疑难杂症的「灵丹妙药」,学习并应用它的意义在于:一套完整的模型驱动的软件设计方法,用于简化软件项目的复杂度,它能带给你从战略设计到战术设计的规范过程,使得你的设计思路能够更加清晰,设计过程更加规范;一种思维方式和概念,可以应用在处理复杂业务的软件项目中,加快项目的交付速度;一组提炼出来的原则和模式,可以帮助开发者开发优雅的软件系统、促进开发者对架构与模型的精心打磨,尤其善于处理系统架构的演进设计、有助于提高团队成员的面向对象设计能力与架构设计能力;领域驱动设计与微服务架构天生匹配,无论是在新项目中设计微服务架构,还是将系统从单体架构演进到微服务设计,都可以遵循领域驱动设计的架构原则。当然,领域驱动能给我们带来很多收获,但如果你是属于以下几种情况的某种,那么你确实不需要学习领域驱动设计了:如果你是独当一面的架构师,并能设计出优雅的软件架构如果你是高效编码的程序员,并只想踏踏实实的写代码如果你是前端的设计人员,并奉行「用户体验至上」的理念如果你负责的软件系统并不复杂,二三人便可轻松维护DDD的关键概念一个软件系统的诞生,一定是为了解决我们遇到的某个问题。比如一家企业的一直采用线下销售产品,耗费大量的财力和物力,希望可以在线上销售自己的产品,用于实现在线销售销售产品的目的,那么就诞生了一个电商系统。通常最初设立的目标或要解决的问题就是一个软件项目的出发点,明确我们要做什么。比如一个电商、一个论坛、一个支付平台等。下文将从领域、问题域、领域模型、设计、驱动这几个词语的含义和联系的角度去阐述DDD是如何融入到软件开发的。要理解什么是领域驱动设计,首先要理解什么是领域,什么是设计,什么是驱动,什么驱动什么。什么是领域/子领域(Domain/Subdomain)领域是与某个特定问题相关的知识和行为。比如支付平台就属于特定的领域,只要是这个领域,都会有账户、会记、收款、付款、风控等核心环节。所以,同一个领域的系统都具有相同的核心业务,他们要解决的问题的本质是一致的。一个领域本质上可以理解为就是一个问题域,只要是同一个领域,那问题域就相同。所以,只要我们确定了系统所属的领域,那这个系统的核心业务,即要解决的关键问题、问题的范围边界就基本确定了。在日常开发中,我们通常会将一个大型的软件系统拆分成若干个子系统。这种划分有可能是基于架构方面的考虑,也有可能是基于基础设施的。在DDD中,我们对系统的划分是基于领域(基于业务)的。比如上文提到支付平台是一个领域,而账户、会记、收款、付款等则为子领域。一个领域由众多子领域聚集而形成。当然,问题随之而来:哪些概念应该建模在哪些子系统里面?有时可能会发现一个领域概念建模在子系统A中是可以的,而建模在子系统B中也合情合理。各个子系统之间的应该如何集成?有人可能会说,这不简单得就像客户端调用服务端那么简单吗?问题在于,两个系统之间的集成涉及到基础设施和不同领域概念在两个系统之间的翻译,稍不注意,这些概念就会对我们精心创建好的领域模型造成污染。DDD中,有标准方法解决上述问题,就是限界上下文(Bounded Context)和上下文映射图。在一个领域/子域中,我们会创建一个概念上的领域边界,在这个边界中,任何领域对象都只表示特定于该边界内部的确切含义。这样的边界便称为限界上下文。限界上下文和领域具有一对一的关系。从物理层面讲,一个限界上下文最终可以是一个Jar/War文件,甚至可以是一个Package中的所有对象。但是,技术本身并不是用来界分限界上下文。上图引自《实现领域驱动设计》。通常情况下,一个领域有且只有一个核心问题,我们称之为该领域的「核心域」。在核心域、通用子域、支撑子域梳理的同时,会定义出子域中的「限界上下文」及其关系,用它来阐述子域之间的关系。界限上下文可以简单理解成一个子系统或组件模块。什么是设计(Design)DDD中的设计主要指领域模型的设计。DDD是一种基于模型驱动开发的软件开发思想,强调领域模型是整个系统的核心,领域模型也是整个平台的核心价值。每一个领域都有一个对应的领域模型,领域模型能够很好的解决负责的业务问题。所以领域模型的设计和架构设计同等重要。什么是驱动(Driven)DDD中,总是以领域为边界,分析领域中的核心问题(核心关注点)。然后设计对应的领域模型,通过领域模型驱动代码的实现。而数据库设计、持久化技术这些都不是DDD的核心,属于外围的东西。与数据库驱动开发的思路形成对比,驱动中需要记住两个原则:领域驱动领域模型设计领域模型驱动代码实现领域驱动设计的最大价值是让我们告别从面向过程式的思想(天马星空,想到哪写到哪)转化为基于系统化的模型驱动思维。我们脑补一下软件开发中的常规心路历程:1、设计表结构2、写代码(代码写的很冗余,不够抽象)3、维护代码(适应业务变化)4、遇到困难(数据结构设计不合理、代码到处冗余、改BUG引入新BUG、新人看代码和无字天书一般)5、愈发难以维护,开始重构(理论上在老基础上改的技术债务堪比重新开发)6、重构完成,新系统上线(兼容历史数据、数据迁移、新老系统并行,等等出发点考虑,其实本质上只是做了代码重构)7、重复执行3-6步……DDD的分层架构四层架构Eric Evans在《领域驱动设计-软件核心复杂性应对之道》这本书中提出了传统的四层架构模式,在后来演进过程中出现了五层架构和六层架构,,如下图所示:User Interface:用户界面层/展示层,负责与用户交互。包含显示信息、解释用户命令等;Application:应用层,用来协调用户与各应用以及各应用之间的交互。不包含业务逻辑、不保存业务对象的状态;Domain:领域层/模型层,负责表达业务概念,业务状态信息以及业务规则。包含领域模型、领域信息、业务对象的状态。领域层是业务软件的核心;Infrastructure:基础设施层,为其他各层提供技术能力。包括为应用层传递消息、为领域层提供持久化机制、为用户界面层绘制屏幕组件等等。基础设施层还能够通过架构框架来支持四个层次间的交互模式。六边形架构随着后续的演进,出现了一种改进分层架构的方法,即Robert C. Martin提出的依赖倒置原则(Dependency Inversion Principle,DIP)。它通过改变不同层之间的依赖关系达到改进目的。高层模块不应该依赖于底层模块,两者都应该依赖于抽象抽象不应该依赖于细节,细节应该依赖于抽象根据该原则的定义,DDD分层架构中的低层组件应该依赖于高层组件提供的接口,即无论高层还是低层都依赖于抽象,整个分层架构好像被推平了,再向其中加入了一些对称性,就出现了一种具有对称性特征的六边形架构风格。六边形架构是Alistair Cockburn在2005年提出的,其本质是倡导不同的客户通过「平等」的方式与系统交互,通过不断的扩展适配器转化成系统API所理解的参数来达到每种特定的输出,而每种特定的输出都有适配器完成相应的转化功能。聚合:一组具有内聚关系的相关对象的集合;– 是一个修改数据的最小原子单元;– 聚合通常使用id访问;实体(Entity):表示具有生命周期并且会在其生命周期中发生改变的东西。含有VO、具有identity的特性,通常具有生命周期的概念 JPA tag @Entity;值对象(Value Object):表示起描述性作用的并且可以相互替换的概念。类似于pojo,不可变immutable,可在不同模型中传递,Spring tag @value;领域事件(Domain Event):所有的领域对象的跨聚合变更需要以事件方式进行通知和记录,聚合内的酌情考虑;工厂(Factory):负责所有对象的生成和组装;领域服务(Domain Service):纯技术层面的服务,例如日志,或者是跨聚合的编排服务,通常是Spring Component;资源层(Repository):类似于DAO层,Spring JPA, Hibernate之类 @CRUDRepository;防腐层:并非是系统间的消息传递机制,它的职责更具体的是指将某个模型或者契约中的概念对象及其行为转换到另一个模型或者契约中;贫血模型VS充血模型读完上面的两种分层架构方式,可能很多人会有疑问,这些是什么?为什么我之前一直都没听到过这种分法?确实是这样,DDD和面向对象、设计模式等等理论有千丝万缕的联系,如果不熟悉OOA、OOD,那么DDD可能也会理解不了。因为我们大部分从开发生涯开始之初接触的都是「Action层、Service层、Dao层、DB层」这样的MVC分层理论。并且在21中设计模式中,「行为型」的设计模式,我们几乎没有什么机会使用,导致这些问题的原因是J2EE经典分层的开发方式是「贫血模型」。Martin Fowler(对,就是提出微服务的那位大牛)曾经提出了两种开发方式,即:以「贫血模型」为基础的「事务脚本」的开发方式以「充血模型」为基础的「领域驱动」的开发方式贫血模型贫血模型是指对象只用于在各层之间传输数据使用,只有数据字段和Get/Set方法,没有逻辑在对象中。而「事务脚本」可以理解为业务是由一条条增删改查的SQL组织而成,是面向过程的编程。充血模型是面向对象设计的本质,一个对象是拥有状态和行为的。将大多数业务逻辑和持久化放在领域对象中,业务逻辑只是完成对业务逻辑的封装、事务、权限、校验等的处理。举例,用户管理模块大概是这样的两种实现:// 贫血模型下的实现public class User{private Integer id;private String name;…// 省略get/set方法}public class UserManager{public void save(User user){ // 持久化操作….}}// 保存用户的操作可能是这样userManager.save(user);// 充血模型下的实现public class User{private Integer id;private String name;…// 省略get/set方法public void save(){ // 持久化操作….}}// 保存用户的操作可能是这样user.save();Martin Fowler定义的「贫血模型」是反模式,面对简单的小系统用事务脚本方式开发没问题;稍微大一些的系统使用事务脚本方式会扩大维护成本,业务逻辑、各种状态散布在大量的函数中,哪怕就是要用户对象中增加一个字段,可能都会涉及到几个类的调整……希望领域对象能够准确地表达出业务意图,但是多数时候,我们所看到的却是充满getter和setter的领域对象,此时的领域对象已经不是领域对象了,反模式的贫血对象了。其实在贫血模型和充血模型模型之外,还有失血模型和胀血模型,但后者两个基本是实际开发中不会去使用,因为走的是两个极端。总结本文宏观角度介绍了领域驱动设计,那么微服务和DDD是什么关系呢?其实在2015年的一次演讲中,DDD的提出者Eric Evans表达了对微服务技术的热爱与支持,认为微服务是让DDD落地的好工具。因为DDD和微服务其本质是降低软件项目的复杂性,而DDD是一种设计理念/设计方法,DDD需要有强制性的原则做保障,否则不同的领域对象终究会混在一起。而微服务本身的一些限制,以及大家都能理解微服务的实施前提和首要条件,会在实现上给DDD增加了一些原则限制。DDD和微服务的不一定要同时使用落地,但是如果将DDD和微服务(两个相差十岁的软件设计方法)结合一起,那么Martin Fowler和Eric Evans两位布道师是会很赞同的。

January 21, 2019 · 1 min · jiezi

springSecurity 中不能抛出异常UserNameNotFoundException 解析

通过查看源码可得知:1. 前言抽象类中AbstractUserDetailsAuthenticationProvider 接口抛出异常AuthenticationException下面源码注释这么描述 * * @throws AuthenticationException if the credentials could not be validated * (generally a <code>BadCredentialsException</code>, an * <code>AuthenticationServiceException</code> or * <code>UsernameNotFoundException</code>) */ protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;AuthenticationException 抛出的情况是 BadCredentialsException,AuthenticationServiceException,UsernameNotFoundException这三个异常。当UserNameNotFoundException 这个异常的情况下会抛出可实际情况下我们 查询的user为null 抛出了 UserNameNotFoundException 这个异常但是实际并没有抛出来,抛出的是 AuthenticationException 通过继续往下查看源码后明白了,原来是做了对UserNameNotFoundException 处理,转换成了AuthenticationException 这个异常;hideUserNotFoundExceptions = true;…boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug(“User ‘” + username + “’ not found”); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } else { throw notFound; } }所以我们没有抛出UsernameNotFoundException 这个异常,而是将这个异常进行了转换。2.解决办法如何抛出这个异常,那就是将hideUserNotFoundExceptions 设置为 false;2.1设置实现类中 @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setHideUserNotFoundExceptions(false); provider.setUserDetailsService(mUserDetailsService); provider.setPasswordEncoder(passwordEncoder); return provider; }最后在WebSecurityConfig配置即可 auth.authenticationProvider(daoAuthenticationProvider());2.2 debug来看一下设置之前设置之后抛出的UsernameNotFoundException 异常已经捕获到了,然后进入if中最后抛出new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”))会将异常信息存入session中 , 根据key即可获取最后在失败的处理器中获取到@Componentpublic class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler { public MyAuthenctiationFailureHandler() { this.setDefaultFailureUrl("/loginPage"); } @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { logger.info(“进入认证失败处理类”); HttpSession session = request.getSession(); AuthenticationException attributes = (AuthenticationException) session.getAttribute(“SPRING_SECURITY_LAST_EXCEPTION”); logger.info(“aaaa " + attributes); super.onAuthenticationFailure(request, response, exception); }}这样子做可以直接在session中获取到,如果自定义抛出一个异常首先控制台会报异常错,其次前台的通过如ajax获取错误信息,又得写ajax。这样子做直接将信息存入session中,springSecurity直接为我们封装到session中了,可以直接根据key获取到。如: ${session.SPRING_SECURITY_LAST_EXCEPTION.message}2.4 判断密码注意 如果用户名不存在,抛了异常不要再在密码验证其中抛出密码错误异常,不然抛出UserNameNotFoundException 后还会验证密码是否正确,如果密码正确还好,返回true,如果不正确抛出异常。此时会将 UsernameNotFoundException 异常覆盖,这里应该返回false。源码如下:protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( “UserDetailsService returned null, which is an interface contract violation”); } return loadedUser; } catch (UsernameNotFoundException ex) { // 、、、、、、、 这里会去匹配密码是否正确 mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }mitigateAgainstTimingAttack 方法 private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); //这里会是自定义密码加密 this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword); } }我的密码加密器 @Override public boolean matches(CharSequence charSequence, String s) { String pwd = charSequence.toString(); log.info(“前端传过来密码为: " + pwd); log.info(“加密后密码为: " + MD5Util.encode(charSequence.toString())); log.info(“数据库存储的密码: " + s); //s 应在数据库中加密 if( MD5Util.encode(charSequence.toString()).equals(MD5Util.encode(s))){ return true; } //throw new DisabledException(”–密码错误–”); //不能抛出异常 return false; }如下是 我们密码验证器里抛出异常后获取到的异常异常密码未验证 之前捕获到的异常信息验证密码后捕获到的异常 (这里跳到ProviderManager中)既然我用户名不对我就不必要验证密码了,所以不应该抛出异常,应该直接返回false。不然。此处密码异常会将 用户不存在进行覆盖!3. 验证页面代码<body>登录页面<div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION!=null?session.SPRING_SECURITY_LAST_EXCEPTION.message:’’}">[…]</div><form method=“post” action="/login” > <input type=“text” name=“username” /><br> <input type=“password” name=“password” /> <input type=“submit” value=“login” /></form> ...

January 20, 2019 · 2 min · jiezi

spring-data-redis 2.0 的使用

在使用Spring Boot2.x运行Redis时,发现百度不到顺手的文档,搞通后发现其实这个过程非常简单和简洁,觉得有必要拿出来分享一下。Spring Boot2.x 不再使用Jedis,换成了Lettuce。Lettuce是基于 Netty 实现的,所以性能更好。但是我看到很多文章居然在Spring Boot 2.x还在写Jedis的配置。依赖依赖比较简单,spring-boot-starter-data-redis、commons-pool2 即可。 <!– redis –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!–spring2.0集成redis所需common-pool2–> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.2</version> </dependency>属性配置在属性中配置Redis Server的访问地址、密码、数据库,并配置连接池的属性。 redis: # reids的连接ip host: 127.0.0.1 port: 6379 password: helloworld # Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0 database: 0 # 连接超时时间(毫秒) timeout: 10000ms # redis client配置,使用lettuce lettuce: pool: # 连接池中的最小空闲连接 默认 0 min-idle: 0 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 max-wait: 1000ms # 连接池最大连接数(使用负值表示没有限制) 默认 8 max-active: 8 # 连接池中的最大空闲连接 默认 8 max-idle: 8注解配置全局使能缓存@EnableSwagger2 // 使用swagger api 功能@EnableCaching // 使用缓存@SpringBootApplicationpublic class Starter { public static void main(String[] args) { SpringApplication.run(Starter.class, args); }}通过注解使用缓存,@Cacheable 将获取值存入缓存 /** * 基于id 获取用户信息 / @Cacheable(value=“user”, key="#id", unless="#result == null") public UserDTO GetUserById(int id) { User userEntity = userMapper.getUserByID(id); if (userEntity == null){ return null; } / entity 转 DTO / UserDTO userDTO = new UserDTO(); userDTO.setAge(userEntity.getAge()); userDTO.setId(id); userDTO.setName(userEntity.getName()); userDTO.setCreateTime(unixTime2String(userEntity.getCreateTime())); userDTO.setPhone(userEntity.getPhone()); userDTO.setEmail(userEntity.getEmail()); return userDTO; }@CachePut 更新缓存 @CachePut(value = “user”, key="#p0.id") public UserDTO updateUser(InputUserInfoDTO inputUserInfoDTO){ userMapper.updateUser(inputUserInfoDTO.getId(), inputUserInfoDTO.getName(), inputUserInfoDTO.getAge()); User userEntity = userMapper.getUserByID(inputUserInfoDTO.getId());/ entity 转 DTO */ if (null == userEntity){ return null; } UserDTO userDTO = new UserDTO(); userDTO.setAge(userEntity.getAge()); userDTO.setId(userEntity.getId()); userDTO.setName(userEntity.getName()); userDTO.setCreateTime(unixTime2String(userEntity.getCreateTime())); userDTO.setPhone(userEntity.getPhone()); userDTO.setEmail(userEntity.getEmail()); return userDTO; }@CacheEvict 删除缓存 @CacheEvict(value = “user”, key="#id") public void deleteUser(int id){ userMapper.deleteUser(id); }当然为了支持序列化,我的UserDTO得implements Serializable@Datapublic class UserDTO implements Serializable {//public class UserDTO implements Serializable { private int id; private String name; private int age; private String createTime; private String phone; private String email;}至此缓存已经可以用起来了,不需要编写RedisConfig代码,有点小遗憾,直接去Redis查看数据,发现是乱码。这是因为我使用的是Java自带的序列化,如果要更换Redis序列化方法,就要重写RedisConfig了。RedisConfig这个配置也不复杂,使用Jackson2JsonRedisSerializer将对象转换为Json串,注意这里一定要使用ObjectMapper,否则再将json串反序列化为对象时会报。@Configuration@ConditionalOnClass(RedisOperations.class)@EnableConfigurationProperties(RedisProperties.class)public class RedisConfig extends CachingConfigurerSupport{ @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); //解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // 配置序列化(解决乱码的问题) RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ZERO) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory).cacheDefaults(config).build(); return cacheManager; }} ...

January 19, 2019 · 2 min · jiezi

IntelliJ IDEA创建第一个Spring boot项目

下载maven:http://maven.apache.org/downl…开发工具:IntelliJ IDEAJDK: Java JDK1.8 1.为了第一个项目初始化速度加快,我们先来配置maven:添加配置:选择Build,Execution,Deployment下, Bulid Tools下的Maven,在勾选右边红框中的Override,选择你下载后的文件夹中的settings.xml2.使用IntelliJ IDEA创建springboot项目1.创建新项目2.选择spring,选择jdk1.83.填写group ,选择packaging— War, 选择Next4.选择Web, 点击Next,下一步点击finish就好了。5.等着项目初始化完成就可以了。6.在项目的applicaiton右键,选择Run “DemoApplication"运行成功的截图:我的网站:https://wayne214.github.io

January 19, 2019 · 1 min · jiezi

关于ClassLoader的学习笔记,详解版

ClassLoader 详解ClassLoader 做什么的?延迟加载各司其职ClassLoader 传递性双亲委派Class.forName自定义加载器Class.forName vs ClassLoader.loadClass钻石依赖分工与合作Thread.contextClassLoaderClassLoader 是 Java 届最为神秘的技术之一,无数人被它伤透了脑筋,摸不清门道究竟在哪里。网上的文章也是一篇又一篇,经过本人的亲自鉴定,绝大部分内容都是在误导别人。本文我带读者彻底吃透 ClassLoader,以后其它的相关文章你们可以不必再细看了。ClassLoader 做什么的?顾名思义,它是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 .class,也可以是 jar 包里的 .class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。有很多字节码加密技术就是依靠定制 ClassLoader 来实现的。先使用工具对字节码文件进行加密,运行时使用定制的 ClassLoader 先解密文件内容再加载这些解密后的字节码。每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。ClassLoader 就像一个容器,里面装了很多已经加载的 Class 对象。class Class<T> { … private final ClassLoader classLoader; …}延迟加载JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。各司其职JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的静态文件服务器来下载字节码再加载。JVM 中内置了三个重要的 ClassLoader,分别是 BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader。BootstrapClassLoader 负责加载 JVM 运行时核心类,这些类位于 ExtensionClassLoader 负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVAHOME/lib/rt.jar文件中,我们常用内置库[java.xxx](https://link.juejin.im?target…,比如swing系列、内置的js引擎、xml解析器等等,这些库名通常以javax开头,它们的jar包位于JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包。AppClassLoader 才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到,它就是我们所说的「系统类加载器」,我们用户平时编写的类代码通常都是由它加载的。当我们的 main 方法执行的时候,这第一个用户类的加载器就是 AppClassLoader。ClassLoader 传递性程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象。前面我们提到每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。双亲委派前面我们提到 AppClassLoader 只负责加载 Classpath 下面的类库,如果遇到没有加载的系统类库怎么办,AppClassLoader 必须将系统类库的加载工作交给 BootstrapClassLoader 和 ExtensionClassLoader 来做,这就是我们常说的「双亲委派」。AppClassLoader 在加载一个未知的类名时,它并不是立即去搜寻 Classpath,它会首先将这个类名称交给 ExtensionClassLoader 来加载,如果 ExtensionClassLoader 可以加载,那么 AppClassLoader 就不用麻烦了。否则它就会搜索 Classpath 。而 ExtensionClassLoader 在加载一个未知的类名时,它也并不是立即搜寻 ext 路径,它会首先将类名称交给 BootstrapClassLoader 来加载,如果 BootstrapClassLoader 可以加载,那么 ExtensionClassLoader 也就不用麻烦了。否则它就会搜索 ext 路径下的 jar 包。这三个 ClassLoader 之间形成了级联的父子关系,每个 ClassLoader 都很懒,尽量把工作交给父亲做,父亲干不了了自己才会干。每个 ClassLoader 对象内部都会有一个 parent 属性指向它的父加载器。class ClassLoader { … private final ClassLoader parent; …}值得注意的是图中的 ExtensionClassLoader 的 parent 指针画了虚线,这是因为它的 parent 的值是 null,当 parent 字段是 null 时就表示它的父加载器是「根加载器」。如果某个 Class 对象的 classLoader 属性值是 null,那么就表示这个类也是「根加载器」加载的。注意这里的 parent 不是 super 不是父类,只是 ClassLoader 内部的字段。Class.forName当我们在使用 jdbc 驱动时,经常会使用 Class.forName 方法来动态加载驱动类。Class.forName(“com.mysql.cj.jdbc.Driver”);其原理是 mysql 驱动的 Driver 类里有一个静态代码块,它会在 Driver 类被加载的时候执行。这个静态代码块会将 mysql 驱动实例注册到全局的 jdbc 驱动管理器里。class Driver { static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException(“Can’t register driver!”); } } …}forName 方法同样也是使用调用者 Class 对象的 ClassLoader 来加载目标类。不过 forName 还提供了多参数版本,可以指定使用哪个 ClassLoader 来加载Class<?> forName(String name, boolean initialize, ClassLoader cl)通过这种形式的 forName 方法可以突破内置加载器的限制,通过使用自定类加载器允许我们自由加载其它任意来源的类库。根据 ClassLoader 的传递性,目标类库传递引用到的其它类库也将会使用自定义加载器加载。自定义加载器ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。下面我使用伪代码表示一下基本过程class ClassLoader { // 加载入口,定义了双亲委派规则 Class loadClass(String name) { // 是否已经加载了 Class t = this.findFromLoaded(name); if(t == null) { // 交给双亲 t = this.parent.loadClass(name) } if(t == null) { // 双亲都不行,只能靠自己了 t = this.findClass(name); } return t; } // 交给子类自己去实现 Class findClass(String name) { throw ClassNotFoundException(); } // 组装Class对象 Class defineClass(byte[] code, String name) { return buildClassFromCode(code, name); }}class CustomClassLoader extends ClassLoader { Class findClass(String name) { // 寻找字节码 byte[] code = findCodeFromSomewhere(name); // 组装Class对象 return this.defineClass(code, name); }}自定义类加载器不易破坏双亲委派规则,不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入。如果父类加载器是 null,那就表示父加载器是「根加载器」。// ClassLoader 构造器protected ClassLoader(String name, ClassLoader parent);双亲委派规则可能会变成三亲委派,四亲委派,取决于你使用的父加载器是谁,它会一直递归委派到根加载器。Class.forName vs ClassLoader.loadClass这两个方法都可以用来加载目标类,它们之间有一个小小的区别,那就是 Class.forName() 方法可以获取原生类型的 Class,而 ClassLoader.loadClass() 则会报错。Class<?> x = Class.forName("[I");System.out.println(x);x = ClassLoader.getSystemClassLoader().loadClass("[I");System.out.println(x);———————class [IException in thread “main” java.lang.ClassNotFoundException: [I…钻石依赖项目管理上有一个著名的概念叫着「钻石依赖」,是指软件依赖导致同一个软件包的两个版本需要共存而不能冲突。我们平时使用的 maven 是这样解决钻石依赖的,它会从多个冲突的版本中选择一个来使用,如果不同的版本之间兼容性很糟糕,那么程序将无法正常编译运行。Maven 这种形式叫「扁平化」依赖管理。使用 ClassLoader 可以解决钻石依赖问题。不同版本的软件包使用不同的 ClassLoader 来加载,位于不同 ClassLoader 中名称一样的类实际上是不同的类。下面让我们使用 URLClassLoader 来尝试一个简单的例子,它默认的父加载器是 AppClassLoader$ cat ~/source/jcl/v1/Dep.javapublic class Dep { public void print() { System.out.println(“v1”); }}$ cat ~/source/jcl/v2/Dep.javapublic class Dep { public void print() { System.out.println(“v1”); }}$ cat ~/source/jcl/Test.javapublic class Test { public static void main(String[] args) throws Exception { String v1dir = “file:///Users/qianwp/source/jcl/v1/”; String v2dir = “file:///Users/qianwp/source/jcl/v2/”; URLClassLoader v1 = new URLClassLoader(new URL[]{new URL(v1dir)}); URLClassLoader v2 = new URLClassLoader(new URL[]{new URL(v2dir)}); Class<?> depv1Class = v1.loadClass(“Dep”); Object depv1 = depv1Class.getConstructor().newInstance(); depv1Class.getMethod(“print”).invoke(depv1); Class<?> depv2Class = v2.loadClass(“Dep”); Object depv2 = depv2Class.getConstructor().newInstance(); depv2Class.getMethod(“print”).invoke(depv2); System.out.println(depv1Class.equals(depv2Class)); }}在运行之前,我们需要对依赖的类库进行编译$ cd ~/source/jcl/v1$ javac Dep.java$ cd ~/source/jcl/v2$ javac Dep.java$ cd ~/source/jcl$ javac Test.java$ java Testv1v2false在这个例子中如果两个 URLClassLoader 指向的路径是一样的,下面这个表达式还是 false,因为即使是同样的字节码用不同的 ClassLoader 加载出来的类都不能算同一个类depv1Class.equals(depv2Class)我们还可以让两个不同版本的 Dep 类实现同一个接口,这样可以避免使用反射的方式来调用 Dep 类里面的方法。Class<?> depv1Class = v1.loadClass(“Dep”);IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance();depv1.print()ClassLoader 固然可以解决依赖冲突问题,不过它也限制了不同软件包的操作界面必须使用反射或接口的方式进行动态调用。Maven 没有这种限制,它依赖于虚拟机的默认懒惰加载策略,运行过程中如果没有显示使用定制的 ClassLoader,那么从头到尾都是在使用 AppClassLoader,而不同版本的同名类必须使用不同的 ClassLoader 加载,所以 Maven 不能完美解决钻石依赖。 如果你想知道有没有开源的包管理工具可以解决钻石依赖的,我推荐你了解一下 sofa-ark,它是蚂蚁金服开源的轻量级类隔离框架。分工与合作这里我们重新理解一下 ClassLoader 的意义,它相当于类的命名空间,起到了类隔离的作用。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader 是类名称的容器,是类的沙箱。不同的 ClassLoader 之间也会有合作,它们之间的合作是通过 parent 属性和双亲委派机制来完成的。parent 具有更高的加载优先级。除此之外,parent 还表达了一种共享关系,当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类可以认为是所有子 ClassLoader 共享的。这也是为什么 BootstrapClassLoader 被所有的类加载器视为祖先加载器,JVM 核心类库自然应该被共享。Thread.contextClassLoader如果你稍微阅读过 Thread 的源代码,你会在它的实例字段中发现有一个字段非常特别class Thread { … private ClassLoader contextClassLoader; public ClassLoader getContextClassLoader() { return contextClassLoader; } public void setContextClassLoader(ClassLoader cl) { this.contextClassLoader = cl; } …}contextClassLoader「线程上下文类加载器」,这究竟是什么东西?首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它。你可以使用下面这种方式来显示使用它Thread.currentThread().getContextClassLoader().loadClass(name);这意味着如果你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader。那些因为代码上的依赖关系而懒惰加载的类也不会自动使用 contextClassLoader来加载。其次线程的 contextClassLoader 默认是从父线程那里继承过来的,所谓父线程就是创建了当前线程的线程。程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是 AppClassLoader。那这个 contextClassLoader 究竟是做什么用的?我们要使用前面提到了类加载器分工与合作的原理来解释它的用途。它可以做到跨线程共享类,只要它们共享同一个 contextClassLoader。父子线程之间会自动传递 contextClassLoader,所以共享起来将是自动化的。如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来。如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。线程的 contextClassLoader 使用场合比较罕见,如果上面的逻辑晦涩难懂也不必过于计较。JDK9 增加了模块功能之后对类加载器的结构设计做了一定程度的修改,不过类加载器的原理还是类似的,作为类的容器,它起到类隔离的作用,同时还需要依靠双亲委派机制来建立不同的类加载器之间的合作关系。完 ...

January 19, 2019 · 3 min · jiezi

Spring Boot [后台脚手架] SanJi Boot v2.0 -去繁就简 重新出发

SanJi Boot v2.0去繁就简 重新出发基于Spring Boot 集成一些常用的功能,你只需要基于它做些简单的修改即可。演示环境:网址: SanJi-Boot v2.0用户名/密码: admin/admin功能列表:[x] 权限认证[x] 权限管理[x] 用户管理[x] 角色管理[x] 日志管理项目结构:sanji-boot├─java│ ├─common 公共模块│ │ ├─spring spring相关的功能│ │ └─utils 常用工具│ │ │ ├─modules 功能模块│ │ └─sys 权限模块│ │ │ └─SanjiBootApplication 项目启动类│ └─resources ├─static 第三方库、插件等静态资源 │ ├─app 项目中自己写的css js img 等资源文件 │ ├─page 页面 │ └─plugins 第三方库、插件等静态资源 │ └─application.yml 项目配置文件注意事项:运行项目前导入sanji-boot.sql技术栈(技术选型):后端:核心框架 :Spring Boot 2.1.1.RELEASE安全框架:Apache security视图框架:Spring MVC持久层框架:Spring Data JPA数据库连接池:HikariDataSource日志管理:LogBackJSON序列号框架: fastjson插件: lombok 前端:主要使用的技术:渐进式JavaScript 框架:VUE 2.2.0弹窗框架: jquery-confirm页面主体框架 :zhengAdmin效果图:源码以托管码云扩展:zhengAdmin使用VueSpring Boot 学习资料

January 18, 2019 · 1 min · jiezi

JAVA | Spring + quartz 实现定时任务

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言很久不见,因为忙着泡妞,断更了一个月,实在是罪过。废话不多说,最近在工作中遇到了使用 quartz 实现定时任务的需求。写出来分享给大家,权当笔记。Spring + quartz 实现定时任务因为在开发中遇到的是非 Maven 的老项目,所以先介绍这种方式。这种实现方式更多的是 xml 的配置。1.首先在 lib 目录下加入 quartz 这l两个jar包2.Scheduler(任务调度器)的配置在 applicationContext.xml 加入 Scheduler 的配置<bean id=“MyScheduler” class=“org.springframework.scheduling.quartz.SchedulerFactoryBean”> <property name=“triggers”> <list> <ref bean=“MyTriggers”></ref> </list> </property> <property name=“autoStartup” value=“true”></property></bean>3.Trigger(触发器)的配置,这里设置了逢5分钟的倍数执行一次<bean id=“MyTriggers” class=“org.springframework.scheduling.quartz.CronTriggerFactoryBean”> <property name=“jobDetail” ref=“MyJobDetail”></property> <property name=“cronExpression”> <!–<value>0 */1 * * * ?</value>–> <!–<value>0 */5 * * * ?</value>–> <!– 早上八点执行 –> <!–<value>0 0 8 * * ?</value>–> <!– 逢5分钟的倍数执行一次 –> <value>0 */5 * * * ?</value> </property></bean>4.JobDetail(任务,即被调度的任务)的配置<bean id=“MyJobDetail” class=“org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean”> <!– 执行的类 –> <property name=“targetObject” ref=“MyJobService”></property> <!– 类中的方法 –> <property name=“targetMethod” value=“doSomething”></property> <property name=“concurrent” value=“false”/> <!– 是否允许任务并发执行。当值为false时,表示必须等到前一个线程处理完毕后才再启一个新的线程 –></bean>5.业务类的配置<bean id=“oltJobService” class=“com.nasus.service.quartz.MyJobService”> <property name=“MyDao” ref=“MyDao” /> <!– 注入属性 –></bean>6.业务实现import java.util.Date;public class MyJobService { public void doSomething() { System.out.println(“date: " + new Date().getTime()); }} 7.启动项目就可以看到控制台每隔五分钟就打印一次当前时间后语以上就是我对 Spring + quartz 的使用理解,希望对你们有帮助。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点个赞。 ...

January 18, 2019 · 1 min · jiezi

springboot+多线程简单实现

搭建springboot环境创建ThreadConfig/** * 线程 * * @author zhoumin * @create 2018-09-18 13:58 /@Configuration@EnableAsyncpublic class ThreadConfig implements AsyncConfigurer{ @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(1000); executor.setQueueCapacity(500); executor.setKeepAliveSeconds(30000); executor.initialize(); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return null; }}创建service和接口void test(int i);service实现类@Override@Asyncpublic void test(int i) { System.out.println(“线程” + Thread.currentThread().getName() + " 执行异步任务:" + i);}测试:@RunWith(SpringRunner.class)@SpringBootTestpublic class BaseTest {}/* * @author zhoumin * @create 2018-09-18 14:12 */public class ThreadTest extends BaseTest{ @Autowired private DeviceStatisticsTaskService deviceStatisticsTaskService; @org.junit.Test public void threadTest() { for (int i = 0; i < 5000; i++) { deviceStatisticsTaskService.test(i); } }} ...

January 17, 2019 · 1 min · jiezi

springBoot 与neo4j的简单整合

Neo4j简介Neo4j是基于java语言实现的世界领先的图形数据库, 是一个高性能的图形存储,具有成熟和强大的数据库所需的所有功能,如友好的查询语言(Cypher)和ACID事务。对于许多应用程序,与关系数据库相比,Neo4j提供了数量级的性能优势。主要应用于图检索和关系计算。其优点在于:节点没上线(3.0以后去掉了限制)扩展性很好,支持集群和企业版数据ETL有丰富的工具支持,自带GUI良好的WebUI更加详细的介绍参考:https://neo4j.com/docs/gettin…Neo4j安装Neo4j在不同部署环境中的安装,例如Linux,Mac OS,Windows,Debian,Docker或CAPI Flash,以下介绍在windows系统中的安装方法:1.下载最新安装包点击以下地址:https://neo4j.com/download-ce…,选择社区服务器下相应版本下载即可。 2.安装Neo4j解压安装包到相应目录后在bin目录下打开命令提示符或者任意地方打开命令提示符进入bin目录下如图的四个命令:(1)进入bin目录下(2)neo4j 查询相应命令(3)install-service 安装Neo4j服务(4)start 启动服务服务安装成功之后浏览器输入 http://localhost:7474/browser/ 即可访问登录页面,其中用户名密码都为neo4j,登陆后可修改密码,登录页面如下图其余环境的服务安装方法可参考:https://neo4j.com/docs/operat…Neo4j与springBoot简单整合大概了解了Neo4j和进行安装之后,接下来就对Neo4j和springBoot进行一个简单的整合,如图实例我们依据图中人物与电影的关系建立一个简单的demo(图片源自官方文档)建立一个springBoot项目 2.引入neo4j的pom <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-neo4j</artifactId> </dependency> </dependencies>3.建立一个Person和Movie的实体类@NodeEntitypublic class Person { @Id @GeneratedValue private Long id; private String name; private String born; public Person() {// 从 Neo4j API 2.0.5开始需要无参构造函数 } public Person(String name, String born) { this.name = name; this.born = born; } //省略getter and setter@NodeEntitypublic class Movie { @Id @GeneratedValue private Long id; private String title; private String released; public Movie() { } public Movie(String title, String released) { this.title = title; this.released = released; } //省略getter and setter需要注意的是类上@NodeEntity,标识这是一个节点,实体类中还需要一个无参构造方法,从 Neo4j API 2.0.5开始需要的。4.建立MovieRepository和PersonRepositorypublic interface MovieRepository extends CrudRepository<Movie, Long> { Movie findByTitle(String title);}public interface PersonRepository extends CrudRepository<Person, Long> { Person findByName(String name);}其中继承CrudRepository已经可以实现简单的CRUD了5.在Person中建立movie与演员、导演的指向关系 @Relationship(type = “ACTED_IN”, direction = Relationship.OUTGOING) public Set<Movie> actors; public void addActor(Movie movie) { if (actors == null) { actors = new HashSet<>(); } actors.add(movie); } @Relationship(type = “DIRECTED”, direction = Relationship.OUTGOING) public Set<Movie> directors; public void addDirector(Movie movie) { if (directors == null) { directors = new HashSet<>(); } directors.add(movie); }@Relationship这个注解表示关系,type是关系名称,direction是方向,例子中的是person——>movie6.application.properties配置文件中建立数据库连接#本地连接可以省略该行,非本地配置改对应ip#spring.data.neo4j.uri=bolt://localhostspring.data.neo4j.username=neo4jspring.data.neo4j.password=1234567.单元测试(1)创建movie @Test public void testSaveMovie() { Movie m1 = new Movie(“无问西东”, “2018”); Movie m2 = new Movie(“罗曼蒂克消亡史”, “2016”); movieRepo.save(m1); movieRepo.save(m2); }可以看到数据库已经建立两部电影的相关节点(2)创建person及其与movie的关系 @Test public void testSavePerson() { Person p1 = new Person(“章子怡”, “1979”); Person p2 = new Person(“李芳芳”, “1976”); Person p3 = new Person(“程耳”, “1970”); Movie m1 = movieRepo.findByTitle(“罗曼蒂克消亡史”); Movie m2 = movieRepo.findByTitle(“无问西东”); if (m1!=null) { p1.addActor(m1); p3.addDirector(m1); } if (m2!=null) { p1.addActor(m2); p2.addDirector(m2); } personRepo.save(p1); personRepo.save(p2); personRepo.save(p3); }如图,测试用例已经建立起了电影与导演、演员之间的联系(3)findMovieByTitle @Test public void testfindByTitle() { Movie movie = movieRepo.findByTitle(“罗曼蒂克消亡史”); }查询出相应的结果符合预期{ “id”:26, “title”:“罗曼蒂克消亡史”, “released”:“2016”}(4)findPersonByName @Test public void testfindByName() { Person person = personRepo.findByName(“章子怡”); }这里需注意的是,该演员对应了两部电影,查询个人信息的同时应该包含两部电影{ “id”:27, “name”:“章子怡”, “born”:“1979”, “actors”:[ { “id”:25, “title”:“无问西东”, “released”:“2018” }, { “id”:26, “title”:“罗曼蒂克消亡史”, “released”:“2016” } ]}查询结果也符合预期还有很多findAll、delete就不在一一列举了,至此springBoot与neo4j的简单整合就完成了 ...

January 16, 2019 · 2 min · jiezi

SpringBoot统一异常处理最佳实践

摘要: SpringBoot异常处理。原文:Spring MVC/Boot 统一异常处理最佳实践作者:赵俊前言在 Web 开发中, 我们经常会需要处理各种异常, 这是一件棘手的事情, 对于很多人来说, 可能对异常处理有以下几个问题:什么时候需要捕获(try-catch)异常, 什么时候需要抛出(throws)异常到上层.在 dao 层捕获还是在 service 捕获, 还是在 controller 层捕获.抛出异常后要怎么处理. 怎么返回给页面错误信息.异常处理反例既然谈到异常, 我们先来说一下异常处理的反例, 也是很多人容易犯的错误, 这里我们同时讲到前端处理和后端处理 :捕获异常后只输出到控制台前端代码$.ajax({ type: “GET”, url: “/user/add”, dataType: “json”, success: function(data){ alert(“添加成功”); }});后端代码try { // do something} catch (Exception e) { e.printStackTrace();}这是见过最多的异常处理方式了, 如果这是一个添加商品的方法, 前台通过 ajax 发送请求到后端, 期望返回 json 信息表示添加结果. 但如果这段代码出现了异常:那么用户看到的场景就是点击了添加按钮, 但没有任何反应(其实是返回了 500 错误页面, 但这里前端没有监听 error 事件, 只监听了 success 事件. 但即使加上了error: function(data) {alert(“添加失败”);}) 又如何呢? 到底因为啥失败了呢, 用户也不得而知.后台 e.printStackTrace() 打印在控制台的日志也会在漫漫的日志中被埋没, 很可能会看不到输出的异常. 但这并不是最糟的情况, 更糟糕的事情是连 e.printStackTrace() 都没有, catch 块中是空的, 这样后端的控制台中更是什么都看不到了, 这段代码会像一个隐形的炸弹一样一直埋伏在系统中.混乱的返回方式前端代码$.ajax({ type: “GET”, url: “/goods/add”, dataType: “json”, success: function(data) { if (data.flag) { alert(“添加成功”); } else { alert(data.message); } }, error: function(data){ alert(“添加失败”); }});后端代码@RequestMapping("/goods/add")@ResponseBodypublic Map add(Goods goods) { Map map = new HashMap(); try { // do something map.put(flag, true); } catch (Exception e) { e.printStackTrace(); map.put(“flag”, false); map.put(“message”, e.getMessage()); } reutrn map;}这种方式捕获异常后, 返回了错误信息, 且前台做了一定的处理, 看起来很完善? 但用 HashMap 中的 flag 和 message 这种字符串来当键很容易处理, 例如你这里叫 message, 别人起名叫 msg, 甚至有时手抖打错了, 怎么办? 前台再改成 msg 或其他的字符?, 前端后端这样一直来回改?更有甚者在情况 A 的情况下, 返回 json, 在情况 B 的情况下, 重定向到某个页面, 这就更乱了. 对于这种不统一的结构处理起来非常麻烦.异常处理规范既然要进行统一异常处理, 那么肯定要有一个规范, 不能乱来. 这个规范包含前端和后端.不要捕获任何异常对的, 不要在业务代码中进行捕获异常, 即 dao、service、controller 层的所以异常都全部抛出到上层. 这样不会导致业务代码中的一堆 try-catch 会混乱业务代码.统一返回结果集不要使用 Map 来返回结果, Map 不易控制且容易犯错, 应该定义一个 Java 实体类. 来表示统一结果来返回, 如定义实体类:public class ResultBean<T> { private int code; private String message; private Collection<T> data; private ResultBean() { } public static ResultBean error(int code, String message) { ResultBean resultBean = new ResultBean(); resultBean.setCode(code); resultBean.setMessage(message); return resultBean; } public static ResultBean success() { ResultBean resultBean = new ResultBean(); resultBean.setCode(0); resultBean.setMessage(“success”); return resultBean; } public static <V> ResultBean<V> success(Collection<V> data) { ResultBean resultBean = new ResultBean(); resultBean.setCode(0); resultBean.setMessage(“success”); resultBean.setData(data); return resultBean; } // getter / setter 略}正常情况: 调用 ResultBean.success() 或 ResultBean.success(Collection<V> data), 不需要返回数据, 即调用前者, 需要返回数据, 调用后者. 如:@RequestMapping("/goods/add")@ResponseBodypublic ResultBean<Goods> getAllGoods() { List<Goods> goods = goodsService.findAll(); return ResultBean.success(goods);}@RequestMapping("/goods/update")@ResponseBodypublic ResultBean updateGoods(Goods goods) { goodsService.update(goods); return ResultBean.success();}一般只有查询方法需要调用 ResultBean.success(Collection<V> data) 来返回 N 条数据, 其他诸如删除, 修改等方法都应该调用 ResultBean.success(), 即在业务代码中只处理正确的功能, 不对异常做任何判断. 也不需要对 update 或 delete 的更新条数做判断(个人建议, 实际需要根据业务). 只要没有抛出异常, 我们就认为用户操作成功了. 且操作成功的提示信息在前端处理, 不要后台返回 “操作成功” 等字段.前台接受到的信息为:{ “code”: 0, “message”: “success”, “data”: [ { “name”: “商品1”, “price”: 50.00, }, { “name”: “商品2”, “price”: 99.99, } ]}抛出异常: 抛出异常后, 我们应该调用 ResultBean.error(int code, String message), 来将状态码和错误信息返回, 我们约定 code 为 0 表示操作成功, 1 或 2 等正数表示用户输入错误, -1, -2 等负数表示系统错误.前台接受到的信息为:{ “code”: -1, “message”: “XXX 参数有问题, 请重新填写”, “data”: null}前端统一处理:返回的结果集规范后, 前端就很好处理了:/** * 显示错误信息 * @param result: 错误信息 /function showError(s) { alert(s);}/* * 处理 ajax 请求结果 * @param result: ajax 返回的结果 * @param fn: 成功的处理函数 ( 传入data: fn(result.data) ) /function handlerResult(result, fn) { // 成功执行操作,失败提示原因 if (result.code == 0) { fn(result.data); } // 用户操作异常, 这里可以对 1 或 2 等错误码进行单独处理, 也可以 result.code > 0 来粗粒度的处理, 根据业务而定. else if (result.code == 1) { showError(result.message); } // 系统异常, 这里可以对 -1 或 -2 等错误码进行单独处理, 也可以 result.code > 0 来粗粒度的处理, 根据业务而定. else if (result.code == -1) { showError(result.message); } // 如果进行细粒度的状态码判断, 那么就应该重点注意这里没出现过的状态码. 这个判断仅建议在开发阶段保留用来发现未定义的状态码. else { showError(“出现未定义的状态码:” + result.code); }}/* * 根据 id 删除商品 */function deleteGoods(id) { $.ajax({ type: “GET”, url: “/goods/delete”, dataType: “json”, success: function(result){ handlerResult(result, deleteDone); } });}function deleteDone(data) { alert(“删除成功”);}showError 和 handlerResult 是公共方法, 分别用来显示错误和统一处理结果集.然后将主要精力放在发送请求和处理正确结果的方法上即可, 如这里的 deleteDone 函数, 用来处理操作成功给用户的提示信息, 正所谓各司其职, 前端负责操作成功的消息提示更合理, 而错误信息只有后台知道, 所以需要后台来返回.后端统一处理异常说了这么多, 还没讲到后端不在业务层捕获任何异常的事, 既然所有业务层都没有捕获异常, 那么所有的异常都会抛出到 Controller 层, 我们只需要用 AOP 对 Controller 层的所有方法处理即可.好在 Spring 为我们提供了一个注解, 用来统一处理异常:@ControllerAdvice@ResponseBodypublic class WebExceptionHandler { private static final Logger log = LoggerFactory.getLogger(WebExceptionHandler.class); @ExceptionHandler public ResultBean unknownAccount(UnknownAccountException e) { log.error(“账号不存在”, e); return ResultBean.error(1, “账号不存在”); } @ExceptionHandler public ResultBean incorrectCredentials(IncorrectCredentialsException e) { log.error(“密码错误”, e); return ResultBean.error(-2, “密码错误”); } @ExceptionHandler public ResultBean unknownException(Exception e) { log.error(“发生了未知异常”, e); // 发送邮件通知技术人员. return ResultBean.error(-99, “系统出现错误, 请联系网站管理员!”); }}在这里统一配置需要处理的异常, 同样, 对于未知的异常, 一定要及时发现, 并进行处理. 推荐出现未知异常后发送邮件, 提示技术人员.总结总结一下统一异常处理的方法:不使用随意返回各种数据类型, 要统一返回值规范.不在业务代码中捕获任何异常, 全部交由 @ControllerAdvice 来处理.一个简单的演示项目: https://github.com/zhaojun1998/exception-handler-demo本文作者: 赵俊本文链接: http://www.zhaojun.im/springboot-exception/版权声明: 本博客所有文章除特别声明外,均采用BY-NC-SA许可协议。转载请注明出处! ...

January 16, 2019 · 3 min · jiezi

网关实现灰度发布

一、背景互联网产品开发有个非常特别的地方,就是不停的升级,升级,再升级。采用敏捷开发的方式,基本上保持每周或者每两周一次的发布频率,系统升级总是伴随着各种风险,新旧版本兼容的风险,用户使用习惯突然改变而造成用户流失的风险,系统宕机的风险,500错误服务不可用的风险等等。为了避免这些风险,很多产品都采用了灰度发布的策略,其主要思想就是把影响集中到一个点,然后再发散到一个面,出现意外情况后很容易就回退,即使影响也是可控的。任何脱离实际业务的技术工作都是耍流氓,技术需要服务于业务。因此,本文尽量淡化了业务方面的因素,聚焦于技术层面,建议在实际运用中还是要根据各自的业务场景去变化和调整。二、什么是灰度灰度发布是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。互联网系统,灰度其实就是根据设定的规则将请求路由到我们的灰度版本(灰度机器)上来。比如对于API来说,一般有如下几个需求:特定用户(比如测试帐号)、 特定的App(比如测试app或者合作App)、特定的模块、接口(只有某些接口需要灰度,这种一般是API Container的修改,拿一些不是很重要的API做灰度测试)、特定的机器(某些请求IP转发到灰度机)等。三、灰度的优势1、 在发布过程中降低上线风险2、 降低影响范围,并且范围可控3、 降低对测试的依赖,减少线下自测的数据构造成本4、 特定的请求能够指向特定的服务器,方便集中监控日志,方便跟踪完整的调用链路5、 方便系统流量切入6、 便于随时回滚7、 指定特定人群,方便系统回访,方便产品需求收集,完善产品功能,提升产品质量8、 在无状态的情况下保障用户使用到的版本一致9、 避免宕机给用户带来不好的体验和使用四、目标1、 做到对现有业务系统无侵入性2、 能够发挥以上提到的灰度的优势3、 发布系统的灵活配置4、 发布系统和业务系统的松耦合5、 和网关系统结合,让操作平滑五、功能1、 路由策略管理/配置2、 灰度规则管理3、 开启/关闭开关六、系统设计需要设计的系统分为两种场景,一种是http方式接入,需要借助网关(gate-way)去实现流量的切换,和系统路由;另一种是rpc接入(目前为dubbo),需要借助dubbo提供的负载均衡策略来实现,结合自带的qos(dubbo的在线运维命令)实现服务启动/关闭。【说明】:服务内部执行线程监控待定,sentinel 、 pinpoint or other。1、http方式接入其中分为几个重要的部分:接入层网关,接入客户端请求,根据下发的配置将符合条件的请求转发到新旧系统上.配置管理后台,这个后台可以配置不同的转发策略给接入层网关.稳定和灰度两种处理客户端请求的业务服务器.http请求的入口都落在网关上,网关会根据管控平台(admin dashboard)的配置进行uri的选择。此时请求数据会判断当前应用是否已经开启灰度,再次判断是应用级别的灰度还是服务级别的灰度,然后根据管控平台配置的灰度策略进行灰度,可以支持白名单、权重、ip段、业务域等。管控平台会调用引擎管理执行相应的指令,进行关闭、开启、更新策略和白名单数据等,每次网关重新reload和重启时会从灰度管理系统调用接口读取配置应用的信息,加入缓存。为了提升性能,应用的基本信息、灰度策略、白名单等数据缓存在内存或者类redis这样的缓冲中,灵活的进行缓存数据的更新。实现功能:1、动态路由2、服务动态编排,实现流量的自由切换3、启服/停服4、服务自检2、rpc(dubbo)接入如果直接停机重启rpc service会有什么影响:服务发布时,直接重启Tomcat,导致节点正在处理的请求会受到影响,严重时会有数据异常。服务发布时如果节点正在作为task_tracker运行lts任务,会导致任务失败并retry。服务发布时如果节点正在消费RocketMQ中的消息,会导致消息消费异常,甚至进入retry或dlq队列。服务发布完成后没有即时验证机制,直接暴露给用户,如有异常影响面很广。线上无法同时存在新老版本的服务来用于长时间的验证。竟然有这么多问题,想想就可怕,泪崩~,因此必须想法优雅的实现服务的启停,因此引出dubbo 服务的持续发布:dubbo-consumer实现不同的负载均衡,在负载的时候进行白名单校验和策略选择。系统对灰度管控平台非强制依赖,管控平台出现问题不影响系统正常运行。负载动态路由,阻止后续流量进入,监控服务是否还有执行的线程,加入钩子offline服务或者接口,进行服务升级,自检,启动online,接入负载均衡。由于很多接口都有在Dubbo中进行注册,因此需要有办法能够对其Provider Service接口进行下线或屏蔽,使其不提供服务,即其它服务无法调用它的接口。Service接口下线后,此consumer机器自然无任何流量流入,因此也无流量返回,达到下线consumer机器的目的,然后即可部署代码。官方有提供Dubbo-Admin工具,用于对Dubbo中各APP及其Service接口进行管理,里面自然也包含有实现下线的功能,可以有3种方法:屏蔽,貌似一直没有效果(尴尬);禁用,可以成功禁用;权重调节,可以设置0-100的权重,设置为0时即不提供服务。经过权重调节方案,通过Dubbo-Admin对需要下线机器的APP应用接口权限设置为0。实现的功能:1、缓存负载策略在系统启动的时候要根据系统配置拉取灰度策略,并且保存在内存中,定时获取最新的负载策略,需要提供及时触发的策略更新接口。2、 负载均衡在系统上线之前选择运行时使用的负载均衡进行调用。3、系统配置系统在上线前需要录入管控平台,并且完成相应的配置,在启动的时候作为唯一标识能够拉取相应的配置。4、监控和统计系统在内存中缓存统计信息,定时上传管控平台,监控出现问题不影响系统正常使用(sentinel,or dubbo-amdin模块扩展)。5、qos运维工具系统的启动使用qos,停服采用延时关闭结合jvm钩子。七、检查机制为了平滑发布的顺利进行,检查确认机制不可或缺,即确保Dubbo/Http中的下线都已生效,并且无流量发生,我们从以下两个维度去检查:接口检查,调用Dubbo、Http的API接口,检查业务服务机器状态,是否为已经下线。当然,在做了下线功能的同时,我们也有检查功能和上线功能,可供调用。监控检查,调用监控平台(ELK)的API接口,检查业务服务机器的请求访问数和日志流量是否都已经为0,已经处于下线状态。经过上述改造后,我们新的发布流程如下,基本解决了平滑发布问题,发布时对业务的影响降到了最低;八、停服/启服后小范围验证1.灰度验证–不影响线上用户2.部分实例发布– 导部分流量到新实例(可通过网关路由规则:用户ID取模,区域限制等等)3.全部发布

January 15, 2019 · 1 min · jiezi

Small Spring系列一:BeanFactory(一)

人生如逆旅,我亦是行人。前言Spring是一个开放源代码的设计层面框架,它解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。准备bean-v1.xml配置bean的信息BeanDefinition用于存放bean的定义BeanFactory获取bean的信息,实例化bean`BeanFactoryTest测试BeanFactory是否可用bean-v1.xml<?xml version=“1.0” encoding=“UTF-8”?><beans> <bean id = “nioCoder” class = “com.niocoder.service.v1.NioCoderService”> </bean> <bean id =“invalidBean” class=“xxx.xxx”> </bean></beans>BeanDefinitionbean-v1.xml中定义了每个bean,但这些信息我们该如何存储呢?spring是通过BeanDefinition接口来描述bean的定义BeanDefinitionpackage com.niocoder.beans;/** * bean.xml bean的定义 * @author zhenglongfei /public interface BeanDefinition { /* * 获取bean.xml中 bean的全名 如 “com.niocoder.service.v1.NioCoderService” * @return / String getBeanClassName();}GenericBeanDefinitionGenericBeanDefinition实现了BeanDefinition接口package com.niocoder.beans.factory.support;import com.niocoder.beans.BeanDefinition;/* * BeanDefinition 实现类 * * @author zhenglongfei /public class GenericBeanDefinition implements BeanDefinition { private String id; private String beanClassName; public GenericBeanDefinition(String id, String beanClassName) { this.id = id; this.beanClassName = beanClassName; } public String getBeanClassName() { return this.beanClassName; }}BeanFactory我们已经使用BeanDefinition来描述bean-v1.xml的bean的定义,下面我们使用BeanFactory来获取bean的实例BeanFactorypackage com.niocoder.beans.factory;import com.niocoder.beans.BeanDefinition;/* * 创建bean的实例 * @author zhenglongfei /public interface BeanFactory { /* * 获取bean的定义 * @param beanId * @return / BeanDefinition getBeanDefinition(String beanId); /* * 获取bean的实例 * @param beanId * @return / Object getBean(String beanId);}DefaultBeanFactoryDefaultBeanFactory实现了BeanFactory接口package com.niocoder.beans.factory.support;import com.niocoder.beans.BeanDefinition;import com.niocoder.beans.factory.BeanCreationException;import com.niocoder.beans.factory.BeanDefinitionStoreException;import com.niocoder.beans.factory.BeanFactory;import com.niocoder.util.ClassUtils;import org.dom4j.Document;import org.dom4j.Element;import org.dom4j.io.SAXReader;import java.io.InputStream;import java.util.Iterator;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;/* * BeanFactory的默认实现类 * * @author zhenglongfei /public class DefaultBeanFactory implements BeanFactory { private static final String ID_ATTRIBUTE = “id”; private static final String CLASS_ATTRIBUTE = “class”; /* * 存放BeanDefinition / private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(); /* * 根据文件名称加载,解析bean.xml * * @param configFile / public DefaultBeanFactory(String configFile) { loadBeanDefinition(configFile); } /* * 具体解析bean.xml的方法 使用dom4j * * @param configFile / private void loadBeanDefinition(String configFile) { ClassLoader cl = ClassUtils.getDefaultClassLoader(); try (InputStream is = cl.getResourceAsStream(configFile)) { SAXReader reader = new SAXReader(); Document doc = reader.read(is); Element root = doc.getRootElement(); Iterator<Element> elementIterator = root.elementIterator(); while (elementIterator.hasNext()) { Element ele = elementIterator.next(); String id = ele.attributeValue(ID_ATTRIBUTE); String beanClassName = ele.attributeValue(CLASS_ATTRIBUTE); BeanDefinition bd = new GenericBeanDefinition(id, beanClassName); this.beanDefinitionMap.put(id, bd); } } catch (Exception e) { throw new BeanDefinitionStoreException(“IOException parsing XML document”, e); } } @Override public BeanDefinition getBeanDefinition(String beanId) { return this.beanDefinitionMap.get(beanId); } @Override public Object getBean(String beanId) { BeanDefinition bd = this.getBeanDefinition(beanId); if (bd == null) { throw new BeanCreationException(“BeanDefinition does not exist”); } ClassLoader cl = ClassUtils.getDefaultClassLoader(); String beanClassName = bd.getBeanClassName(); try { // 使用反射创建bean的实例,需要对象存在默认的无参构造方法 Class<?> clz = cl.loadClass(beanClassName); return clz.newInstance(); } catch (Exception e) { throw new BeanCreationException(“Bean Definition does not exist”); } }}BeanFactoryTest以上,我们已经创建了bean.xml,BeanDefinition来描述bean的定义,并且使用BeanFactory来获取bean的实例。下面我们来测试一下BeanFactory是否可用。package com.niocoder.test.v1;import com.niocoder.beans.BeanDefinition;import com.niocoder.beans.factory.BeanCreationException;import com.niocoder.beans.factory.BeanDefinitionStoreException;import com.niocoder.beans.factory.BeanFactory;import com.niocoder.beans.factory.support.DefaultBeanFactory;import com.niocoder.service.v1.NioCoderService;import org.junit.Assert;import org.junit.Test;import static org.junit.Assert.assertEquals;import static org.junit.Assert.assertNotNull;/* * BeanFactory 测试类 /public class BeanFactoryTest { /* * 测试获取bean / @Test public void testGetBean() { BeanFactory factory = new DefaultBeanFactory(“bean-v1.xml”); BeanDefinition bd = factory.getBeanDefinition(“nioCoder”); assertEquals(“com.niocoder.service.v1.NioCoderService”, bd.getBeanClassName()); NioCoderService nioCoderService = (NioCoderService) factory.getBean(“nioCoder”); assertNotNull(nioCoderService); } /* * 测试无效的bean / @Test public void testInvalidBean() { BeanFactory factory = new DefaultBeanFactory(“bean-v1.xml”); try { factory.getBean(“invalidBean”); } catch (BeanCreationException e) { return; } Assert.fail(“expect BeanCreationException “); } /* * 测试无效的xml */ @Test public void testInvalidXML() { try { new DefaultBeanFactory(“xxx.xml”); } catch (BeanDefinitionStoreException e) { return; } Assert.fail(“expect BeanDefinitionStoreException “); }}代码下载github:https://github.com/longfeizheng/small-spring/tree/20190914_BeanFactory_v1类图 ...

January 14, 2019 · 2 min · jiezi

为自己搭建一个分布式 IM 系统二【从查找算法聊起】

前言最近这段时间确实有点忙,这篇的目录还是在飞机上敲出来了的。言归正传,上周更新了 cim 第一版;没想到反响热烈,最高时上了 GitHub Trending Java 版块的首位,一天收到了 300+ 的 star。现在总共也有 1.3K+ 的 star,有几十个朋友参加了测试,非常感谢大家的支持。在这过程中也收到一些 bug 反馈,feature 建议;因此这段时间我把一些影响较大的 bug 以及需求比较迫切的 feature 调整了,本次更新的 v1.0.1 版本:客户端超时自动下线。新增 AI 模式。聊天记录查询。在线用户前缀模糊匹配。下面谈下几个比较重点的功能。客户端超时自动下线 这个功能涉及到客户端和服务端的心跳设计,比较有意思,也踩了几个坑;所以准备留到下次单独来聊。AI 模式大家应该还记得这个之前刷爆朋友圈的 估值两个一个亿的 AI 核心代码。和我这里的场景再合适不过了。于是我新增了一个命令用于一键开启 AI 模式,使用情况大概如下。欢迎大家更新源码体验,融资的请私聊我????。聊天记录聊天记录也是一个比较迫切的功能。使用命令 :q 关键字 即可查询与个人相关的聊天记录。这个功能其实比较简单,只需要在消息发送及接收消息时保存即可。但要考虑的一点是,这个保存消息是 IO 操作,不可避免的会有耗时;需要尽量避免对消息发送、接收产生影响。异步写入消息因此我把消息写入的过程异步完成,可以不影响真正的业务。实现起来也挺简单,就是一个典型的生产者消费者模式。主线程收到消息之后直接写入队列,另外再有一个线程一直源源不断的从队列中取出数据后保存聊天记录。大概的代码如下:写入消息的同时会把消费消息的线程打开:而最终存放消息记录的策略,考虑后还是以最简单的方式存放在客户端,可以降低复杂度。简单来说就是根据当前日期+用户名写入到磁盘里。当客户端关闭时利用线程中断的方式停止了消费队列的线程。这点的设计其实和 logback 写日志的方式比较类似,感兴趣的可以去翻翻 logback 的源码,更加详细。回调接口至于收到其他客户端发来的消息时则是利用之前预留的消息回调接口来写入日志。收到消息后会执行自定义的回调接口。于是在这个回调方法中实现写入逻辑即可,当后续还有其他的消息处理逻辑时也能在这里直接添加。当处理逻辑增多时最好是改为责任链模式,更加清晰易维护。查找算法接下来是本文着重要讨论的一个查找算法,准确的说是一个前缀模糊匹配的算法。实现的效果如下:使用命令 :qu prefix 可以按照前缀的方式搜索用户信息。当然在命令行中其实意义不大,但是在移动端中确是比较有用的。类似于微信按照用户名匹配:因为后期打算出一个移动端 APP,所以就先把这个功能实现了。从效果也看得出来:就是按照输入的前缀匹配字符串(目前只支持英文)。在没有任何限制的条件下最快、最简单的实现方式可以直接把所有的字符串存放在一个容器中 (List、Set),查询时则挨个遍历;利用 String.startsWith(“prefix”) 进行匹配。但这样会有几个问题:存储资源比较浪费,不管是 list 还是 Set 都会有额外的损耗。查询效率较低,需要遍历集合后再遍历字符串的 char 数组(String.startsWith 的实现方式)。字典树基于以上的问题我们可以考虑下:假设我需要存放 java,javascript,jsp,php 这些字符串时在 ArrayList 中会怎么存放?很明显,会是这样完整的存放在一个数组中;同时这个数组还可能存在浪费,没有全部使用完。但其实仔细观察这些数据会发现有一些共同特点,比如 java,javascript 有共同的前缀 java;和 jsp 有共同的前缀 j。那是否可以把这些前缀利用起来呢?这样就可以少存储一份。比如写入 java,javascript 这两个字符串时存放的结构如下:当再存入一个 jsp 时:最后再存入 jsf 时:相信大家应该已经看明白了,按照这样的存储方式可以节省很多内存,同时查询效率也比较高。比如查询以 jav 开头的数据,只需要从头结点 j 开始往下查询,最后会查询到 ava 以及 script 这两个个结点,所以整个查询路径所经历的字符拼起来就是查询到的结果java+javascript。如果以 b 开头进行查询,那第一步就会直接返回,这样比在 list 中的效率高很多。但这个图还不完善,因为不知道查询到啥时候算是匹配到了一个之前写入的字符串。比如在上图中怎么知道 j+ava 是一个我们之前写入的 java 这个字符呢。因此我们需要对这种是一个完整字符串的数据打上一个标记:比如这样,我们将 ava、script、p、f 这几个节点都换一个颜色表示。表明查询到这个字符时就算是匹配到了一个结果。而查到 s 这个字符颜色不对,代表还需要继续往下查。比如输入关键字 js 进行匹配时,当它的查询路径走到 s 这里时判断到 s 的颜色不对,所以不会把 js 作为一个匹配结果。而是继续往下查,发现有两个子节点 p、f 颜色都正确,于是把查询的路径 jsp 和 jsf 都作为一个匹配结果。而只输入 j,则会把下面所有有色的字符拼起来作为结果集合。这其实就一个典型的字典树。具体实现下面则是具体的代码实现,其实算法不像是实现一个业务功能这样好用文字分析;具体还是看源码多调试就明白了。谈下几个重点的地方吧:字典树的节点实现,其中的 isEnd 相当于图中的上色。利用一个 Node[] children 来存放子节点。为了可以区分大小写查询,所以子节点的长度相当于是 26*2。写入数据这里以一个单测为例,写入了三个字符串,那最终形成的数据结构如下:图中有与上图有几点不同:每个节点都是一个字符,这样树的高度最高为52。每个节点的子节点都是长度为 52 的数组;所以可以利用数组的下标表示他代表的字符值。比如 0 就是大 A,26 则是小 a,以此类推。有点类似于之前提到的布隆过滤器,可以节省内存。debug 时也能看出符合上图的数据结构:所以真正的写入步骤如下:把字符串拆分为 char 数组,并判断大小写计算它所存放在数组中的位置 index。将当前节点的子节点数组的 index 处新增一个节点。如果是最后一个字符就将新增的节点置为最后一个节点,也就是上文的改变节点颜色。最后将当前节点指向下一个节点方便继续写入。查询总的来说要麻烦一些,其实就是对树进行深度遍历;最终的思想看图就能明白。所以在 cim 中进行模糊匹配时就用到了这个结构。字典树的源码在此处:https://github.com/crossoverJie/cim/blob/master/cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/TrieTree.java其实利用这个结构还能实现判断某个前缀的单词是否在某堆数据里、某个前缀的单词出现的次数等。总结目前 cim 还在火热内测中(虽然群里只有20几人),感兴趣的朋友可以私聊我拉你入伙☺️ 再没有新的 BUG 产生前会着重把这些功能完成了,不出意外下周更新 cim 的心跳重连等机制。完整源码:https://github.com/crossoverJie/cim如果这篇对你有所帮助还请不吝转发。 ...

January 14, 2019 · 1 min · jiezi

SpringBoot之MongoTemplate的查询可以怎么耍

学习一个新的数据库,一般怎么下手呢?基本的CURD没跑了,当可以熟练的增、删、改、查一个数据库时,可以说对这个数据库算是入门了,如果需要更进一步的话,就需要了解下数据库的特性,比如索引、事物、锁、分布式支持等本篇博文为mongodb的入门篇,将介绍一下基本的查询操作,在Spring中可以怎么玩原文可参看: 190113-SpringBoot高级篇MongoDB之查询基本使用姿势I. 基本使用0. 环境准备在正式开始之前,先准备好环境,搭建好工程,对于这一步的详细信息,可以参考博文: 181213-SpringBoot高级篇MongoDB之基本环境搭建与使用接下来,在一个集合中,准备一下数据如下,我们的基本查询范围就是这些数据1. 根据字段进行查询最常见的查询场景,比如我们根据查询user=一灰灰blog的数据,这里主要会使用Query + Criteria 来完成@Componentpublic class MongoReadWrapper { private static final String COLLECTION_NAME = “demo”; @Autowired private MongoTemplate mongoTemplate; /** * 指定field查询 / public void specialFieldQuery() { Query query = new Query(Criteria.where(“user”).is(“一灰灰blog”)); // 查询一条满足条件的数据 Map result = mongoTemplate.findOne(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | specialFieldQueryOne: " + result); // 满足所有条件的数据 List<Map> ans = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | specialFieldQueryAll: " + ans); }}上面是一个实际的case,从中可以知道一般的查询方式为:Criteria.where(xxx).is(xxx)来指定具体的查询条件封装Query对象 new Query(criteria)借助mongoTemplate执行查询 mongoTemplate.findOne(query, resultType, collectionName)其中findOne表示只获取一条满足条件的数据;find则会将所有满足条件的返回;上面执行之后,删除结果如query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { } | specialFieldQueryOne: {_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { } | specialFieldQueryAll: [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]2. and多条件查询前面是只有一个条件满足,现在如果是要求同时满足多个条件,则利用org.springframework.data.mongodb.core.query.Criteria#and来斜街多个查询条件/* * 多个查询条件同时满足 /public void andQuery() { Query query = new Query(Criteria.where(“user”).is(“一灰灰blog”).and(“age”).is(18)); Map result = mongoTemplate.findOne(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | andQuery: " + result);}删除结果如下query: Query: { “user” : “一灰灰blog”, “age” : 18 }, Fields: { }, Sort: { } | andQuery: {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}3. or或查询and对应的就是or,多个条件中只要一个满足即可,这个与and的使用有些区别, 借助org.springframework.data.mongodb.core.query.Criteria#orOperator来实现,传参为多个Criteria对象,其中每一个表示一种查询条件/* * 或查询 /public void orQuery() { // 等同于 db.getCollection(‘demo’).find({“user”: “一灰灰blog”, $or: [{ “age”: 18}, { “sign”: {$exists: true}}]}) Query query = new Query(Criteria.where(“user”).is(“一灰灰blog”) .orOperator(Criteria.where(“age”).is(18), Criteria.where(“sign”).exists(true))); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | orQuery: " + result); // 单独的or查询 // 等同于Query: { “$or” : [{ “age” : 18 }, { “sign” : { “$exists” : true } }] }, Fields: { }, Sort: { } query = new Query(new Criteria().orOperator(Criteria.where(“age”).is(18), Criteria.where(“sign”).exists(true))); result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | orQuery: " + result);}执行后输出结果为query: Query: { “user” : “一灰灰blog”, “$or” : [{ “age” : 18 }, { “sign” : { “$exists” : true } }] }, Fields: { }, Sort: { } | orQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]query: Query: { “$or” : [{ “age” : 18 }, { “sign” : { “$exists” : true } }] }, Fields: { }, Sort: { } | orQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}, {_id=5c3b0538e3ac8e8d2d392390, user=二灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]4. in查询标准的in查询case/* * in查询 /public void inQuery() { // 相当于: Query query = new Query(Criteria.where(“age”).in(Arrays.asList(18, 20, 30))); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | inQuery: " + result);}输出query: Query: { “age” : { “$in” : [18, 20, 30] } }, Fields: { }, Sort: { } | inQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]5. 数值比较数值的比较大小,主要使用的是 get, gt, lt, let/* * 数字类型,比较查询 > /public void compareBigQuery() { // age > 18 Query query = new Query(Criteria.where(“age”).gt(18)); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | compareBigQuery: " + result); // age >= 18 query = new Query(Criteria.where(“age”).gte(18)); result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | compareBigQuery: " + result);}/* * 数字类型,比较查询 < /public void compareSmallQuery() { // age < 20 Query query = new Query(Criteria.where(“age”).lt(20)); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | compareSmallQuery: " + result); // age <= 20 query = new Query(Criteria.where(“age”).lte(20)); result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | compareSmallQuery: " + result);}输出query: Query: { “age” : { “$gt” : 18 } }, Fields: { }, Sort: { } | compareBigQuery: [{_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]query: Query: { “age” : { “$gte” : 18 } }, Fields: { }, Sort: { } | compareBigQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]query: Query: { “age” : { “$lt” : 20 } }, Fields: { }, Sort: { } | compareSmallQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}]query: Query: { “age” : { “$lte” : 20 } }, Fields: { }, Sort: { } | compareSmallQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]6. 正则查询牛逼高大上的功能/* * 正则查询 /public void regexQuery() { Query query = new Query(Criteria.where(“user”).regex("^一灰灰blog”)); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | regexQuery: " + result);}输出query: Query: { “user” : { “$regex” : “^一灰灰blog”, “$options” : "” } }, Fields: { }, Sort: { } | regexQuery: [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afacde3ac8e8d2d392389, user=一灰灰blog2, desc=帅气逼人的码农界老秀2}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}, {_id=5c3afafbe3ac8e8d2d39238b, user=一灰灰blog4, desc=帅气逼人的码农界老秀4}, {_id=5c3afb0ae3ac8e8d2d39238c, user=一灰灰blog5, desc=帅气逼人的码农界老秀5}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]7. 查询总数统计常用,这个主要利用的是mongoTemplate.count方法/* * 查询总数 /public void countQuery() { Query query = new Query(Criteria.where(“user”).is(“一灰灰blog”)); long cnt = mongoTemplate.count(query, COLLECTION_NAME); System.out.println(“query: " + query + " | cnt " + cnt);}输出query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { } | cnt 58. 分组查询这个对应的是mysql中的group查询,但是在mongodb中,更多的是通过聚合查询,可以完成很多类似的操作,下面借助聚合,来看一下分组计算总数怎么玩/ * 分组查询 /public void groupQuery() { // 根据用户名进行分组统计,每个用户名对应的数量 // aggregate([ { “$group” : { “_id” : “user” , “userCount” : { “$sum” : 1}}}] ) Aggregation aggregation = Aggregation.newAggregation(Aggregation.group(“user”).count().as(“userCount”)); AggregationResults<Map> ans = mongoTemplate.aggregate(aggregation, COLLECTION_NAME, Map.class); System.out.println(“query: " + aggregation + " | groupQuery " + ans.getMappedResults());}注意下,这里用Aggregation而不是前面的Query和Criteria,输出如下query: { “aggregate” : “collection”, “pipeline” : [{ “$group” : { “_id” : “$user”, “userCount” : { “$sum” : 1 } } }] } | groupQuery [{_id=一灰灰blog, userCount=5}, {_id=一灰灰blog2, userCount=1}, {_id=一灰灰blog4, userCount=1}, {_id=二灰灰blog, userCount=1}, {_id=一灰灰blog5, userCount=1}]9. 排序sort,比较常见的了,在mongodb中有个有意思的地方在于某个字段,document中并不一定存在,这是会怎样呢?/* * 排序查询 /public void sortQuery() { // sort查询条件,需要用with来衔接 Query query = Query.query(Criteria.where(“user”).is(“一灰灰blog”)).with(Sort.by(“age”)); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | sortQuery " + result);}输出结果如下,对于没有这个字段的document也被查出来了query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { “age” : 1 } | sortQuery [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]10. 分页数据量多的时候,分页查询比较常见,用得多就是limit和skip了/* * 分页查询 */public void pageQuery() { // limit限定查询2条 Query query = Query.query(Criteria.where(“user”).is(“一灰灰blog”)).with(Sort.by(“age”)).limit(2); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | limitPageQuery " + result); // skip()方法来跳过指定数量的数据 query = Query.query(Criteria.where(“user”).is(“一灰灰blog”)).with(Sort.by(“age”)).skip(2); result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | skipPageQuery " + result);}输出结果表明,limit用来限制查询多少条数据,skip则表示跳过前面多少条数据query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { “age” : 1 } | limitPageQuery [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}]query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { “age” : 1 } | skipPageQuery [{_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]11. 小结上面给出的一些常见的查询姿势,当然并不全面,比如我们如果需要查询document中的部分字段怎么办?比如document内部结果比较复杂,有内嵌的对象或者数组时,嵌套查询可以怎么玩?索引什么的又可以怎么利用起来,从而优化查询效率?如何通过传说中自动生成的_id来获取文档创建的时间戳?先留着这些疑问,后面再补上II. 其他0. 项目工程:spring-boot-demomodule: mongo-template相关博文: 181213-SpringBoot高级篇MongoDB之基本环境搭建与使用1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

January 13, 2019 · 5 min · jiezi

spring security使用详解

spring security使用详解由于公司新的后台项目独立,和其他服务没有关系,所以考虑单独实现自己的用户登录模块。 所以,我将对spring security基于 “springboot” 框架的使用方式总结如下: (1)当我们在pom文件中,添加依赖后 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> 启动服务,访问 http://localhost:8080这个地址,端口可以自己设置通过server.port=进行配置,下面弹出如下页面: 此时,系统已经处于受保护状态,这是spring security默认的登录页面。系统生成默认用户名为 “user”,密码打印到控制台总,如下: 我们输入用户名和密码便可以登录进去,我们也可以application.properties文件中自定义用户名和密码如下: 到此,最简单的使用方式已经完成,但是这还远远不能满足我们的需要,截下来介绍自定义表单登录。(2)spring security 表单登录 具体流程如下图: 本文参考地址:https://blog.csdn.net/mj86534…

January 11, 2019 · 1 min · jiezi

是时候给大家介绍SpringBoot背后豪华的研发团队了。

摘要: SpringBoot的来龙去脉。原文:为什么说 Java 程序员到了必须掌握 Spring Boot 的时候?微信公众号:纯洁的微笑Fundebug经授权转载,版权归原作者所有。看了 Pivotal 公司的发展历史,这尼玛就是一场商业大片呀。我们刚开始学习 Spring Boot 的时候肯定都会看到这么一句话:Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。这里的 Pivotal 团队肯定就是 Spring Boot 的研发团队了,那么这个 Pivotal 团队到底是个什么来头呢?和 Spring 又有那些关系?不着急且听我慢慢道来。要说起这个 Pivotal 公司的由来,我得先从 Spring 企业的这条线来说起。Spring 的发展时间回到 2002 年,当时正是 Java EE 和 EJB 大行其道的时候,很多知名公司都是采用此技术方案进行项目开发。这时候有一个美国的小伙子认为 EJB 太过臃肿,并不是所有的项目都需要使用 EJB 这种大型框架,应该会有一种更好的方案来解决这个问题。他为了证明自己的想法是正确的,在 2002 年 10 月写了一本书《Expert One-on-One J2EE》,介绍了当时 Java 企业应用程序开发的情况,并指出了 Java EE 和 EJB 组件框架中存在的一些主要缺陷。在这本书中,他提出了一个基于普通 Java 类和依赖注入的更简单的解决方案。在书中,他展示了如何在不使用 EJB 的情况下构建高质量、可扩展的在线座位预留系统。为了构建应用程序,他编写了超过 30,000 行的基础结构代码,项目中的根包命名为 com.interface21,所以人们最初称这套开源框架为 interface21,这就是 Spring 的前身。这个小伙子是谁呢?他就是大名鼎鼎的 Rod Johnson(下图),Rod Johnson 在悉尼大学不仅获得了计算机学位,同时还获得了音乐学位,更令人吃惊的是在回到软件开发领域之前,他还获得了音乐学的博士学位。现在 Rod Johnson 已经离开了 Spring,成为了一个天使投资人,同时也是多个公司的董事,早已走上人生巅峰。在这本书发布后,一对一的 J2EE 设计和开发一炮而红。这本书免费提供的大部分基础架构代码都是高度可重用的。2003 年 Rod Johnson 和同伴在此框架的基础上开发了一个全新的框架命名为 Spring,据 Rod Johnson 介绍 Spring 是传统 J2EE 新的开始,随后 Spring 发展进入快车道。2004 年 03 月,1.0 版发布。2006 年 10 月,2.0 版发布。2007 年 11 月,更名为 SpringSource,同时发布了 Spring 2.5。2009 年 12 月,Spring 3.0 发布。2013 年 12 月,Pivotal 宣布发布 Spring 框架 4.0。2017 年 09 月,Spring 5.0 发布。网上有一张图,清晰的展示了 Spring 发展:从上面这个时间线我们可以看出 Pivotal 团队和 Spring 在 2013 年交上了线,这是为什么呢?友情提示,接下来科技行业的一系列商业并购大片即将开启。Pivotal 公司上面说的 Pivotal 团队是指 Pivotal 公司,先给大家来一段 Pivotal 公司的简介:Pivotal 成立于2013年4月,致力于“改变世界构造软件的方式(We are transforming how the world builds software)”,提供云原生应用开发 PaaS 平台及服务,帮助企业客户采用敏捷软件开发方法论,从而提高软件开发人员工作效率、减少运维成本,实现数字化转型、IT 创新,并最终实现业务创新。截至目前,财富 100 强中超过三分之一的企业使用 Pivotal 云原生平台。Pivotal 部分大型客户在采用 Pivotal 产品后,开发人员与运营人员比例可提高到 200:1,开发人员专注于编写软件代码时间增长了 50%。看了简介大家可能会有点犯迷糊,这不是一个 2013 年成立的 IT 服务公司吗,和 2002 年发展起来的 Spring 又是怎么扯上关系的呢?其实呀,要说起 Pivotal 公司的起源要追溯到 1989 年的 Pivotal Labs 实验室。Pivotal Labs 公司1989 年,Rob Mee 创立的咨询公司 Pivotal Labs,专注于快速的互联网式软件开发,即敏捷编程。创立 Pivotal Labs 的时候,它还是一家非常小的软件顾问公司,它的主营业务就是与客户合作,帮助客户开发软件。Pivotal Labs 一直是敏捷开发领域的领导者,为部分硅谷最有影响力的公司塑造了软件开发文化,并树立了良好口碑,其中 Google、Twitter 都曾是 Pivotal Labs 客户。时间很快到了 2012 年,深受客户喜爱的 Pivotal 终于引起了商用软件巨头 EMC 的关注,EMC 在 2012 年以现金方式收购了 Pivotal 并照单全收了它的 200 名员工。刚开始的时候,公司并没有发生太大的变化,只是作为新部门成为了 EMC 的一部分,Pivotal Labs 仍然继续像以前样与客户合作。但是到 2013 年的时候,EMC 突然扔下了一颗重磅炸弹。它将 Pivotal Labs 的核心业务分拆出去,成立了一家名为 Pivotal Software 的新公司。这家新公司的股东是 EMC 、 VMware 和通用电气,之前在 EMC 子公司 VMware 担任首席执行官的马瑞兹出任公司的首席执行官。EMC 和 VMware 分拆出其 Cloud Foundry、Pivotal Labs、Greenplum 等云计算、大数据资源,GE 投资 1.05 亿美元,成立新公司 Pivotal。新生的 Pivotal 是名副其实的“富二代”,这轮估值高达 10.5 亿美元。那么 EMC 和 VMware 又有什么关联呢?2003 年 12 月, EMC 公司宣布以 6.35 亿美元收购了 VMware 公司。EMC 于 1979 年成立于美国麻州 Hopkinton 市,1989 年开始进入企业数据储存市场。二十多年来,EMC 全心投注在各项新的储存技术,已获得了 1,300 个已通过或审核中的储存技术专利。无论是全球外接 RAID 储存系统、网络储存亦或是储存管理软件等储存专业领域,EMC 均是业界公认的领导厂商。EMC 是全球第六大企业软件公司,全球信息基础架构技术与解决方案的领先开发商与提供商。同时也是美国财富五百强之一,在全世界拥有超过四万二千名员工,在全球 60 个国家或地区拥有分支机构。我们接触比较多就是 EMC 的各种存储产品。EMC 公司做大 EMC 的秘诀,就是研发与并购双轮驱动,研发与并购的投入占当年营业收入的 22% 左右,并购投入略高于研发。从 2003 年到2 015 年的 12 年间,EMC 总共投入超过 420 亿美元用于研发和收购。其中,206 亿美元用于研发,213 亿美元用于并购,总共并购了 100 多家公司。VMware 收购 Spring2009 年是 Spring 企业的一个转折点,VMware 以 4.2 亿美元收购 Spring Source (3.6亿现金外加5800万股份) 。可以说虚拟化就是 VMware 发明的VMware 于 1998 年成立,公司总部位于美国加州帕洛阿尔托,是全球云基础架构和移动商务解决方案厂商,提供基于VMware的解决方案,企业通过数据中心改造和公有云整合业务,借助企业安全转型维系客户信任,实现任意云端和设备上运行、管理、连接及保护任意应用。2018 财年全年收入 79.2 亿美元。相信作为研发人员肯定都使用过 VMware 公司的产品,最常用的是 VMware 的虚拟机产品,但其实 VMware 公司的产品线非常多。从发展路线来看,VMware 具备三大特点:第一,是技术具备领先性,虚拟化技术在70年代就已出现,但VMware是第一个将这项技术应用到X86服务器上,并在这个基础上不断完善,使其能够满足企业级客户需求;第二,是瞄准大型企业客户。VMware 刚刚上市时,年营收不到4亿美金,但已经覆盖80%的财富1000强客户;第三,是高度产品化。VMware 的毛利率长期保持在 85% 左右,咨询业务占比非常少,几乎将所有部署工作都交给合作伙伴。VMware 也是一个并购大户,通过投资和收购补全业务线,客户资源是一大优势。2012 年 Rod Johnson 宣布他将要离开 Spring Source 。EMC 又被收购2015 年的时候,曾经被大量报道 EMC 考虑被子公司 VMware 收购,让人大跌眼镜,竟然可以有这样的骚动作,这是为什么呢?EMC 在 2003 年斥资 6.25 亿美元收购了 VMware,四年之后,EMC 选择让 VMware 分拆上市,结果独立上市的 VMware 发展越来越好,反观 EMC 的各项业务持续陷入低潮。到 2015 年的时候,VMware 的市值已达到约 370 亿美元,占据了 EMC 总市值的近 75%。可能各方利益不能达成一致,最终 EMC 却被戴尔(dell)收购。2015 年 10 月 12 日,戴尔(Dell)和EMC(易安信)公司宣布签署最终协议,戴尔公司与其创始人、主席和首席执行官麦克尔•戴尔,与 马上到! Partner 以及银湖资本一起,收购 EMC 公司,交易总额达 670亿 美元,成为科技史上最大并购。当时业界最关心的云计算软件商 VMware 仍然保持独立上市公司的身份。据悉,EMC 当前持有 VMware 大约 80% 的股权,市值约为 320 亿美元。而戴尔收购 EMC 实际上是项庄舞剑,VMware 才是戴尔收购 EMC 的关键。戴尔的故事1984 年,创办人迈克尔·戴尔在德州大学奥斯汀分校就学时创立了 PCs Limited 这家计算机公司。在 1985 年,公司生产了第一部拥有自己独特设计的计算机“Turbo PC”,售价为 795 美元。从此开启了戴尔公司的发展史,下面为戴尔公司的里程碑1984年 - 年仅19岁的Michael Dell凭借1,000美元的资金建立了PC’s Limited,并且树立了颠覆技术行业的愿景。1988年 - 我们完成了首次公开募股,募集了3,000万美元资金,公司市值从1,000美元增长到8500万美元。1992年 - 戴尔跻身财富500强公司行列,Michael Dell也成为榜单上最年轻的CEO。1996年 - Dell.com上线,该站点上线仅六个月之后,每天销售额即达100万美元。2001年 - 戴尔成为 全球第一大计算机系统提供商。2005年 - 在《财富》杂志的“美国最受赞赏公司”排名中,戴尔位列第一。2010年 - 戴尔被 Gartner, Inc.评为世界第一大医疗保健信息技术服务提供商。2013年 - Michael Dell携手私人股本公司Silver Lake Partners,从公众股东手里买回了戴尔股份,旨在加快解决方案战略的实施并专注于大多数客户重视的创新和长期投资。2016年 - 戴尔与EMC合并为Dell Technologies,这是业内最大的技术集成事件。戴尔提供的工作2018年的时候又传出,VMware 反收购戴尔?写到这里的时候我都感觉有点乱了?戴尔收购了 EMC, ECM 收购了 VMware ,那么 VMware 就差不多算戴尔的重孙子,那么怎么又来 VMware 反收购戴尔?原来是这样,在 2015 年 10 月 12 日业界正式爆料戴尔收购 EMC(包括 VMware),当时的 VMware 股价在 60-70 美元左右。到了 2016 年 9 月戴尔宣布正式并购 EMC 包括 VMware,只是让 VMware 独立运营,VMware 当时股价也还是在 70 美元左右。可是到了 2018 年初一看,VMware 股价已经到达了 130 多美元,在 2018 年的最高点,股价甚至达到了 160 多美元,股价又 TM 涨了一倍多,VMware 公司简直发展太好了。VMware 最新的市值快到了 6000 亿美金,当初收购时 VMware 市值也就 200 多亿美金,简直赚翻了呀!传言只是传言,最终 2018 年 7 月,戴尔还是选择了独立上市,拥有 VMware 80% 的股份。并购时间表上面写的有点乱,大家看完之后也许有点迷糊,在这里重新整理一下这里面几个关键公司的收购时间点:1989 年,Rob Mee 创立的咨询公司 Pivotal Labs;2003 年,Rod Johnson 和同伴创建了 Spring;2003 年,EMC 收购了 VMware 公司;2009 年,VMware 收购了 Spring ;2012 年,EMC 又收购了 Pivotal Labs 公司;2013 年,EMC 、 VMware 和收购来的 Pivotal Labs 公司重新组建了新的公司 Pivotal;2015 年,戴尔又并购了 EMC;2018 年,戴尔独立上市。接着说 Pivotal 公司上面一系列的商业并购搞的眼花缭乱的,但是大家只要知道 Pivotal 公司出身高贵,来自几个都不太差钱的世界 500 强公司联合组建而成,Pivotal 公司的产品非常的高大上,就连我们平时使用的 12306 都使用了他们公司的产品。Pivotal 公司可谓是大牛云集,公司的开源产品有:Spring 以及 Spring 衍生产品、Web 服务器 Tomcat、缓存中间件 Redis、消息中间件 RabbitMQ、平台即服务的 Cloud Foundry、Greenplum 数据引擎、还有大名鼎鼎的 GemFire(12306 系统解决方案组件之一)。这些著名开源产品背后的开发者都在 Pivotal 公司,其研发团队汇集了全球的一流开发者,Spring Boot 为什么如此优秀,或许在这里可以找到一些答案。Pivotal 中国研发中心在中国创建于 2010 年,它的前身是 EMC Greenplum 部门,其团队成员分布在北京和上海两地,目前正致力于以下产品的研发和服务的提供:Pivotal Web Service (PWS), Pivotal Hadoop (PHD), Hawq 和 Greenplum Database (GPDB)。毕威拓科技(北京)有限公司(Pivotal中国公司)2015年3月1日正式成立并单独运营。Pivotal 公司成立之后,于 2014 年发布了 Spring Boot,2015 年发布了 Spring Cloud,2018 年 Pivotal 公司在纽约上市。我们可以通过一张图来了解 Pivotal 公司的发展史。Pivotal 的定位是一家下一代云计算和大数据应用相结合的公司,而 VMWare 和原 EMC 的业务方向则依然是软件定义数据中心和信息基础架构。官网这样介绍他们的产品:Pivotal 提供的工具能够帮助开发人员构建更出色软件,可让您在任意云环境中运行应用的平台,帮助您打造未来。公司的产品主要分为三大类:部署和运行软件,规划、构建和集成软件,分析和决策部署和运行软件Pivotal Cloud Foundry (PCF),用于快速交付应用、容器和函数的多云平台。PCF: Pivotal Application Service,在具有内置日志记录、监控和自动扩展功能且高度可用的自助服务平台上,运行使用任意语言构建的应用。PCF: Pivotal Container Service,基于企业级Kubernetes环境构建应用,该环境采用按需群集、滚动升级和VMware NSX提供的软件定义的网络。Pivotal Services Marketplace,将您的应用与托管、代理或按需服务相结合。产品涵盖数据管理、API管理、消息传递、日志记录等。规划、构建和集成软件Spirng Boot,借助领先的Java框架快速构建功能强大的应用和服务。Spirng Cloud,将经过验证的微服务模式融入您的软件。提供配置存储、服务发现、消息传递等功能。Steeltoe,受 Spring Cloud 启发,用该框架构建恢复力强、可扩展的.NET应用。Pivotal Cloud Cache,采用基于 Pivotal GemFire 的快速且高度可用的缓存,可提供地理复制和后台写入功能。Pivotal GemFire,利用可扩展、事件驱动的分布式数据网格执行内存中计算。12306采用的商业方案。RabbitMQ,借助这款广受欢迎的消息传递代理,分离服务并扩展处理进程。Pivotal Tracker,经过验证的项目管理工具,帮您打造成功的敏捷团队。Concourse,利用自动化管道实现 PCF 的持续升级。分析和决策Pivotal Greenplum,使用这个功能齐全的多云大规模并行处理(MPP)数据平台,可以对大型数据集进行高级分析。。Apache MADlib,通过采用数据并行方式实施多结构数据的数学、统计和机器学习方法来执行数据库内分析。Pivotal 公司的产品有 Spring Boot 、Spring Cloud 、RabbitMQ 等非常著名的开源软件,也有很多类似 GemFire 等商业解决方案,通过他们公司的产品即可发现,一边通过开源软件打造生态,一方面通过商业解决方案来挣钱。曾经有一段时间,有人就问我一个问题,说开源的是不是就意味着是免费的,不免费的服务,是不是就意味着不是开源的软件?这种商业模式其实就是对这种观点的一种反驳,开源不等于免费,开源是一种开放分享的精神,不要什么东西来到国内都变味了。Pivotal 掌握很多最新前沿的开源技术,公司提供的从云端部署到一整套的大数据解决方案,从开发到平台到提供解决方案到提供咨询,可以说真正依赖技术挣钱的典范,我辈之楷模!最后送大家一个学习 Spring Boot 的开源项目: spring-boot-examples参考是时候说说Pivotal这个富二代了!五年做到60亿美金市值,PaaS第一股Pivotal的崛起之路 | 爱分析调研原文链接:https://www.cnblogs.com/ityou… ...

January 11, 2019 · 3 min · jiezi

Spring详解4.容器内幕

点击进入我的博客1 Spring容器整体流程1.1 ApplicationContext内部原理AbstractApplicationContext是ApplicationContext的抽象实现类,其中最重要的是refresh()方法,它定义了容器在加载配置文件以后的各项处理过程。public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. prepareRefresh(); // (1)初始化BeanFactory ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. postProcessBeanFactory(beanFactory); // (2)调用工厂后处理器 invokeBeanFactoryPostProcessors(beanFactory); // (3)注册Bean后处理器 registerBeanPostProcessors(beanFactory); // (4)初始化消息源 initMessageSource(); // (5)初始化应用上下文事件广播器 initApplicationEventMulticaster(); // (6)初始化其他特殊Bean,由具体子类实现 onRefresh(); // (7)注册事件监听器 registerListeners(); // (8)初始化所有单实例的Bean(Lazy加载的除外) finishBeanFactoryInitialization(beanFactory); // (9)完成刷新并发布容器刷新事件 finishRefresh(); } catch (BeansException ex) { // Destroy already created singletons to avoid dangling resources. destroyBeans(); // Reset ‘active’ flag. cancelRefresh(ex); // Propagate exception to caller. throw ex; } finally { // Reset common introspection caches in Spring’s core, since we // might not ever need metadata for singleton beans anymore… resetCommonCaches(); } } }初始化BeanFactory:根据配置文件实例化BeanFactory,在obtainFreshBeanFactory()方法中,首先调用refreshBeanFactory()刷新BeanFactory,然后调用getBeanFactory()方法获取BeanFactory,这两个方法都是需要子类实现的抽象方法。在这一步里,Spring将配置文件的信息装入到容器的Bean定义注册表(BeanDefinitionRegistry)中,但此时Bean还未初始化。调用工厂后处理器:根据反射机制从BeanDefinitionRegistry中找出所有BeanFactoryPostProcessor类型的Bean,并调用其postProcessBeanFactory()接口方法。注册Bean后处理器:根据反射机制从BeanDefinitionRegistry中找出所有BeanPostProcessor类型的Bean,并将它们注册到容器Bean后处理器的注册表中。初始化消息源:初始化容器的国际化信息资源。初始化应用上下文事件广播器。初始化其他特殊的Bean:这是一个钩子方法,子类可以借助这个钩子方法执行一些特殊的操作——如AbstractRefreshableWebApplicationContext就使用该钩子方法执行初始化ThemeSource的操作。注册事件监听器。初始化singleton的Bean:实例化所有singleton的Bean(使用懒加载的吹),并将它们放入Spring容器的缓存中。发布上下文刷新事件:创建上下文刷新事件,事件广播器负责将些事件广播到每个注册的事件监听器中。1.2 Spring创建Bean流程下图描述了Spring容器从加载配置文件到创建一个Bean的完整流程:ResourceLoader从存储介质中加载Spring配置文件,并使用Resource表示这个配置文件的资源。BeanDefinitionReader读取Resource所指向的配置文件资源,然后解析配置文件。配置文件中每一个<bean>解析成一个BeanDefinition对象,并保存到BeanDefinitionRegistry中;容器扫描BeanDefinitionRegistry中的BeanDefinition,使用Java的反射机制自动识别出Bean工厂后处理器(实现BeanFactoryPostProcessor接口)的Bean,然后调用这些Bean工厂后处理器对BeanDefinitionRegistry中的BeanDefinition进行加工处理。主要完成以下两项工作:3.1 对使用到占位符的<bean>元素标签进行解析,得到最终的配置值,这意味对一些半成品式的BeanDefinition对象进行加工处理并得到成品的BeanDefinition对象。3.2 对BeanDefinitionRegistry中的BeanDefinition进行扫描,通过Java反射机制找出所有属性编辑器的Bean(实现java.beans.PropertyEditor接口的Bean),并自动将它们注册到Spring容器的属性编辑器注册表中(PropertyEditorRegistry)。Spring容器从BeanDefinitionRegistry中取出加工后的BeanDefinition,并调用InstantiationStrategy着手进行Bean实例化的工作;在实例化Bean时,Spring容器使用BeanWrapper对Bean进行封装,BeanWrapper提供了很多以Java反射机制操作Bean的方法,它将结合该Bean的BeanDefinition以及容器中属性编辑器,完成Bean属性的设置工作。利用容器中注册的Bean后处理器(实现BeanPostProcessor接口的Bean)对已经完成属性设置工作的Bean进行后续加工,直接装配出一个准备就绪的Bean。1.3 Spring中的组件Spring中的组件按照所承担的角色可以划分为两类:在Bean创建过程中被处理的元素:Resource、BeanDefinition、PropertyEditor以及最终的Bean。处理上述元素的工具类:ResourceLoader、BeanDefinitionReader、BeanFactoryPostProcessor、InstantiationStrategy、BeanWrapper等。1.4 BeanDefinitionorg.springframework.beans.factory.config.BeanDefinition是配置文件<bean>元素标签在容器中的内部表示,是与<bean>一一对应的。一般的<bean>和父<bean>用RootBeanDefinition表示,而子<bean>用ChildBeanDefinition表示。一般情况下,BeanDefinition只在容器启动时加载并解析,除非容器重启或刷新。当然用户也可以在运行时通过编程调整BeanDefinition的定义。创建BeanDefinition主要包括两个步骤:利用BeanDefinitionReader读取承载配置信息的Resource,通过XML解析器解析配置信息的DOM对象,简单地每个<bean>生成对应地BeanDefinition对象。但是这里生成的BeanDefinition可能是半成品,因为在配置文件中,可能通过占位符变量引用外部属性文件的属性,这些占位符变量在这一步里还没有被解析出来。利用容器中注册的BeanFactoryPostProcessor对半成品的BeanDefinition进行加工处理,将以占位符表示的配置解析为最终的实际值,这样半成品的BeanDefinition就成为成品的BeanDefinition。1.5 InstantiationStrategyorg.springframework.beans.factory.support.InstantiationStrategy负责根据BeanDefinition对象创建一个Bean实例。InstantiationStrategy仅负责实例化Bean(相当于new的操作),不会设置的Bean属性,所以InstantiationStrategy返回的并不是最终的Bean实例,还需要通过BeanWrapper进行属性的设置。SimpleInstantiationStrategy是最常用的实例化策略,通过使用Bean的默认构造方法、带参数的构造方法或工厂方法创建Bean的实例。CglibSubclassingInstantiationStrategy利用CGLib类库为Bean动态生成子类,在子类中生成方法注入的逻辑,然后使用这个动态生成的子类创建Bean的实例。1.6 BeanWrapperBeanWrapper相当于一个代理器,Spring委托BeanWrapper完成Bean属性填充工作。PropertyAccessor:属性访问接口定义了各种访问Bean属性的方法,如getPropertyValue、setPropertyValue等。PropertyEditorRegistry:是属性编辑器的注册表,主要作用就是注册和保存属性编辑器。BeanWrapperImpl:一个BeanWrapperImpl实例内部封装了两类组件——被封装的待处理的Bean和一套用于设置Bean属性的属性编辑器。BeanWrapperImpl的三重身份——Bean的包裹器、属性访问器和属性编辑器注册表。Spring首先从容器的BeanDefinitionRegistry中获取对应的BeanDefinition,然后从BeanDefinition中获取Bean属性的配置信息PropertyValue,然后使用属性编辑器对PropertyValue进行转换以得到Bean的属性值。2 属性编辑器我们在配置文件中配置的都是字面值,如果把它们转换成对应数据类型(如double、int)的值或对象呢?2.1 JavaBean的属性编辑器任何实现了java.beans.PropertyEditor接口的类都是属性编辑器,其主要功能就是将外部的设置值转换成JVM内部的对应类型。PropertyEditorPropertyEditor是属性编辑器的接口,它规定了将外部设置值转换为内部JavaBean属性值的接口方法,是内部属性值和外部设置值的桥梁。Object getValue():返回属性的当前值。基本类型被封装成对应的封装类实例。void setValue(Object newValue):设置属性的值,基本类型以封装类传入。String getAsText():将属性对象用一个字符串表示,以便外部的属性编辑器能以可视化的方式显示。缺省返回null,表示该属性不能以字符串表示。void setAsText(String text):用一个字符串去更新属性的内部值,这个字符串一般从外部属性编辑器传入。String[] getTags():返回表示有效属性值的字符串数组(如boolean属性对应的有效Tag为true和false),以便属性编辑器能以下拉框的方式显示出来。缺省返回null,表示属性没有匹配的字符值有限集合。String getJavaInitializationString():为属性提供一个表示初始值的字符串,属性编辑器以此值作为属性的默认值。我们一般不去直接实现PropertyEditor,而是扩展PropertyEditorSupport来实现自己类。BeanInfoBeanInfo主要描述了JavaBean的哪些属性可以编辑及对应的属性编辑器。BeanInfo和JavaBean的对应关系通过二者命名规范确定:对应JavaBean的BeanInfo的命名规范应该是<Bean>BeanInfo,如Car对应的BeanInfo为CarBeanInfo。JavaBean的每个属性对应一个属性描述器PropertyDescriptor。BeanInfo最重要的方法就是PropertyDescriptor[] getPropertyDescriptors(),该方法返回JavaBean的属性描述数组。BeanInfo接口常用其实现类SimpleBeanInfo,可以扩展此类实现功能。PropertyEditorManagerJavaBean规范提供了一个默认的属性编辑器PropertyEditorManager,保存一些常见类型的属性编辑器。2.2 Spring属性编辑器Spring为常见的属性类型提供了默认的属性编辑器PropertyEditorRegistrySupport,里边有多个用于保存属性编辑器的Map类型变量,键为属性类型,值为对应的属性编辑器实例。常见的类型如下所示。类 别说 明基本数据类型如:boolean、byte、short、int等;基本数据类型封装类如:Long、Character、Integer等;两个基本数据类型的数组char[]和byte[];大数类BigDecimal和BigInteger集合类为5种类型的集合类Collection、Set、SortedSet、List和SortedMap提供了编辑器资源类用于访问外部资源的8个常见类Class、Class[]、File、InputStream、Locale、Properties、Resource[]和URL2.3 自定义属性编辑器Step1:我们可以通过扩展java.beans.PropertyEditorSupport类,并覆盖其中的setAsText()方法即可自定义属性编辑器。class KFCWaitress { private KFCCombo kfcCombo; // getters & setters}class KFCCombo { private String burger; private String drink; // getters & setters}/** * KFCCombo的Editor */class KFCComboEditor extends PropertyEditorSupport { @Override public void setAsText(String text) throws IllegalArgumentException { // 将字面值转换为属性类型对象 String[] textArr = text.split(","); KFCCombo kfcCombo = new KFCCombo(); kfcCombo.setBurger(textArr[0]); kfcCombo.setDrink(textArr[1]); // 调用父类的setValue()方法设置转换后的属性对象 setValue(kfcCombo); }}Step2:如果使用BeanFactory需要手动调用registerCustomEditor(class requiredType, PropertyEditor propertyEditor)方法注册自定义的属性编辑器;如果使用ApplicationContext,只需要在配置文件中通过CustomEditorConfigurer注册即可。<!– (1)配置自动注册属性编辑器的CustomEditorConfigurer –><bean class=“org.springframework.beans.factory.config.CustomEditorConfigurer”> <property name=“customEditors”> <map> <!– (2)属性编辑器对应的属性类型 –> <entry key=“com.ankeetc.spring.KFCCombo” value=“com.ankeetc.spring.KFCComboEditor”/> </map> </property></bean><bean id=“myWaitress” class=“com.ankeetc.spring.KFCWaitress”> <!– (3)该属性将使用(2)处的属性编辑器完成属性填充操作 –> <property name=“kfcCombo” value=“Zinger Burger,Mirinda”/></bean>在(3)处,直接通过一个字符串配置一个Bean。BeanWrapper在设置KFCCombo类型的属性时,将会检索自定义属性编辑器的注册表,如果发现有KFCCombo属性类型有对应的属性编辑器时,就会使用该方法的setAsText()转换该对象。3 使用外部属性文件Spring提供了一个PropertyPlaceholderConfigurer来引用外部属性文件,它实现了BeanFactoryPostProcessor接口,因此也是一个Bean工厂后处理器。3.1 使用PropertyPlaceholderConfigurer简单的例子通过PropertyPlaceholderConfigurer并引入属性文件,实现使用属性名来引用属性值。 <!– 创建PropertyPlaceholderConfigurer的Bean并引入属性文件 –> <bean class=“org.springframework.beans.factory.config.PropertyPlaceholderConfigurer”> <property name=“location” value=“classpath:application.properties”/> <property name=“fileEncoding” value=“utf-8”/> </bean> <!– 也可以使用这种方式引用属性文件 –> <context:property-placeholder location=“classpath:application.properties” file-encoding=“utf-8”/> <!– 通过属性名引用属性值 –> <bean id=“myCat” class=“com.ankeetc.spring.Cat”> <property name=“name” value="${name}"/> </bean>PropertyPlaceholderConfigurer的其他属性location:如果只有一个属性文件,则直接使用location属性指定就可以了;如果有多个属性文件,则可以通过locations属性进行设置。可以像配置List一样配置locations属性。fileEncoding:属性文件的编码格式。Spring使用操作系统默认编码读取属性文件。如果属性文件采用了特殊编码,则需要通过该属性显示指定。order:如果配置文件中定义了多个PropertyPlaceholderConfigurer,则通过该属性指定优先顺序。placeholderPrefix:在上面的例子中,通过${属性名}引用属性文件中的属性项,其中${为默认的占位符前缀,可以根据需要改为其他的前缀符。placeholderSuffix:占位符后缀,默认为}。@Value引用属性在使用基于注解配置Bean时,可以通过@Value注解为Bean的成员变量或方法入参自动注入容器中已有的属性,也可以使用@Value注入字面值。public class Main { public static void main(String[] args) { ApplicationContext applicationContext = new AnnotationConfigApplicationContext(“com.ankeetc.spring); System.out.println(applicationContext.getBean(Cat.class).getName()); }}@Configurationclass Config { @Bean public PropertyPlaceholderConfigurer configurer() { PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer(); configurer.setLocation(new PathMatchingResourcePatternResolver().getResource(“classpath:application.properties”)); configurer.setFileEncoding(“utf-8”); return configurer; }}@Componentclass Cat { @Value("${name}") private String name; public String getName() { return name; }}3.2 使用加密的属性文件如果属性是敏感的,一般不允许使用明文形式保存,此时需要对属性进行加密.PropertyPlaceHolderConfigurer继承自PropertyResourceConfigurer类,后者有几个有用的protected方法(方法默认是空的即不会转换),用于在属性使用之前对属性列表中的属性进行转换。void convertProperties(Properties props):属性文件的所有属性值都封装在props中,覆盖该方法,可以对所有的属性值进行转换处理。String convertProperty(String propertyName, String propertyValue):在加载属性文件并读取文件中的每个属性时,都会调用此方法进行转换处理。String convertPropertyValue(String originalValue):和上一个方法类似,只不过没有传入属性名。简单例子public class Main { public static void main(String[] args) { ApplicationContext applicationContext = new AnnotationConfigApplicationContext(“com.ankeetc.spring”); // userName没有被改变 System.out.println(applicationContext.getBean(DataSource.class).getUserName()); // password值被改变 System.out.println(applicationContext.getBean(DataSource.class).getPassword()); }}@Componentclass DataSource { @Value("${userName}") private String userName; @Value("${password}") private String password; public String getUserName() { return userName; } public String getPassword() { return password; }}@Configurationclass Config { @Bean public EncryptPropertyPlaceholderConfigurer encryptPropertyPlaceholderConfigurer() { EncryptPropertyPlaceholderConfigurer configurer = new EncryptPropertyPlaceholderConfigurer(); configurer.setLocation(new PathMatchingResourcePatternResolver().getResource(“classpath:application.properties”)); configurer.setFileEncoding(“utf-8”); return configurer; }}class EncryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer { @Override protected String convertProperty(String propertyName, String propertyValue) { if (“password”.equals(propertyName)) { // 在此过滤并实现相关的揭秘逻辑 return “Decrypt” + propertyValue; } else { return propertyValue; } }}3.3 属性文件可以在属性文件中使用${}来实现属性之间的相互引用dbName=myDatabaseurl=jdbc:mysql://localhost:3306/${dbName}如果一个属性值太长,可以在行后添加\将属性分为多行4 应用Bean的属性值基于XML的配置在XML配置文件中,可以使用#{beanName.propName}的方式引用其他Bean的属性值。public class Main { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“classpath:beans.xml”); System.out.println(applicationContext.getBean(Application.class).getDatabaseName()); System.out.println(applicationContext.getBean(Application.class).getDatabasePassword()); }}class DatabaseConfig { private String userName; private String password; // getters & setters}class Application { private String databaseName; private String databasePassword; // getters & setters} <bean id=“databaseConfig” class=“com.ankeetc.spring.DatabaseConfig”> <property name=“userName” value=“lucas”/> <property name=“password” value=“123456”/> </bean> <!–通过#{databaseConfig.propName}的方式引用databaseConfig的属性值–> <bean id=“applicationConfig” class=“com.ankeetc.spring.Application”> <property name=“databaseName” value="#{databaseConfig.userName}"/> <property name=“databasePassword” value="#{databaseConfig.password}"/> </bean>基于注解和Java类的配置使用@Value("#{beanName.propName}")的形式也可以引用其他类的属性值。@Componentclass Application { @Value("#{databaseConfig.userName}") private String databaseName; @Value("#{databaseConfig.password}") private String databasePassword;}5 国际化信息国际化信息的含义是根据不同的地区语言类型返回不同的信息,简单来说就是为每种语言配置一套对应的资源文件。5.1 基础知识本地化类java.util.Locale国际化信息也称为本地化信息,由java.util.Locale类表示一个本地化对象。它由语言参数和国家/地区参数构成。语言参数:每种语言由两位小写字母表示,如zh、en国家/地区参数:用两个大写字母表示,如CN、TW、HK、EN、US本地化工具类java.util包下的NumberFormat、DateFormat、MessageFormat都支持本地化的格式化操作,而且MessageFormat还支持占位符的格式化操作。ResourceBundle使用ResourceBundle可以访问不同本地化资源文件,文件名必须按照如下格式来命名:资源名_语言代码_国家地区代码.properties,其中语言代码和国家/地区代码是可选的。假如默认资源文件的文件名为application.properties,则中文中国大陆的资源文件名为application_zh_CN.properties。public class Main { public static void main(String[] args) { // 如果找不到对应的资源文件,将会使用默认的资源文件 // getBundle是类路径的文件名,而且不带.properties后缀 ResourceBundle zhCN = ResourceBundle.getBundle(“application”, Locale.SIMPLIFIED_CHINESE); ResourceBundle enUs = ResourceBundle.getBundle(“application”, Locale.US); System.out.println(zhCN.getString(“name”)); System.out.println(enUs.getString(“name”)); }}5.2 MessageSourceSpring定义了访问国际化信息的MessageSource接口,主要方法如下:String getMessage(String code, Object[] args, String defaultMessage, Locale locale):code表示国际化资源中的属性名;args用于传递格式化串占位符所用的运行期参数;当在资源找不到对应属性名时,返回defaultMessage参数所指定的默认信息;locale表示本地化对象;String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException:与上面的方法类似,只不过在找不到资源中对应的属性名时,直接抛出NoSuchMessageException异常;String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException:MessageSourceResolvable将属性名、参数数组以及默认信息封装起来,它的功能和第一个接口方法相同。类结构HierarchicalMessageSource接口的作用是建立父子层级的MessageSource结构。StaticMessageSource主要用于程序测试,它允许通过编程的方式提供国际化信息。ResourceBundleMessageSource实现类允许通过beanName指定一个资源名(包括类路径的全限定资源名),或通过beanNames指定一组资源名。ReloadableResourceBundleMessageSource提供了定时刷新功能,允许在不重启系统的情况下,更新资源的信息。public class Main { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“beans.xml”); System.out.println(applicationContext.getBean(“myMessageSource1”, MessageSource.class).getMessage(“name”, null, Locale.US)); System.out.println(applicationContext.getBean(“myMessageSource1”, MessageSource.class).getMessage(“name”, null, Locale.SIMPLIFIED_CHINESE)); }} <!–可以使用basename指定资源文件的路径–> <bean id=“myMessageSource1” class=“org.springframework.context.support.ResourceBundleMessageSource”> <property name=“basename” value=“application”/> </bean> <!–可以使用basenames指定多组资源文件的路径–> <bean id=“myMessageSource2” class=“org.springframework.context.support.ResourceBundleMessageSource”> <property name=“basenames”> <list> <value>application</value> </list> </property> </bean> <!–当使用ReloadableResourceBundleMessageSource可以使用cacheSeconds指定刷新周期–> <!–刷新周期默认为-1即不刷新,单位是秒–> <bean id=“myMessageSource3” class=“org.springframework.context.support.ReloadableResourceBundleMessageSource”> <property name=“basename” value=“application”/> <property name=“cacheSeconds” value=“100”/> </bean>5.3 容器级的国际化信息由于ApplicationContext本身也继承了MessageSource接口,所以ApplicationContext的所有实现类本身也是一个MessageSource对象,国际化信息是整个容器的公共设施。在本章(1.1 ApplicationContext内部原理)我们提到,在ApplicationContext会在initMessageSource()方法中,Spring通过反射机制找出bean名为messageSource(bean名必须是messageSource)且类型为MessageSource子类的Bean,将这个Bean定义的信息资源加载为容器级的国际化信息资源。public class Main { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“beans.xml”); System.out.println(applicationContext.getMessage(“name”, null, Locale.US)); }} <bean id=“messageSource” class=“org.springframework.context.support.ResourceBundleMessageSource”> <property name=“basename” value=“application”/> </bean>6 容器事件6.1 Java的事件体系事件体系是观察者模式的一种具体实现方式,一共有如下几个角色:事件:java.util.EventObject是Java中的事件。监听器:java.util.EventListener是用于描述事件的接口,是一个没有任何方法的标记接口。事件源:事件的生产者,任何一个EventObject都有一个事件源。事件监听器注册表:框架必须有一个地方来保存事件监听器,当事件源产生事件时,就会通知这些位于注册表中的监听器。事件广播器:是事件和事件监听器之间的桥梁,负责把事件通知给事件监听器。public class Main { public static void main(String[] args) { Waitress waitress = new Waitress(“田二妞”); waitress.addEventListener(new Chef(“王二狗”)); waitress.order(“宫保鸡丁”); // 厨师[王二狗]收到服务员[田二妞]的订单,开始做[宫保鸡丁] }}// 一个餐厅的点单事件,继承了EventObjectclass OrderEventObject extends EventObject { private String order; public String getOrder() { return order; } public OrderEventObject(Object source, String order) { super(source); this.order = order; }}// 服务员是事件源,由她产生点单事件class Waitress { private String name; // 服务员维护了所有在餐厅的厨师,即监听器注册表 private List<Chef> eventListenerList = new ArrayList<>(); public Waitress(String name) { this.name = name; } public String getName() { return name; } public void addEventListener(Chef chef) { eventListenerList.add(chef); } // 该方法是广播器,即把点单事件通知给注册表中的全部厨师 public void order(String order) { OrderEventObject orderEventObject = new OrderEventObject(this, order); eventListenerList.forEach(var -> var.cook(orderEventObject)); }}// 厨师是事件监听器class Chef implements EventListener { private String name; public Chef(String name) { this.name = name; } // 监听到点单事件并作出相关反应 public void cook(EventObject o) { System.out.println(String.format(“厨师[%s]收到服务员[%s]的订单,开始做[%s]”, name, ((Waitress)o.getSource()).getName(), ((OrderEventObject)o).getOrder())); }}6.2 Spring事件类结构事件类ApplicationEvent:Spring的事件类的基类,其类结构如下所示。ApplicationContextEvent:容器事件,它拥有4个子类分别表示容器的启动、刷新、停止、关闭事件。RequestHandleEvent:与Web应用有关的事件,当一个HTTP请求被处理后产生该事件。只有在web.xml中定义了DispatcherServlet时才会产生该事件。它有两个子类,分别代表Servlet和Portlet的请求事件。事件监听器接口ApplicationListener:该接口只定义了一个方法onApplicationEvent(E event),该方法接受ApplicationEvent事件对象,在该方法中编写事件的响应处理逻辑。SmartApplicationListener:定义了两个方法boolean supportsEventType(Class<? extends ApplicationEvent> eventType):指定监听器支持哪种类型的容器事件,即它只会对该类型的事件做出响应;boolean supportsSourceType(Class<?> sourceType):指定监听器仅对何种事件源对象做出响应。GenericApplicationListener:Spring 4.2新增的类,使用可解析类型ResolvableType增强了对范型的支持。事件广播器当发生容器事件时,容器主控程序将调用事件广播器将事件通知给事件监听器注册表中的事件监听器。Spring为事件广播器提供了接口和实现类。6.3 Spring事件体系具体实现Spring在ApplicationContext接口的抽象实现类AbstractApplicationContext中完成了事件体系的搭建。AbstractApplicationContext拥有一个applicationEventMulticaster(应用上下文事件广播器)成员变量,它提供了容器监听器的注册表。AbstractApplicationContext在refresh()这个容器启动启动方法中通过以下3个步骤搭建了事件的基础设施:public void refresh() throws BeansException, IllegalStateException { // (5)初始化应用上下文事件广播器 initApplicationEventMulticaster(); // (7)注册事件监听器 registerListeners(); // (9)完成刷新并发布容器刷新事件 finishRefresh();}在(5)处,Spring初始化事件的广播器,可以在配置文件中为容器定义一个自定义的事件广播器,只要实现ApplicationEventMulticaster即可,Spring会通过反射机制将其注册容器的事件广播器。如果没有找到配置的外部事件广播器,则Spring自动使用SimpleApplicationEventMulticaster作为事件广播器。在(7)处,Spring根据反射机制,从BeanDefinitionRegistry中找出所有实现ApplicationListener的Bean,将它们注册为容器的事件监听器,即将其添加到事件广播器所提供的事件监听器注册表中在(9)处,容器启动完成,调用事件发布接口向容器中所有的监听器发布事件6.4 一个例子假如我们希望容器刷新时打印一行文字,可以继承GenericApplicationListener并实现相关方法。public class Main { public static void main(String[] args) { // new AnnotationConfigApplicationContext()会调用refresh方法,MyListener会监听到并处理 ApplicationContext applicationContext = new AnnotationConfigApplicationContext(“com.ankeetc.spring”); // stop事件不会被监听到 ((AnnotationConfigApplicationContext) applicationContext).stop(); }}@Componentclass MyListener implements GenericApplicationListener { // 判断是否是刷新事件 @Override public boolean supportsEventType(ResolvableType eventType) { return ResolvableType.forClass(ContextRefreshedEvent.class).equals(eventType); } @Override public boolean supportsSourceType(Class<?> sourceType) { return true; } // 在此实现监听到相关事件的处理逻辑 @Override public void onApplicationEvent(ApplicationEvent event) { System.out.println(“Hello world”); } @Override public int getOrder() { return 0; }} ...

January 10, 2019 · 4 min · jiezi

关于跨域攻击和网络信标

本人工作中偶尔会和浏览器打交道,也遇到过一些坑,在此分享一下网页跨域访问的相关场景和知识,希望对读者有帮助。本文来自于我的博客网站:www.51think.net一、什么是跨域访问凡是与主站地址的域名、端口、协议不一致的其他请求,都可以认为是跨域访问。例如某网站的主站地址是https://www.abc.com,但网页又…(地址是https://img.abc.com),这就是一种跨域访问。二、浏览器的同源策略所谓的同源策略是浏览器所遵循的一种安全约定。其限制了来自不同源的document或者脚本对当前的document读取或设置某些属性。具体限制如下: 跨源网络访问:AJAX请求。 跨源 DOM 访问:DOM。 跨源脚本API访问: iframe.contentWindow, window.parent, window.open 和 window.opener 如果没有这些限制,那我们就可以肆无忌惮的破坏其他网站的网页了。 三、如何进行跨域访问跨域访问不是跨域攻击,业务上我们的确有跨域访问的需要。 #### 1、通过标签的src或者href属性。 例如< script >、< img >、< iframe >、< link >,访问静态资源文件虽然是跨域,但不受同源策略限制,因为使用的是标签访问。src属性访问的地址是一次性的get访问,且是主站主动设置,相对安全。2、form表单提交。form表单提交到其他域也是被允许的。因为form提交意味着跳转到新的站点,是一个有去无回的页面跳转,不存在对原站点的脚本操作。3、jsonp访问。这一般是跨域访问的常用手段,jsonp可以帮助我们从另一方站点获取数据,并作用于本地站点的页面。这是本地站点开发人员的主动行为。jsonp跨域使用的是script标签的跨域功能,通过src属性访问第三方系统并获取返回数组作用于本地函数中。假设在ablog.com的页面中访问bblog.com的服务,可以在页面中写入如下代码,callback参数定义是回调函数: < script src=“http://bblog.com:8083/remote?callback=jsonhandle"></script> <script type=“text/javascript”> function jsonhandle(data){ alert(“age:” + data.age + “name:” + data.name); } </script>bblog.com的服务端代码如下,需要按照回调函数名+(json数据)的格式返回:@GetMapping("/remote”) @ResponseBody public String remote(HttpServletRequest request, Model model) { String callback=request.getParameter(“callback”); String jsonpStr = callback + “(” + “{"age" : 15,"name": "jack",}"+ “)”; return jsonpStr; }4、cors跨资源共享。CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。四、跨域攻击跨域攻击可以理解为:诱导受害者访问非法网站,黑客利用受害者的会话信息模拟请求,以达到篡改数据的目的。由此看来,跨域攻击有几个先决条件:1、要有页面入口,供受害者点击或者页面自动加载。2、攻击请求可模拟,黑客对目标网站参数进行了深入的研究,从而进行模拟。第一点的页面入口非常重要,如何在目标网站(地址:http://ablog.com:8080)植入攻击者的代码?假设目标网站有评论功能,攻击者可以将自己的代码输入到评论区,如果目标网站没有XSS防御,则会将攻击者的代码以html的方式显示在网页上,这也就完成了第一点,提供了攻击入口。例如攻击者可以在评论区输入以下内容: <a href=“http://ablog.com:8080/admin/comments/delete?coid=39" >java速成,点我免费领取</a>或者如下内容: <img src=‘‘http://ablog.com:8080/admin/comments/delete?coid=39“></img>以上两个标签都会请求当前服务器,从而进行删除操作。我们也发现到这两个请求都是get请求,如果服务端拒绝接受get请求,只接受post请求,是不是就没招了?毕竟标签里没法模拟post提交。但是攻击者可以模拟表单,代码如下: <form action=“http://ablog.com:8080/admin/comments/delete" method=“post”> <input type=“hidden” name=“coid” value=“39” /> <input type=“submit” name=“button” value=“java速成,点我免费领取” /> </form>将这段代码输入到评论区并显示,依然可以诱导受害者点击,完成post请求。攻击者也可以将更复杂的逻辑封装在自己搭建的网站中,假设黑客网站地址是http://bblog.com:8083,攻击者将参数传递给自己的服务器,实现跨域攻击,在目标网站ablog.com的评论区中留下如下代码: <a href=“http://bblog.com:8083/admin/comments/delete?coid=39" >java速成,点我免费领取</a> 在黑客网站bblog.com里模拟post请求到ablog.com: <form action=“http://ablog.com:8080/admin/comments/delete" method=“post”> <input type=“hidden” name=“coid” value=“39” /> <input type=“submit” name=“button” value=“java速成,点我免费领取” /> </form>由于受害者在ablog.com中的会话仍然保持,这个模拟请求会带上受害者的会话信息,进行删除操作,而对于服务器端来说是无感的。在bblog.com里模拟post请求到ablog.com,为何没有被跨域拦截?上文有提到过,form表单提交是没有跨域限制的,这为跨域攻击也提供了便利。 上述攻击方式还不算隐蔽,毕竟需要受害者点击触发按钮,还需要页面跳转,太low。我们可以使用一个影藏的iframe完成攻击,使得攻击操作神不知鬼不觉。在网站ablog.com评论区中植入如下代码: <iframe style=“display:none;” src=“http://bblog.com:8083/csrf?coid=41"></iframe> form模拟提交的部分依然放在bblog.com中,使用脚本自动执行。部分代码如下: <script> function dianwoSub() { document.getElementById(“dianwoForm”).submit(); } </script> <body onload=“dianwoSub()"> <div class=“container”> <form action=“http://ablog.com:8080/admin/comments/delete" method=“post” id=“dianwoForm”> <input type=“hidden” name=“coid” value="${coid}” /> <input type=“submit” name=“button” value=“点我” /> </form> </div> </body>在bblog.com中如果使用ajax来模拟请求攻击ablog.com会被浏览器拦截,ajax脚本如下: function dianwoSub() { $.ajax({ type: ‘post’, url: ‘http://ablog.com:8080/admin/comments/delete', data: $(’#dianwoForm’).serialize(), async: false, dataType: ‘json’, success: function (result) { alert(” delete ok”); } }); }运行时,浏览器会报如下错误,即ablog和blog非同源,跨域访问被限制: 五、如何避免跨域攻击1、网站系统一定要有xss防御。用户的任何输入必须要经过后台的校验,如果出现非法字符一定要拦截,将代码植入入口堵死。2、系统请求优先使用post提交。get提交会降低攻击门槛。3、refer来源判断。网站系统在接受请求时,判断请求来源是否是可信任的,如果是非法的则需要拦截。4、增加验证码校验。在做增删改操作时,强行让用户再次与后台交互,这能很大程度上避免攻击,但是影响用户体验。5、token校验。用户在访问某一网页时,后端生成一个随机加密字符串放到session中,用户再次请求时携带此token,后端对比token是否正确,不正确则拦截请求。六、网络信标网络信标又名网络臭虫,通过植入第三方代码来收集访问者信息。例如在ablog.com网站中植入如下代码: <img src=“http://bblog.com:8083/netflag" height=“1” width=“1” ></img>大小仅为一个像素,用户很难发现。凡是打开植入此代码的网页,都会访问bblog.com,bblog.com后台能够收集到如下信息: 通过以上信息,我们可以给用户设置一个唯一标记,并写入到cookie中,例如bloguser=user_127.0.0.11540367865328。后端同时将此标记以及对应信息保存到数据库中,这样可以跟踪某一特定用户的访问路径。假设一个集团公司的业务范围非常广,其信息化系统包含多个二级域名,比如注册页面是login.blog.com,充值页面是deposit.xyz.com,购物页面是shopping.abc.com等,这些域名的cookie是无法共享的,这时候可以采取网络信标的方式,在所有主页上均植入上述代码,通过第三方cookie的方式,将访问者信息全部串联起来。 网络信标的另外一种使用场景是广告推荐。百度的广告联盟就是很好的例子。我们在百度上搜索一些关键字之后,访问其他网站时(例如CSDN)会发现,为何我刚刚搜索的关键字图片会在CSDN网页上显示?那CSDN很有可能放置了百度的脚本代码。用户在百度上进行搜索之后,百度将搜索关键字写入到用户的cookie信息中,CSDN内置了百度的广告代码,这个代码会访问百度服务器,同时会带上百度之前设置的cookie,百度后台根据关键字来响应相关图片或者文字链接,达到精准投放广告的效果。 现在我们来模拟一下百度广告联盟的效果。假设bblog.com就是百度系统,我们模拟一个搜索页面,并搜索关键字“手机”: bblog.com的后端将手机写入到cookie,key为sosuoPara: setCookie(response,“sosuoPara”,sosuoPara,60*60); bblog.com的合作网站ablog.com内置了bblog.com广告代码: <div class=“card mb-3”> <div class=“card-header”> 广告页 </div> <div class=“card-body”> <iframe src=“http://bblog.com:8083/guanggao"></iframe> </div> </div>这段广告代码的后端逻辑是取出cookie信息,得到搜索关键字,后端进行匹配处理,返回给前端广告。这时候我们看一下ablog.com的主页广告,见如下红框位置: ...

January 10, 2019 · 1 min · jiezi

@PropertySource 分环境读取配置

工作的时候,一般来说代码都是分环境的,比如dev,test,prd什么的,在用到@PropertySource 注解的时候,发现好像不能根据环境读取自定义的.properties文件,比如我有个systemProperties-dev.properties文件,一开始只是systemProperties-${spring.profiles.active}.properties这样的方式勉强能用,但是后来当我的环境变量变成多环境的时候,也就是spring.profiles.active = dev,test这样的是,这个方法就不奏效了,(多傻啊,其实早就想到了,他会直接在“-”后面拼了一个“dev,test”)然后在网上看了看资料,参考了以下的一篇文章,然后参照了下源码,用了一个比较简单,但是很难看的方法实现了:P(感觉也是暂时解决问题。)。参照文章:Springboot中PropertySource注解多环境支持以及原理主要思想,重写PropertySourceFactory,在PropertySourceFactory中,重新取得resource,SystemProperties.java@Component@PropertySource(name=“systemConfig”, value = {“classpath:/systemConfig-${spring.profiles.active}.properties”}, factory = SystemPropertySourceFactory.class)public class SystemProperties { // 自己的内容…. }这里指定了 factory = SystemPropertySourceFactory.class,接下来SystemPropertySourceFactory.java@Configurationpublic class SystemPropertySourceFactory implements PropertySourceFactory { @Override public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException { FileSystemResourceLoader resourceLoader = new FileSystemResourceLoader(); //取得当前活动的环境名称(因为直接获取spring.profiles.active 失败,所以才把环境名称拼在文件名后面来拿) //其实感觉应该有可以直接取的方法比如从环境里取 String[] actives = encodedResource.getResource().getFilename().split("\.")[0].replace(name + “-”, “”).split(","); //如果只有一个,就直接返回 if (actives.length <= 1) { return (name != null ? new ResourcePropertySource(name, encodedResource) : new ResourcePropertySource(encodedResource)); } //如果是多个 List<URL> resourceUrls = new ArrayList<>(); //遍历后把所有环境的url全部抓取到list中 Arrays.stream(actives).forEach(active -> { //在resource目录下读取配置文件 URL url = this.getClass().getResource("/" + name.concat("-" + active).concat(".properties")); if (url != null) { resourceUrls.add(url); } }); if (resourceUrls != null && resourceUrls.size() > 0) { List<InputStream> inputStreamList = new ArrayList<>(); //取得所有资源的inputStream for (URL url : resourceUrls) { Resource resource0 = resourceLoader.getResource(url.getPath()); InputStream in = resource0.getInputStream(); inputStreamList.add(in); } //串行流,将多个文件流合并车一个流 SequenceInputStream inputStream = new SequenceInputStream(Collections.enumeration(inputStreamList)); //转成resource InputStreamResource resource = new InputStreamResource(inputStream); return (name != null ? new ResourcePropertySource(name, new EncodedResource(resource)) : new ResourcePropertySource(new EncodedResource(resource))); } else { return (name != null ? new ResourcePropertySource(name, encodedResource) : new ResourcePropertySource(encodedResource)); } }}这样实现后,就能将多个环境的Property文件加载进去了。然后是关于spring.profiles.active 为什么要这么取,我试过@value,和用Environment 对象,都取不到,可能跟bean创建的先后顺序有关。没有继续调查,希望知道原因的朋友能帮忙解答~ ...

January 9, 2019 · 1 min · jiezi

SpringBoot究竟是如何跑起来的?

SpringBoot究竟是如何跑起来的?摘要: 神奇的SpringBoot。原文:SpringBoot 究竟是如何跑起来的?作者:老钱Fundebug经授权转载,版权归原作者所有。不得不说 SpringBoot 太复杂了,我本来只想研究一下 SpringBoot 最简单的 HelloWorld 程序是如何从 main 方法一步一步跑起来的,但是这却是一个相当深的坑。你可以试着沿着调用栈代码一层一层的深入进去,如果你不打断点,你根本不知道接下来程序会往哪里流动。这个不同于我研究过去的 Go 语言、Python 语言框架,它们通常都非常直接了当,设计上清晰易懂,代码写起来简单,里面的实现同样也很简单。但是 SpringBoot 不是,它的外表轻巧简单,但是它的里面就像一只巨大的怪兽,这只怪兽有千百只脚把自己缠绕在一起,把爱研究源码的读者绕的晕头转向。但是这 Java 编程的世界 SpringBoot 就是老大哥,你却不得不服。即使你的心中有千万头草泥马在奔跑,但是它就是天下第一。如果你是一个学院派的程序员,看到这种现象你会怀疑人生,你不得不接受一个规则 —— 受市场最欢迎的未必就是设计的最好的,里面夹杂着太多其它的非理性因素。经过了一番痛苦的折磨,我还是把 SpringBoot 的运行原理摸清楚了,这里分享给大家。Hello World首先我们看看 SpringBoot 简单的 Hello World 代码,就两个文件 HelloControll.java 和 Application.java,运行 Application.java 就可以跑起来一个简单的 RESTFul Web 服务器了。// HelloController.javapackage hello;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.bind.annotation.RequestMapping;@RestControllerpublic class HelloController { @RequestMapping("/") public String index() { return “Greetings from Spring Boot!”; }}// Application.javapackage hello;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}当我打开浏览器看到服务器正常地将输出呈现在浏览器的时候,我不禁大呼 —— SpringBoot 真他妈太简单了。但是问题来了,在 Application 的 main 方法里我压根没有任何地方引用 HelloController 类,那么它的代码又是如何被服务器调用起来的呢?这就需要深入到 SpringApplication.run() 方法中看个究竟了。不过即使不看代码,我们也很容易有这样的猜想,SpringBoot 肯定是在某个地方扫描了当前的 package,将带有 RestController 注解的类作为 MVC 层的 Controller 自动注册进了 Tomcat Server。还有一个让人不爽的地方是 SpringBoot 启动太慢了,一个简单的 Hello World 启动居然还需要长达 5 秒,要是再复杂一些的项目这样龟漫的启动速度那真是不好想象了。再抱怨一下,这个简单的 HelloWorld 虽然 pom 里只配置了一个 maven 依赖,但是传递下去,它一共依赖了 36 个 jar 包,其中以 spring 开头的 jar 包有 15 个。说这是依赖地狱真一点不为过。批评到这里就差不多了,下面就要正是进入主题了,看看 SpringBoot 的 main 方法到底是如何跑起来的。SpringBoot 的堆栈了解 SpringBoot 运行的最简单的方法就是看它的调用堆栈,下面这个启动调用堆栈还不是太深,我没什么可抱怨的。public class TomcatServer { @Override public void start() throws WebServerException { … }}接下来再看看运行时堆栈,看看一个 HTTP 请求的调用栈有多深。不看不知道一看吓了一大跳!我通过将 IDE 窗口全屏化,并将其它的控制台窗口源码窗口统统最小化,总算勉强一个屏幕装下了整个调用堆栈。不过转念一想,这也不怪 SpringBoot,绝大多数都是 Tomcat 的调用堆栈,跟 SpringBoot 相关的只有不到 10 层。探索 ClassLoaderSpringBoot 还有一个特色的地方在于打包时它使用了 FatJar 技术将所有的依赖 jar 包一起放进了最终的 jar 包中的 BOOT-INF/lib 目录中,当前项目的 class 被统一放到了 BOOT-INF/classes 目录中。<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins></build>这不同于我们平时经常使用的 maven shade 插件,将所有的依赖 jar 包中的 class 文件解包出来后再密密麻麻的塞进统一的 jar 包中。下面我们将 springboot 打包的 jar 包解压出来看看它的目录结构。├── BOOT-INF│ ├── classes│ │ └── hello│ └── lib│ ├── classmate-1.3.4.jar│ ├── hibernate-validator-6.0.12.Final.jar│ ├── jackson-annotations-2.9.0.jar│ ├── jackson-core-2.9.6.jar│ ├── jackson-databind-2.9.6.jar│ ├── jackson-datatype-jdk8-2.9.6.jar│ ├── jackson-datatype-jsr310-2.9.6.jar│ ├── jackson-module-parameter-names-2.9.6.jar│ ├── javax.annotation-api-1.3.2.jar│ ├── jboss-logging-3.3.2.Final.jar│ ├── jul-to-slf4j-1.7.25.jar│ ├── log4j-api-2.10.0.jar│ ├── log4j-to-slf4j-2.10.0.jar│ ├── logback-classic-1.2.3.jar│ ├── logback-core-1.2.3.jar│ ├── slf4j-api-1.7.25.jar│ ├── snakeyaml-1.19.jar│ ├── spring-aop-5.0.9.RELEASE.jar│ ├── spring-beans-5.0.9.RELEASE.jar│ ├── spring-boot-2.0.5.RELEASE.jar│ ├── spring-boot-autoconfigure-2.0.5.RELEASE.jar│ ├── spring-boot-starter-2.0.5.RELEASE.jar│ ├── spring-boot-starter-json-2.0.5.RELEASE.jar│ ├── spring-boot-starter-logging-2.0.5.RELEASE.jar│ ├── spring-boot-starter-tomcat-2.0.5.RELEASE.jar│ ├── spring-boot-starter-web-2.0.5.RELEASE.jar│ ├── spring-context-5.0.9.RELEASE.jar│ ├── spring-core-5.0.9.RELEASE.jar│ ├── spring-expression-5.0.9.RELEASE.jar│ ├── spring-jcl-5.0.9.RELEASE.jar│ ├── spring-web-5.0.9.RELEASE.jar│ ├── spring-webmvc-5.0.9.RELEASE.jar│ ├── tomcat-embed-core-8.5.34.jar│ ├── tomcat-embed-el-8.5.34.jar│ ├── tomcat-embed-websocket-8.5.34.jar│ └── validation-api-2.0.1.Final.jar├── META-INF│ ├── MANIFEST.MF│ └── maven│ └── org.springframework└── org └── springframework └── boot这种打包方式的优势在于最终的 jar 包结构很清晰,所有的依赖一目了然。如果使用 maven shade 会将所有的 class 文件混乱堆积在一起,是无法看清其中的依赖。而最终生成的 jar 包在体积上两也者几乎是相等的。在运行机制上,使用 FatJar 技术运行程序是需要对 jar 包进行改造的,它还需要自定义自己的 ClassLoader 来加载 jar 包里面 lib 目录中嵌套的 jar 包中的类。我们可以对比一下两者的 MANIFEST 文件就可以看出明显差异// Generated by Maven Shade PluginManifest-Version: 1.0Implementation-Title: gs-spring-bootImplementation-Version: 0.1.0Built-By: qianwpImplementation-Vendor-Id: org.springframeworkCreated-By: Apache Maven 3.5.4Build-Jdk: 1.8.0_191Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo ot-starter-parent/gs-spring-bootMain-Class: hello.Application// Generated by SpringBootLoader PluginManifest-Version: 1.0Implementation-Title: gs-spring-bootImplementation-Version: 0.1.0Built-By: qianwpImplementation-Vendor-Id: org.springframeworkSpring-Boot-Version: 2.0.5.RELEASEMain-Class: org.springframework.boot.loader.JarLauncherStart-Class: hello.ApplicationSpring-Boot-Classes: BOOT-INF/classes/Spring-Boot-Lib: BOOT-INF/lib/Created-By: Apache Maven 3.5.4Build-Jdk: 1.8.0_191Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo ot-starter-parent/gs-spring-bootSpringBoot 将 jar 包中的 Main-Class 进行了替换,换成了 JarLauncher。还增加了一个 Start-Class 参数,这个参数对应的类才是真正的业务 main 方法入口。我们再看看这个 JarLaucher 具体干了什么public class JarLauncher{ … static void main(String[] args) { new JarLauncher().launch(args); } protected void launch(String[] args) { try { JarFile.registerUrlProtocolHandler(); ClassLoader cl = createClassLoader(getClassPathArchives()); launch(args, getMainClass(), cl); } catch (Exception ex) { ex.printStackTrace(); System.exit(1); } } protected void launch(String[] args, String mcls, ClassLoader cl) { Runnable runner = createMainMethodRunner(mcls, args, cl); Thread runnerThread = new Thread(runner); runnerThread.setContextClassLoader(classLoader); runnerThread.setName(Thread.currentThread().getName()); runnerThread.start(); }}class MainMethodRunner { @Override public void run() { try { Thread th = Thread.currentThread(); ClassLoader cl = th.getContextClassLoader(); Class<?> mc = cl.loadClass(this.mainClassName); Method mm = mc.getDeclaredMethod(“main”, String[].class); if (mm == null) { throw new IllegalStateException(this.mainClassName + " does not have a main method"); } mm.invoke(null, new Object[] { this.args }); } catch (Exception ex) { ex.printStackTrace(); System.exit(1); } }}从源码中可以看出 JarLaucher 创建了一个特殊的 ClassLoader,然后由这个 ClassLoader 来另启一个单独的线程来加载 MainClass 并运行。又一个问题来了,当 JVM 遇到一个不认识的类,BOOT-INF/lib 目录里又有那么多 jar 包,它是如何知道去哪个 jar 包里加载呢?我们继续看这个特别的 ClassLoader 的源码class LaunchedURLClassLoader extends URLClassLoader { … private Class<?> doLoadClass(String name) { if (this.rootClassLoader != null) { return this.rootClassLoader.loadClass(name); } findPackage(name); Class<?> cls = findClass(name); return cls; }}这里的 rootClassLoader 就是双亲委派模型里的 ExtensionClassLoader ,JVM 内置的类会优先使用它来加载。如果不是内置的就去查找这个类对应的 Package。private void findPackage(final String name) { int lastDot = name.lastIndexOf(’.’); if (lastDot != -1) { String packageName = name.substring(0, lastDot); if (getPackage(packageName) == null) { try { definePackage(name, packageName); } catch (Exception ex) { // Swallow and continue } } }}private final HashMap<String, Package> packages = new HashMap<>();protected Package getPackage(String name) { Package pkg; synchronized (packages) { pkg = packages.get(name); } if (pkg == null) { if (parent != null) { pkg = parent.getPackage(name); } else { pkg = Package.getSystemPackage(name); } if (pkg != null) { synchronized (packages) { Package pkg2 = packages.get(name); if (pkg2 == null) { packages.put(name, pkg); } else { pkg = pkg2; } } } } return pkg;}private void definePackage(String name, String packageName) { String path = name.replace(’.’, ‘/’).concat(".class"); for (URL url : getURLs()) { try { if (url.getContent() instanceof JarFile) { JarFile jf= (JarFile) url.getContent(); if (jf.getJarEntryData(path) != null && jf.getManifest() != null) { definePackage(packageName, jf.getManifest(), url); return null; } } } catch (IOException ex) { // Ignore } } return null;}ClassLoader 会在本地缓存包名和 jar包路径的映射关系,如果缓存中找不到对应的包名,就必须去 jar 包中挨个遍历搜寻,这个就比较缓慢了。不过同一个包名只会搜寻一次,下一次就可以直接从缓存中得到对应的内嵌 jar 包路径。深层 jar 包的内嵌 class 的 URL 路径长下面这样,使用感叹号 ! 分割jar:file:/workspace/springboot-demo/target/application.jar!/BOOT-INF/lib/snakeyaml-1.19.jar!/org/yaml/snakeyaml/Yaml.class不过这个定制的 ClassLoader 只会用于打包运行时,在 IDE 开发环境中 main 方法还是直接使用系统类加载器加载运行的。不得不说,SpringbootLoader 的设计还是很有意思的,它本身很轻量级,代码逻辑很独立没有其它依赖,它也是 SpringBoot 值得欣赏的点之一。HelloController 自动注册还剩下最后一个问题,那就是 HelloController 没有被代码引用,它是如何注册到 Tomcat 服务中去的?它靠的是注解传递机制。SpringBoot 深度依赖注解来完成配置的自动装配工作,它自己发明了几十个注解,确实严重增加了开发者的心智负担,你需要仔细阅读文档才能知道它是用来干嘛的。Java 注解的形式和功能是分离的,它不同于 Python 的装饰器是功能性的,Java 的注解就好比代码注释,本身只有属性,没有逻辑,注解相应的功能由散落在其它地方的代码来完成,需要分析被注解的类结构才可以得到相应注解的属性。那注解是又是如何传递的呢?@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}@ComponentScanpublic @interface SpringBootApplication {…}public @interface ComponentScan { String[] basePackages() default {};}首先 main 方法可以看到的注解是 SpringBootApplication,这个注解又是由ComponentScan 注解来定义的,ComponentScan 注解会定义一个被扫描的包名称,如果没有显示定义那就是当前的包路径。SpringBoot 在遇到 ComponentScan 注解时会扫描对应包路径下面的所有 Class,根据这些 Class 上标注的其它注解继续进行后续处理。当它扫到 HelloController 类时发现它标注了 RestController 注解。@RestControllerpublic class HelloController {…}@Controllerpublic @interface RestController {}而 RestController 注解又标注了 Controller 注解。SpringBoot 对 Controller 注解进行了特殊处理,它会将 Controller 注解的类当成 URL 处理器注册到 Servlet 的请求处理器中,在创建 Tomcat Server 时,会将请求处理器传递进去。HelloController 就是如此被自动装配进 Tomcat 的。扫描处理注解是一个非常繁琐肮脏的活计,特别是这种用注解来注解注解(绕口)的高级使用方法,这种方法要少用慎用。SpringBoot 中有大量的注解相关代码,企图理解这些代码是乏味无趣的没有必要的,它只会把你的本来清醒的脑袋搞晕。SpringBoot 对于习惯使用的同学来说它是非常方便的,但是其内部实现代码不要轻易模仿,那绝对算不上模范 Java 代码。最后老钱表示自己真的很讨厌 SpringBoot 这只怪兽,但是很无奈,这个世界人人都在使用它。这就好比老人们常常告诫年轻人的那句话:如果你改变不了世界,那就先适应这个世界吧! ...

January 9, 2019 · 4 min · jiezi

Spring详解3.Bean的装配

点击进入我的博客1 Spring容器与Bean配置信息Bean配置信息Bean配置信息是Bean的元数据信息,它由一下4个方面组成:Bean的实现类Bean的属性信息,如数据库的连接数、用户名、密码。Bean的依赖关系,Spring根据依赖关系配置完成Bean之间的装配。Bean的行为信息,如生命周期范围及生命周期各过程的回调函数。Bean元数据信息Bean元数据信息在Spring容器中的内部对应物是一个个BeanDefinition形成的Bean注册表,Spring实现了Bean元数据信息内部表示和外部定义之间的解耦。Spring支持的配置方式Spring1.0仅支持基于XML的配置,Spring2.0新增基于注解配置的支持,Spring3.0新增基于Java类配置的支持,Spring4.0则新增给予Groovy动态语言配置的支持。2 基于XML的配置2.1 理解XML与Schema<?xml version=“1.0” encoding=“utf-8” ?><beans (1)xmlns=“http://www.springframework.org/schema/beans" (2)xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns:(3)context=(4)“http://www.springframework.org/schema/context" xsi:(5)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"></beans>(1)处是默认命名空间,无命名空间前缀的元素属于默认命名空间。(2)xsi标准命名空间,用于指定自定义命名空间的Schema文件(3)自定义命名空间的简称,可以任意命名(4)自定义命名空间的全称,必须在xsi命名空间为其指定空间对应的Schema文件,可以任意命名,习惯上用文档发布机构的相关网站目录。(5)为每个命名空间指定Schema文件位置,详解xmlns定义:xml namespace的缩写,可译为“XML命名空间”。作用:防止XML文档含有相同的元素命名冲突,如<table>既可以表示表格,又可以表示桌子。如果增加了命名空间如<table>和<t:table>就可以使两者区分开来。使用:xmlns:namespace-prefix=“namespaceURI”,其中namespace-prefix为自定义前缀,只要在这个XML文档中保证前缀不重复即可;namespaceURI是这个前缀对应的XML Namespace的定义。理解xsi:schemaLocationxsi:schemaLocation定义了XML Namespace和对应的 XSD(Xml Schema Definition)文档的位置的关系。它的值由一个或多个URI引用对组成,两个URI之间以空白符分隔(空格和换行均可)。第一个URI是定义的 XML Namespace的值,第二个URI给出Schema文档的位置,Schema处理器将从这个位置读取Schema文档,该文档的targetNamespace必须与第一个URI相匹配。例如:<beans xsi:schemaLocation=“http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"></beans>这里表示Namespace为http://www.springframework.or…://www.springframework.org/schema/context/spring-context.xsd。2.2 使用XML装配Bean直接装配Bean<!–id不可以配置多个: context.getBean(“myBean1, myBean2”)–><bean class=“com.ankeetc.spring.MyBean” id=“myBean1, myBean2”/> <!–context.getBean(“myBean1”) == context.getBean(“myBean2”)–><bean class=“com.ankeetc.spring.MyBean” name=“myBean1, myBean2”/>id:用于表示一个Bean的名称,在容器内不能重复;不可以配置多个id。name:用于表示一个Bean的名称,在容器内不能重复;可以配置多个名称,用,分割;id和name可以都为空,此时则可以通过获取全限定类名来获取Bean。class:全限定类名静态工厂方法装配静态工厂无需创建工厂类示例就可以调用工厂类方法。factory-method:工厂方法public class MyBeanFactory { public static MyBean createMyBean() { return new MyBean(); }}<bean id=“myBean” class=“com.ankeetc.spring.MyBeanFactory” factory-method=“createMyBean”/>非静态工厂方法装配非静态工厂方法必须首先定义一个工厂类的Bean,然后通过factory-bean引用工厂类实例。factory-bean:指向定义好的工厂Beanpublic class MyBeanFactory { public MyBean createMyBean() { return new MyBean(); }}<bean id=“myBeanFactory” class=“com.ankeetc.spring.MyBeanFactory”/><bean id=“myBean” factory-bean=“myBeanFactory” factory-method=“createMyBean”/>Bean的继承和依赖parent:通过设置<bean>标签的parent属性,子<bean>将自动继承父<bean>的配置信息。depends-on:通过设置<bean>标签的depends-on属性,Spring允许显示的设置当前Bean前置依赖的Bean,确保前置依赖的Bean在当前Bean实例化之前已经创建好。自动装配autowire<beans>元素提供了一个default-autowire属性可以全局自动匹配,默认为no。<bean>元素提供了一个指定自动装配类型的autowire属性,可以覆盖<beans>元素的default-autowire属性,该属性有如下选项:自动装配类型说明no显式指定不使用自动装配。byName如果存在一个和当前属性名字一致的 Bean,则使用该 Bean 进行注入。如果名称匹配但是类型不匹配,则抛出异常。如果没有匹配的类型,则什么也不做。byType如果存在一个和当前属性类型一致的 Bean ( 相同类型或者子类型 ),则使用该 Bean 进行注入。byType 能够识别工厂方法,即能够识别 factory-method 的返回类型。如果存在多个类型一致的 Bean,则抛出异常。如果没有匹配的类型,则什么也不做。constructor与 byType 类似,只不过它是针对构造函数注入而言的。如果当前没有与构造函数的参数类型匹配的 Bean,则抛出异常。使用该种装配模式时,优先匹配参数最多的构造函数。default根据 Bean 的自省机制决定采用 byType 还是 constructor 进行自动装配。如果 Bean 提供了默认的构造函数,则采用 byType;否则采用 constructor 进行自动装配。通过util命名空间配置集合类型的Bean<util:list></util:list><util:set></util:set><util:map></util:map>2.3 使用XML依赖注入属性配置Bean有一个无参数的构造器属性有对应的Setter函数属性命名满足JavaBean的属性命名规范<bean class=“com.ankeetc.spring.MyBean” id=“myBean”> <property name=“prop” value=“prop”/></bean>构造方法constructor-arg中的type和index可以没有,只要能保证可以唯一的确定对应的构造方法即可type中基本数据类型和对应的包装类不能通用循环依赖:如果两个Bean都采用构造方法注入,而且都通过构造方法入参引用对方,就会造成循环依赖导致死锁。<bean class=“com.ankeetc.spring.MyBean” id=“myBean”> <constructor-arg type=“java.lang.String” index=“0” value=“abc”/> <constructor-arg type=“int” index=“1” value=“10”/></bean>2.4 注入参数字面值基本数据类型及其封装类、String都可以采取字面值注入。特殊字符可以使用<![CDATA[]]>节或者转义序列引用其他Bean<ref>元素可以通过以下三个属性引用容器中的其他Bean:bean:通过该属性可以引用同一容器或父容器的Bean,这是最常见的形式。local:通过该属性只能引用同一配置文件中定义的Bean,它可以利用XML解析器自动检验引用的合法性,以便在开发编写配置时能够及时发现并纠正配置的错误。parent:引用父容器中的Bean,如<ref parent=“car”>的配置说明car的Bean是父容器中的Bean。内部Bean内部Bean只会被当前Bean引用,不会被容器中其他的Bean引用内部Bean即使提供了id、name、scope也会被忽略,Scope默认为prototype类型。 <bean id=“prop” class=“com.ankeetc.spring.Prop”> <property name=“value” value=“1314”/> </bean> <bean id=“myBean” class=“com.ankeetc.spring.MyBean”> <property name=“prop”> <!–内部Bean即使提供了id、name、scope也会被忽略–> <bean id=“prop” class=“com.ankeetc.spring.Prop”> <property name=“value” value=“520”/> </bean> </property> </bean>null值使用<null/>代表null值级联属性Spring支持级联属性如prop.value,而且支持多层级联属性级联属性必须有初始值,否则会抛出NullValueInNestedPathExceptionpublic class MyBean { // 必须初始化 private Prop prop = new Prop(); public Prop getProp() { return prop; } public void setProp(Prop prop) { this.prop = prop; }} <bean id=“myBean” class=“com.ankeetc.spring.MyBean”> <property name=“prop.value” value=“1314”/> </bean>集合类型属性List、Set、Map:通过<list><set><map><entry>等标签可以设置List、Set、Map的属性Properties:可以通过<props><prop>等标签设置Properties的属性,Properties属性的键值都只能是字符串。集合合并:子Bean可以继承父Bean的同名属性集合元素,并且使用merge属性选择是否合并,默认不合并。 <bean id=“parentBean” class=“com.ankeetc.spring.MyBean”> <property name=“list”> <list> <value>1314</value> </list> </property> </bean> <bean id=“myBean” class=“com.ankeetc.spring.MyBean” parent=“parentBean”> <property name=“list”> <list merge=“true”> <value>520</value> </list> </property> </bean>2.5 多配置文件整合可以通过ApplicationContext加载多个配置文件,此时多个配置文件中的<bean>是可以互相访问的。可以通过XML中的<import>将多个配置文件引入到一个文件中,这样只需要加载一个配置文件即可。2.6 Bean的作用域类型说明singleton在Spring IoC容器中仅存在一个Bean实例,Bean以单实例的方式存在prototype每次从容器中调用Bean时,都返回一个新的实例request每次HTTP请求都会创建一个新的Bean,该作用域仅适用于WebApplicationContext环境session同一个HTTP session共享一个Bean,不同的HTTP session使用不同的Bean,该作用域仅适用于WebApplicationContext环境globalSession同一个全局Session共享一个Bean,一般用于Portlet环境,该作用域仅适用于WebApplicationContext环境singleton作用域无状态或者状态不可变的类适合使用单例模式如果用户不希望在容器启动时提前实例化singleton的Bean,可以使用lazy-init属性进行控制如果该Bean被其他需要提前实例化的Bean所引用,那么Spring将会忽略lazy-init的设置prototype作用域设置为scope=“prototype"之后,每次调用getBean()都会返回一个新的实例默认情况下,容器在启动时不会实例化prototype的BeanSpring容器将prototype的Bean交给调用者后就不再管理它的生命周期Web应用环境相关的Bean作用域见后续章节作用域依赖的问题见后续章节3 FactoryBean由于实例化Bean的过程比较负责,可能需要大量的配置,这是采用编码的方式可能是更好的选择。Spring提供了FactoryBean工厂类接口,用户可以实现该接口定制实例化Bean的逻辑。当配置文件中<bean>的class属性配置的是FactoryBean的子类时,通过getBean()返回的不是FactoryBean本身,而是getObject()方法所返回的对象,相当于是FactoryBean#getObject()代理了getBean()方法。T getObject() throws Exception;:返回由FactoryBean创建的Bean实例,如果isSingleton()返回的是true,该实例会放到Spring容器的实例缓存池中。Class<?> getObjectType();:返回该FactoryBean创建的Bean的类型boolean isSingleton();:创建的Bean是singleton的还是prototype/** * 实现,分割的方式配置 KFCCombo 属性 /public class KFCFactoryBean implements FactoryBean<KFCCombo> { private String prop; public String getProp() { return prop; } // 接受,分割的属性设置信息 public void setProp(String prop) { this.prop = prop; } // 实例化KFCCombo public KFCCombo getObject() throws Exception { KFCCombo combo = new KFCCombo(); String[] props = prop.split(”,”); combo.setBurger(props[0]); combo.setDrink(props[1]); return combo; } public Class<?> getObjectType() { return KFCCombo.class; } // true则放进容器缓存池,false则每次都调用getObject()方法返回新的对象 public boolean isSingleton() { return false; }} <bean id=“combo” class=“com.ankeetc.spring.KFCFactoryBean”> <property name=“prop” value=“ZingerBurger, PepsiCola”/> </bean>4 基于注解的配置4.1 支持的注解@Component:在Bean的实现类上直接标注,可以被Spring容器识别@Repository:用于对DAO实现类进行标柱@Service:用于对Service实现类进行标注@Controller:用于对Controller实现类进行标注4.2 扫描注解定义对BeanSpring提供了一个context命名空间,用于扫描以注解定义Bean的类。<!–生命context命名空间–><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" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <context:component-scan base-package=“com.ankeetc.spring”/></beans>base-package属性指定一个需要扫描的基类包,Spring容器会扫描这个包下的所有类,并提取标注了相关注解的Bean。resource-pattern属性如果不希望扫描base-package下的所有类,可以使用该属性提供过滤该属性默认是*/*.class,即基包下的所有类<context:exclude-filter>与<context:include-filter><context:exclude-filter>:表示要排除的目标类<context:include-filter>:表示要包含的目标类<context:component-scan>可以有多个上述两个子元素;首先根据exclude-filter列出需要排除的黑名单,然后再根据include-filter流出需要包含的白名单。类别示例说明annotationcom.ankeetc.XxxAnnotation所有标注了XxxAnnotation的类。该类型采用目标类是否标志了某个注解进行过滤。assignablecom.ankeetc.XxService所有继承或扩展XXXService的类。该类型采用目标类是否继承或者扩展了某个特定类进行过滤aspectjcom.ankeetc..*Service+所有类名以Service结束的类及继承或者扩展他们的类。该类型采用AspectJ表达式进行过滤regexcom.ankeetc.auto..*所有com.ankeetc.auto类包下的类。该类型采用正则表达式根据目标类的类名进行过滤customcom.ankeetc.XxxTypeFilter采用XxxTypeFilter代码方式实现过滤规则,该类必须实现org.springframework.core.type.TypeFilter接口use-default-filters属性use-default-filters属性默认值为true,表示会对标注@Component、@Controller、@Service、@Reposity的Bean进行扫描。如果想仅扫描一部分的注解,需要将该属性设置为false。<!– 仅扫描标注了@Controller注解的类–><context:component-scan base-package=“com.ankeetc.spring” use-default-filters=“false”> <context:exclude-filter type=“annotation” expression=“org.springframework.stereotype.Controller”/></context:component-scan>4.3 自动装配@Componentpublic class KFCCombo { @Autowired private PepsiCola cola; @Autowired private Map<String, Cola> colaMap; @Autowired private List<ZingerBurger> burgerList; private ZingerBurger burger; @Autowired public void setBurger(@Qualifier(value = “zingerBurger”) ZingerBurger burger) { this.burger = burger; } public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext(new String[]{“classpath:/beans.xml”}); KFCCombo combo = context.getBean(KFCCombo.class); }}interface Cola {}@Order(value = 1)@Componentclass CocaCola implements Cola {}@Order(value = 2)@Componentclass PepsiCola implements Cola {}@Component(value = “zingerBurger”)class ZingerBurger {}@Autowired注解使用该注解可以按类型自动装配对应的Bean没有找到对应的Bean则会抛出NoSuchBeanDefinitionException异常使用required=false属性可以设置即使找不到对应的Bean(即为null)也不会抛出异常@Autowired可以对类成员变量及方法入参进行标注@Quaifiler如果容器中有一个以上匹配的Bean时,可以按照Bean名字查找对应的Bean@Quaifiler需要与@Autowired配合使用对集合类进行标注可以使用@Autowired对集合类进行标注,Spring会讲容器中按类型匹配对所有Bean注入进来可以使用@Order指定加载顺序,值越小的越先加载@Lazy延迟加载可以使用@Lazy实现延迟加载,不会立即注入属性值,而是延迟到调用此属性对时候才会注入属性值。@Resource和@InjectSpring支持JSR-250中@Resource注解和JSR-330的@Inject注解@Resource采用的是按照名称加载的方式,它要求提供一个Bean名称的属性,如果属性为空,则自动采用标注处的变量名或方法名作为Bean的名称。@Inject是按照类型匹配注入Bean的。由于这两个注解功能没有@Autowired功能强大,一般不需要使用。4.4 Bean作用范围及生命周期注解配置的Bean默认作用范围为singleton,可以使用@Scope显示指定作用范围可以使用@PostConstruct和@PreDestroy注解来达到init-method和destroy-method属性的功能。@PostConstruct和@PreDestroy注解可以有多个5 基于Java类的配置5.1 @Configuration注解JavaConfig是Spring的一个子项目,旨在通过Java类的方式提供Bean的定义信息。普通的POJO标注了@Configuration注解,就可以被Spring容器提供Bean定义信息。@Configuration注解本身已经标注了@Component注解,所以任何标注了@Configuration的类都可以作为普通的Bean。5.2 @Bean注解@Bean标注在方法上,用于产生一个BeanBean的类型由方法的返回值的类型确定,Bean名称默认与方法名相同,也可以显示指定Bean的名称。可以使用@Scope来控制Bean的作用范围。5.3 启动Spring容器通过@Configuration类启动Spring容器可以直接设置容器启动要注册的类可以向容器中注册新的类,注册了新的类要记得refresh可以通过@Import将多个配置类组装称一个配置类public class Main { public static void main(String[] args) { // (1)可以直接设置容器启动要加载的类 ApplicationContext applicationContext = new AnnotationConfigApplicationContext(DaoConfig.class); // (2)可以向容器中注册新的类 ((AnnotationConfigApplicationContext) applicationContext).register(ServiceConfig.class); // (3)注册了新的类要记得refresh ((AnnotationConfigApplicationContext) applicationContext).refresh(); }}@Configurationclass DaoConfig { @Bean public String getStr() { return “1314”; }}@Configuration@Import(DaoConfig.class)// (4)可以通过@Import将多个配置类组装称一个配置类class ServiceConfig {}通过XML配置文件引用@Configuration的配置标注了@Configureation的配置类本身也是一个bean,它可以被Spring的<context:component-scan>扫描到。如果希望将此配置类组装到XML配置文件中,通过XML配置文件启动Spring容器,仅在XML文件中通过<context:component-scan>扫描到相应的配置类即可。<context:component-scan base-package=“com.ankeetc.spring” resource-pattern=“Config.class”/>通过@Configuration配置类引用XML配置信息在标注了@Configuration的配置类中,可以通过@ImportResource引入XML配置文件。@Configuration@ImportResource(“classpath:beans.xml”)public class Config {} ...

January 8, 2019 · 2 min · jiezi

Fundebug后端Java异常监控插件更新至0.2.0,支持Spring及Maven

摘要: 0.2.0支持监控Spring应用,并且支持使用Maven接入插件,请大家及时更新。支持监控Spring应用1. pom.xml配置fundebug-spring依赖<dependency> <groupId>com.fundebug</groupId> <artifactId>fundebug-spring</artifactId> <version>0.2.0</version></dependency>2. 在项目中引入fundebug并配置apikey新增FundebugConfig.javaimport org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Import;import com.fundebug.Fundebug;import com.fundebug.SpringConfig;@Configuration@Import(SpringConfig.class)public class FundebugConfig { @Bean public Fundebug getBean() { return new Fundebug(“apikey”); }}注意:获取apikey需要免费注册帐号并且创建项目。可以参考Demo项目Fundebug/fundebug-spring-demo。支持使用Maven接入插件Fundebug的Java异常监控插件fundebug-java与fundebug-spring都发布到了Maven中央仓库,因此可以在pom.xml直接配置依赖。接入fundebug-java<dependency> <groupId>com.fundebug</groupId> <artifactId>fundebug-java</artifactId> <version>0.2.0</version></dependency>接入fundebug-spring<dependency> <groupId>com.fundebug</groupId> <artifactId>fundebug-spring</artifactId> <version>0.2.0</version></dependency>参考Fundebug文档 - JavaMaven入门教程关于FundebugFundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了9亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用!版权声明转载时请注明作者Fundebug以及本文地址:https://blog.fundebug.com/2019/01/07/fundebug-java-0-2-0/

January 8, 2019 · 1 min · jiezi

Spring详解2.理解IoC容器

点击进入我的博客1 如何理解IoC1.1 依然是KFC的案例interface Burger { int getPrice();}interface Drink { int getPrice();}class ZingerBurger implements Burger { public int getPrice() { return 10; }}class PepsiCola implements Drink { public int getPrice() { return 5; }}/** * 香辣鸡腿堡套餐 /class ZingerBurgerCombo { private Burger burger = new ZingerBurger(); private Drink drink = new PepsiCola(); public int getPrice() { return burger.getPrice() + drink.getPrice(); }}上述案例中我们实现了一个香辣鸡腿堡套餐,在ZingerBurgerCombo中,我们发现套餐与汉堡、套餐与饮品产生了直接的耦合。要知道肯德基中的套餐是非常多的,这样需要建立大量不同套餐的类;而且如果该套餐中的百事可乐如果需要换成其他饮品的话,是不容易改变的。class KFCCombo { private Burger burger; private Drink drink; public KFCCombo(Burger burger, Drink drink) { this.burger = burger; this.drink = drink; }}class KFCWaiter { public KFCCombo getZingerBurgerCombo() { return new KFCCombo(new ZingerBurger(), new PepsiCola()); } // else combo…}为了防止套餐和汉堡、饮品的耦合,我们统一用KFCCombo来表示所有的套餐组合,引入了一个新的类KFCWaiter,让她负责所有套餐的装配。1.2 控制反转与依赖注入控制反转IoCIoC的字面意思是控制反转,它包括两部分的内容:控制:在上述案例中,控制就是选择套餐中汉堡和饮品的控制权。反转:反转就是把该控制权从套餐本身中移除,放到专门的服务员手中。对于Spring来说,我们通过Spring容器管理来管理和控制Bean的装配。依赖注入由于IoC这个重要的概念比较晦涩隐讳,Martin Fowler提出了DI(Dependency Injection,依赖注入)的概念用以代替IoC,即让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。Spring容器Spring就是一个容器,它通过配置文件或注解描述类和类之间的依赖关系,自动完成类的初始化和依赖注入的工作。让开发着可以从这些底层实现类的实例化、依赖关系装配等工作中解脱出来,专注于更有意义的业务逻辑开发。2 IoC的类型从注入方法上看,IoC可以分为:构造函数注入、属性注入、接口注入构造函数注入class KFCCombo { private Burger burger; private Drink drink; // 在此注入对应的内容 public KFCCombo(Burger burger, Drink drink) { this.burger = burger; this.drink = drink; }}属性注入class KFCCombo { private Burger burger; private Drink drink; // 在此注入对应的内容 public void setBurger(Burger burger) { this.burger = burger; } // 在此注入对应的内容 public void setDrink(Drink drink) { this.drink = drink; }}接口注入interface InjectFood { void injectBurger(Burger burger); void injectDrink(Drink drink);}class KFCCombo implements InjectFood { private Burger burger; private Drink drink; // 在此注入对应的内容 public void injectBurger(Burger burger) { this.burger = burger; } // 在此注入对应的内容 public void injectDrink(Drink drink) { this.drink = drink; }}接口注入和属性注入并无本质的区别,而且还增加了一个额外的接口导致代码增加,因此不推荐该方式。3 资源访问JDK提供的访问资源的类(如java.net.URL、File等)并不能很好地满足各种底层资源的访问需求,比如缺少从类路径或者Web容器上下文中获取资源的操作类。因此,Spring提供了Resource接口,并由此装载各种资源,包括配置文件、国际化属性文件等资源。3.1 Resource类图WritableResource:可写资源接口,Spring3.1新增的接口,有2个实现类:FileSystemResource和PathResource。ByteArrayResource:二进制数组表示的资源,二进制数组资源可以在内存中通过程序构造。ClassPathResource:类路径下的资源,资源以相对于类路径的方式表示。FileSystemResouce:文件系统资源,资源以文件系统路径的方式表示。InputStreamResource:以输入流返回标识的资源ServletContextResource:为访问Web容器上下文中的资源而设计的类,负责以相对于Web应用根目录的路径来加载资源。支持以流和URL的形式访问,在war包解包的情况下,也可以通过File方式访问。 该类还可以直接从JAR包中访问资源。UrlResource:封装了java.net.URL,它使用户能够访问任何可以通过URL表示的资源,如文件系统的资源、HTTP资源、FTP资源PathResource:Spring 4.0提供的读取资源文件的新类。PathResource封装了java.net.URL、java.nio.file.Path、文件系统资源,它使用户能够访问任何可以通过URL、Path、系统文件路径标识的资源,如文件系统的资源、HTTP资源、FTP资源3.2 资源加载为了访问不同类型的资源,Resource接口下提供了不同的子类,这造成了使用上的不便。Spring提供了一个强大的加载资源的方式,在不显示使用Resource实现类的情况下,仅通过不同资源地址的特殊标示就可以访问对应的资源。地址前缀实例释义classpath:classpath:com/ankeetc/spring/Main.class从类不经中加载资源,classpath: 和 classpath:/ 是等价的,都是相对于类的根路径,资源文件可以在标准的文件系统中,也可以在jar或者zip的类包中file:file:/Users/zhengzhaoxi/.gitconfig使用UrlResource从文件系统目录中装载资源,可以采用绝对路径或者相对路径http://http://spiderlucas.github.io/…使用UrlResource从web服务器中加载资源ftp://ftp://spiderlucas.github.io/atom.xml使用UrlResource从FTP服务器中装载资源没有前缀com/ankeetc/spring/Main.class根据ApplicationContext的具体实现类采用对应类型的Resourceclasspath:与classpath:假设有多个Jar包或者文件系统类路径下拥有一个相同包名(如com.ankeetc):classpath:只会在第一个加载的com.ankeetc包的类路径下查找classpath*:会扫描到所有的这些JAR包及类路径下出现的com.ankeetc类路径。这对于分模块打包的应用非常有用,假设一个应用分为2个模块,每一个模块对应一个配置文件,分别为module1.yaml 、module2.yaml,都放在了com.ankeetc的目录下,每个模块单独打成JAR包。可以使用 classpath*:com/ankeetc/module*.xml加载所有模块的配置文件。Ant风格的资源地址通配符? 匹配文件名中的一个字符* 匹配文件名中的任意字符** 匹配多层路径资源加载器ResourceLoader接口仅有一个getResource(String loacation)方法,可以根据一个资源地址加载文件资源。不过,资源地址仅支持带资源类型前缀的表达式,不支持Ant风格的资源路径表达式。ResourcePatternResolver扩展ResourceLoader接口,定义了一个新的接口方法getResource(String locationPattern),改方法支持带资源类型前缀及Ant风格的资源路径表达式。PathMatchingResourcePatternResolver是Spring提供的标准实现类。4 BeanFactoryBeanFactory是Spring框架最核心的接口,它提供了高级IoC的配置机制。我们一般称BeanFactory为IoC容器。BeanFactory是Spring框架等基础设施,面向Spring本身。BeanFactory是一个类工厂,可以创建并管理各种类的对象。所有可以被Spring容器实例化并管理的Java类都可以成为Bean。BeanFactory主要方法就是getBean(String beanName);BeanFactory启动IoC容器时,并不会初始化配置文件中定义的Bean,初始化动作在第一个调用时产生。对于单实例的Bean来说,BeanFactory会缓存Bean实例,在第二次使用geBean()获取Bean时,将直接从IoC容器的缓存中获取Bean实例。4.1 BeanFactory体系结构BeanFactory:BeanFactory接口位于类结构树的顶端,它最主要的方法就是getBean(String beanName) ,该方法从容器中返回特定名称的Bean,BeanFactory的功能通过其他接口而得到不断扩展。ListableBeanFactory:该接口定义了访问容器中Bean基本信息的若干方法,如:查看 Bean 的个数、获取某一类型Bean的配置名、或查看容器中是否包含某一个Bean。HierarhicalBeanFactory:父子级联 IoC 容器的接口,子容器可以通过接口方法访问父容器。ConfigurableBeanFactory:该接口增强了IoC容器的可定制性,它定义了设置类装载器、属性 编辑器、容器初始化后置处理器等方法。AutowireCapableBeanFactory:定义了将容器中的Bean按某种规则(如:按名称匹配、按类型匹配)进行自动装配的方法。SingletonBeanFactory:定义了允许在运行期间向容器注册单实例Bean的方法。BeanDefinitionRegistry:Spring配置文件中每一个Bean节点元素在Spring容器里都通过一个 BeanDefinition对象表示,它描述了Bean的配置信息。而BeanDefinitionRegistry接口提供了向容器手工注册BeanDefinition对象的方法。4.2 初始化BeanFactoryBeanFactory有多种实现,最常用的是 XmlBeanFactory,但在Spring 3.2时已被废弃。目前建议使用XmlBeanDefinitionReader与DefaultListableBeanFactory。<?xml version=“1.0” encoding=“utf-8” ?><beans xmlns=“http://www.springframework.org/schema/beans" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd"> <bean id=“car” class=“com.ankeetc.spring.Car”></bean></beans> public static void main(String[] args) throws Exception { ResourceLoader resourceLoader = new DefaultResourceLoader(); Resource resource = resourceLoader.getResource(“beans.xml”); DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions(resource); beanFactory.getBean(””); }4.3 BeanFactory中Bean的生命周期当调用者通过getBean()向容器请求一个Bean时,如果容器注册了InstantiationAwareBeanPostProcessor接口,则在实例化Bean之前,调用postProcessBeforeInstantiation()方法。根据配置调用构造方法或者工厂方法实例化Bean。调用InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation()。调用InstantiationAwareBeanPostProcessor#postProcessPropertyValues()方法。设置属性值。如果Bean实现了BeanNameAware接口,则将调用BeanNameAware#setBeanName()接口方法,将配置文件中该Bean对应的名称设置到Bean中。如果Bean实现了BeanFactoryAware接口,将调用BeanFactoryAware#setBeanFactory()接口方法。如果容器注册了BeanPostProcessor接口,将调用BeanPostProcessor#postProcessBeforeInitialization()方法。入參Bean是当前正在处理的Bean,BeanName是当前Bean的配置名,返回的对象为处理后的Bean。BeanPostProcessor在Spring框架中占有重要的地位,为容器提供对Bean进行后续加工处理的切入点,AOP、动态代理都通过BeanPostProcessor来实现。如果Bean实现了InitializingBean接口,则将调用InitializingBean#afterPropertiesSet()方法。如果<bean>中定义了init-method初始化方法,则执行这个方法。调用BeanPostProcessor#postProcessAfterInitialization()方法再次加工Bean。如果<bean>中指定了Bean的作用范围为scope=‘prototype’,则将Bean返回给调用者,Spring不再管理这个Bean的生命周期。如果scope=‘singleton’,则将Bean放入Spring IoC容器的缓存池中,并返回Bean。对于scope=‘singleton’的Bean,当容器关闭时,将触发Spring对Bean的后续生命周期的管理工作。如果Bean实现了DisposableBean接口,将调用DisposableBean#destroy()方法。对于`scope=‘singleton’的Bean,如果通过<bean>的destroy-method属性指定了Bean的销毁方法,那么Spring将执行这个方法。Bean方法的分类Bean自身的方法:构造方法、Setter设置属性值、init-method、destroy-method。Bean级生命周期接口方法:如上图中蓝色的部分。BeanNameAware、BeanFactoryAware、InitializingBean、DisposableBean,这些由Bean本身直接实现。容器级生命周期接口方法:如上图中的红色的部分。InstantiationAwareBeanPostProcessor和BeanPostProcessor这两个接口,一般称它们的实现类为后处理器。后处理器接口不由Bean本身实现,实现类以容器附加装置的形式注册到Spring容器中。当Spring容器创建任何Bean的时候,这些后处理器都会起作用,所以这些后处理器的影响是全局的。如果需要多个后处理器,可以同时实现Ordered接口,容器将按照特定的顺序依此调用这些后处理器。工厂后处理器接口方法:AspectJWeavingEnabler、CustomAutowireConfigurer、ConfigurationClassPostProcessor等方法,工厂后处理器也是容器级的,在应用上下文装配配置文件后立即使用。4.4 BeanFactory生命周期案例public class Main { public static void main(String[] args) { Resource resource = new PathMatchingResourcePatternResolver().getResource(“classpath:beans.xml”); DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions(resource); beanFactory.addBeanPostProcessor(new InstantiationAwareBeanPostProcessor() { public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException { System.out.println(“InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()”); return null; } public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { System.out.println(“InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation()”); return true; } public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException { System.out.println(“InstantiationAwareBeanPostProcessor.postProcessPropertyValues()”); return pvs; } public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println(“BeanPostProcessor.postProcessBeforeInitialization()”); return bean; } public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println(“BeanPostProcessor.postProcessAfterInitialization()”); return bean; } }); MyBean myBean = beanFactory.getBean(“myBean”, MyBean.class); beanFactory.destroySingletons(); }}class MyBean implements BeanNameAware, BeanFactoryAware, InitializingBean, DisposableBean { private String prop; public MyBean() { System.out.println(“MyBean:构造方法”); } public String getProp() { System.out.println(“MyBean:get方法”); return prop; } public void setProp(String prop) { System.out.println(“MyBean:set方法”); this.prop = prop; } public void setBeanFactory(BeanFactory beanFactory) throws BeansException { System.out.println(“MyBean:BeanFactoryAware.setBeanFactory()”); } public void setBeanName(String name) { System.out.println(“MyBean:BeanNameAware.setBeanName()”); } public void destroy() throws Exception { System.out.println(“MyBean:DisposableBean.destroy()”); } public void afterPropertiesSet() throws Exception { System.out.println(“MyBean:InitializingBean.afterPropertiesSet()”); } // 配置文件中init-method public void myInit() { System.out.println(“MyBean:myInit()”); } // 配置文件中destroy-method public void myDestroy() { System.out.println(“MyBean:myDestroy()”); }}<beans xmlns=“http://www.springframework.org/schema/beans" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd"> <bean class=“com.ankeetc.spring.MyBean” name=“myBean” init-method=“myInit” destroy-method=“myDestroy”> <property name=“prop” value=“prop”/> </bean></beans>4.5 关于Bean生命周期接口的探讨Bean生命周期带来的耦合:通过实现Spring的Bean生命周期接口对Bean进行额外控制,虽然让Bean具有了更细致的生命周期阶段,但也带来了一个问题,Bean和Spring框架紧密绑定在一起了,这和Spring一直推崇的“不对应用程序类作任何限制”的理念是相悖的。业务类本应该完全POJO化,只实现自己的业务接口,不需要和某个特定框架(包括Spring框架)的接口关联。init-method和destroy-method:可以通过<bean>的init-method和destroy-method属性配置方式为Bean指定初始化和销毁的方法,采用这种方式对Bean生命周期的控制效果和通过实现InitializingBean、DisposableBean接口所达到的效果是完全相同的,而且达到了框架解耦的目的。BeanPostProcessor:BeanPostProcessor接口不需要Bean去继承它,可以像插件一样注册到Spring容器中,为容器提供额外功能。5 ApplicationContext5.1 Application体系结构ApplicationEventPublisher:让容器拥有了发布应用上下文事件的功能,包括容器启动事件 、关闭事件等。实现了ApplicationListener事件监听接口的Bean可以接收到容器事件,并对容器事件进行响应处理。在ApplicationContext抽象实现类AbstractApplicationContext中存在一个 ApplicationEventMulticaster,它负责保存所有的监听器,以便在容器产生上下文事件时通知这些事件监听者。MessageSource:为容器提供了i18n国际化信息访问的功能。ResourcePatternResolver:所有ApplicationContext实现类都实现了类似于PathMatchingResourcePatternResolver的功能,可以通过带前缀 Ant 风格的资源类文件路径来装载Spring的配置文件。LifeCycle:它提供了start()和stop()两个方法,主要用于控制异步处理过程。在具体使用时,该接口同时被ApplicationContext实现及具体Bean实现,ApplicationContext会将start/stop的信息传递给容器中所有实现了该接口的Bean,以达到管理和控制JMX、任务调度等目的。ConfigurableApplicationContext:它扩展了ApplicationContext,让ApplicationContext具有启动、刷新和关闭应用上下文的能力。上下文关闭时,调用 refresh()即可启动上下文;如果已经启动,则调用refresh()可清除缓存并重新加载配置信息;调用close()可关闭应用上下文。5.2 初始化ApplicationContextClassPathXmlApplicationContext:从类路径中加载XML配置文件,也可以显示的使用带资源类型前缀的路径。FileSystemXmlApplicationContext:从文件系统路径下加载XML配置文件,也可以显示的使用带资源类型前缀的路径。AnnotationConfigApplicationContext:一个标注@Configuration注解的POJO即可提供Spring所需的Bean配置信息。GenericGroovyApplicationContext:从Groovy配置中提取Bean。 public static void main(String[] args) throws Exception { ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext(“beans.xml”); FileSystemXmlApplicationContext fileSystemXmlApplicationContext = new FileSystemXmlApplicationContext(“file:/Users/zhengzhaoxi/Git/spring/src/main/resources/beans.xml”); AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(Config.class); }5.3 WebApplicationContext体系结构见后续章节5.4 ApplicationContext中Bean的生命周期不同点Bean在ApplicationContext和BeanFactory中生命周期类似,但有以下不同点如果Bean实现了ApplicationContextAware接口,则会增加一个调用该接口方法的ApplicationContextAware.setApplicationContext的方法。如果在配置文件中生命了工厂后处理器接口BeanFactoryPostProcessor的实现类,则应用上下文在装在配置文件之后、初始化Bean实例之前会调用这些BeanFactoryPostProcessor对配置信息进行加工处理。Spring提供了多个工厂容器,如CustomEditorConfigure、PropertyPlaceholderConfigurer等。工厂后处理器是容器级的,只在应用上下文初始化时调用一次,其目的是完成一些配置文件加工处理工作。ApplicationContext可以利用Java反射机制自动识别出配置文件中定义的BeanPostProcessor、InstantiationAwareBeanPostProcessor、BeanFactoryPostProcessor,并自动将它们注册到应用上下文中(如下所示);而BeanFactory需要通过手工调用addBeanPostProcessor()方法注册。<beans xmlns=“http://www.springframework.org/schema/beans" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd"> <bean class=“com.ankeetc.spring.MyBean” name=“myBean” init-method=“myInit” destroy-method=“myDestroy”> <property name=“prop” value=“prop”/> </bean> <!– 工厂后处理器 –> <bean id=“myBeanPostProcessor” class=“com.ankeetc.spring.MyBeanPostProcessor”/> <!– 注册Bean后处理器 –> <bean id=“myBeanFactoryPostProcessor” class=“com.ankeetc.spring.MyBeanFactoryPostProcessor”/></beans>6 BeanFactory与ApplicationContext区别一般称BeanFactory为IoC容器:Bean工厂(com.springframework.beans.factory.BeanFactory)是Spring框架中最核心的接口,它提供了高级 IoC 的配置机制 。BeanFactory使管理不同类型的Java对象成为可能。BeanFactory是Spring框架的基础设施,面向Spring本身。一般称ApplicationContext为应用上下文或Spring容器:应用上下文(com.springframework.context.ApplicationContext)建立在BeanFactory基础之上,提供了更多面向应用的功能,它提供了国际化支持和框架事件体系,更易于创建实际应用 。 ApplicationContext 面向使用 Spring 框架的开发者,几乎所有的应用场合都可以直接使用 ApplicationContext。BeanFactory在初始化容器的时候,并未实例化Bean,直到第一次访问某个Bean时才实例化Bean;ApplicationContext在初始化应用上下文的时候就实例化全部单实例Bean。ApplicationContext可以利用Java反射机制自动识别出配置文件中定义的BeanPostProcessor、InstantiationAwareBeanPostProcessor、BeanFactoryPostProcessor,并自动将它们注册到应用上下文中;而BeanFactory需要通过手工调用addBeanPostProcessor()方法注册。7 父子容器通过HierarchicalBeanFactory接口,Spring的IoC容器可以建立父子层级关联的容器体系,子容器可以访问父容器中的 Bean,但父容器不能访问子容器的Bean。在容器内,Bean的id必须是唯一的,但子容器可以拥有一个和父容器id相同的 Bean。父子容器层级体系增强了Spring容器架构的扩展性和灵活性,因此第三方可以通过编程的方式,为一个已经存在的容器添加一个或多个特殊用途的子容器,以提供一些额外的功能。Spring使用父子容器的特性实现了很多能力,比如在Spring MVC中,展现层Bean位于一个子容器中,而业务层和持久层的Bean位于父容器中。 这样展现层Bean就可以引用业务层和持久层的Bean,而业务层和持久层的Bean则看不到展现层的Bean。 ...

January 8, 2019 · 3 min · jiezi

分布式系统关注点——99%的人都能看懂的「补偿」以及最佳实践

如果这是第二次看到我的文章,欢迎文末扫码订阅我哟~ ????本文长度为4229字,建议阅读11分钟。这是本系列中既「数据一致性」后的第二章节——「高可用」的完结篇。前面几篇中z哥跟你聊了聊做「高可用」的意义,以及如何做「负载均衡」和「高可用三剑客」(熔断、限流、降级,文末会附上前文连接:))。这次,我们来聊一聊在保证对外高可用的同时,憋出的“内伤”该如何通过「补偿」机制来自行消化。一、「补偿」机制的意义?以电商的购物场景为例:客户端 —->购物车微服务 —->订单微服务 —-> 支付微服务。这种调用链非常普遍。那么为什么需要考虑补偿机制呢?正如之前几篇文章所说,一次跨机器的通信可能会经过DNS 服务,网卡、交换机、路由器、负载均衡等设备,这些设备都不一定是一直稳定的,在数据传输的整个过程中,只要任意一个环节出错,都会导致问题的产生。而在分布式场景中,一个完整的业务又是由多次跨机器通信组成的,所以产生问题的概率成倍数增加。但是,这些问题并不完全代表真正的系统无法处理请求,所以我们应当尽可能的自动消化掉这些异常。可能你会问,之前也看到过「补偿」和「事务补偿」或者「重试」,它们之间的关系是什么?你其实可以不用太纠结这些名字,从目的来说都是一样的。就是一旦某个操作发生了异常,如何通过内部机制将这个异常产生的「不一致」状态消除掉。题外话:在Z哥看来,不管用什么方式,只要通过额外的方式解决了问题都可以理解为是「补偿」,所以「事务补偿」和「重试」都是「补偿」的子集。前者是一个逆向操作,而后者则是一个正向操作。只是从结果来看,两者的意义不同。「事务补偿」意味着“放弃”,当前操作必然会失败。▲事务补偿「重试」则还有处理成功的机会。这两种方式分别适用于不同的场景。▲重试因为「补偿」已经是一个额外流程了,既然能够走这个额外流程,说明时效性并不是第一考虑的因素,所以做补偿的核心要点是:宁可慢,不可错。因此,不要草率的就确定了补偿的实施方案,需要谨慎的评估。虽说错误无法100%避免,但是抱着这样的一个心态或多或少可以减少一些错误的发生。二、「补偿」该怎么做?做「补偿」的主流方式就前面提到的「事务补偿」和「重试」,以下会被称作「回滚」和「重试」。我们先来聊聊「回滚」。相比「重试」,它逻辑上更简单一些。「回滚」Z哥将回滚分为2种模式,一种叫「显式回滚」(调用逆向接口),一种叫「隐式回滚」(无需调用逆向接口)。最常见的就是「显式回滚」。这个方案无非就是做2个事情:首先要确定失败的步骤和状态,从而确定需要回滚的范围。一个业务的流程,往往在设计之初就制定好了,所以确定回滚的范围比较容易。但这里唯一需要注意的一点就是:如果在一个业务处理中涉及到的服务并不是都提供了「回滚接口」,那么在编排服务时应该把提供「回滚接口」的服务放在前面,这样当后面的工作服务错误时还有机会「回滚」。其次要能提供「回滚」操作使用到的业务数据。「回滚」时提供的数据越多,越有益于程序的健壮性。因为程序可以在收到「回滚」操作的时候可以做业务的检查,比如检查账户是否相等,金额是否一致等等。由于这个中间状态的数据结构和数据大小并不固定,所以Z哥建议你在实现这点的时候可以将相关的数据序列化成一个json,然后存放到一个nosql类型的存储中。「隐式回滚」相对来说运用场景比较少。它意味着这个回滚动作你不需要进行额外处理,下游服务内部有类似“预占”并且“超时失效”的机制的。例如:电商场景中,会将订单中的商品先预占库存,等待用户在 15 分钟内支付。如果没有收到用户的支付,则释放库存。下面聊聊可以有很多玩法,也更容易陷入坑里的「重试」。「重试」「重试」最大的好处在于,业务系统可以不需要提供「逆向接口」,这是一个对长期开发成本特别大的利好,毕竟业务是天天在变的。所以,在可能的情况下,应该优先考虑使用「重试」。不过,相比「回滚」来说「重试」的适用场景更少一些,所以我们第一步首先要判断,当前场景是否适合「重试」。比如:下游系统返回「请求超时」、「被限流中」等临时状态的时候,我们可以考虑重试而如果是返回“余额不足”、“无权限”等明确无法继续的业务性错误的时候就不需要重试了一些中间件或者rpc框架中返回Http503、404等没有何时恢复的预期的时候,也不需要重试如果确定要进行「重试」,我们还需要选定一个合适的「重试策略」。主流的「重试策略」主要是以下几种。策略1.立即重试。有时故障是候暂时性,可能是因网络数据包冲突或硬件组件流量高峰等事件造成的。在此情况下,适合立即重试操作。不过,立即重试次数不应超过一次,如果立即重试失败,应改用其它的策略。策略2.固定间隔。应用程序每次尝试的间隔时间相同。 这个好理解,例如,固定每 3 秒重试操作。(以下所有示例代码中的具体的数字仅供参考。)策略1和策略2多用于前端系统的交互式操作中。策略3.增量间隔。每一次的重试间隔时间增量递增。比如,第一次0秒、第二次3秒、第三次6秒,9、12、15这样。return (retryCount - 1) * incrementInterval;使得失败次数越多的重试请求优先级排到越后面,给新进入的重试请求让道。策略4.指数间隔。每一次的重试间隔呈指数级增加。和增量间隔“殊途同归”,都是想让失败次数越多的重试请求优先级排到越后面,只不过这个方案的增长幅度更大一些。return 2 ^ retryCount;策略5.全抖动。在递增的基础上,增加随机性(可以把其中的指数增长部分替换成增量增长。)。适用于将某一时刻集中产生的大量重试请求进行压力分散的场景。return random(0 , 2 ^ retryCount);策略6.等抖动。在「指数间隔」和「全抖动」之间寻求一个中庸的方案,降低随机性的作用。适用场景和「全抖动」一样。var baseNum = 2 ^ retryCount;return baseNum + random(0 , baseNum);3、4、5、6策略的表现情况大致是这样。(x轴为重试次数)为什么说「重试」有坑呢?正如前面聊到的那样,出于对开发成本考虑,你在做「重试」的时候可能是复用的常规调用的接口。那么此时就不得不提一个「幂等性」问题。 如果实现「重试」选用的技术方案不能100%确保不会重复发起重试,那么「幂等性」问题是一个必须要考虑的问题。哪怕技术方案可以确保100%不会重复发起重试,出于对意外情况的考量,尽量也考虑一下「幂等性」问题。幂等性:不管对程序发起几次重复调用,程序表现的状态(所有相关的数据变化)与调用一次的结果是一致的话,就是保证了幂等性。这意味着可以根据需要重复或重试操作,而不会导致意外的影响。对于非幂等操作,算法可能必须跟踪操作是否已经执行。所以,一旦某个功能支持「重试」,那么整个链路上的接口都需要考虑幂等性问题,不能因为服务的多次调用而导致业务数据的累计增加或减少。 满足「幂等性」其实就是需要想办法识别重复的请求,并且将其过滤掉。思路就是:给每个请求定义一个唯一标识。在进行「重试」的时候判断这个请求是否已经被执行或者正在被执行,如果是则抛弃该请求。第1点,我们可以使用一个全局唯一id生成器或者生成服务(可以扩展阅读,分布式系统中的必备良药 —— 全局唯一单据号生成)。 或者简单粗暴一些,使用官方类库自带的Guid、uuid之类的也行。然后通过rpc框架在发起调用的客户端中,对每个请求增加一个唯一标识的字段进行赋值。第2点,我们可以在服务端通过Aop的方式切入到实际的处理逻辑代码之前和之后,一起配合做验证。大致的代码思路如下。【方法执行前】if(isExistLog(requestId)){ //1.判断请求是否已被接收过。 对应序号3 var lastResult = getLastResult(); //2.获取用于判断之前的请求是否已经处理完成。 对应序号4 if(lastResult == null){ var result = waitResult(); //挂起等待处理完成 return result; } else{ return lastResult; } }else{ log(requestId); //3.记录该请求已接收}//do something..【方法执行后】logResult(requestId, result); //4.将结果也更新一下。如果「补偿」这个工作是通过MQ来进行的话,这事就可以直接在对接MQ所封装的SDK中做。在生产端赋值全局唯一标识,在消费端通过唯一标识消重。三、「重试」的最佳实践再聊一些Z哥积累的最佳实践吧(划重点:)),都是针对「重试」的,的确这也是工作中最常用的方案。「重试」特别适合在高负载情况下被「降级」,当然也应当受到「限流」和「熔断」机制的影响。当「重试」的“矛”与「限流」和「熔断」的“盾”搭配使用,效果才是最好。需要衡量增加补偿机制的投入产出比。一些不是很重要的问题时,应该「快速失败」而不是「重试」。过度积极的重试策略(例如间隔太短或重试次数过多)会对下游服务造成不利影响,这点一定要注意。一定要给「重试」制定一个终止策略。当回滚的过程很困难或代价很大的情况下,可以接受很长的间隔及大量的重试次数,DDD中经常被提到的「saga」模式其实也是这样的思路。不过,前提是不会因为保留或锁定稀缺资源而阻止其他操作(比如1、2、3、4、5几个串行操作。由于2一直没处理完成导致3、4、5没法继续进行)。四、总结这篇我们先聊了下做「补偿」的意义,以及做补偿的2个方式「回滚」和「重试」的实现思路。然后,提醒你要注意「重试」的时候需要考虑幂等性问题,并且z哥也给出了一个解决思路。最后,分享了几个z哥总结的针对「重试」的最佳实践。希望对你有所帮助。Question:你之前有哪些时候是通过自己人工来做「补偿」的经历吗?欢迎吐槽~z哥自己就有多次熬到半夜才把“意外”造成的混乱清理干净,刻骨铭心啊????。相关文章:分布式系统关注点——初识「高可用」分布式系统关注点——仅需这一篇,吃透「负载均衡」妥妥的分布式系统关注点——「负载均衡」到底该如何实施?分布式系统关注点——做了「负载均衡」就可以随便加机器了吗?这三招来帮你!分布式系统关注点——99%的人都能看懂的「熔断」以及最佳实践分布式系统关注点——想通关「限流」?只要这一篇分布式系统关注点——让你的系统“坚挺不倒”的最后一个大招——「降级」分布式系统中的必备良药 —— 全局唯一单据号生成作者:Zachary出处:https://www.cnblogs.com/Zacha…▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描下方的二维码加入哦~。定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。 ...

January 2, 2019 · 1 min · jiezi

追踪解析Spring ioc启动源码(3)

4 在bean factory中创造 bean写在前面:该 part 是 Spring ioc 的核心,显得非常冗杂,Spring 内不知名的组件非常的多,有很多笔者也难以描述清楚,甚至也没见过。在介绍的时候会做适当的忽略。该 part 的起点:public AnnotationConfigApplicationContext(Class<?>… annotatedClasses) { this(); register(annotatedClasses); refresh(); // 4 在 bean factory 中创造 bean}来追踪这个方法的实现://AbstractApplicationContext.classpublic void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { //准备工作的配置 //4.3 prepareRefresh(); //获取 bean factory //4.4 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); //配置 bean factory //4.5 prepareBeanFactory(beanFactory); try { //4.6 postProcessBeanFactory(beanFactory); //4.7 invokeBeanFactoryPostProcessors(beanFactory); //4.8 registerBeanPostProcessors(beanFactory); //4.9 initMessageSource(); //4.10 initApplicationEventMulticaster(); //4.11 onRefresh(); //4.12 registerListeners(); //4.13 finishBeanFactoryInitialization(beanFactory); //4.15 finishRefresh(); }catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn(“Exception encountered during context initialization - " + “cancelling refresh attempt: " + ex); } //4.16 destroyBeans(); //4.17 cancelRefresh(ex); throw ex; }finally { //4.18 resetCommonCaches(); } }}4.1在开始之前先来看一下 BeanPostProcessor:public interface BeanPostProcessor { //此方法在 bean 初始化之前、bean 的构造方法调用之后执行 @Nullable default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } //此方法在 bean 初始化之后执行 @Nullable default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; }}BeanPostProcessor 在 bean 的注册阶段就已经大量接触到了,在下列 bean 的创建阶段会更多的遇到,是 Spring ioc 的重要组成部分。Spring 容器本身在初始化的时候就会注册很多个 BeanPostProcessor 接口的实现类到 BeanFactory 中。这些类会被 BeanFactory 实例成 bean,并特殊存放到一个列表中。在其它普通 bean 的初始化(即为 init 方法被调用时)之前,轮循 BeanPostProcessor 列表,执行 postProcessBeforeInitialization(…) 方法;在其它普通 bean 的初始化之后,再次轮训 BeanPostProcessor 列表,执行 postProcessAfterInitialization(…) 方法。从出入参可知,这两个方法用于在 bean 的初始化阶段对 bean 进行功能增强操作,包括但不限于代码织入(asm)、切面操作(aop)等。作为 Spring 的使用者,只要是自行编写实现了该接口的类,然后通过配置将该类作为 Bean 注册到 Spring 中,就会被 Spring 一视同仁的作为内部 BeanPostProcessor 对待。4.2再来看一下创建 bean 的核心方法,即 BeanUtil.instantiateClass(…)://BeanUtil.classpublic static <T> T instantiateClass(Constructor<T> ctor, Object… args) throws BeanInstantiationException { Assert.notNull(ctor, “Constructor must not be null”); try { //设置 accessible = true,即为去掉 privite 关键词对构造的影响 ReflectionUtils.makeAccessible(ctor); //这个返回语句主要是为了兼容 kotlin 语言,对于 java 来说主要是 ctor.newInstance(args) //本质是调用 bean 的构造器来实例化 bean return (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ? KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args)); }catch (InstantiationException ex) { throw new BeanInstantiationException(ctor, “Is it an abstract class?”, ex); }catch (IllegalAccessException ex) { throw new BeanInstantiationException(ctor, “Is the constructor accessible?”, ex); }catch (IllegalArgumentException ex) { throw new BeanInstantiationException(ctor, “Illegal arguments for constructor”, ex); }catch (InvocationTargetException ex) { throw new BeanInstantiationException(ctor, “Constructor threw exception”, ex.getTargetException()); }}BeanUtil 是 bean 的实例化的关键,Spring 的其它代码都是在做各种判断,但是真正实例化 bean 的就这一句。4.3看下方代码片段://AbstractApplicationContext.classprotected void prepareRefresh() { //记录下启动时间 this.startupDate = System.currentTimeMillis(); //这两个变量与优雅关闭有关,这里确定 Spring 未关闭 this.closed.set(false); this.active.set(true); //logger 日志相关代码均不作描述了 if (logger.isDebugEnabled()) { if (logger.isTraceEnabled()) { logger.trace(“Refreshing " + this); } else { logger.debug(“Refreshing " + getDisplayName()); } } //初始化 property //该方法是空的 initPropertySources(); //检查读取到的 properties 键值对 getEnvironment().validateRequiredProperties(); //新建一个集合,这个集合用于储存需要立即通知的事件 this.earlyApplicationEvents = new LinkedHashSet<>();}closed 和 active 都是定义在 AbstractApplicationContext 中的 AtomicBoolean,用以管理 Spring 容器的状态://容器是否处于活动中private final AtomicBoolean active = new AtomicBoolean();//容器是否已经关闭private final AtomicBoolean closed = new AtomicBoolean();initPropertySources() 方法其实是一个预留下的空方法://AbstractApplicationContext.classprotected void initPropertySources() { }getEnvironment() 在之前的代码里看到过,用来获取一个新创建出来的 StandardEnvironment 对象,而 validateRequiredProperties() 是定义在 AbstractEnvironment 中的方法://AbstractEnvironment.classpublic void validateRequiredProperties() throws MissingRequiredPropertiesException { this.propertyResolver.validateRequiredProperties();}propertyResolver 是一个定义在 AbstractEnvironment 中的 PropertySourcesPropertyResolver 对象,顾名思义,是用来管理 properties 配置文件中读取到的值的。来看一下 validateRequiredProperties()://AbstractPropertyResolver.classpublic void validateRequiredProperties() { //新建了一个 Exception,用于在出错情况下进行返回 MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException(); //这里的字面意思上可以看出,这个 for 循环语句用于从 requiredProperties 这个集合里获取每个 properties key for (String key : this.requiredProperties) { //getProperty(key) 方法会用 key 去获取 value 值,然后进行空值比对 if (this.getProperty(key) == null) { //如果存在 null,则加入到错误信息里 ex.addMissingRequiredProperty(key); } } //错误信息不为空,证明上述代码中存在值为 null 的,就直接抛出异常 if (!ex.getMissingRequiredProperties().isEmpty()) { throw ex; }}4.4看下方代码片段:ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();追踪 obtainFreshBeanFactory() 方法://AbstractApplicationContext.classprotected ConfigurableListableBeanFactory obtainFreshBeanFactory() { refreshBeanFactory(); return getBeanFactory();}追踪 refreshBeanFactory() 方法的内部实现://GenericApplicationContext.classprotected final void refreshBeanFactory() throws IllegalStateException { if (!this.refreshed.compareAndSet(false, true)) { //抛出错误,提示大意为:已经调用过一次 “refresh” 了 throw new IllegalStateException( “GenericApplicationContext does not support multiple refresh attempts: just call ‘refresh’ once”); } //存入一个用于序列化的 id this.beanFactory.setSerializationId(getId());}refreshed 是一个定义在 GenericApplicationContext 中的 AtomicBoolean 类型对象。AtomicBoolean 的 compareAndSet(…) 方法等同于进行如下操作://比较 AtomicBoolean 当前的值和第一个参数是否相等,如果一致,则将 AtomicBoolean 当前的值换成第二个值。//以下为模拟代码,AtomicBoolean 的真实实现大多数是使用虚拟机底层代码完成,是原子化的操作if(atomicBoolean == false){ atomicBoolean = true; return true;}else{ return false;}this.beanFactory.setSerializationId(getId()) 会将 AbstractApplicationContext 内的 id 存入一个定义在 DefaultListableBeanFactory 中的 map 对象里。4.5看下方代码片段:prepareBeanFactory(beanFactory);追踪代码实现://AbstractApplicationContext.classprotected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) { //存入 AnnotationConfigApplicationContext 中的 classLoader beanFactory.setBeanClassLoader(getClassLoader()); //StandardBeanExpressionResolver 用于解析 Spring EL 表达式的解析器 beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader())); //ResourceEditorRegistrar 用于各种 bean 与 String 之间进行转换的属性编辑器 beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment())); //ApplicationContextAwareProcessor 用于注入各类 aware bean beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this)); //在自动装配(autowire)阶段要忽略的接口类 beanFactory.ignoreDependencyInterface(EnvironmentAware.class); beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class); beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class); beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class); beanFactory.ignoreDependencyInterface(MessageSourceAware.class); beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); //自动装配的规则 beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); beanFactory.registerResolvableDependency(ResourceLoader.class, this); beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this); beanFactory.registerResolvableDependency(ApplicationContext.class, this); //ApplicationListenerDetector 用于类型是 ApplicationListener 的bean添加到事件广播器 beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this)); //代码动态织入 //LOAD_TIME_WEAVER_BEAN_NAME = “loadTimeWeaver” if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) { //LoadTimeWeaverAwareProcessor 用于在 bean 初始化之前检查 bean 是否实现了LoadTimeWeaverAware 接口 //与 Spring aop 代码织入相关的 BeanPostProcessor beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); //用于类型匹配的 classloader beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); } //注册 properties 和 environment 相关的 bean //这里不仅会注册,而且会直接将这些 bean 存放到 singleObject 中(即为直接实例化出来) if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) { beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment()); } if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) { beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties()); } if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) { beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment()); }}代码动态织入是 Spring 底层 cglib 的相关概念,暂时不展开。4.6看下方代码片段:postProcessBeanFactory(beanFactory);在 AbstractApplicationContext 中该方法是一个空方法://AbstractApplicationContext.classprotected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {}该方法预留给 AbstractApplicationContext 的子类去实现,目的是在 bean factory 装配完成之后做一些定制化处理 AbstractApplicationContext。在 AnnotationConfigApplicationContext 及其父类中没有重写该方法。4.7看下方代码片段:invokeBeanFactoryPostProcessors(beanFactory);追踪代码实现://AbstractApplicationContext.classprotected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) { beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); }}这段代码和上述 4.4 中的一段几乎是一摸一样的,Spring 在这里做了二次验证。4.8看下方代码片段:registerBeanPostProcessors(beanFactory);从字面意思可以看出就是在 beanFactory 中注册 BeanPostProcessor。注册的本质是将 BeanPostProcessor 保存到一个列表里。从源码里看,该列表是定义在 AbstractBeanFactory 中的 beanPostProcessors。追踪代码实现://AbstractApplicationContext.classprotected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) { PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);}继续追踪://PostProcessorRegistrationDelegate.classpublic static void registerBeanPostProcessors( ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) { //根据 class 获取到所有符合的 bean,即 BeanPostProcessor 的子类 String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false); //beanFactory.getBeanPostProcessorCount() 获取到的数字是在 4.4 中 set 的 BeanPostProcessor 的数量 int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length; //BeanPostProcessorChecker 是 PostProcessorRegistrationDelegate 的私有静态内部类 //如果一个 bean 没有被所有的 BeanPostProcessor 处理完毕,BeanPostProcessorChecker 就会打印日志 beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount)); //处理 Order 相关接口 //Order 是一个用于排序的接口 //用于存放实现了 PriorityOrdered 接口的 BeanPostProcessor List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<>(); //用于存放实现了 MergedBeanDefinitionPostProcessor 接口的 BeanPostProcessor List<BeanPostProcessor> internalPostProcessors = new ArrayList<>(); //用于存放实现了 Ordered 接口的 BeanPostProcessor List<String> orderedPostProcessorNames = new ArrayList<>(); //其它 BeanPostProcessor List<String> nonOrderedPostProcessorNames = new ArrayList<>(); for (String ppName : postProcessorNames) { //isTypeMatch(…) 方法会判断该名称的 bean 和 class 类型是否一致 //这里查看是否实现了 PriorityOrdered 接口 if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { //获取 bean 并添加到一个列表中 BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); priorityOrderedPostProcessors.add(pp); if (pp instanceof MergedBeanDefinitionPostProcessor) { //继续判断是否实现了 MergedBeanDefinitionPostProcessor 接口 internalPostProcessors.add(pp); } }else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { //如果并非实现了 PriorityOrdered 接口就判断是否实现了 Ordered 接口 orderedPostProcessorNames.add(ppName); }else { //均无,则存入 nonOrderedPostProcessorNames 列表中 nonOrderedPostProcessorNames.add(ppName); } } //对于实现了 PriorityOrdered 接口的 BeanPostProcessor 进行排序 sortPostProcessors(priorityOrderedPostProcessors, beanFactory); //依次注册这些 BeanPostProcessor registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors); //下方代码块和上方很雷同,注册实现了 Ordered 接口的 BeanPostProcessor List<BeanPostProcessor> orderedPostProcessors = new ArrayList<>(); for (String ppName : orderedPostProcessorNames) { BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); orderedPostProcessors.add(pp); if (pp instanceof MergedBeanDefinitionPostProcessor) { internalPostProcessors.add(pp); } } sortPostProcessors(orderedPostProcessors, beanFactory); registerBeanPostProcessors(beanFactory, orderedPostProcessors); //注册没有实现任何排序接口的 BeanPostProcessor List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<>(); for (String ppName : nonOrderedPostProcessorNames) { BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); nonOrderedPostProcessors.add(pp); if (pp instanceof MergedBeanDefinitionPostProcessor) { internalPostProcessors.add(pp); } } registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors); //对于实现了 MergedBeanDefinitionPostProcessor 接口的 BeanPostProcessor 会统一进行排序并注册 sortPostProcessors(internalPostProcessors, beanFactory); registerBeanPostProcessors(beanFactory, internalPostProcessors); //ApplicationListenerDetector 用于在 bean 初始化后检查是否实现了 ApplicationListener 接口 //从代码来看,实现了该接口的 bean 会被存入一个叫 applicationListeners 的列表中 beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));}综合来看这个方法的主体是对 BeanPostProcessor 进行依次的存储(会影响到 BeanPostProcessor 的执行顺序)。次序的依据主要是看这些 BeanPostProcessor 是否实现了 Order 及其相关的接口。4.9看下方代码片段:initMessageSource();追踪代码实现://AbstractApplicationContext.classprotected void initMessageSource() { //获取 beanFactory ConfigurableListableBeanFactory beanFactory = getBeanFactory(); //MESSAGE_SOURCE_BEAN_NAME = “messageSource” //先去查看 beanFactory 中是否有该名称的 bean if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { //获取到这个 bean this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class); //主体思路是将数据源存入这个 bean 中 if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) { HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource; if (hms.getParentMessageSource() == null) { //存入默认的数据源 //getInternalParentMessageSource() 方法会先 hms.setParentMessageSource(getInternalParentMessageSource()); } } if (logger.isTraceEnabled()) { logger.trace(“Using MessageSource [” + this.messageSource + “]”); } }else { //没有这个 bean 的情况下就自定义一个,并存入到 beanFactory 中 DelegatingMessageSource dms = new DelegatingMessageSource(); dms.setParentMessageSource(getInternalParentMessageSource()); this.messageSource = dms; beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource); if (logger.isTraceEnabled()) { logger.trace(“No ‘” + MESSAGE_SOURCE_BEAN_NAME + “’ bean, using [” + this.messageSource + “]”); } }}Spring 中可以配置多套配置文件,之间通过 messageSource 进行切换。4.10看下方代码片段:initApplicationEventMulticaster();追踪代码实现://AbstractApplicationContext.classprotected void initApplicationEventMulticaster() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); //APPLICATION_EVENT_MULTICASTER_BEAN_NAME = “applicationEventMulticaster” //检查是否存在名称为 applicationEventMulticaster 的 bean if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) { //如果存在则赋值给 applicationEventMulticaster this.applicationEventMulticaster = beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class); if (logger.isTraceEnabled()) { logger.trace(“Using ApplicationEventMulticaster [” + this.applicationEventMulticaster + “]”); } }else { //没有的话就创建一个,并注册与赋值 this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory); beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster); if (logger.isTraceEnabled()) { logger.trace(“No ‘” + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + “’ bean, using " + “[” + this.applicationEventMulticaster.getClass().getSimpleName() + “]”); } }}applicationEventMulticaster 是一个观察者模式的应用,可以理解为是用来分发数据给监听器(Listener)的中转站。4.11看下方代码片段:onRefresh();默认该方法为空://AbstractApplicationContext.classprotected void onRefresh() throws BeansException {}预留用于处理特殊的 bean。4.12registerListeners();追踪代码实现://AbstractApplicationContext.classprotected void registerListeners() { //遍历 applicationListeners 列表 //往 applicationEventMulticaster 内部的列表 applicationListeners 里添加 //applicationListener 用于在容器初始化时期监听事件 for (ApplicationListener<?> listener : getApplicationListeners()) { getApplicationEventMulticaster().addApplicationListener(listener); } //获取可能存在的使用者自行定义的监听器,同样添加到列表里 String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false); for (String listenerBeanName : listenerBeanNames) { getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName); } //获取所有的 event,放到一个列表里 Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents; this.earlyApplicationEvents = null; if (earlyEventsToProcess != null) { for (ApplicationEvent earlyEvent : earlyEventsToProcess) { getApplicationEventMulticaster().multicastEvent(earlyEvent); } }}特定的监听器用于监听某一类的 event,在容器启动时生效,对 event 做出处理。监听器被储存在 applicationEventMulticaster 中,发生事件的时候被告知。4.13finishBeanFactoryInitialization(beanFactory);追踪代码实现://AbstractApplicationContext.classprotected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { //CONVERSION_SERVICE_BEAN_NAME = “conversionService” //conversionService 这个 bean 用于映射数据类型,比如将前端的字符串类型数据转成日期等 //由使用者自主实现并配置,默认情况下没有这个 bean if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) && beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) { beanFactory.setConversionService( beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)); } //如果 beanFactory 里没有 embeddedValueResolver,就会从 environment 里获取并加载到里面 //embeddedValueResolver 是和配置文件读取相关的组件 if (!beanFactory.hasEmbeddedValueResolver()) { beanFactory.addEmbeddedValueResolver((strVal) -> { return this.getEnvironment().resolvePlaceholders(strVal); }); } //以下代码用于提前实例化代码织入相关的 bean String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false); String[] var3 = weaverAwareNames; int var4 = weaverAwareNames.length; for(int var5 = 0; var5 < var4; ++var5) { String weaverAwareName = var3[var5]; this.getBean(weaverAwareName); } //将 classLoader 置空,因为已经用不到了,所以清除掉,节省内存 beanFactory.setTempClassLoader((ClassLoader)null); //这个方法会将 beanName 列表转换成一个字符串数组,节省内存 beanFactory.freezeConfiguration(); //实例化 bean 的核心方法 beanFactory.preInstantiateSingletons();}来看一下 beanFactory.preInstantiateSingletons() 方法://DefaultListableBeanFactory.classpublic void preInstantiateSingletons() throws BeansException { if (logger.isTraceEnabled()) { logger.trace(“Pre-instantiating singletons in " + this); } //将 beanName 列表拷贝一份 List<String> beanNames = new ArrayList<>(this.beanDefinitionNames); //遍历 for (String beanName : beanNames) { //获取每个 bean RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); //需要最终确认 bean 不是抽象类,且为单例的,且不是惰性加载的 if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { //判断其是否实现了 FactoryBean 接口 //FactoryBean 是 Spring 留出的可供使用者选择的用于生产 bean 的工厂模式接口 if (isFactoryBean(beanName)) { Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); //二次判断 if (bean instanceof FactoryBean) { final FactoryBean<?> factory = (FactoryBean<?>) bean; //是否期望被 init boolean isEagerInit; //系统存在权限问题的时候才会进入到这个判断语句中 if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) { //AccessController.doPrivileged(…) 方法用于 java 的权限控制 //此处调用了此方法用于剔除权限对 Spring 的控制 //此处与下方代码都调用了 SmartFactoryBean 接口的 isEagerInit() 方法进行判断 isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>) ((SmartFactoryBean<?>) factory)::isEagerInit, getAccessControlContext()); }else { isEagerInit = (factory instanceof SmartFactoryBean && ((SmartFactoryBean<?>) factory).isEagerInit()); } //实例化 if (isEagerInit) { getBean(beanName); } } }else { //其实对于绝大多数普通的 bean,是直接调用这个语句进行实例化的 getBean(beanName); } } } //此处再次循环获取 bean ,用于对实现了 SmartInitializingSingleton 接口的 bean 进行特殊操作 //SmartInitializingSingleton 接口内有一个 afterSingletonsInstantiated() 方法,会在完成 bean 的实例化之后执行 for (String beanName : beanNames) { Object singletonInstance = getSingleton(beanName); if (singletonInstance instanceof SmartInitializingSingleton) { final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance; if (System.getSecurityManager() != null) { AccessController.doPrivileged((PrivilegedAction<Object>) () -> { smartSingleton.afterSingletonsInstantiated(); return null; }, getAccessControlContext()); }else { smartSingleton.afterSingletonsInstantiated(); } } }}4.14所以最终实例化 bean 的是 getBean(…) 方法,此方法不仅可以用于从容器中取出 bean,也是实例化 bean 的具体实现。追踪其具体代码实现://AbstractBeanFactory.classpublic Object getBean(String name) throws BeansException { return doGetBean(name, null, null, false);}继续来看 doGetBean(…) 方法://AbstractBeanFactory.classprotected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { final String beanName = transformedBeanName(name); Object bean; //对于之前已经实例过的 bean,会在这个方法里获取到 //如果没有实例化过,那么此处获取的是 null Object sharedInstance = getSingleton(beanName); //args 是用于实例化 bean 过程中传入构造器的参数 //此处传入的 args 为 null if (sharedInstance != null && args == null) { if (logger.isTraceEnabled()) { if (isSingletonCurrentlyInCreation(beanName)) { logger.trace(“Returning eagerly cached instance of singleton bean ‘” + beanName + “’ that is not fully initialized yet - a consequence of a circular reference”); }else { logger.trace(“Returning cached instance of singleton bean ‘” + beanName + “’”); } } //此方法用于处理 FactoryBean 的情况,如果没有的话是直接返回 sharedInstance 的 bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); }else { //如果 bean 正在创建,会抛出异常 if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } //先取出父类中存放的 parentBeanFactory,如果有实现的话就用该工厂来创建 bean //本例中没有使用 parentBeanFactory BeanFactory parentBeanFactory = getParentBeanFactory(); if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { String nameToLookup = originalBeanName(name); if (parentBeanFactory instanceof AbstractBeanFactory) { return ((AbstractBeanFactory) parentBeanFactory).doGetBean( nameToLookup, requiredType, args, typeCheckOnly); }else if (args != null) { return (T) parentBeanFactory.getBean(nameToLookup, args); }else if (requiredType != null) { return parentBeanFactory.getBean(nameToLookup, requiredType); }else { return (T) parentBeanFactory.getBean(nameToLookup); } } //这里会将 bean 标记为已经创建 if (!typeCheckOnly) { markBeanAsCreated(beanName); } try { //获取 bean 的包装类 final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); //bean 不能是一个 abstract 修饰的类,否则会报错 checkMergedBeanDefinition(mbd, beanName, args); //depends-on 用来处理要实例化某个 bean,必须先实例化另一个 bean 的情况 String[] dependsOn = mbd.getDependsOn(); if (dependsOn != null) { for (String dep : dependsOn) { //处理循环依赖的问题 if (isDependent(beanName, dep)) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, “Circular depends-on relationship between ‘” + beanName + “’ and ‘” + dep + “’”); } //注册要依赖的 bean registerDependentBean(dep, beanName); try { //实例化要依赖的 bean getBean(dep); }catch (NoSuchBeanDefinitionException ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, “’” + beanName + “’ depends on missing bean ‘” + dep + “’”, ex); } } } //是否是单例的 if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { try { //实例化 bean 的核心方法 return createBean(beanName, mbd, args); }catch (BeansException ex) { //如果报错的话会销毁掉这个 bean destroySingleton(beanName); throw ex; } }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); }else if (mbd.isPrototype()) { //如果 scope 注解为 prototype,则每次获取 bean 的时候都会创建新的 bean 实例 Object prototypeInstance = null; try { beforePrototypeCreation(beanName); prototypeInstance = createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); }else { //获取 scope 注解的值,如果是 null 的话会报错 String scopeName = mbd.getScope(); final Scope scope = this.scopes.get(scopeName); if (scope == null) { throw new IllegalStateException(“No Scope registered for scope name ‘” + scopeName + “’”); } try { //这一步的操作和上方代码块几乎是一样的 Object scopedInstance = scope.get(beanName, () -> { beforePrototypeCreation(beanName); try { return createBean(beanName, mbd, args); }finally { afterPrototypeCreation(beanName); } }); bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); }catch (IllegalStateException ex) { throw new BeanCreationException(beanName, “Scope ‘” + scopeName + “’ is not active for the current thread; consider " + “defining a scoped proxy for this bean if you intend to refer to it from a singleton”, ex); } } }catch (BeansException ex) { cleanupAfterBeanCreationFailure(beanName); throw ex; } } //requiredType 是使用者传入的 class 对象,使用者可以将 bean 转换成该类型并输出 if (requiredType != null && !requiredType.isInstance(bean)) { try { T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType); if (convertedBean == null) { throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } return convertedBean; }catch (TypeMismatchException ex) { if (logger.isTraceEnabled()) { logger.trace(“Failed to convert bean ‘” + name + “’ to required type ‘” + ClassUtils.getQualifiedName(requiredType) + “’”, ex); } throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } } //返回 bean return (T) bean;}继续追踪 createBean(…) 方法://AbstractAutowireCapableBeanFactory.classprotected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { if (logger.isTraceEnabled()) { logger.trace(“Creating instance of bean ‘” + beanName + “’”); } RootBeanDefinition mbdToUse = mbd; //获取 bean 的 class,然后在再次确定了 bean 的 class 无误之后,克隆一份 bean 并存入 bean class Class<?> resolvedClass = resolveBeanClass(mbd, beanName); if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { mbdToUse = new RootBeanDefinition(mbd); mbdToUse.setBeanClass(resolvedClass); } //重写的方法解析 try { mbdToUse.prepareMethodOverrides(); }catch (BeanDefinitionValidationException ex) { throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), beanName, “Validation of method overrides failed”, ex); } try { //根据注释来看,这个方法是使用 BeanPostProcessor 去进行动态代理 //如果存在对应的 BeanPostProcessor,返回的是动态代理的类,而不是 bean 本身 Object bean = resolveBeforeInstantiation(beanName, mbdToUse); if (bean != null) { return bean; } }catch (Throwable ex) { throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, “BeanPostProcessor before instantiation of bean failed”, ex); } try { //实例化 bean 的核心方法 Object beanInstance = doCreateBean(beanName, mbdToUse, args); if (logger.isTraceEnabled()) { logger.trace(“Finished creating instance of bean ‘” + beanName + “’”); } return beanInstance; }catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) { throw ex; }catch (Throwable ex) { throw new BeanCreationException( mbdToUse.getResourceDescription(), beanName, “Unexpected exception during bean creation”, ex); }}继续追踪 doCreateBean(…) 方法://AbstractAutowireCapableBeanFactory.classprotected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { //BeanWrapper 是 bean 的另一种包装 BeanWrapper instanceWrapper = null; if (mbd.isSingleton()) { //factoryBeanInstanceCache 是一个 map 对象,用于缓存 wrapper //此处从缓存中删除并返回此 wrapper instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); } //如果没有的话会创建一个 if (instanceWrapper == null) { //这一步会实例化 bean instanceWrapper = createBeanInstance(beanName, mbd, args); } //获取 bean 实例 final Object bean = instanceWrapper.getWrappedInstance(); //获取 class Class<?> beanType = instanceWrapper.getWrappedClass(); if (beanType != NullBean.class) { mbd.resolvedTargetType = beanType; } synchronized (mbd.postProcessingLock) { if (!mbd.postProcessed) { try { //该方法会轮询 BeanPostProcessor 列表进行 bean 的增强操作 applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); }catch (Throwable ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, “Post-processing of merged bean definition failed”, ex); } mbd.postProcessed = true; } } //是否是单例,是否允许循环引用,该单例 bean 是否在创建中 //isSingletonCurrentlyInCreation(beanName) 方法会去集合 singletonsCurrentlyInCreation 中搜寻 beanName //如果存在,证明该 bean 正在被初始化,初始化完成之后会从该集合中删除掉,功能相当于锁 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { if (logger.isTraceEnabled()) { logger.trace(“Eagerly caching bean ‘” + beanName + “’ to allow for resolving potential circular references”); } //将 beanName 注册到 singletonFactories 和 registeredSingletons 中 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } //添加一个 bean 的引用 exposedObject Object exposedObject = bean; try { //该方法会根据配置来填充 mbd 中 populateBean(beanName, mbd, instanceWrapper); //将信息填充到 exposedObject 中 exposedObject = initializeBean(beanName, exposedObject, mbd); }catch (Throwable ex) { if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) { throw (BeanCreationException) ex; }else { throw new BeanCreationException( mbd.getResourceDescription(), beanName, “Initialization of bean failed”, ex); } } if (earlySingletonExposure) { //getSingleton(…) 方法的第二个参数代表是否允许循环引用,这里输入的是 false(不允许) //对于一般的 bean,这里获取到的 earlySingletonReference = null Object earlySingletonReference = getSingleton(beanName, false); if (earlySingletonReference != null) { if (exposedObject == bean) { exposedObject = earlySingletonReference; }else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { String[] dependentBeans = getDependentBeans(beanName); Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); for (String dependentBean : dependentBeans) { if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); } } if (!actualDependentBeans.isEmpty()) { throw new BeanCurrentlyInCreationException(beanName, “Bean with name ‘” + beanName + “’ has been injected into other beans [” + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + “] in its raw version as part of a circular reference, but has eventually been " + “wrapped. This means that said other beans do not use the final version of the " + “bean. This is often the result of over-eager type matching - consider using " + “‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.”); } } } } //注册需要执行销毁方法的 bean try { registerDisposableBeanIfNecessary(beanName, bean, mbd); }catch (BeanDefinitionValidationException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, “Invalid destruction signature”, ex); } return exposedObject;}继续追踪 createBeanInstance(…) 方法://AbstractAutowireCapableBeanFactory.classprotected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) { //获取到 bean 的 class Class<?> beanClass = resolveBeanClass(mbd, beanName); //如果 class 不为空,且修饰语非 public,且没有设置 accessible = true if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, “Bean class isn’t public, and non-public access not allowed: " + beanClass.getName()); } //如果 BeanDefinition 中有保存 Supplier,则使用该方式去获取 bean //Supplier 是 jdk8 中配合函数式编程所添加的用于获取 bean 的接口 //Supplier 每次获取的 bean 都不是同一个 Supplier<?> instanceSupplier = mbd.getInstanceSupplier(); if (instanceSupplier != null) { return obtainFromSupplier(instanceSupplier, beanName); } //如果有工厂方法,就用工厂方法进行实例化 if (mbd.getFactoryMethodName() != null) { return instantiateUsingFactoryMethod(beanName, mbd, args); } boolean resolved = false; boolean autowireNecessary = false; if (args == null) { synchronized (mbd.constructorArgumentLock) { //查看是否缓存了构造器 if (mbd.resolvedConstructorOrFactoryMethod != null) { resolved = true; //这里要判断构造器的参数是否使用了 Autowire 注解 autowireNecessary = mbd.constructorArgumentsResolved; } } } if (resolved) { if (autowireNecessary) { //如果使用了 Autowire 注解就会进入这个方法进行实例化 return autowireConstructor(beanName, mbd, null, null); }else { //常规的实例化 bean return instantiateBean(beanName, mbd); } } //如果一个继承了 BeanPostProcessor 接口的 bean 的构造器参数使用了 Autowire 注解,会在此处单独处理 Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR || mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) { return autowireConstructor(beanName, mbd, ctors, args); } //获取首选的构造器 //在该版本的 RootBeanDefinition 里,该方法没有方法体,直接返回 null //也就是说,该代码是不启用的 ctors = mbd.getPreferredConstructors(); if (ctors != null) { return autowireConstructor(beanName, mbd, ctors, null); } //实例化 bean return instantiateBean(beanName, mbd);}继续追踪 instantiateBean(…) 方法://AbstractAutowireCapableBeanFactory.classprotected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) { try { Object beanInstance; final BeanFactory parent = this; //再次检测权限环境 //此例中没有涉及 if (System.getSecurityManager() != null) { beanInstance = AccessController.doPrivileged((PrivilegedAction<Object>) () -> getInstantiationStrategy().instantiate(mbd, beanName, parent), getAccessControlContext()); }else { //实例化 bean beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent); } //将 bean 包装成 BeanWrapper 并返回 BeanWrapper bw = new BeanWrapperImpl(beanInstance); initBeanWrapper(bw); return bw; }catch (Throwable ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, “Instantiation of bean failed”, ex); }}继续追踪 instantiate(…) 方法://SimpleInstantiationStrategy.classpublic Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { //不需要方法重载,就不需要 cglib 帮助了 if (!bd.hasMethodOverrides()) { Constructor<?> constructorToUse; synchronized (bd.constructorArgumentLock) { //先尝试查看 BeanDefinition 中是否保存了构造器,如果没有保存,就在下方从 class 中拿出来 constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod; if (constructorToUse == null) { final Class<?> clazz = bd.getBeanClass(); if (clazz.isInterface()) { //bean 不能是一个接口 throw new BeanInstantiationException(clazz, “Specified class is an interface”); } try { //这里验证权限设置 if (System.getSecurityManager() != null) { constructorToUse = AccessController.doPrivileged( (PrivilegedExceptionAction<Constructor<?>>) clazz::getDeclaredConstructor); }else { //获取构造器 constructorToUse = clazz.getDeclaredConstructor(); } bd.resolvedConstructorOrFactoryMethod = constructorToUse; }catch (Throwable ex) { throw new BeanInstantiationException(clazz, “No default constructor found”, ex); } } } //实例化 bean return BeanUtils.instantiateClass(constructorToUse); }else { //使用 cglib 进行 bean 的实例化 return instantiateWithMethodInjection(bd, beanName, owner); }}此小节是整个实例化 bean 过程中最冗长,也最核心的部分。这中间 Spring 做了大量的验证和业务判断,但是实际上最终 bean 的实例化并不复杂。4.15finishRefresh();追踪代码实现://AbstractApplicationContext.classprotected void finishRefresh() { //清空缓存,即将 resourceCaches 这个 map 对象置空 clearResourceCaches(); //实例化一个类型为 DefaultLifecycleProcessor 的 bean,用于控制 bean 的生命周期 initLifecycleProcessor(); getLifecycleProcessor().onRefresh(); //刷新上下文事件 //ContextRefreshedEvent 并没有业务逻辑 publishEvent(new ContextRefreshedEvent(this)); //此方法会将 applicationContext 保存到 LiveBeansView 中的一个集合内 LiveBeansView.registerApplicationContext(this);}到此为止,正常的 ApplicationContext 的初始化流程就完成了。4.16看下方代码片段:destroyBeans();这个方法的实现://AbstractApplicationContext.classprotected void destroyBeans() { getBeanFactory().destroySingletons();}destroySingletons() 是定义在 DefaultListableBeanFactory 中的方法://DefaultListableBeanFactory.classpublic void destroySingletons() { super.destroySingletons(); //manualSingletonNames 是一个用来存放已经被创建的单例 bean 的名字的 Set集合 this.manualSingletonNames.clear(); clearByTypeCache();}这里的 super.destroySingletons() 调用的是其父类 DefaultSingletonBeanRegistry 中的方法://DefaultSingletonBeanRegistry.classpublic void destroySingletons() { if (logger.isTraceEnabled()) { logger.trace(“Destroying singletons in " + this); } synchronized (this.singletonObjects) { this.singletonsCurrentlyInDestruction = true; } String[] disposableBeanNames; synchronized (this.disposableBeans) { disposableBeanNames = StringUtils.toStringArray(this.disposableBeans.keySet()); } for (int i = disposableBeanNames.length - 1; i >= 0; i–) { destroySingleton(disposableBeanNames[i]); } this.containedBeanMap.clear(); this.dependentBeanMap.clear(); this.dependenciesForBeanMap.clear(); //清空相关的集合内的引用对象,并且销毁已经创建的 bean clearSingletonCache();}再来看一下 clearByTypeCache() 方法://DefaultListableBeanFactory.classprivate void clearByTypeCache() { this.allBeanNamesByType.clear(); this.singletonBeanNamesByType.clear();}4.17看下方代码片段:cancelRefresh(ex);这个方法的实现://AbstractApplicationContext.classprotected void cancelRefresh(BeansException ex) { //传入的 exception 其实并没有用到 this.active.set(false);}active 是定义在 AbstractApplicationContext 中的一个 AtomicBoolean:private final AtomicBoolean active = new AtomicBoolean();这里将其的值设置为 false。4.18看下方代码片段:resetCommonCaches();具体实现//AbstractApplicationContext.classprotected void resetCommonCaches() { //主要是将 util 类里的一些 static 修饰的集合和 map 对象进行置空。 ReflectionUtils.clearCache(); AnnotationUtils.clearCache(); ResolvableType.clearCache(); CachedIntrospectionResults.clearClassLoader(getClassLoader());}从 4.16 到 4.18 的代码总体都比较简单,就是把各种的相关的集合、map、列表都清空掉,将 bean 的引用对象也都 remove 掉。实际上是优雅关闭的过程。二 一点唠叨· Spring 的代码实在是太庞大了,刚开始想要尽可能详细的去解释每个组件,但是后来觉得难度略大· Spring 的小部分组件和代码在笔者看来是有些莫名其妙的,且都没有查到详细而合理的解释· Spring 的代码很明显使用了防御性的编程原则,保证了每个方法都足够健壮,但是同时造成了重复验证和冗余· Spring 显然非常强调易用性和泛用性,提供了繁多的功能,甚至有部分是显得过于灵活的,如果笔者未读源码大概一辈子都不会知道· Spring 对组件状态的控制异常复杂且精确· Spring 对内存的控制很苛刻· 仅为个人的学习笔记,可能存在错误或者表述不清的地方,有缘补充 ...

January 1, 2019 · 15 min · jiezi

TOP100summit分享实录 | 如何构建一套全链路的故障追踪和故障自愈系统?

本文内容节选自由msup主办的第七届TOP100summit,三七互娱运维开发负责人童传江分享的《三七互娱故障追踪和故障自愈系统》实录。分享者童传江在维行业7年工作经验,做过网络管理,做过应用运维,目前专注于运维开发,对于行业所要解决的质量、成本、效率、安全,有完整的交付和实践经验,爱好广泛,热衷于解决疑难问题和分享。编者按:2018年11月30日-12月3日,第七届全球软件案例研究峰会在北京国家会议中心盛大开幕,现场解读「壹佰案例榜单」。本文为三七互娱运维开发负责人童传江老师分享的《三七互娱故障追踪和故障自愈系统》案例实录。在实际运维过程中,因为业务系统越来越复杂,变更越来越频繁,总是存在各种各样监控未覆盖或者以前未知的故障发生。如何构建一套全链路的故障追踪和故障自愈系统,成了质量保证部门的刚需,通过行业标准化的PaaS平台模式和Trace追踪技术,从而实现整个架构的质量可控。今天,我将从两个方面分享故障追踪的实例,第一方面,关于运维平台的整体架构,分别从成本、效率和质量三个维度解决的大致方向;第二方面,关于在链路追踪具体的案例实践。运维平台的整体架构现阶段,关于运维平台有三个大方向的问题需要解决:第一个是成本,有些公司认为这个问题不是非常严重,但公司发展到一定规模,运维成本对于运维部门来说是一个非常大的挑战。像服务器、CDN、宽带的成本可以占到营收的百分之几以上,是一个很大的支出。运维成本核心要解决的是搞清楚具体钱花在哪个方向,并对这些成本问题进行优化。第二个是效率,这是运维面临的主要问题。对于基础设施的交付、中间件的交付,或是代码发布,分解业务需求交付从上到下每一个步骤,并让每个一个步骤变得更快,这是我们要解决的第二个问题。第三个是质量,这个问题很简单,业务是否正常、用户体验是否良好等,如果说有问题,到底哪里出现问题,这是运维部门要保障的。那么,如何解决以上三个问题呢?从技术方面来看,主要划分三个模块,第一个模块,对基础设施的交付;第二个模块,运维开发能力层面;第三个模块,前端接入层面。从基础设施能力方面来看,因为运维的场景不同,现有情况在基础设施层面交付非常繁杂,有些应用需要裸机,我们就要直接进行裸机自动化系统完整交付;有些业务需要自动扩容,我们就要提供IAAS的平台;在基础设施方面,操作系统交付能力上有一个“封装”。将裸机,虚拟化IAAS,容器化IAAS,公有云统一向上封装。核心为提供操作系统能力。从运维层面来看,第一块是CMDB,它包括了资产到应用,以及整个关联关系,所有需要关联到成本或质量的相关数据都存在CDMB中。第二块是任务通道,对所有下层交付的系统实现各种各样的自动化,通过任务通道来执行的,分化细节来看,提供了脚本执行、文件传输、配置分发、任务编排、定时调度、以及一套API。第三块是数据通道,以前的监控数据,日志数据、APM数据或者像交换机Netflow等各种各样的数据都在数据通道中。 数据通道的核心是做收集数据、传输数据、计算数据、存储收集、展示数据,数据通道核心在用一套逻辑,提供同一个数据处理能力。第四块是第三方API,如上图所示,涉及到各种公有云,微信,DNS等平台。接入能力层面,主要是提供前端的WEB端,包括移动端app上的封装,关于API Gateway,我们现有的实现API网关主要做Web防火墙这样的应用规则。现在,我们来详细看一下CMDB数据的具体模型。如下图所示:CDMB主要分为两大块,第一块是资源,第二块是应用。资源是通用的,而应用这一块是应用分类、应用单体,这里要注意的是,所有的应用CMDB层面,我们是将它拆分到具体的PaaS平台,所有的应用和单体应用跑到中间件上面,而数据是记录在CDMB中的。如果说一个应用如果需要依赖集群,则需要Master、VIP各种各样的属性。实际上关联到基础设施的操作系统的进程上去了,所有运维的数据都是放入CMDB这张表里面,我如果需要解决成本问题,是可以依据一条链路的,先从业务出发,找到我的资产花费了多少,因为核心的成本在资产上,关于成本问题,我们可以通过CMDB将这个数据找出来。关于效率问题,其实核心是在交付基础设施、交付中间件、交付代码做持续集成。通过基础设施的交付效率提升,中间件交付效率的提升,代码交付效率的提升,来实现到整体效率提升,所有的数据依旧是放在CMDB这张表里面。关于质量问题,我们的业务依然是运行在中间件上,中间件运行在真实的资源上,在运维层面的上一层或中间一层保证业务的质量。综上所述,我们大致介绍了一个平台最开始的整体架构,包括CMDB的详细解释。接下来,再介绍一下,在这个模型上如何对成本、效率、质量问题如何具体解决?关于运维成本,CMDB简化了逻辑关系,固定资产存在账单,我们的业务存在收入,并不是说一个业务它的运维成本高,就一定有问题。实际上,我们是按一个收入占比的,运维成本高,收入也随着增加。基于以上,我们可以非常清晰的得到一个财务数据,这个财务数据的核心是某一个应用收入了多少钱,它的收入占比是多少。当然,因为有详细的关联数据,依然可以知道各种各样的钱花在了什么地方。关于提升运维效率的大体方向如何解决?我们对整个业务做了抽象,第一块作为基础设施,第二块是中间件这一层,第三块是持续交付,真实的业务在最上面。IAAS如何提升需求?我们在对于IAAS实现交互就像实现云平台一样,操作系统安装不管做自动系统安装还是云API,其他的IAAS提供一套统一的API/用户界面自己申请/自己获取基础设施的资源。关于效率PaaS平台,在中间件这层,我们会使用MySQL。在产品集群把MySQL做成云化模型的管理模式,主要是在改变思路,以前的业务需要一个MySQL,DBA会给它一个MySQL。但实际在进行云化模式管理的时候,我们会将普通的MySQL实现成一个云化的产品,在业务需要时,它将自己进行申请。所有的自动化能够将一个MySQL集群实现成云化模式的话,核心需要的技术能力主要还是运维能力、开发能力、交付能力、平台能力。CMDB、数据通道和第三方API,我们只能使用基础的运维能力层,能够实现到类似于把普通的MySQL实现云化,比如,腾讯云、阿里云一样的MySQL集群,看一下详细界面,云化监控或是需要人工接入的都把它云化成云平台模式,提供给业务方块,自助操作。在运维效率方面的提升,从IAAS、PaaS层面我们核心要做的是,虽然现在没有能力从技术能力、各个组件开发成一个云化产品,但实际上,我们在运维能力上能做到在产品层面类似于云的产品,提升运维的效率。现在,我们详细介绍一下关于运维质量的方面。关注质量解决的方式主要分为四大方面,如上图所示,我们对于应用依赖的模块做了抽象化,因此,下面两块是通用的;第一块,保证的是我们在IAAS层面或是操作系统层面的质量;第二块,我们所有业务都是跑在中间件上,虽然中间件是用了通用的开源软件,抽象化后依然是云平台;第三块,是关于链路追踪,这在下面会详细解释;第四块,是常规的业务本身的监控,比如,游戏行业有可能会游戏登录、注册、充值。对于IAAS、PaaS层面,主要核心是接入CMDB数据,让Open-Falcon数据和业务关联,实现最开始的监控。链路跟踪技术的实践链路跟踪是什么样的技术?我们在质量保证层面分为四块,IAAS、PaaS到业务本身再到链路跟踪,这项技术是基于中间层面的。在微服务时代,任何一个应用完成时都要通过其他接口共同实现。如上图所示,如果这里是一个登录业务A,用户来登录这个业务时,先调用第三方API、D和E、存储、各自的中间件。用户A在登录的流程中,每一个细节都可以进行链路回放,能够清楚的知道,一个用户A完整的登录过程,以及具体细节。因为链路跟踪技术完成的整个细节,业界关于链路跟踪技术有非常多的实践,但我们在自己实现时,还有很多的困难。想要完整的实现一个链路跟踪的基础,有哪些问题需要解决呢?第一个,性能。我们现在是一个用于解决用户质量的辅助功能,在引入这个功能时,最好不要影响到业务,特别是有些业务对消耗性能非常敏感,引入链路跟踪后,为了保证用户质量而影响业务的性能,会不能被接受。第二个,应用无感知。我们现在引入一个新技术,对于质量保证,需要开发来配合。应用不需要知道跟踪系统的存在,也不需要开发二次配合接入。第三个,扩展性。业务随着规模增长,整套链路跟踪系统依然要能够满足对于系统需求的完成。现在,我们来详细分析基于PHP链路跟踪自实现原地。在Java或是其他语言中,语言本身/社区本身对于链路跟踪技术是比较成熟的,我们可以直接拿来用,但是对于PHP技术,已实现的大部分都是侵入式的,侵入式需要开发配合做代码修改。在只有维护没有开发的情况下,会出现一些老旧的业务,这会导致无法接入,特别是侵入式,开发团队比较忙或是不愿意接入你的链路跟踪,那么,整个跟踪的实际效果会大打折扣。上图左边是一个常规的PHP代码,这是Curl函数的实现,右边是真实的Curl扩展的源代码,这个扩展最终是一个C函数,我们想要实现的链路跟踪是访问函数的时候,能够知道代码的顺序、函数的输入/输出以及像调用时间。基于以上PHP函数的实现以及扩展性基于C语言的实现,我们可以实现到什么?我们依然调用了PHP的Curl的函数,这个函数本身对它进行替换,依然访问C函数,但函数本身对它做了一个替换,请求该函数的时候,是指请求我自己的函数,自己的函数本身会调用原始函数,对原始函数进行返回,经过简单的封装后,我们能拿到Curl任何一个函数的参数;关于函数的返回值,我们要知道,访问一串代码函数,在一串链路中,可以获取到参数和返回值,在中间插入最终ID。因为参数有请求时间、返回时间,所以基于这样一个更改实现,就可以知道扩展函数调用参数、调用返回值、调动时间,我们可以对这个参数返回时间进行更改,并植入另一个ID。对于这些函数做的更改,在PHP代码中再调用函数时,就可以像一条链路一样从上到下,把整个链路接入。关于单台Server,如果跨机器识别,传输链路ID如何传输?如上图所示,基于PHP实现大部分都是Web开发的,在HTTP中追踪一个ID,可以实现多台机器之间。假设同样是一个Login请求,一个请求的传递,基于以上的实现,我们就可以实现到类似于第一块,最开始Curl是一个扩展,我们开启了新扩展,任何一个PHP语言只需要打开一个新的扩展包,并植入到PHP中就可以实现链路跟踪了。实现本身是接入到一个开源软件,我们实现的过程中做了一些更改,更多的是函数支持,那这是什么意思?最开始讲到实现原理的核心是做一个函数替换。我们会在测试过程中发现,如果没有函数,我们可以增加更多自己的函数,类似于Curl Post参数抓取,增加了这个功能。我们基于链路跟踪获取到数据码,再进行V2格式进行输出,最后修复bug。接下来我们看一下真实的基于链路跟踪输出的详细数据。上图所示,左边是一个常规的PHP代码,右边是链路输出的数据,我们详细分析一下PHP代码,这一串链路追踪最终核心截取了一个函数细节,函数的耗时时间、函数的参数,我们基于最开始的函数替换原理实现到的是,任何一个PHP函数,它在运行时可以将输入参数、返回值和调用时间,获取到这一串代码整个执行的链路。当然,因为做了数据替换,所以会出现性能损耗,我们需要通过采样率规避这个问题,根据实际业务做到一个测试数据。关于以上内容都是作为单机处理的,那么,如何实现扩展到几百台或几千台规模机器?首先,对PHP做一个扩展,再做函数替换,实现基于Zipkin-V2数据写入到文件,数据平台有一个数据日志,将日志写到平台中。用到Vipkin-V2协议将隔离转化格式后,再做数据转换,如果只是用来查询,它是非常简单的Web页面,HTML的页面进行重写插入到平台里面。在实现过程中,有几个细节需要注意,第一块Logstash配置,需要做一个重命名操作,第二块ElasticSearch搜索,一般不进行索引,我们可以进行字符串查询,查询到指定的API或指定的数据类型链路跟踪。当加了链路跟踪后,最终解决什么业务问题?第一块是性能统计,我们通过函数替换可以知道输入、输出的时间,和MySQL、API接口访问时间。虽然数据可以在第三方系统查到,但也可以在MySQL集群或是自己的系统、API性能分析中查到,如果能查到链路跟踪,这是非常贴合业务的。第二块是应用拓扑,如上图所示,我们可以非常清晰的看到,在通过链路跟踪的时候,我们可以知道语言目的地,并把拓扑画出来,如果仅仅只是基于链路跟踪画拓扑图,是不适合现实情况的。一个8001Web端口,MySQL有50提机器,仅仅基于链路跟踪画出来的数据图,50个Web介点连接MySQL,在运维层面属于集群的概念,但只是链路跟踪的话,画出来的拓扑图可能不适合运维层面。MySQL比较简单,基于链路跟踪的数据看到的ID只是VIP,架构图对于运维制造,是想了解真实访问到哪个集群了,基于这样的实现,我们之前详细介绍到CMDB,VIP数据和MySQL是有关联的。关于这两个数据的结合,可以非常轻松的知道运维的构架图。上图是一个简化过的两地三中心的架构,这不是单机Web,只是一个Web集群。MySQL依然是最开始业务所支持的中间件MySQL集群,基于链路架构图画出来,包括应用跟踪可以实现到的错误故障或耗时,可以实际的展示到图表上,这种应用拓扑其实是真实的反映了现有业务运维架构。第三块是链路回放,上图是Login页面,一个用户登录需要完成什么?所有的程序调用会像链路一样展现出来,例如,调用第三方API、传入、传出、消耗时间、连接中间件、插入的数据、反馈的数据等。主要核心是做故障定位,例如,基于应用层面,某一个业务登录发生非常严重的下降,我们在链路跟踪中打开登录页面,就能详细的知道,API无法提供服务,导致业务故障。整体上关于链路,整个质量保证处于中间那一块,核心是从上到下访问链路,来解决质量的相关问题。以上内容来自童传江老师的分享。声明:本文是由壹佰案例原创,转载请联系 meixu.feng@msup.com.cn

December 29, 2018 · 1 min · jiezi

springboot:shiro注入dubbo服务空指针问题

问题描述最近搭建springboot+dubbo+shiro微服务时,自定义的shiro的realm组件中:@Componentpublic class AuthRealm extends AuthorizingRealm { @Reference private AccountService accountService;调用dubbo服务accountService时,出现了空指针的异常。问题原因dubbo的@Reference机制,是在spring bean全部注册完成后,再注入的spring bean中。而shiro的authRealm调用代码: @Autowired private AuthRealm authRealm; @Bean public SessionsSecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(authRealm); return securityManager; }在代码中我们可以看到,注册securityManager这个bean的时候,会将authRealm赋值给securityManager,但是此时accountService还未注入到authRealm中去,所以securityManager的authRealm属性中accountService字段为null。解决方案在将authRealm赋值给securityManager时,手动将accountService注入到authRealm中。@Componentpublic class AuthRealm extends AuthorizingRealm { private AccountService accountService; @Autowired private DubboProperties properties; … /** * 手动注入dubbo服务 */ public void setAccountService() { ReferenceConfig<AccountService> referenceConfig = new ReferenceConfig<>(); referenceConfig.setApplication(properties.getApplication()); referenceConfig.setRegistry(properties.getRegistry()); referenceConfig.setInterface(AccountService.class); this.accountService = referenceConfig.get(); }}@Configurationpublic class ShiroConfig { @Autowired private AuthRealm authRealm; @Bean public SessionsSecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); authRealm.setAccountService(); securityManager.setRealm(authRealm); return securityManager; } …}项目源码:https://github.com/ksyzz/spri… ...

December 29, 2018 · 1 min · jiezi

百度云曲显平:AIOps时代下如何用运维数据系统性地解决运维问题?

本文是根据百度云智能运维负责人曲显平10月20日在msup携手魅族、Flyme、百度云主办的第十三期魅族技术开放日《百度云智能运维实践》演讲中的分享内容整理而成。内容简介:本文主要从百度运维技术的发展历程、如何做智能运维、故障管理场景、服务咨询场景和面对的挑战等几个方面介绍了百度云智能运维实践。百度运维技术的三个阶段第一阶段:基础运维平台 2008年2012年2008年,在百度运维部建立之前,还没有一个标准而统一的运维平台。例如,搜索、广告、贴吧都有各自的运维平台。存在的问题:技术和平台能力无法复用,业务之间需要交互时比较复杂。解决方法:①为帮助业务解决问题,我们把各个分散在不同业务的运维平台整合起来做成一套标准化运维平台;②有了统一运维平台后,运维部门内的角色就分为了两个,即标准的运维工程师和运维平台研发工程师。第二阶段:开放的运维平台 2012年2014年第一阶段仍然存在的问题: ①个性化需求很多,统一平台很难全部解决②PaaS出现之后,运维平台和PaaS的关系解决方法:①开放运维平台,即全部API化。②通过提供标准化的监控数据的采集、计算、报警能力,最基础的程序分发、数据分发、任务调度能力,解决自身平台的需求。③利用PaaS方法,把一些研发的技术平台和运维技术平台整合在一起,解决重复造轮子的问题。第三阶段:AIOps阶段 2014年开始百度从2014年就开始了智能运维的实践。最早的时候,我们更多是通过完善底层的大数据平台能力,提供一些数据分析和挖掘的算法和工具,解决运维数据没有得到合理运用,运维人工效率低等问题,这是偏大数据的方法。百度对于AIOps的理解在2015年,AI变得异常火热,百度也是想将自身先进的机器学习算法应用到运维领域之中,于是我们和百度的大数据实验室、深度学习实验室进行了合作。运维研究人员把需求和归整好的数据提交给实验室的人员,然后他们会根据数据训练模型,最终提供一些库和方法供业务使用。2016年,Gartner提出了AIOps这个词,也就是我们说的智能运维,这和百度的实践是不谋而合的。三个核心内容随着智能运维的发展,百度也是把数据、工程和策略三个,作为最核心内容来系统地解决运维行业的应用。从数据角度来讲,首先要构建一个完整的数据仓库,接着要建设运维知识库。知识库是在数据仓库上抽象进行的。从工程角度,一方面,分析数据和训练算法模型需要大数据平台和框架,另一方面,运维业务研发人员还做了一套运维工程研发框架,用以解决标准化、可扩展和复用的问题。这个框架十月份刚刚开源,感兴趣的朋友可以看下。在百度内部,一致的运维“语言”非常关键。我们要统一不同的工具和平台,形成一致的运维模式。所以不管是故障感知、故障诊断决策、弹性伸缩决策还是运维操作和执行,只有统一起来才能解决这个问题。一致不仅是数据一致、工程一致,还需要策略本身的一致性。自动驾驶分级在构建整个百度智能运维体系的过程中,我们重点参考了自动驾驶里的分级理论。百度是有这样两个部门的,一个叫L3,一个叫L4。L3部门重点在做类似于辅助驾驶或者高度辅助驾驶;L4部门做的是高度完全自动驾驶。下图是关于自动驾驶的分级。运维能力分级自动化运维能力分级当时我们团队参照这个自动驾驶分级,构建出了一个自动化运维能力的分级标准,用以评估我们各个方向的自动化水平,一共分为六个能力等级,即人工、工具辅助、部分自动化、有条件的自动化、高速自动化和完全自动化。关键点:决策规划由运维系统做出,而不是人人负责:制定优化目标(比如,可用性、效率、成本等)运维系统负责:根据其对待处理的需求、待解决的问题的理解,以及对运维对象的认知(经验),自主做出解决方案(规划)并在控制执行过程中根据目标和运维对象的状态反馈来适时调整执行规划。智能化运维能力分级在自动化能力分级之中,我们还细化出了一个智能化运维能力分级(我们始终认为智能运维是实现完全自动化运维的一种手段)。实现智能化能力,重点解决的是在运维感知和决策过程中,人工效率低和准确率不足的问题。关键点:决策规划由运维系统做出,而不是人人负责:制定优化目标(比如,可用性、效率、成本等)运维系统负责:根据其对待处理的需求、待解决的问题的理解,以及对运维对象的认知(经验),自主做出解决方案(规划)并在控制执行过程中根据目标和运维对象的状态反馈来适时调整执行规划。如何做运维我们希望每一个运维工具都像一个小型的运维机器人一样,解决运维的问题。运维工程师需要把每一个运维工具抽象化,同时也要像一个标准框架一样,可以在代码库里克隆,把框架代码复制下来。通过三个基本核心,感知、决策和执行来进行编写执行器,接着可以通过配置实现一些具体任务调度的配置或者并发执行的配置;每一个运维工程师要实现感知逻辑、决策逻辑、执行逻辑,利用运维核心解决可靠性的问题。在测试方面,要在线下建立看代码的逻辑去验证。结合这个看代码,把比较核心的运维故障抽象出来,再把一些常见的故障模拟出来,具体的情况可以在这里面运行;写完一个运维工具或者算法,需要直接在上面运行,从而检测出是否有效。故障处理场景百度内部如何解决故障处理场景故障处理场景一般分四个主要阶段:故障发现、服务止损、服务恢复、故障总结。在服务止损方面,核心是如何让用户感知不到这个故障,对于运维来讲,更多用的方法是隔离、降级,而非从代码BUG入手解决的问题。在服务恢复方面,这个一般是在服务止损或者说故障被隔离之后,很大程度上需要运维和研发共同合作,比如定位代码的BUG,最终要决定如何把线上的问题真正解决掉。恢复,更多用的是修复来解决。在百度,大多数的故障都是可以用隔离和降级解决的,只有那些极特殊的case,才会通过程序回滚来恢复。回滚风险很大,而且效率很低。在整个解决故障处理场景的阶段,每一个阶段都可以结合智能运维的方法。从开始服务部署、监控添加、故障发现、止损决策、止损操作、根因诊断、恢复操作,最后报告自动生成。把AIOps应用到故障处理最核心的基础是,全面覆盖监控。在百度,做的最全面的是云上的监控,所以包含这四个维度的监控:系统监控、业务监控、内网监控和外网监控。系统监控主要的监控对象是机器/容器和服务的动态内容;业务监控针对业务和用户的访问日志等;内网监控则针对IDC内网设备和内网链路;外网监控为了保障用户、运营商链路到百度IDC中间的状态。有了全面的监控之后,才能开始现在业界常提到的一个智能运维技术,自动异常检测。典型的异常检测场景有关异常检测场景,我为大家举三个典型的例子,第一个,周期波动的数据。上图中的蓝、绿、黄三条线分别代表着今天、昨天、上周的时间线,蓝线比较明显,后面还有绿线和黄线。它们相对来说周期性体现得特别强。这种数据很难用传统的计算方法设置阈值。针对这种场景,我们会使用不同类的算法,专门解决这种问题。第二个,关心突变的数据。突变的数据也是一个比较典型的场景,周期性数据更多参考的是天级和周级的数据,而这个场景更多说的是某一个细节层面,可以理解为它是对一小块数据的放大。第三个,关心是否超出了一定波动范围的数据。这种场景是我们用普通的监控方法很难覆盖的,很多情况下,其均值或基线不会有特别明显的变化,但系统现在确实出现了很大的不同状态,可能仅仅是波动更剧烈了,对于这类场景,我们更多的是去看波动的情况,就是除基线以外的一些特征。今年八月份,百度云开源了一个数据标注的工具-Curve 。我们始终觉得算法虽然很重要,但远没有数据本身重要。做机器学习时,数据的建设才是最需要花时间解决的问题,百度的运维工程师也是重点在解决数据标准和数据获取的问题。如何应对报警风暴当出现大规模报警时,手机可能会直接被打爆。异常检测重点解决的是故障感知的问题。当故障被感知后,需要通知给运维工程师。首先,做逐级通告,对报警进行分级。接着做数据的整理,整理出每一个数据,最后抽象化数据的特征,按照每个维度或特征进行报警的归并。完成前两步之后,报警会有一定改善。最后要用数据分析方法或者机器学习的方法处理。数据的特征已经被抽象化,所以有很多方法可以解决,第一种方法是传统数据挖掘,比如关联分析,频繁项集挖掘是最被广泛使用到的方法,它可以有效将同类报警进行合并。第二种方法是机器学习,因为前面抽象出了特征,那做分类聚类都是比较直接的事情。从我们的实践情况看,最后的效果两者相差不大,大家都可以尝试。报警产生后,就相当于感知阶段结束,之后就到达故障处理阶段。接下来,我分享几个百度内部觉得效果最好的处理方法。第一个方法,多维度定位。这个更多偏业务问题的定位。业务都有访问日志,日志由各个不同维度的数据组成。一个故障的出现可能有不同维度,运维工程师需要通过访问日志的数据进行计算分析,分析出真正影响故障的维度。在这个基础上,可以做可视化。这是一类结合业务特征的可视化方法,如上图,这是一个模块拓扑图,很多圈圈,很多研发,这里有健康度、响应时间等等各种维度的展示。像模块响应时间,又可能会分很多类、很多维度或者很多模块,底下是每一个不同的模块,都可能产生对应的一些情况。接下来,百度现在大部分在用的是基于信息熵的维度特征推荐。例如,一个出现故障问题的指标,大的流量下降,可能有不同的维度。运维工程师会对每一个维度里的子维度数据进行分析,分析下降的程度,以及对于现在整个流量总体的下降程度的不同占比,然后做一个排序,就可以得到故障影响较高的某几个维度,从而帮助工程师尽快定位到这个问题或者缩小问题的范围。第二个方法,基于服务拓扑或者服务关联做定位。这是内部比较重要的故障判断基础和指导意见。百度运维倾向于把一个问题的分析分成六个维度:①时间维度,缩小时间范围;②网络拓扑模型,缩小空间范围,区分整体和局部故障;③服务管理模型,推导异常集群、实例或者机器;④变更关联模型,定位程序、配置、数据、运营活动上线;⑤模块关联模型,上下游关联服务的异常传播链;⑥多维度模型,维度关联层级分析,缩小业务范围。上图是一类典型的故障诊断框架。我们可能有很多故障的分类,比如有网络故障,细分一点是有交换机故障、链路故障,可能有系统故障,业务问题、操作问题等各种各样的,都是属于假说生成,可能都是备选故障问题。中间有一个证据评分,相当于基于前面的模型拓扑关系,对不同的故障做评分,把拓扑关系的线做权重,然后做置信计算和排序,最后给出最优决策判断。有关自愈的问题· 故障自愈 通过自动化、智能化处理故障节省人力投入,通过预设定的处理流程和只能化判断策略,提高故障处理可靠性,同时降低故障时间,为业务可用性保驾护航。· 智能自愈①感知:通过监控系统获取业务运行指标、智能异常检测、网络异常事件多种触发方式②决策:根据不同感知方式可以配置不同决策模型③执行:在单机执行基础上,提供集群级别、分布式的处理方式在执行故障自愈过程中,并不止是一个工具的执行,而是包括了调度、伸缩、隔离预案处理甚至多个不同业务的联动。自愈本身的核心并非自动化过程,更多是决策的过程。举一个典型案例叫单机房故障自愈。单机房,不仅仅指机房网络故障,更多指的是故障范围只要限定在一个IDC内部,不管这个故障是代码BUG,还是外面流量接入出了问题,还是机房整个掉电,只要故障范围是在一个IDC内都可以解决。基础能力达标后,我们要设计一个故障自愈系统,核心部分是外网流量调度止损决策器和内网流量调度止损决策器。外网比较简单,而内网则涉及到一些负载均衡策略、弹性伸缩策略、主备切换策略等。盲测验收最后讲一下盲测验收。有了故障自愈的系统后,怎么证明你的方案好用呢?在不通知业务的情况下,我们会和IDC同事进行配合,拔网线或是制造网络拥塞,这时候才能进行完整的切换,从而可以证明基础能力是否达标。百度现在单机房故障自愈已经覆盖了所有核心业务线,自愈时效控制在5分钟内,并且对于非数据库依赖的业务,可以做到1-2分钟完成机房级自愈。咨询服务场景服务咨询的场景可分为以下三种:①通过聊天窗口(IM软件、浏览器等)实时查询业务状态,用户可视化、可追查各种问题;②通过聊天窗口(IM软件、浏览器等)实时触发运维操作,运维工程师可远程上线、启停任务等;③当运维操作完成,出现状态变化或异常等情况时,运维工程师可主动发送相关通知,或按照策略自动进行后续操作。在百度内部,我们将这种场景称为ChatOps:•“放心”:分级发布和可用性干预、保障•“贴心”:监控、部署一站式集成,信息主动推送和确认•“省心”:高度自动化,减少人工介入和等待•“开心”:助力业务发展,如迭代效率提升•将运维人员从日渐琐碎、枯燥、疲惫、低价值、高事故率的工作中解放出来•实现运维人员的转型和增值AIOps的挑战最后说一下AIOps的挑战。现有的AIOps技术,比如指标异常检测、故障自愈等,更多解决的是数据本身的特征和问题,还没抽象到服务、程序本身的特征这个层次上,也就是说,我们并没有真正地了解和解决问题本身。比如,不同类的服务所产生的故障和表征是不一样的,我们希望让数据更多、业务场景可扩展,而非针对几个横向的场景;在业务运营方面,我们不仅仅局限在IDC、操作系统、机器,而是注重资源和性能优化,运维还可以继续拓展。对内,可以做系统优化、成本优化;对外,帮助所有用户做云服务资源池优化,让大家更好的节约成本,提升服务能力。以上内容来自曲显平老师的分享。声明:本文是由msup原创,转载请联系 meixu.feng@msup.com.cn

December 29, 2018 · 1 min · jiezi