首页 文章 精选 留言 我的

精选列表

搜索[面试],共5092篇文章
优秀的个人博客,低调大师

金三银四,那些烧脑的JS面试题及原理

Q: JS代码是按顺序执行的吗? A: JS代码执行过程中,需要先做变量提升,而之所以需要实现变量提升是因为JS代码在执行之前需要先编译 1、变量提升 变量和函数声明会被存放到变量环境中,变量的默认值会被设置为undefined var scope = 'global scope' function a(){ // 3、顶层变量环境声明了scope初始化为undefined function b(){ // 2、b函数的上层作用域是a,向上找scope console.log(scope) } return b; // 1、虽然声明在return语句后面,依然会提升到a函数作用域的顶层 var scope = 'local scope' } a()() // undefined 1.1、同名处理 同名函数,选择最后声明的 变量和函数同名,选择函数 var a = 1 var getNum = function() { a = 2 } function getNum() { a = 3 } getNum() console.log(a) // 2 // 变量和函数同名选择提升函数,函数提升包含初始化和赋值,接着执行函数,var声明的getNum被赋值为一个函数执行完成更改变量a为2 1.2、提升阶段 创建 初始化 赋值 let 提升 x x var 提升 提升 x function 提升 提升 提升 在块作用域内,let声明的变量仅在创建时被提升,在初始化之前使用变量,就会形成一个暂时性死区 var name = 'World' ;(function () { if (typeof name === 'undefined') { var name = "HuaMu"; console.info('Goodbye ' + name) } else { console.info('Hello ' + name) } })() // if分支内的name会被提升到外层,且同全局变量同名,则访问不到外层的name,var仅创建和初始化,并未赋值,则值为undefined,满足if条件 // Goodbye HuaMu 2、调用栈 在执行上下文创建完成后,JS引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文,又称调用栈 2.1、函数调用 JS引擎会为函数创建执行上下文,并将其压入调用栈 JS引擎执行函数代码 执行完毕后,JS引擎将该函数的执行上下文弹出栈 2.2、栈溢出 当分配的调用栈空间被占满时,会引发“栈溢出”问题。即超过了最大栈容量或最大调用深度 2.2.1、场景 <!-- todo --> 3、作用域 作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。主要有全局作用域、函数作用域以及块级作用域 当前作用域与上层作用域有同名变量时,无法访问和影响上层变量 let a = 1 function b(a) { a = 2 console.log(a) } b(a) // 2 console.log(a) // 1 4、作用域链 通过作用域查找变量的链条称为作用域链,而作用域链是通过词法作用域来确定的。词法作用域由函数声明的位置来决定,是静态的,即在代码阶段就决定好了,和函数是怎么调用的没有关系 // 连等操作是从右向左执行的,相当于b = 10、let a = b,相当于隐式创建为一个全局变量b let a = b = 10; ;(function(){ // 跟着作用域链查找到全局变量b,并修改为20 // 由于重新声明了,a变量只是局部变量,不影响全局变量a let a = b = 20 })() console.log(a) // 10 console.log(b) // 20 函数只会在第一次执行的时候被编译,因此编译时变量环境和词法环境最顶层数据已确定 var i = 1 function b() { console.log(i) } function a() { var i = 2 b() } // 由于a函数在全局作用域被定义,即便b函数在a函数内执行,它也只能访问到全局的作用域 a() // 1 5、闭包 一个作用域引用着一个本该被销毁的作用域,称之为闭包。即一个函数引用着父作用域的变量,在父函数执行结束后依然进行调用 6、this this是函数执行上下文对象,是动态变化的值,它没有作用域的限制,嵌套函数中的this不会从外层函数中继承,可通过箭头函数、self处理 6.1、类型 全局执行上下文中的this: window 函数执行上下文中的this: 严格模式 ? undefined: window var v_out = 'v_out'; let c_out = 'c_out'; var inner = { v_out: 'v_in', c_out: 'c_in', v_func: function () { return this.v_out }, c_func: function () { return this.c_out }, func:()=>{ return this.v_out } }; // 获取对象作用域内的函数,在全局环境下调用 this 指向 window const v_method = inner.v_func; const c_method = inner.c_func; // 顶层 v_out 变量会提升挂载到 window v_method(); // 'v_out' // 在块作用域内,const声明的变量不会挂载到 window,且父作用域不能访问子作用域 c_method(); // undefined // 赋值表达式和逗号表达式会返回最后一个值本身,即inner.v_func函数本身,调用位置是全局环境 (inner.v_func, inner.v_func)(); // 'v_out' (inner.v_func = inner.v_func)(); // 'v_out' // 对象的方法调用,this指向该对象 inner.v_func() // 'v_in' (inner.v_func)() // 'v_in' // 箭头函数没有自己的执行上下文,它继承调用函数的this,在这里是window inner.func() // 'v_out' 6.2、更改this指向 绑定优先级为:new > 显示绑定(call、apply、bind) > 隐式绑定(调用函数对象) > 默认绑定(window) 6.2.1、通过函数的call、apply、bind方法设置 c_method.call(inner) c_method.apply(inner) c_method.bind(inner)() 简单实现:call、apply 将当前函数链接到指定的上下文中,即将函数设置为对象属性 当前函数在context上下文中执行 移除context中已执行的当前函数 /** * 简单实现apply * @param {Function} fn 当前运行的函数 * @param {Object} context 指定的上下文 * @param {Array} args 参数集合 * @returns */ const apply = (fn,context=window,args=[])=>{ // Symbol是es6增加的第六个基本类型,对于对象属性就是uuid const id = Symbol(); // 将当前函数链接到指定的上下文中 context[id] = fn; // 当前函数在context上下文中执行 const res = context[id](...args) // 移除context中已执行的当前函数 delete context[id] return res; } // -------test------- // const context = { value:1 } function fn (name,isGirl){ console.log("🚀 ~ ", name,isGirl,this.value) } apply(fn,context,['huamu',true]) // 🚀 ~ huamu true 1 简单实现:bind 与call、apply不一样的点是bind返回一个新函数 function bind (fn, context=window, ...args) { return (...args2)=> apply(fn,context,[...args,...args2]) } 6.2.2、通过调用函数对象:指向对象本身 this的绑定是函数真正执行的位置 6.2.3、通过构造函数 function Foo() { getName = function () { console.log(1) } return this } Foo.getName = function () { console.log(2) } Foo.prototype.getName = function () { console.log(3) } var getName = function () { console.log(4) } function getName() { console.log(5) } // 执行Foo函数的静态方法 Foo.getName() // 2 // 函数getName提升并赋值,执行getName函数表达式 getName() // 4 // 在全局环境下执行Foo函数,this指向window,执行函数内的getName方法,覆盖了全局环境下的getName Foo().getName() // 1 getName() // 1 // new用于调用函数,即 new Foo.getName() 相当于 new (Foo.getName)(),执行了Foo函数的静态方法 new Foo.getName() // 2 // new 和 . 的优先级一样高,从左往右执行,相当于 (new Foo()).getName(),new会创建一个新对象,执行新对象的getName方法,在新对象本身找不到该方法,因此向原型找 new Foo().getName() // 3 new new Foo().getName() 简单实现:new 创建一个新对象,并指向函数原型 绑定this到新对象 返回对象 const newFn = (fn, ...arg) => { const obj = Object.create(fn.prototype); fn.apply(obj,arg) return obj } 拓展:Object.create方法会创建一个对象,并且将该对象的__proto__属性指向传入的对象 const person = { address: { country:"china" }, number: 111, say: function () { console.log(`it's ${this.name}, from ${this.address.country}, nums ${this.number}`) }, setCountry:function (country,number) { this.address.country=country this.number = number } } // 1、p1、p2 的原型对象指向了同一个对象 const p1 = Object.create(person) const p2 = Object.create(person) // 2、添加属性 p1.name = "huahua" // 3、在原型上找到setCountry函数,并且找到引用值address和原始值numbe属性,引用值会在所有实例共享 p1.setCountry("nanji",666) p2.name = "mumu" // 4、p2 的修改值会覆盖 p1的,最终country的值都为beiji p2.setCountry("beiji",999) p1.say() // it's huahua, from beiji, nums 666 p2.say() // it's mumu, from beiji, nums 999 6.2.4、实例 const value = 1; const objContext = { value : 2, getThis:function() { // 嵌套函数中的`this`不会从外层函数中继承 function fn1() { console.log("🚀 ~ fn1",this.value) // undefined } fn1() // 把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数 const self = this; function fn2() { console.log("🚀 ~ fn2 self",self.value) // 2 } fn2() // 箭头函数没有自己的执行上下文,会继承调用函数中的 this const fn3 = () => { console.log("🚀 ~ fn3 箭头函数",this.value) // 2 } fn3() // 箭头函数不会绑定局部变量,所有涉及它们的引用都会沿袭向上查找外层作用域链来处理,因此this的绑定只有一次 function fn4() { return () => { return () => { return () => { console.log("🚀 ~ fn4 箭头函数",this.value) // 42 }; }; }; } fn4.call( { value: 42 } )()()() // 构造函数优先级最高 function fn5(value) { this.value = value } const fn = new fn5(100) console.log("🚀 ~ fn5 构造函数", fn.value, this.value) // 100 2 } } 7、原型 7.1、函数对象 & 普通对象 通过 new Function 创建的对象称之为函数对象,其他则为普通对象,普通对象的构造函数是 Object // 函数对象 function fn(){}; const fn = () =>{}; const fn = new Function('str') // 普通对象 const obj = {} const obj = new Object() const obj = new fn() 每个对象都有内置__proto__属性,指向创建它的构造函数的原型对象,但只有函数对象才有prototype属性,指向函数的原型对象 7.2、原型对象 每个原型对象默认拥有一个constructor指针,指向prototype属性所在的函数 7.2.1、person1.__proto__ 是什么? 因为:person1 的构造函数是 Person 所以:person1.__proto__ === Person.prototype 7.2.2、Person.__proto__ 是什么? 因为:Person 的构造函数是 Function 所以:Person.__proto__ === Function.prototype 7.2.3、Person.prototype.__proto__ 是什么? 因为:Person.prototype 是构造函数的一个实例,是个普通对象,其构造函数是 Object 所以:Person.prototype.__proto__ === Object.prototype Function.prototype.__proto__ === Object.prototype 7.2.4、Object.__proto__ 是什么? 因为:所有函数对象的__proto__都指向Function.prototype,它是一个空函数 所以:Object.__proto__ === Function.prototype 7.2.5、Object.prototype.__proto__ 是什么? 因为:Object.prototype 处于 原型链的顶端,为null 所以:Object.prototype.__proto__ === null 7.2.6、12个JS内置构造器对象 8个可访问构造器 同 Object 指向 Function.prototype Number、Boolean、String、Function、Array、RegExp、Error、Date 2个以对象形式存在, 其 proto 指向 Object.prototype Math、JSON 1个不能直接访问的 Global、1个仅在函数调用时由JS引擎创建的 Arguments 8、继承 原型继承 构造函数继承 组合继承 class 继承:主要依靠extends、super(让JavaScript引擎去实现原来需要我们自己编写的原型链代码) 9、模块 9.1、模块化的意义 若不采用模块化,则引入js文件时必须确保引入顺序正确,否则无法运行。在文件数量大、依赖关系不明确的情况很难保证,因此出现了模块化 9.2、CommonJS & AMD 在ES6以前,JS没有模块体系。只有社区指定的一些模块加载方案,如用于服务器端同步加载的CommonJS和用于浏览器端异步加载的AMD、CMD 9.2.1、CommonJS(Node.js) 一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。拥有四个重要变量:module、exports、require、global exports本身是一个变量对象,指向module.exports的{}模块,只能通过.语法向外暴露变量。而module.exports既可通过.也可使用=赋值,其中exports是module的属性,指向{}模块 //在这里写上需要向外暴露的函数、变量 module.exports = { add, update } // 引用自定义模块必须加./路径,不加的话只会去node_modules文件找 var math = require('./math') // 引用核心模块时,不需要带路径 var http = require('http') 9.2.2、AMD(require.js)、CMD(sea.js) 虽然都是并行加载js文件,但AMD推崇依赖前置、提前执行,即预加载,CMD推崇依赖就近、延迟执行,即懒加载。拥有三个重要变量:指定引用路径的require.config、定义模块的definde以及加载模块的require /** AMD写法 **/ define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { // 在最前面声明并初始化了要用到的所有模块 a.doSomething(); if (false) { // 即便没用到某个模块 b,但 b 还是提前执行了 b.doSomething() } }); /** CMD写法 **/ define(function(require, exports, module) { //在需要时申明 var a = require('./a'); a.doSomething(); if (false) { var b = require('./b'); b.doSomething(); } }); 9.3、ESM CommonJS和AMD输出的是对象,引入时需查找对象属性,只能在运行时确定模块的依赖关系以及输入输出变量,即运行时加载,而ES6模块的设计思想,是尽量的静态化,它导出的不是对象,而是一个个接口,使得编译时就能确定模块的依赖关系和输入输出变量,即静态加载 9.3.1、原理解析 // index.js import { m } from './module'; // module.js const m = 1; const n = 2; export { m, n }; 将上面源码进行打包 // 1. 是一个立即执行函数 (function (modules) { var installedModules = {}; // 4. 处理入口文件模块 function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } // 5. 创建一个模块 var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // 6. 执行入口文件模块函数 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); module.l = true; // 7. 返回 return module.exports; } __webpack_require__.d = function (exports, name, getter) { if (!__webpack_require__.o(exports, name)) { // 判断name是不是exports自己的属性 Object.defineProperty(exports, name, {enumerable: true, get: getter}); } }; __webpack_require__.r = function (exports) { if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { // Symbol.toStringTag作为对象的属性,值表示这个对象的自定义类型 [Object Module] // 通常只作为Object.prototype.toString()的返回值 Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'}); } Object.defineProperty(exports, '__esModule', {value: true}); }; __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // 3. 传入入口文件id return __webpack_require__(__webpack_require__.s = "./index.js"); })( // 2. 模块对象作为参数传入 { "./index.js": (function (module, __webpack_exports__, __webpack_require__) { // __webpack_exports__就是module.exports "use strict"; // 添加了__esModule和Symbol.toStringTag属性 __webpack_require__.r(__webpack_exports__); var _module__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module.js"); }), "./module.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); // 把m/n这些变量添加到module.exports中,并设置getter为直接返回值 __webpack_require__.d(__webpack_exports__, "m", function () {return m;}); __webpack_require__.d(__webpack_exports__, "n", function () {return n;}); var m = 1; var n = 2; }) }); 将模块对象传入一个立即执行的函数 传入文件id,执行__webpack_require__函数 创建一个module,并绑定this到module.exports,同时传入module和module.exports对象 __webpack_require__.r给module.exports对象添加一个Symbol.toStringTag属性,值为{value: 'Module'},使得module.exports调用toString可返回[Object Module]表示一个模块 __webpack_require__.d将要导出的变量添加到module.exports,设置getter返回同名变量的值,使得变量更改,外边的引用也会变化 返回module.exports 注意喔:ESM遇到加载命令import时,只生成一个动态的只读引用,在需要调用时才去模块里取值 9.4、require和import的区别 CommonJS 模块化方案 require/exports 是为服务器端开发设计的。服务器模块系统同步读取模块文件内容,编译执行后得到模块接口。而ES6 模块化方案 import/export 是为浏览器设计的,浏览器模块系统异步加载脚本文件 require/exports 是运行时动态加载,,import/export 是静态编译 require/exports 输出的是一个值的拷贝,import/export 模块输出的是值的引用,即文件引用的模块值改变,require 引入的模块值不会改变,而 import 引入的模块值会改变 用法不同 ES6 模块可以在 import 引用语句前使用模块,CommonJS 则需要先引用后使用 import/export 只能在模块顶层使用,不能在函数、判断语句等代码块之中引用,而require/exports可以 import/export 默认采用严格模式 // require/exports const fs = require('fs') exports.fs = fs module.exports = fs // import/export import fileSystem, {readFile} from 'fs' // 引入 export default 导出的模块不用加 {},引入非 export default 导出的模块需要加 {} 10、babel 对源码字符串进行 parse,生成 AST,把对代码的修改转为对 AST 的增删改,转换完 AST 之后再打印成目标代码字符串 10.1、babel插件开发的API 10.1.1、parse 阶段 使用@babel/parser 把源码转成 AST require('@babel/parser').parse(source, { sourceType: 'module', // 解析 es module 语法 plugins: ['jsx'], // 指定jsx 等插件来解析对应的语法 }); 10.1.2、transform 阶段 使用 @babel/traverse 遍历 AST,并调用 visitor 函数修改 AST,@babel/types 用于创建、判断 AST 节点,提供了 isX、assertX 等 api,若批量创建,则可使用@babel/template require('@babel/traverse').default(ast, { // do something }) 10.1.3、generate 阶段 使用@babel/generate 把 AST 打印为目标代码字符串,同时生成 sourcemap,@babel/code-frame 用于错误时打印代码位置 const { code,map } = generator(ast, { sourceMaps: true }) 11、正则 11.1、基础语法 匹配模式 // + 前导字符必须在目标字符串中连续出现1次起 /\d+/ // * ~ 连续0次起 /\d*/ // ? ~ 0、1次 /\d?/ // ^ 定位字符串首个字符,$ 末尾字符 /^\d$/ // () 为一个捕获组,[]匹配单个字符,元素关系为或 /([\s\S]*?)["']/ // 任意字符 + "|' 匹配字符 // \s 匹配空白字符(空格、换行、缩进等)、\S相反 /[\s\S]*/ // 匹配全部内容 // \w 匹配单词([A-Za-z0-9_])、\W相反 /[\w\W]*/ // 匹配全部内容 // \b 匹配不全是\w的位置 /\bnice\b/ // a nice day -> a是显式位置,a和n之间的位置则为隐式位置,即a位置到n位置前则是\b匹配的位置,同样,e位置到d位置前也是\b匹配的,因此可匹配到 nice /\b.\bnice\b/ // a nice day -> 匹配到 a nice 匹配数字 /[0-9]/ /\d/ // 单个数字 /[0-9]+/ /\d*/ // 多个数字 /[\d]{1,3}/ // 指定个数,1-3个数字 11.2、实例解析 实用http路由表达式 /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/ 拆解匹配 http + (0|1个)s + :// + (0|1个) www. + (1-256个) -|a-z|A-Z|0-9|@|:|%|.|_|+|~|#|= + . + (1-6个) a-z|A-Z|0-9|(|) + 匹配不全是\w的位置 + (任意个)-|a-z|A-Z|0-9|(|)|@|:|%|_|+|.|~|#|?|&|/|=

优秀的个人博客,低调大师

线程安全你不会,你面试,你怎么敢的呀,细到恐怖.......

线程安全 概念:多个线程对临界资源的访问是安全的; 实现:同步与互斥 互斥:通过同一时间对临界资源的唯一访问保证访问操作的安全; 同步:通过条件判断使对临界资源的访问更加合理; 互斥的实现: 互斥锁:本质是一个个0/1技术器,用于标记对临界资源的访问;0——不可访问,1——可访问; 互斥锁自身操作是一个原子操作(直接用0与临界资源进行交换,然后判断临界资源可否访问): //linux,打印会出错 //多线程打印,判断不及时,多线程进入导致--过多,打印错误 #include<stdio.h> 2 #include<pthread.h> 3 4 int tickets=100; 5 void *scalpers(void *arg){ 6 while(1){ 7 if(tickets>0){ 8 usleep(1); 9 printf("i get a ticket:%d\n",tickets); 10 tickets--; 11 } 12 else{ 13 pthread_exit(NULL); 14 } 15 } 16 return NULL; 17 } 18 int main(int argc,int *argv[]) 19 { 20 pthread_t tid[4]; 21 int ret; 22 int i=0; 23 for(i=0;i<4;++i){ 24 ret=pthread_create(&tid[i],NULL,scalpers,NULL); 25 if(ret!=0){ 26 printf("create error\n"); 27 return -1; 28 } 29 } 30 int j=0; 31 for(j=0;j<4;++j){ 32 pthread_join(tid[j],NULL); 33 34 } 35 return 0; 36 } 接口介绍 1.定义互斥锁变量: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 2. 初始化互斥锁(线程创建之前): int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); //mutex:要初始化的互斥量; // attr:NULL 3.在临界资源访问之前加锁: int pthread_mutex_lock(pthread_mutex_t *mutex); //——阻塞接口 int pthread_mutex_trylock(pthread_mutex_t *mutex); //——非阻塞接口 4.在临界资源访问完毕后解锁: int pthread_mutex_unlock(pthread_mutex_t *mutex); 5.销毁互斥锁: int pthread_mutex_destroy(pthread_mutex_t *mutex); 代码如下 1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <pthread.h> 5 6 int tickets = 100; 7 8 void *scalpers(void *arg) 9 { 10 pthread_mutex_t *mutex = (pthread_mutex_t*)arg; 11 while(1) { 12 pthread_mutex_lock(mutex);//3.加锁 13 if (tickets > 0) { 14 usleep(1); 15 printf("I got a ticket:%d\n", tickets); 16 tickets--; 17 }else { 18 pthread_mutex_unlock(mutex);//4.解锁 19 pthread_exit(NULL); 20 } 21 pthread_mutex_unlock(mutex);//4.解锁 22 } 23 return NULL; 24 } 25 int main (int argc, char *argv[]) 26 { 27 pthread_mutex_t mutex;//1.定义互斥锁变量 28 pthread_t tid[4]; 29 int ret; 30 pthread_mutex_init(&mutex, NULL);//2.初始化互斥锁 31 int i=0; 32 for(i = 0; i < 4; i++) { 33 ret = pthread_create(&tid[i], NULL, scalpers, &mutex); 34 if (ret != 0) { 35 printf("thread create error\n"); 36 return -1; 37 } 38 } 39 int j=0; 40 for (j = 0; j < 4; j++) { 41 pthread_join(tid[j], NULL); 42 } 43 pthread_mutex_destroy(&mutex);//5.销毁互斥锁 44 return 0; 45 } 线程的知识点:https://blog.csdn.net/weixin_52270223/article/details/115820547 下个一博客介绍死锁的知识点;如有错误或者补充,评论下;互相学习,互关一波,抱拳了

优秀的个人博客,低调大师

美团面试题:为什么能直接调用userMapper接口的方法?

字数:2434,阅读耗时:3分40秒。 老规矩,先上案例代码,这样大家可以更加熟悉是如何使用的,看过Mybatis系列的小伙伴,对这段代码差不多都可以背下来了。 哈哈~,有点夸张吗?不夸张的,就这行代码。 public class MybatisApplication { public static final String URL = "jdbc:mysql://localhost:3306/mblog"; public static final String USER = "root"; public static final String PASSWORD = "123456"; ​ public static void main(String[] args) { String resource = "mybatis-config.xml"; InputStream inputStream = null; SqlSession sqlSession = null; try { inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); sqlSession = sqlSessionFactory.openSession(); //今天主要这行代码 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); System.out.println(userMapper.selectById(1)); ​ } catch (Exception e) { e.printStackTrace(); } finally { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } sqlSession.close(); } } 看源码有什么用? 通过源码的学习,我们可以收获Mybatis的核心思想和框架设计,另外还可以收获设计模式的应用。 前两篇文章我们已经Mybatis配置文件解析到获取SqlSession,下面我们来分析从SqlSession到userMapper: UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 前面那篇文章已经知道了这里的sqlSession使用的是默认实现类DefaultSqlSession。所以我们直接进入DefaultSqlSession的getMapper方法。 //DefaultSqlSession中 private final Configuration configuration; //type=UserMapper.class @Override public <T> T getMapper(Class<T> type) { return configuration.getMapper(type, this); } 这里有三个问题: 问题1:getMapper返回的是个什么对象? 上面可以看出,getMapper方法调用的是Configuration中的getMapper方法。然后我们进入Configuration中 //Configuration中 protected final MapperRegistry mapperRegistry = new MapperRegistry(this); ////type=UserMapper.class public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } 这里也没做什么,继续调用MapperRegistry中的getMapper: //MapperRegistry中 public class MapperRegistry { //主要是存放配置信息 private final Configuration config; //MapperProxyFactory 的映射 private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); ​ //获得 Mapper Proxy 对象 //type=UserMapper.class,session为当前会话 public <T> T getMapper(Class<T> type, SqlSession sqlSession) { //这里是get,那就有add或者put final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { //创建实例 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } ​ //解析配置文件的时候就会调用这个方法, //type=UserMapper.class public <T> void addMapper(Class<T> type) { // 判断 type 必须是接口,也就是说 Mapper 接口。 if (type.isInterface()) { //已经添加过,则抛出 BindingException 异常 if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { //添加到 knownMappers 中 knownMappers.put(type, new MapperProxyFactory<>(type)); //创建 MapperAnnotationBuilder 对象,解析 Mapper 的注解配置 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); //标记加载完成 loadCompleted = true; } finally { //若加载未完成,从 knownMappers 中移除 if (!loadCompleted) { knownMappers.remove(type); } } } } } MapperProxyFactory对象里保存了mapper接口的class对象,就是一个普通的类,没有什么逻辑。 在MapperProxyFactory类中使用了两种设计模式: 单例模式methodCache(注册式单例模式)。 工厂模式getMapper()。 继续看MapperProxyFactory中的newInstance方法。 public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>(); ​ public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public T newInstance(SqlSession sqlSession) { //创建MapperProxy对象 final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } //最终以JDK动态代理创建对象并返回 protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } } 从代码中可以看出,依然是稳稳的基于 JDK Proxy 实现的,而 InvocationHandler 参数是 MapperProxy 对象。 //UserMapper 的类加载器 //接口是UserMapper //h是mapperProxy对象 public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h){ } 问题2:为什么就可以调用他的方法? 上面调用newInstance方法时候创建了MapperProxy对象,并且是当做newProxyInstance的第三个参数,所以MapperProxy类肯定实现了InvocationHandler。 进入MapperProxy类中: //果然实现了InvocationHandler接口 public class MapperProxy<T> implements InvocationHandler, Serializable { ​ private static final long serialVersionUID = -6424540398559729838L; private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache; ​ public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } //调用userMapper.selectById()实质上是调用这个invoke方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //如果是Object的方法toString()、hashCode()等方法 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (method.isDefault()) { //JDK8以后的接口默认实现方法 return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } //创建MapperMethod对象 final MapperMethod mapperMethod = cachedMapperMethod(method); //下一篇再聊 return mapperMethod.execute(sqlSession, args); } } 也就是说,getMapper方法返回的是一个JDK动态代理对象(类型是$Proxy+数字)。这个代理对象会继承Proxy类,实现被代理的接口UserMpper,里面持有了一个MapperProxy类型的触发管理类。 当我们调用UserMpper的方法时候,实质上调用的是MapperProxy的invoke方法。 userMapper=$Proxy6@2355。 为什么要在MapperRegistry中保存一个工厂类? 原来他是用来创建并返回代理类的。这里是代理模式的一个非常经典的应用。 MapperProxy如何实现对接口的代理? JDK动态代理 我们知道,JDK动态代理有三个核心角色: 被代理类(即就是实现类) 接口 实现了InvocationHanndler的触发管理类,用来生成代理对象。 被代理类必须实现接口,因为要通过接口获取方法,而且代理类也要实现这个接口。 而Mybatis中并没有Mapper接口的实现类,怎么被代理呢?它忽略了实现类,直接对Mapper接口进行代理。 MyBatis动态代理: 在Mybatis中,JDK动态代理为什么不需要实现类呢? 这里我们的目的其实就是根据一个可以执行的方法,直接找到Mapper.xml中statement ID ,方便调用。 最后返回的userMapper就是MapperProxyFactory的创建的代理对象,然后这个对象中包含了MapperProxy对象, 问题3:到底是怎么根据Mapper.java找到Mapper.xml的? 最后我们调用userMapper.selectUserById(),本质上调用的是MapperProxy的invoke()方法。 请看下面这张图: 如果根据(接口+方法名找到Statement ID ),这个逻辑在InvocationHandler子类(MapperProxy类)中就可以完成了,其实也就没有必要在用实现类了。 总结 本文中主要是讲getMapper方法,该方法实质上是获取一个JDK动态代理对象(类型是Proxy+数字),这个代理类会继承MapperProxy类,实现被代理的接口UserMapper,并且里面持有一个MapperProxy类型的触发管理类。这里我们就拿到代理类了,后面我们就可以使用这个代理对象进行方法调用。 问题涉及到的设计模式: 代理模式。 工厂模式。 单例模式。 整个流程图: 冰冻三尺,非一日之寒表面意义是冰冻了三尺,并不是一天的寒冷所能达到的效果。学习亦如此,你每一天的一点点努力,都是为你以后的成功做铺垫。

资源下载

更多资源
Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册