-
Notifications
You must be signed in to change notification settings - Fork 4
/
content.json
1 lines (1 loc) · 811 KB
/
content.json
1
{"meta":{"title":"Liz'blog","subtitle":"","description":"","author":"Chemlez","url":"https://chemlez.github.io","root":"/"},"pages":[{"title":"404 Not Found","date":"2020-03-13T22:15:48.705Z","updated":"2020-03-12T14:05:20.867Z","comments":true,"path":"404.html","permalink":"https://chemlez.github.io/404.html","excerpt":"","text":"404 很抱歉,您访问的页面不存在 可能是输入地址有误或该地址已被删除"},{"title":"关于","date":"2020-09-19T01:55:00.167Z","updated":"2020-09-19T01:55:00.164Z","comments":false,"path":"about/index.html","permalink":"https://chemlez.github.io/about/index.html","excerpt":"","text":""},{"title":"所有分类","date":"2020-03-14T13:13:15.519Z","updated":"2020-03-14T13:13:15.519Z","comments":true,"path":"categories/index.html","permalink":"https://chemlez.github.io/categories/index.html","excerpt":"","text":""},{"title":"友链","date":"2020-05-03T09:42:15.240Z","updated":"2020-04-07T12:51:55.849Z","comments":true,"path":"friends/index.html","permalink":"https://chemlez.github.io/friends/index.html","excerpt":"左 邻 右 舍","text":"左 邻 右 舍 友链的话可以在下方留言,必须要有名称、头像链接、和至少一个标签哦~ 名称: Chemlez头像: https://s1.ax1x.com/2020/03/13/8mvbCj.jpg网址: https://liizhi.com标签: Java背景颜色参考: #87977F文字颜色参考: #EFEFEF"},{"title":"所有标签","date":"2021-05-16T08:46:45.853Z","updated":"2020-03-14T13:11:57.988Z","comments":true,"path":"tags/index.html","permalink":"https://chemlez.github.io/tags/index.html","excerpt":"","text":""},{"title":"","date":"2020-03-13T19:45:21.019Z","updated":"2019-10-04T12:54:50.000Z","comments":true,"path":"photos/data.json","permalink":"https://chemlez.github.io/photos/data.json","excerpt":"","text":"{\"list\":[{\"date\":\"2019-9\",\"arr\":{\"year\":2019,\"month\":9,\"link\":[\"2019-9-14_晴空.jpg\",\"2019-9-14_晴空于校园.jpg\"],\"text\":[\"晴空\",\"晴空于校园\"],\"type\":[\"image\",\"image\"]}},{\"date\":\"2019-8\",\"arr\":{\"year\":2019,\"month\":8,\"link\":[\"2019-8-24_16年暑假某一傍晚拍摄于宁德海边.jpg\",\"2019-8-24_用于相册测试.jpg\"],\"text\":[\"16年暑假某一傍晚拍摄于宁德海边\",\"用于相册测试\"],\"type\":[\"image\",\"image\"]}}]}"},{"title":"相册","slug":"photos","date":"2020-03-13T19:45:21.015Z","updated":"2019-08-24T06:40:38.000Z","comments":false,"path":"photos/index.html","permalink":"https://chemlez.github.io/photos/index.html","excerpt":"","text":"Photos 图片正在加载中… (function() { var loadScript = function(path) { var $script = document.createElement('script') document.getElementsByTagName('body')[0].appendChild($script) $script.setAttribute('src', path) } setTimeout(function() { loadScript('./ins.js') }, 0) })()"},{"title":"","date":"2020-03-13T19:45:21.026Z","updated":"2019-08-24T07:36:08.000Z","comments":true,"path":"photos/ins.css","permalink":"https://chemlez.github.io/photos/ins.css","excerpt":"","text":"#post-instagram{ padding: 30px; } #post-instagram .article-entry{ padding-right: 0; } .instagram{ position: relative; min-height: 500px; } .instagram img { width: 100%; } .instagram .year { font-size: 16px; } .instagram .open-ins{ padding: 10px 0; color: #cdcdcd; } .instagram .open-ins:hover{ color: #657b83; } .instagram .year{ display: inline; } .instagram .thumb { width: 25%; height: 0; padding-bottom: 25%; position: relative; display: inline-block; text-align: center; background: #ededed; outline: 1px solid #ddd; } .instagram .thumb a { position: relative; } .instagram .album h1 em{ font-style: normal; font-size: 14px; margin-left: 10px; } .instagram .album ul{ display: flex; flex-wrap: wrap; clear: both; width: 100%; text-align: left; } .instagram .album li{ list-style: none; display: inline-block; box-sizing: border-box; padding: 0 5px; margin-bottom: -10px; height: 0; width: 25%; position: relative; padding-bottom: 25%; } .instagram .album li:before{ display: none; } .instagram .album div.img-box{ position: absolute; width: 90%; height: 90%; -webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.4), 0 1px 0 1px rgba(255,255,255,0.1); -moz-box-shadow: 0 1px 0 rgba(255,255,255,0.4), 0 1px 0 1px rgba(255,255,255,0.1); box-shadow: 0 1px 0 rgba(255,255,255,0.4), 0 1px 0 1px rgba(255,255,255,0.1); } .instagram .album div.img-box img{ width: 100%; height: 100%; position: absolute; z-index: 2; } .instagram .album div.img-box .img-bg{ position: absolute; top: 0; left: 0; bottom: 0px; width: 100%; margin: -5px; padding: 5px; -webkit-box-shadow: 0 0 0 1px rgba(0,0,0,.04), 0 1px 5px rgba(0,0,0,0.1); -moz-box-shadow: 0 0 0 1px rgba(0,0,0,.04), 0 1px 5px rgba(0,0,0,0.1); box-shadow: 0 0 0 1px rgba(0,0,0,.04), 0 1px 5px rgba(0,0,0,0.1); -webkit-transition: all 0.15s ease-out 0.1s; -moz-transition: all 0.15s ease-out 0.1s; -o-transition: all 0.15s ease-out 0.1s; transition: all 0.15s ease-out 0.1s; opacity: 0.2; cursor: pointer; display: block; z-index: 3; } .instagram .album div.img-box .icon { font-size: 14px; position: absolute; left: 50%; top: 50%; margin-left: -7px; margin-top: -7px; color: #999; z-index: 1; } .instagram .album div.img-box .img-bg:hover{ opacity: 0; } .photos-btn-wrap { border-bottom: 1px solid #e5e5e5; margin-bottom: 20px; } .photos-btn { font-size: 16px; color: #333; margin-bottom: -4px; padding: 5px 8px 3px; } .photos-btn.active { color: #08c; border: 1px solid #e5e5e5; border-bottom: 5px solid #fff; } @media screen and (max-width:600px) { .instagram .thumb { width: 50%; padding-bottom: 50%; } .instagram .album li { width: 100%; position: relative; padding-bottom: 100%; text-align: center; } .instagram .album div.img-box{ margin: 0; width: 90%; height: 90%; } } /* ====== video ===== */ .video-container { z-index: 1; position: relative; padding-bottom: 56.25%; margin: 0 auto; } .video-container iframe, .video-container object, .video-container embed { z-index: 1; position: absolute; top: 0; left: 7%; width: 85%; height: 85%; box-shadow: 0px 0px 20px 2px #888888; }"},{"title":"","date":"2020-03-13T19:45:21.017Z","updated":"2019-08-29T07:01:20.000Z","comments":true,"path":"photos/ins.js","permalink":"https://chemlez.github.io/photos/ins.js","excerpt":"","text":"/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if (installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = \"/dist/\"; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; __webpack_require__(1); var _view = __webpack_require__(2); var _view2 = _interopRequireDefault(_view); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * @name impush-client * @description 这个项目让我发家致富… * @date 2016-12-1 */ var _collection = []; var _count = 0; var searchData; function addMask(elem) { var rect = elem.getBoundingClientRect(); var style = getComputedStyle(elem, null); var mask = document.createElement('i'); mask.className = 'icon-film'; mask.style.color = '#fff'; mask.style.fontSize = '26px'; mask.style.position = 'absolute'; mask.style.right = '10px'; mask.style.bottom = '10px'; mask.style.zIndex = 1; elem.parentNode.appendChild(mask); } var createVideoIncon = function createVideoIncon() { var $videoImg = document.querySelectorAll('.thumb a[data-type=\"video\"]'); for (var i = 0, len = $videoImg.length; i < len; i++) { addMask($videoImg[i]); } }; // 修改这里render()函数:修改图片的路径地址.minSrc 小图的路径. src 大图的路径.修改为自己的图片路径(github的路径) // https://raw.githubusercontent.com/ChemLez/blog-Picture/master/photos/ // https://raw.githubusercontent.com/ChemLez/blog-Picture/master/min_photos/ var render = function render(res) { var ulTmpl = \"\"; for (var j = 0, len2 = res.list.length; j < len2; j++) { var data = res.list[j].arr; var liTmpl = \"\"; for (var i = 0, len = data.link.length; i < len; i++) { var minSrc = 'https://raw.githubusercontent.com/ChemLez/blog-Picture/master/min_photos/' + data.link[i]; var src = 'https://raw.githubusercontent.com/ChemLez/blog-Picture/master/photos/' + data.link[i]; var type = data.type[i]; var target = src + (type === 'video' ? '.mp4' : '.jpg'); src += ''; liTmpl += '\\ \\ \\ \\ ' + data.text[i] + '\\ '; } ulTmpl = ulTmpl + '' + data.year + '年' + data.month + '月\\ ' + liTmpl + '\\ '; } document.querySelector('.instagram').innerHTML = '' + ulTmpl + ''; createVideoIncon(); _view2.default.init(); }; var replacer = function replacer(str) { var arr = str.split(\"/\"); return \"/assets/ins/\" + arr[arr.length - 1]; }; var ctrler = function ctrler(data) { var imgObj = {}; for (var i = 0, len = data.length; i < len; i++) { var y = data[i].y; var m = data[i].m; var src = replacer(data[i].src); var text = data[i].text; var key = y + \"\" + ((m + \"\").length == 1 ? \"0\" + m : m); if (imgObj[key]) { imgObj[key].srclist.push(src); imgObj[key].text.push(text); } else { imgObj[key] = { year: y, month: m, srclist: [src], text: [text] }; } } render(imgObj); }; function loadData(success) { if (!searchData) { var xhr = new XMLHttpRequest(); xhr.open('GET', './data.json?t=' + +new Date(), true); xhr.onload = function() { if (this.status >= 200 && this.status < 300) { var res = JSON.parse(this.response); searchData = res; success(searchData); } else { console.error(this.statusText); } }; xhr.onerror = function() { console.error(this.statusText); }; xhr.send(); } else { success(searchData); } } var Ins = { init: function init() { loadData(function(data) { render(data); }); } }; Ins.init(); // export default impush; /***/ }, /* 1 */ /***/ function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */ (function(global) { 'use strict'; var inViewport = __webpack_require__(3); var lazyAttrs = ['data-src']; global.lzld = lazyload(); // Provide libs using getAttribute early to get the good src // and not the fake data-src replaceGetAttribute('Image'); replaceGetAttribute('IFrame'); function registerLazyAttr(attr) { if (indexOf.call(lazyAttrs, attr) === -1) { lazyAttrs.push(attr); } } function lazyload(opts) { opts = merge({ 'offset': 333, 'src': 'data-src', 'container': false }, opts || {}); if (typeof opts.src === 'string') { registerLazyAttr(opts.src); } var elts = []; function show(elt) { var src = findRealSrc(elt); if (src) { elt.src = src; } elt.setAttribute('data-lzled', true); elts[indexOf.call(elts, elt)] = null; } function findRealSrc(elt) { if (typeof opts.src === 'function') { return opts.src(elt); } return elt.getAttribute(opts.src); } function register(elt) { elt.onload = null; elt.removeAttribute('onload'); elt.onerror = null; elt.removeAttribute('onerror'); if (indexOf.call(elts, elt) === -1) { inViewport(elt, opts, show); } } return register; } function replaceGetAttribute(elementName) { var fullname = 'HTML' + elementName + 'Element'; if (fullname in global === false) { return; } var original = global[fullname].prototype.getAttribute; global[fullname].prototype.getAttribute = function(name) { if (name === 'src') { var realSrc; for (var i = 0, max = lazyAttrs.length; i < max; i++) { realSrc = original.call(this, lazyAttrs[i]); if (realSrc) { break; } } return realSrc || original.call(this, name); } // our own lazyloader will go through theses lines // because we use getAttribute(opts.src) return original.call(this, name); }; } function merge(defaults, opts) { for (var name in defaults) { if (opts[name] === undefined) { opts[name] = defaults[name]; } } return opts; } // http://webreflection.blogspot.fr/2011/06/partial-polyfills.html function indexOf(value) { for (var i = this.length; i-- && this[i] !== value;) {} return i; } module.exports = lazyload; // export default impush; /* WEBPACK VAR INJECTION */ }.call(exports, (function() { return this; }()))) /***/ }, /* 2 */ /***/ function(module, exports) { 'use strict'; var initPhotoSwipeFromDOM = function initPhotoSwipeFromDOM(gallerySelector) { // parse slide data (url, title, size ...) from DOM elements // (children of gallerySelector) var parseThumbnailElements = function parseThumbnailElements(el) { el = el.parentNode.parentNode; var thumbElements = el.getElementsByClassName('thumb'), numNodes = thumbElements.length, items = [], figureEl, linkEl, size, type, // video or not target, item; for (var i = 0; i < numNodes; i++) { figureEl = thumbElements[i]; // // include only element nodes if (figureEl.nodeType !== 1) { continue; } linkEl = figureEl.children[0]; // size = linkEl.getAttribute('data-size').split('x'); type = linkEl.getAttribute('data-type'); target = linkEl.getAttribute('data-target'); // create slide object item = { src: linkEl.getAttribute('href'), w: parseInt(size[0], 10), h: parseInt(size[1], 10) }; if (figureEl.children.length > 1) { item.title = figureEl.children[1].innerHTML; } if (linkEl.children.length > 0) { item.msrc = linkEl.children[0].getAttribute('src'); item.type = type; item.target = target; item.html = ''; if (type === 'video') { //item.src = null; } } item.el = figureEl; // save link to element for getThumbBoundsFn items.push(item); } return items; }; // find nearest parent element var closest = function closest(el, fn) { return el && (fn(el) ? el : closest(el.parentNode, fn)); }; // triggers when user clicks on thumbnail var onThumbnailsClick = function onThumbnailsClick(e) { e = e || window.event; e.preventDefault ? e.preventDefault() : e.returnValue = false; var eTarget = e.target || e.srcElement; // find root element of slide var clickedListItem = closest(eTarget, function(el) { return el.tagName && el.tagName.toUpperCase() === 'FIGURE'; }); if (!clickedListItem) { return; } // find index of clicked item by looping through all child nodes // alternatively, you may define index via data- attribute var clickedGallery = clickedListItem.parentNode, // childNodes = clickedListItem.parentNode.childNodes, // numChildNodes = childNodes.length, childNodes = document.getElementsByClassName('thumb'), numChildNodes = childNodes.length, nodeIndex = 0, index; for (var i = 0; i < numChildNodes; i++) { if (childNodes[i].nodeType !== 1) { continue; } if (childNodes[i] === clickedListItem) { index = nodeIndex; break; } nodeIndex++; } if (index >= 0) { // open PhotoSwipe if valid index found openPhotoSwipe(index, clickedGallery); } return false; }; // parse picture index and gallery index from URL (#&pid=1&gid=2) var photoswipeParseHash = function photoswipeParseHash() { var hash = window.location.hash.substring(1), params = {}; if (hash.length < 5) { return params; } var vars = hash.split('&'); for (var i = 0; i < vars.length; i++) { if (!vars[i]) { continue; } var pair = vars[i].split('='); if (pair.length < 2) { continue; } params[pair[0]] = pair[1]; } if (params.gid) { params.gid = parseInt(params.gid, 10); } return params; }; var openPhotoSwipe = function openPhotoSwipe(index, galleryElement, disableAnimation, fromURL) { var pswpElement = document.querySelectorAll('.pswp')[0], gallery, options, items; items = parseThumbnailElements(galleryElement); // define options (if needed) options = { // define gallery index (for URL) galleryUID: galleryElement.getAttribute('data-pswp-uid'), getThumbBoundsFn: function getThumbBoundsFn(index) { // See Options -> getThumbBoundsFn section of documentation for more info var thumbnail = items[index].el.getElementsByTagName('img')[0], // find thumbnail pageYScroll = window.pageYOffset || document.documentElement.scrollTop, rect = thumbnail.getBoundingClientRect(); return { x: rect.left, y: rect.top + pageYScroll, w: rect.width }; } }; // PhotoSwipe opened from URL if (fromURL) { if (options.galleryPIDs) { // parse real index when custom PIDs are used // http://photoswipe.com/documentation/faq.html#custom-pid-in-url for (var j = 0; j < items.length; j++) { if (items[j].pid == index) { options.index = j; break; } } } else { // in URL indexes start from 1 options.index = parseInt(index, 10) - 1; } } else { options.index = parseInt(index, 10); } // exit if index not found if (isNaN(options.index)) { return; } if (disableAnimation) { options.showAnimationDuration = 0; } // Pass data to PhotoSwipe and initialize it gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options); gallery.init(); var $tempVideo; var stopVideoHandle = function stopVideoHandle() { if ($tempVideo) { $tempVideo.remove(); $tempVideo = null; } }; var changeHandle = function changeHandle() { var item = gallery.currItem; stopVideoHandle(); if (item.type === 'video') { var $ctn = item.container; var style = $ctn.getElementsByClassName('pswp__img')[0].style; var $video = document.createElement('video'); $video.setAttribute('autoplay', 'autoplay'); $video.setAttribute('controls', 'controls'); $video.setAttribute('src', item.target); $video.style.width = style.width; $video.style.height = style.height; $video.style.position = 'absolute'; $video.style.zIndex = 2; $tempVideo = $video; $ctn.appendChild($video); } }; gallery.listen('initialZoomIn', changeHandle); gallery.listen('afterChange', changeHandle); gallery.listen('initialZoomOut', stopVideoHandle); }; // loop through all gallery elements and bind events var galleryElements = document.querySelectorAll(gallerySelector); for (var i = 0, l = galleryElements.length; i < l; i++) { galleryElements[i].setAttribute('data-pswp-uid', i + 1); galleryElements[i].onclick = onThumbnailsClick; } // Parse URL and open gallery if it contains #&pid=3&gid=1 var hashData = photoswipeParseHash(); if (hashData.pid && hashData.gid) { openPhotoSwipe(hashData.pid, galleryElements[hashData.gid - 1], true, true); } }; var Viewer = function() { function init() { initPhotoSwipeFromDOM('.photos'); } return { init: init }; }(); module.exports = Viewer; /***/ }, /* 3 */ /***/ function(module, exports) { /* WEBPACK VAR INJECTION */ (function(global) { module.exports = inViewport; var instances = []; var supportsMutationObserver = typeof global.MutationObserver === 'function'; function inViewport(elt, params, cb) { var opts = { container: global.document.body, offset: 0 }; if (params === undefined || typeof params === 'function') { cb = params; params = {}; } var container = opts.container = params.container || opts.container; var offset = opts.offset = params.offset || opts.offset; for (var i = 0; i < instances.length; i++) { if (instances[i].container === container) { return instances[i].isInViewport(elt, offset, cb); } } return instances[ instances.push(createInViewport(container)) - 1 ].isInViewport(elt, offset, cb); } function addEvent(el, type, fn) { if (el.attachEvent) { el.attachEvent('on' + type, fn); } else { el.addEventListener(type, fn, false); } } function debounce(func, wait, immediate) { var timeout; return function() { var context = this, args = arguments; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); function later() { timeout = null; if (!immediate) func.apply(context, args); } }; } // https://github.com/jquery/sizzle/blob/3136f48b90e3edc84cbaaa6f6f7734ef03775a07/sizzle.js#L708 var contains = function() { if (!global.document) { return true; } return global.document.documentElement.compareDocumentPosition ? function(a, b) { return !!(a.compareDocumentPosition(b) & 16); } : global.document.documentElement.contains ? function(a, b) { return a !== b && (a.contains ? a.contains(b) : false); } : function(a, b) { while (b = b.parentNode) { if (b === a) { return true; } } return false; }; } function createInViewport(container) { var watches = createWatches(); var scrollContainer = container === global.document.body ? global : container; var debouncedCheck = debounce(watches.checkAll(watchInViewport), 15); addEvent(scrollContainer, 'scroll', debouncedCheck); if (scrollContainer === global) { addEvent(global, 'resize', debouncedCheck); } if (supportsMutationObserver) { observeDOM(watches, container, debouncedCheck); } // failsafe check, every 200ms we check for visible images // usecase: a hidden parent containing eleements // when the parent becomes visible, we have no event that the children // became visible setInterval(debouncedCheck, 150); function isInViewport(elt, offset, cb) { if (!cb) { return isVisible(elt, offset); } var remote = createRemote(elt, offset, cb); remote.watch(); return remote; } function createRemote(elt, offset, cb) { function watch() { watches.add(elt, offset, cb); } function dispose() { watches.remove(elt); } return { watch: watch, dispose: dispose }; } function watchInViewport(elt, offset, cb) { if (isVisible(elt, offset)) { watches.remove(elt); cb(elt); } } function isVisible(elt, offset) { if (!contains(global.document.documentElement, elt) || !contains(global.document.documentElement, container)) { return false; } // Check if the element is visible // https://github.com/jquery/jquery/blob/740e190223d19a114d5373758127285d14d6b71e/src/css/hiddenVisibleSelectors.js if (!elt.offsetWidth || !elt.offsetHeight) { return false; } var eltRect = elt.getBoundingClientRect(); var viewport = {}; if (container === global.document.body) { viewport = { top: -offset, left: -offset, right: global.document.documentElement.clientWidth + offset, bottom: global.document.documentElement.clientHeight + offset }; } else { var containerRect = container.getBoundingClientRect(); viewport = { top: containerRect.top - offset, left: containerRect.left - offset, right: containerRect.right + offset, bottom: containerRect.bottom + offset }; } // The element must overlap with the visible part of the viewport var visible = ( (eltRect.right > viewport.left) && (eltRect.left < viewport.right) && (eltRect.bottom > viewport.top) && (eltRect.top < viewport.bottom) ); return visible; } return { container: container, isInViewport: isInViewport }; } function createWatches() { var watches = []; function add(elt, offset, cb) { if (!isWatched(elt)) { watches.push([elt, offset, cb]); } } function remove(elt) { var pos = indexOf(elt); if (pos !== -1) { watches.splice(pos, 1); } } function indexOf(elt) { for (var i = watches.length - 1; i >= 0; i--) { if (watches[i][0] === elt) { return i; } } return -1; } function isWatched(elt) { return indexOf(elt) !== -1; } function checkAll(cb) { return function() { for (var i = watches.length - 1; i >= 0; i--) { cb.apply(this, watches[i]); } }; } return { add: add, remove: remove, isWatched: isWatched, checkAll: checkAll }; } function observeDOM(watches, container, cb) { var observer = new MutationObserver(watch); var filter = Array.prototype.filter; var concat = Array.prototype.concat; observer.observe(container, { childList: true, subtree: true, // changes like style/width/height/display will be catched attributes: true }); function watch(mutations) { // some new DOM nodes where previously watched // we should check their positions if (mutations.some(knownNodes) === true) { setTimeout(cb, 0); } } function knownNodes(mutation) { var nodes = concat.call([], Array.prototype.slice.call(mutation.addedNodes), mutation.target ); return filter.call(nodes, watches.isWatched).length > 0; } } /* WEBPACK VAR INJECTION */ }.call(exports, (function() { return this; }()))) /***/ } /******/ ]);"},{"title":"","date":"2020-03-13T19:45:21.013Z","updated":"2019-07-29T06:56:40.000Z","comments":true,"path":"photos/lazyload.min.js","permalink":"https://chemlez.github.io/photos/lazyload.min.js","excerpt":"","text":"/*! * An jQuery | zepto plugin for lazy loading images. * author -> jieyou * see https://github.com/jieyou/lazyload * use some tuupola's code https://github.com/tuupola/jquery_lazyload (BSD) * use component's throttle https://github.com/component/throttle (MIT) */ !function(a){\"function\"==typeof define&&define.amd?define([\"jquery\"],a):a(window.jQuery||window.Zepto)}(function(a){function g(){}function h(a,b){var e;return e=b._$container==d?(\"innerHeight\"in c?c.innerHeight:d.height())+d.scrollTop():b._$container.offset().top+b._$container.height(),e=b.offset().left+e.threshold+b.width()}function l(a,b){var c=0;a.each(function(d){function g(){f.trigger(\"_lazyload_appear\"),c=0}var f=a.eq(d);if(!(f.width()b.failure_limit)return!1}else g()})}function m(a){return a.filter(function(b){return!a.eq(b)._lazyload_loadStarted})}function n(a,b){function h(){f=0,g=+new Date,e=a.apply(c,d),c=null,d=null}var c,d,e,f,g=0;return function(){c=this,d=arguments;var a=new Date-g;return f||(a>=b?h():f=setTimeout(h,b-a)),e}}var f,c=window,d=a(c),e={threshold:0,failure_limit:0,event:\"scroll\",effect:\"show\",effect_params:null,container:c,data_attribute:\"original\",data_srcset_attribute:\"original-srcset\",skip_invisible:!0,appear:g,load:g,vertical_only:!1,check_appear_throttle_time:300,url_rewriter_fn:g,no_fake_img_loader:!1,placeholder_data_img:\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC\",placeholder_real_img:\"http://ditu.baidu.cn/yyfm/lazyload/0.0.1/img/placeholder.png\"};f=function(){var a=Object.prototype.toString;return function(b){return a.call(b).replace(\"[object \",\"\").replace(\"]\",\"\")}}(),a.fn.hasOwnProperty(\"lazyload\")||(a.fn.lazyload=function(b){var i,j,k,h=this;return a.isPlainObject(b)||(b={}),a.each(e,function(g,h){var i=f(b[g]);-1!=a.inArray(g,[\"threshold\",\"failure_limit\",\"check_appear_throttle_time\"])?\"String\"==i?b[g]=parseInt(b[g],10):\"Number\"!=i&&(b[g]=h):\"container\"==g?(b._$container=b.hasOwnProperty(g)?b[g]==c||b[g]==document?d:a(b[g]):d,delete b.container):!e.hasOwnProperty(g)||b.hasOwnProperty(g)&&i==f(e[g])||(b[g]=h)}),i=\"scroll\"==b.event,k=0==b.check_appear_throttle_time?l:n(l,b.check_appear_throttle_time),j=i||\"scrollstart\"==b.event||\"scrollstop\"==b.event,h.each(function(c){var e=this,f=h.eq(c),i=f.attr(\"src\"),k=f.attr(\"data-\"+b.data_attribute),l=b.url_rewriter_fn==g?k:b.url_rewriter_fn.call(e,f,k),n=f.attr(\"data-\"+b.data_srcset_attribute),o=f.is(\"img\");return 1==f._lazyload_loadStarted||i==l?(f._lazyload_loadStarted=!0,h=m(h),void 0):(f._lazyload_loadStarted=!1,o&&!i&&f.one(\"error\",function(){f.attr(\"src\",b.placeholder_real_img)}).attr(\"src\",b.placeholder_data_img),f.one(\"_lazyload_appear\",function(){function i(){d&&f.hide(),o?(n&&f.attr(\"srcset\",n),l&&f.attr(\"src\",l)):f.css(\"background-image\",'url(\"'+l+'\")'),d&&f[b.effect].apply(f,c?b.effect_params:[]),h=m(h)}var d,c=a.isArray(b.effect_params);f._lazyload_loadStarted||(d=\"show\"!=b.effect&&a.fn[b.effect]&&(!b.effect_params||c&&0==b.effect_params.length),b.appear!=g&&b.appear.call(e,f,h.length,b),f._lazyload_loadStarted=!0,b.no_fake_img_loader||n?(b.load!=g&&f.one(\"load\",function(){b.load.call(e,f,h.length,b)}),i()):a(\"\").one(\"load\",function(){i(),b.load!=g&&b.load.call(e,f,h.length,b)}).attr(\"src\",l))}),j||f.on(b.event,function(){f._lazyload_loadStarted||f.trigger(\"_lazyload_appear\")}),void 0)}),j&&b._$container.on(b.event,function(){k(h,b)}),d.on(\"resize load\",function(){k(h,b)}),a(function(){k(h,b)}),this})});"},{"title":"相册","slug":"photos","date":"2020-03-13T19:45:21.022Z","updated":"2019-08-24T07:36:10.000Z","comments":true,"path":"photos/videos.html","permalink":"https://chemlez.github.io/photos/videos.html","excerpt":"","text":"Photos Videos 指弹_女儿情 指弹_友谊地久天长 指弹_Always with me"},{"title":"","date":"2020-03-16T12:31:07.241Z","updated":"2020-03-12T14:01:14.183Z","comments":true,"path":"mylist/index.html","permalink":"https://chemlez.github.io/mylist/index.html","excerpt":"","text":""}],"posts":[{"title":"初学NIO之基本介绍","slug":"初学NIO之基本介绍","date":"2022-03-19T08:34:04.000Z","updated":"2022-03-19T08:35:33.089Z","comments":true,"path":"2022/03/19/初学NIO之基本介绍/","link":"","permalink":"https://chemlez.github.io/2022/03/19/%E5%88%9D%E5%AD%A6NIO%E4%B9%8B%E5%9F%BA%E6%9C%AC%E4%BB%8B%E7%BB%8D/","excerpt":"","text":"","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"NIO","slug":"NIO","permalink":"https://chemlez.github.io/tags/NIO/"}]},{"title":"数据结构之前缀树","slug":"数据结构之前缀树","date":"2022-03-17T12:02:15.000Z","updated":"2022-03-18T08:31:44.969Z","comments":true,"path":"2022/03/17/数据结构之前缀树/","link":"","permalink":"https://chemlez.github.io/2022/03/17/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%8B%E5%89%8D%E7%BC%80%E6%A0%91/","excerpt":"前缀树,又称为Trie树,是一种树形数据结构,可用于高效地存储和检索字符串数据集合中的键,常用于自动补完和拼写检查等应用场景。下面对是对前缀树节点的定义: 1234class WordNetTree { boolean end; WordNetTree[] words = new WordNetTree[26];} 其中end代表当前节点是否为结尾;words代表每个节点中的集合,数组长度为26,代表每次创建一个WordNetTree节点时,都会在该节点内部创建一个长度为26的WordNetTree数组。","text":"前缀树,又称为Trie树,是一种树形数据结构,可用于高效地存储和检索字符串数据集合中的键,常用于自动补完和拼写检查等应用场景。下面对是对前缀树节点的定义: 1234class WordNetTree { boolean end; WordNetTree[] words = new WordNetTree[26];} 其中end代表当前节点是否为结尾;words代表每个节点中的集合,数组长度为26,代表每次创建一个WordNetTree节点时,都会在该节点内部创建一个长度为26的WordNetTree数组。 如果对单词app、apple、apply、ben采用前缀树进行存储,则该前缀树存储的单词可形象的表示为下面形式: 如上图所示,每一个WordNetTree对象中,都包含长度为26的WordNetTree数组,数组中的下标i对应字母a~z,这里以小写字母为例。在存储apple和apply时,可以对相同前缀的字母存储时,复用其节点。同时,通过end值为False或者True,对该字符串进行判断——以root开始,当前节点结尾的字符串是否为完整字符串。 假设root节点为起始节点。那么对字符串插入,则有: 123456789101112// 插入某一字符串public void insert(String word) { WordNetTree start = root; for(int i = 0;i < word.length();++i) { int index = word.charAt(i) - 'a'; if(start.words[index] == null) { // 当前位置为null,创建插入 start.words[index] = new WordNetTree(); } start = start.words[index]; } start.end = true; // 代表当前树的结尾} 该函数的主要逻辑是,遍历该字符串中的每个字符,通过该字符定位到WordNetTree中的下标位置(下标位置和字符做一一对应的映射关系)。如果当前位置的对象为null,那么创建新的节点对象,用于标记当前index位置的对象数组,并将指针移动到当前字符位置,用于定位下一个字符位置。当遍历完成整个字符串时,将最后一个位置的end标记为True,表明当前结尾的字符串为完整字符串。 那么对字符串进行搜索时,则有: 123456789101112// 查找某一字符串public boolean search(String word) { WordNetTree start = root; for(int i = 0;i < word.length();++i) { int index = word.charAt(i) - 'a'; if(start.words[index] == null) { // 不存在 return false; } start = start.words[index]; } return start.end;} 如果,在搜索过程中,对象为null,则表明未存储当前字符串;在遍历完整个字符串时,返回end的值,如果end为False,表明尽管搜索到了当前字符串,但是该字符串只是某一字符串的前缀子串,并非为完整存储的字符串。 那么对字符串进行前缀匹配时,则有: 12345678910111213// 寻找前缀为prefix的字符串是否存在public boolean startsWith(String prefix) { WordNetTree start = root; for(int i = 0;i < prefix.length();++i) { int index = prefix.charAt(i) - 'a'; if(start.words[index] == null){ return false; } start = start.words[index]; } return true;} 下面给出完整Trie树的数据结构定义: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354class Trie { private WordNetTree root; public Trie() { root = new WordNetTree(); } // 插入某一字符串 public void insert(String word) { WordNetTree start = root; for(int i = 0;i < word.length();++i) { int index = word.charAt(i) - 'a'; if(start.words[index] == null) { // 当前位置为null,创建插入 start.words[index] = new WordNetTree(); } start = start.words[index]; } start.end = true; // 代表当前树的结尾 } // 查找某一字符串 public boolean search(String word) { WordNetTree start = root; for(int i = 0;i < word.length();++i) { int index = word.charAt(i) - 'a'; if(start.words[index] == null) { // 不存在 return false; } start = start.words[index]; } return start.end; } // 寻找前缀为prefix的字符串是否存在 public boolean startsWith(String prefix) { WordNetTree start = root; for(int i = 0;i < prefix.length();++i) { int index = prefix.charAt(i) - 'a'; if(start.words[index] == null){ return false; } start = start.words[index]; } return true; } // 数组类节点 class WordNetTree { boolean end; WordNetTree[] words = new WordNetTree[26]; }}","categories":[{"name":"数据结构与算法","slug":"数据结构与算法","permalink":"https://chemlez.github.io/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/"}],"tags":[{"name":"前缀树","slug":"前缀树","permalink":"https://chemlez.github.io/tags/%E5%89%8D%E7%BC%80%E6%A0%91/"}]},{"title":"设计模式之建造者模式","slug":"设计模式之建造者模式","date":"2021-08-30T06:49:17.000Z","updated":"2021-08-30T07:36:41.693Z","comments":true,"path":"2021/08/30/设计模式之建造者模式/","link":"","permalink":"https://chemlez.github.io/2021/08/30/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E5%BB%BA%E9%80%A0%E8%80%85%E6%A8%A1%E5%BC%8F/","excerpt":"一、定义建造者模式:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式称为建造者模式。 概念较为复杂,我们还是通过实例需求进行建造者模式的理解。","text":"一、定义建造者模式:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式称为建造者模式。 概念较为复杂,我们还是通过实例需求进行建造者模式的理解。 二、需求这里顶一个电脑类,并且实例化电脑类的对象,并给出该对象的属性赋值。 三、代码实现3.1 版本一通过直接创建出电脑类,并通过客户端进行赋值,创建出该对象,如: 123456789101112131415161718192021class Computer { private String cpu; private String gpu; private String memery; private String hd; // 省略setter getter和toString方法}// ========================服务单/客户端=================================public class AppTest { public static void main(String[] args) { Computer c = new Computer(); c.setCpu(\"i7 7500u\"); c.setMemery(\"16g\"); c.setGpu(\"RTX2080t\"); c.setHd(\"1T机械\"); System.out.println(c); }} 即:服务端(代码提供者)提供了Computer这个类,具体需要什么样的电脑交给了客户端自己去实现,根据自己的需求自己设定即可。 缺点: 客户端,在实例化好产品的对象以后,必须为该对象的每一个属性赋值,这样对于客户端来说,太过麻烦。 违反了迪米特法则(一个类应该对自己依赖的类知道的越少越好,而这里却要自己去进行对象的赋值); 这相当于在现实中,去电脑城买电脑,商家给零件扔给你,你自己回家组装。 3.2 版本二1234567891011121314151617181920212223242526272829303132class Computer { private String cpu; private String gpu; private String memery; private String hd; // ....}// 电脑建造者类,建造者类,必须关联电脑产品class ComputerBuilder { private Computer computer = new Computer(); public Computer builder() { computer.setCpu(\"i7 7500u\"); computer.setMemery(\"32g\"); computer.setGpu(\"RTX2080t\"); computer.setHd(\"1T机械\"); return computer; }}// ========================服务端/客户端================================public class AppTest { public static void main(String[] args) { ComputerBuilder cb = new ComputerBuilder(); Computer computer = cb.builder(); System.out.println(computer); }} 目前的优点: 客户端需要一个产品时,直接向建造者要即可,建造者封装了创建电脑的”复杂”过程。 目前的缺点: 封装得太死了,无论客户端需求什么样的电脑,都只能采用这一种的配置进行使用。 3.3 版本三继而改进,针对不同需求,我们需要创建不同的创造者,来分别生产不同配置的产品。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667class Computer { private String cpu; private String gpu; private String memery; private String hd; // ....}// 电脑建造者类,建造者类,必须关联电脑产品class AdviceComputerBuilder { private Computer computer = new Computer(); public Computer builder() { computer.setCpu(\"i7 7500u\"); computer.setMemery(\"32g\"); computer.setGpu(\"RTX2080t\"); computer.setHd(\"1T机械\"); return computer; }}class MiddleComputerBuilder { private Computer computer = new Computer(); public Computer builder() { computer.setCpu(\"i7 7500u\"); computer.setMemery(\"8g\"); computer.setGpu(\"RTX1060t\"); computer.setHd(\"1T机械\"); return computer; }}class LowComputerBuilder { private Computer computer = new Computer(); public Computer builder() { computer.setCpu(\"i7 7500u\"); computer.setMemery(\"2g\"); computer.setGpu(\"gtx940m\"); computer.setHd(\"500g机械\"); return computer; }}// ============================服务端/客户端=============================public class AppTest { public static void main(String[] args) { AdviceComputerBuilder acb = new AdviceComputerBuilder(); MiddleComputerBuilder mid = new MiddleComputerBuilder(); LowComputerBuilder low = new LowComputerBuilder(); // 玩游戏 Computer c = acb.builder(); System.out.println(c); // 开发 Computer c1 = mid.builder(); System.out.println(c1); // 办公 Computer c2 = low.builder(); System.out.println(c2); }} 这样根据不同的需求,给出不同的建造者类。 优点: 可以根据客户端的不同需求,使用不同的建造者来生产产品 缺点: 多个不同的建造者中的代码,在重复!既然代码中出现了重复的代码,那么就能继续优化。 建造的过程不稳定,如果在某个建造者创建产品时,漏掉了一步,编译器也不会存在报错。(等于在组装电脑的时候少了某一个步骤,原因就是没有标准,这里标准就是要定义接口) 3.4 版本四继而继续进行优化: 创建一个建造者接口,把制作产品的具体步骤,稳定下来! 我们让建造者类,去实现建造者接口,接口中的方法步骤,类就必须都要实现,少实现一个抽象方法就会报错。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162class Computer { private String cpu; private String gpu; private String memery; private String hd; // ....}interface ComputerBuilder { void setCpu(); void setGpu(); void setMemery(); void setHd(); Computer computer();}// 通过接口 过程就会稳定下来class LowComputerBuilder implements ComputerBuilder { private Computer computer = new Computer(); @Override public void setCpu() { computer.setCpu(\"i7 7500u\"); } @Override public void setGpu() { computer.setGpu(\"gtx940m\"); } @Override public void setMemery() { computer.setMemery(\"2g\"); } @Override public void setHd() { computer.setHd(\"500g机械\"); } @Override public Computer computer() { return computer; }}/** 省略了上面其他两种电脑的创建**/// =========================================================public class AppTest { public static void main(String[] args) { // 只以低配版的电脑建造为例 ComputerBuilder c = new LowComputerBuilder(); c.setCpu(); c.setGpu(); c.setHd(); c.setMemery(); Computer computer = c.computer(); System.out.println(computer); }} 目前这种方式,将电脑创建的过程稳定下来,因为我们让具体电脑的类去实现接口,而接口的功能是稳定的,要想使用这个类,必须要实现定义好的接口的全部方法。同时,如果客户端想要扩展电脑的种类,只需要实现ComputerBuilder接口即可,自己定义一个Builder。 优点: 建造者类中的建造过程是稳定的,不会漏掉某一步!当客户端需要扩展建造者时,也不会漏掉一步。 缺点: 代码仍然存在重复 现在又变成了客户端自己去配置电脑(在客户端处,又要进行一系列的set方法),又违反了迪米特法则。(相当于去配置电脑,虽然不需要亲自去组装–配置型号等问题,但是必须”指挥”装机者进行装机…) 3.5 最终版本12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970class Computer { private String cpu; private String gpu; private String memery; private String hd; // ....}interface ComputerBuilder { void setCpu(); void setGpu(); void setMemery(); void setHd(); Computer computer();}class Director { // 作用是将指挥的过程从客户端中分离出来,交给服务端来实现 public Computer build(ComputerBuilder cb) { cb.setCpu(); cb.setGpu(); cb.setMemery(); cb.setHd(); return cb.computer(); }}// 通过接口 过程就会稳定下来class LowComputerBuilder implements ComputerBuilder { private Computer computer = new Computer(); @Override public void setCpu() { computer.setCpu(\"i7 7500u\"); } @Override public void setGpu() { computer.setGpu(\"gtx940m\"); } @Override public void setMemery() { computer.setMemery(\"2g\"); } @Override public void setHd() { computer.setHd(\"500g机械\"); } @Override public Computer computer() { return computer; }}/** 省略了上面其他两种电脑的创建**/// =========================================================public class AppTest { public static void main(String[] args) { // 只以低配版的电脑建造为例 ComputerBuilder c = new LowComputerBuilder(); // 设定需求 Director director = new Director(); // 这里相当于把需求告诉了指挥者,指挥者进行装机过程,该过程多客户端是不可见的,完成了封装 Computer computer = director.build(c); System.out.println(computer); }} 优点: 创建对象的过程是稳定不变的(因为有ComputerBuilder接口来稳定过程) 创建对象的过程只写了一次,没有重复代码(指挥者完成) 当需要扩展指挥者的时候,不需要修改之前的代码,符合了开闭原则。 3.5 类图 Builder完成我们具体的需求,即创建我们所需的对象,但最后由Director指挥者设定具体的过程;它是通过Builder建立对象,Director来将指挥的过程从客户端中分离出来,交给服务端来实现。 四、总结建造者与工厂模式的区别: 工厂模式只需要一个简单的new,new出产品即可; 建造者更注重,在new出产品之后的,为产品属性赋值的过程; 建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的对象都一样; 从最后的代码改进中可以看出,builder用来定义对象的属性(定义对象的模板),而Director将对对象进行属性的初始化,给对象进行装配。所以,当需要进行扩展时,只需要实现一个builder类,定义对象的属性模板,然后交给Director去进行初始化,完成对象的封装。 Builder用来定义对象的模板,而Director用于将对象初始化,给对象进行装配。最终达到了,对象的创建和对象的属性装配进行了分离。使得不稳定的创建过程,达到了稳定的效果。","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"建造者模式","slug":"建造者模式","permalink":"https://chemlez.github.io/tags/%E5%BB%BA%E9%80%A0%E8%80%85%E6%A8%A1%E5%BC%8F/"}]},{"title":"谈一谈对ThreadLocal的理解","slug":"谈一谈对ThreadLocal的理解","date":"2021-08-22T08:19:25.000Z","updated":"2021-08-30T08:54:41.892Z","comments":true,"path":"2021/08/22/谈一谈对ThreadLocal的理解/","link":"","permalink":"https://chemlez.github.io/2021/08/22/%E8%B0%88%E4%B8%80%E8%B0%88%E5%AF%B9ThreadLocal%E7%9A%84%E7%90%86%E8%A7%A3/","excerpt":"最近看面经经常能看到面试官对ThreadLocal方面的提问,于是就去翻了翻ThreadLocal的源码,发现源码并不长,大概看了一通,能看出其中的七七八八,打算借此来梳理一下。PS:HashMap的源码能看明白,看这个源码也不是问题。 一、基本认识点进源码,看见ThreadLocal是java.lang包下的类;在网上也看了看其他人对它的概括,加上自己对看完源码后对ThreadLocal的理解: 用于线程之间数据的隔离。简单说就是通过ThreadLocal来开辟一块区间存放数据,这个区间作为线程的本地线程存储,只有当前线程才能获取到这个数据,这个数据对其他线程是不可见的。 可以看到ThreadLocal的公有构造方法: 12public ThreadLocal() {} 就是用来创建ThreadLocal对象,没有在其中做其他的工作。 平常用ThreadLocal最多的两个方法就是set、get两个方法;那我们来看一看这两个方法的源码。","text":"最近看面经经常能看到面试官对ThreadLocal方面的提问,于是就去翻了翻ThreadLocal的源码,发现源码并不长,大概看了一通,能看出其中的七七八八,打算借此来梳理一下。PS:HashMap的源码能看明白,看这个源码也不是问题。 一、基本认识点进源码,看见ThreadLocal是java.lang包下的类;在网上也看了看其他人对它的概括,加上自己对看完源码后对ThreadLocal的理解: 用于线程之间数据的隔离。简单说就是通过ThreadLocal来开辟一块区间存放数据,这个区间作为线程的本地线程存储,只有当前线程才能获取到这个数据,这个数据对其他线程是不可见的。 可以看到ThreadLocal的公有构造方法: 12public ThreadLocal() {} 就是用来创建ThreadLocal对象,没有在其中做其他的工作。 平常用ThreadLocal最多的两个方法就是set、get两个方法;那我们来看一看这两个方法的源码。 二、set方法探析12345678public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value);} 首先在使用set方法时,能够看到它先通过Thread的静态方法,来获取到当前线程。这个很关键,因为后面的操作,都是基于当前线程进行的操作,这也是为什么ThreadLocal能够做到线程之间数据隔离的。具体如何做到的,还要继续往下面看。 获取到当前线程后,通过getMap(t)方法来获取到一个ThreadLocalMap类型的map对象;来,点进getMap方法看看里面是啥: 123ThreadLocalMap getMap(Thread t) { return t.threadLocals;} 返回的是当前这个线程的属性——threadLocals,继续点进: 1ThreadLocal.ThreadLocalMap threadLocals = null; 可以看到初始时,这个线程的该属性为null;现在先不管ThreadLocalMap具体是什么,只要明白该类可以由实例化的一个线程内部的属性指向ThreadLocalMap的实例即可。 下面的逻辑就比较简单了,如果map不为null,就能将value设置到map中,可以看见key是由当前的ThreadLocal对象作为的key;而值是由我们传递的value;如果为null,则进行createMap操作,从方法名也能看出,是先创建出这个map,然后后续再进行赋值的操作。 好了,可以看出,搞明白这个ThreadLocalMap是个啥,对我们很关键。那么下面我们继续看看ThreadLocalMap: 123456789101112131415161718192021222324static class ThreadLocalMap { private Entry[] table; private static final int INITIAL_CAPACITY = 16; static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }} 这里只列出了该类的部分方法和属性,如果谈到其他的方法和属性,到时再列出来。 首先该类是ThreadLocal中的静态类,诶,那为什么Thread也能访问到呢??别忘了,我们开头就提到ThreadLocal是java.lang包下的类,当然ThreadLocalMap同样是,而Thread也是,所以该类对Thread具有包可见性。 能够看到它其中一个属性为Entry继承了WeakReference,弱引用。也就是说正常情况下,Entry对象的生命周期只能存活到下一次垃圾收集为止。这里Entry是作为ThreadLocalMap中的table数组,也就是map用来散列的table; 通过其构造方法,能够看出去。将table设置为容量为16,然后通过key的hash来计算散列到的位置i;然后创建Entry节点设置到table[i]处,最后设定table此时的容量为1并设定阈值;设定阈值的方法就不继续看了,就是为了后续方便扩容的判断。到这里大致能看明白ThreadLocalMap的数据结构大致长什么样了; 再回到上面createMap方法处: 123void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);} 创建ThreadLocalMap,并以当前线程引用这个Map;同时,当前的ThreadLocal对象作为key,我们传入的value作为Entry节点的value。此时结构如下图所示: 这个时候,我们再看一看当map已经存在时,调用map.set(this,value)时的操作: 1234567891011121314151617181920212223242526private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; // 首先引用当前ThreadLocalMap中的table int len = tab.length; // 计算table的长度 int i = key.threadLocalHashCode & (len-1); // 除留余数法 计算出关键字应该插入的位置 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 发生冲突 进行探查/比较 ThreadLocal<?> k = e.get(); if (k == key) { // 与当前探查的key相等直接替换 entry中的值 返回即可 e.value = value; return; } if (k == null) { // 当前探查的key为Null 表明存在过期的entry,那么就开始在set的过程中清除这些过期的entry replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; // hashtable中元素增加一个 if (!cleanSomeSlots(i, sz) && sz >= threshold) // 从当前插入位置探查 rehash(); // rehash操作其中,包含删除旧条目及扩容操作} 其实就是hash的插入方式,通过hash计算到索引的位置,也就是table中的位置,只是这里解决哈希冲突的方式是通过线性探查法进行冲突的解决(HashMap中是通过拉链法进行的解决)。 其中整个for循环的逻辑是,根据hash函数定位到关键字的位置后,如果当前table[i]上面存在了元素,那么表明可能发生了冲突或者就是同一个key,进入for循环中,一个一个判断是否插入的key和冲突的key相等,如果相等那就直接替换该entry上的value; 如果在探查过程中发现table中的key为null时:这块逻辑比较复杂,目前读源码理解的是,将过时的entry进行删除,用没有过时的key-value进行覆盖,目的是为了防止在后续线程探查解决冲突的时候,冲突的key在数组中是离散的并不是紧凑的; 最后当探查到一个不存在entry时,表明查找到了应该插入的位置,那么就新建一个entry节点,并将该entry节点设置到该位置上。下面是清除过期entry的逻辑: 123456789101112131415private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; // 当前的entry节点 if (e != null && e.get() == null) { // 如果key不为Null,但是value为Null n = len; removed = true; // 标记 存在清除过程 i = expungeStaleEntry(i); // 移除这个过期的entry节点 } } while ( (n >>>= 1) != 0); // 每次 除2 探查,采用启发式扫描探查过期的entry return removed;} 三、get方法探析大致明白了set方法后,再看get方法会比较轻松。 12345678910111213public T get() { Thread t = Thread.currentThread(); // 获取到当前线程 ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); // 通过key获取到value if (e != null) { @SuppressWarnings(\"unchecked\") T result = (T)e.value; return result; } } return setInitialValue();} 同样的获取到当前线程,并且从当前线程中获取到ThreadLocalMap;我们之前看到ThreadLocalMap中set值时,是将ThreadLocal作为key,所以我们通过将ThreadLocal传入获取到value;其中具体的逻辑就不看了,和set差不多,就是通过关键字定位进行比较,线性探查法那一套逻辑。 四、一些细节4.1 ThreadLocalMap中的相关参数12345678910111213141516/*** The number of entries in the table.*/private int size = 0; // table中初始容量为0/*** The next size value at which to resize.*/private int threshold; // Default to 0;扩容的阈值/*** Set the resize threshold to maintain at worst a 2/3 load factor.*/private void setThreshold(int len) { // 装载因子为 2 / 3 threshold = len * 2 / 3;} 4.2 ThreadLocal中的hash问题我们知道这里的ThreadLocalMap是通过计算关键字key的hash进行散列的,而key就是不同的ThreadLocal对象,所以看看系统是如何分配这些散列值的。 12345678910private final int threadLocalHashCode = nextHashCode();private static AtomicInteger nextHashCode = new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647; // 固定值private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);} 开始时ThreadLocal存在一个final静态变量HASH_INCREMENT;每次实例化ThreadLocal时,都带调用nextHashCode()方法,继而调用原子类的getAndAdd()方法,即: 123public final int getAndAdd(int delta) { // delta -> HASH_INCREMENT -> 0x61c88647 return unsafe.getAndAddInt(this, valueOffset, delta);} 所以看到是通过cas操作,给每个ThreadLocal对象加上固定的值——HASH_INCREMENT -> 0x61c88647;所以每个ThreadLocal对象的hash值是0x61c88647倍数; 4.3 ThreadLocalMap的扩容操作123456789101112131415161718192021222324252627282930/** * Double the capacity of the table. */private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; // 两倍扩容 Entry[] newTab = new Entry[newLen]; int count = 0; for (int j = 0; j < oldLen; ++j) { // 进行节点的搬迁 Entry e = oldTab[j]; if (e != null) { // entry不为空进行搬迁 ThreadLocal<?> k = e.get(); if (k == null) { // 如果key 为null,表示过期 e.value = null; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); // 重新根据hash计算散列的位置 while (newTab[h] != null) // 发生冲突,线性探查位置 h = nextIndex(h, newLen); newTab[h] = e; count++; // 计数 } } } setThreshold(newLen); // 设置新的阈值 size = count; table = newTab; // 新table代替旧table} 通过注释也能看出,每次进行两倍的扩容。 五、Thread/ThreadLocal/ThreadLocalMap三者之间的关系通过一张图进行说明: 上面表明两个线程,Thread-1,Thread-2;创建了三个ThreadLocal对象;每个Thread都有自己的ThreadLocalMap;而一个ThreadLocal对于一个Thread只能存放一个变量;所以每个线程的ThreadLocalMap中的key是不同的ThreadLocal;所以,看到ThreadLocal是每个线程可以共用的,但是由于相同的ThreadLocal是作为每个线程中独有的ThreadLocalMap中的key;所以,在不同的线程中,通过ThreadLocal获取到值不一样;是通过给线程设定独有的ThreadLocalMap来进行的数据隔离。 六、简单的例子1234567891011121314151617181920212223242526272829303132private static ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(16);@Testpublic void f1() throws InterruptedException { ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); ThreadLocal<String> threadLocal2 = new ThreadLocal<>(); new Thread(() -> { threadLocal1.set(\"张四\"); threadLocal2.set(\"李五\"); System.out.println(Thread.currentThread().getName()+\":\" + threadLocal1.get()); System.out.println(Thread.currentThread().getName()+\":\" + threadLocal2.get()); },\"Thread-1\").start(); Thread.sleep(10); new Thread(() -> { System.out.println(Thread.currentThread().getName()+\":\" + threadLocal1.get()); System.out.println(Thread.currentThread().getName()+\":\" + threadLocal2.get()); System.out.println(\"==========\"); threadLocal1.set(\"张四\"); threadLocal2.set(\"李五\"); System.out.println(Thread.currentThread().getName()+\":\" + threadLocal1.get()); System.out.println(Thread.currentThread().getName()+\":\" + threadLocal2.get()); },\"Thread-2\").start();}结果:Thread-1:张三Thread-1:李四Thread-2:nullThread-2:null==========Thread-2:张四Thread-2:李五 看到在Thread-1中设置的值,在Thread-2中并获取不到。 七、ThreadLocalMap中的弱引用可以看到ThreadLocalMap中的Entry节点继承了弱引用引用关系的ThreadLocal;也就是说ThreadLocalMap中的key是弱引用,当他不再关联强引用关系时,在下一次垃圾回收时,便会被回收;但是,其中的value是具有强引用关系的,因此会出现key为null,但是value不为null的情况,也就是说entry节点不为null,而强引用关系的value,它的生命周期同线程的生命周期,因此如果不手动设置为null,会造成内存泄露,继而会引发内存溢出。 在前面的set、get等方法中,也能看到,他们在遍历的时候,如果发现key为Null的情况,便会进行清除工作,即将value设置为null; 因此[1]: 由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。 但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。 因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。 正确使用方式: 每次使用完ThreadLocal都调用它的remove()方法清除数据; 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。 八、使用示例需求:在SpringBoot中自定义一个日志类,用于记录一个接口执行花费的时间。 思路:首先采用注解类,对于标志了注解的接口,统计该接口的一个请求所花费的时间;而对该接口的解析,通过设定过滤器,拦截每一个请求,检查请求的接口是否注解了该接口,如果采用了该注解那么就对该请求进行处理。 注解类: 1234567891011import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.METHOD) // 作用在方法上@Retention(RetentionPolicy.RUNTIME) // 运行时public @interface MyLog { String desc() default \"\"; // 类的描述信息} Interceptor: 1234567891011121314151617181920212223242526272829303132333435363738394041424344/** * 日志拦截器 */public class MyLogIntercepter extends HandlerInterceptorAdapter { // 采用ThreadLocal来隔离每个请求接口的线程 private static final ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; // 执行的方法 Method method = handlerMethod.getMethod(); // 获取方法对象 MyLog myLog = method.getAnnotation(MyLog.class); // 获取方法上的注解 if (myLog != null) { // 注解不为空 long startTime = System.currentTimeMillis(); // 开始时间 startTimeThreadLocal.set(startTime); // 设置执行的时间 } return true; } /** * 后置处理器 */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; // 方法拦截器 Method method = handlerMethod.getMethod(); // 获得被拦截的方法对象 MyLog myLog = method.getAnnotation(MyLog.class); // 获取方法上的注解 if (myLog != null) { // 存在该注解 则需要进行日志记录 long endTime = System.currentTimeMillis(); Long startTime = startTimeThreadLocal.get(); long optTime = endTime - startTime; String requestURI = request.getRequestURI(); // URI String methodName = method.getDeclaringClass().getName() + \".\" + method.getName(); // 全类名.方法名 String desc = myLog.desc(); // 方法描述 System.out.println(\"请求uri: \" + requestURI); System.out.println(\"请求方法名:\" + methodName); System.out.println(\"方法描述:\" + desc); System.out.println(\"方法执行时间:\" + optTime + \"ms\"); } }} 添加拦截器到配置中: 1234567891011121314151617/** * 这是一个自动配置类,容器启动时,将MyLogAutoConfiguration注入到容器中 */@Configurationpublic class MyLogAutoConfiguration implements WebMvcConfigurer { /** * 想容器中注册新的拦截器 * 这个拦截器的作用:统计加入MyLog注解中的方法一个执行周期内的花费的时间 * * @param registry: 被注册的拦截器 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyLogIntercepter()).addPathPatterns(\"/**\").excludePathPatterns(\"/images/**\", \"/js/**\", \"/css/**\", \"/toLogin\", \"/login\", \"/bootstrap-3.3.7-dist/**\", \"/favicon.ico\", \"/bizhi.jpg\"); }} 将自定义的过滤器进行注册,并设定拦截的路径;","categories":[{"name":"多线程","slug":"多线程","permalink":"https://chemlez.github.io/categories/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"}],"tags":[{"name":"多线程","slug":"多线程","permalink":"https://chemlez.github.io/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"},{"name":"ThreadLocal","slug":"ThreadLocal","permalink":"https://chemlez.github.io/tags/ThreadLocal/"}]},{"title":"设计模式之适配器模式","slug":"设计模式之适配器模式","date":"2021-07-10T11:17:31.000Z","updated":"2021-08-30T07:38:09.214Z","comments":true,"path":"2021/07/10/设计模式之适配器模式/","link":"","permalink":"https://chemlez.github.io/2021/07/10/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F/","excerpt":"一、介绍概念:一个类的接口转换成客户端希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。即根据已有的接口,生成想要的接口。 先看下面这个小例子体会一下适配器模式: 123456789101112131415161718192021222324252627282930313233public class AppTest { /** * * 符合开闭原则,源代码没有进行修改 * 也符合组合优于继承原则(只是进行复用) */ public static void main(String[] args) { Calculate calculate = new Calculate(); // 原来的接口是接两个 CalcAdapter calcAdapter = new CalcAdapter(calculate); // 做个适配器,可以接收三个 int add = calcAdapter.add(1, 2, 3); System.out.println(add); }}// 基本计算器class Calculate { public int add(int a, int b) { return a + b; }}// 变化来了,客户想要计算三个数的和 - 采用适配器模式的方式,将接口转换成我们需要的形式class CalcAdapter { private Calculate calculate; // 复用其功能,采用组合的方式。组合需要被是适配的类 public CalcAdapter(Calculate calculate) { this.calculate = calculate; } public int add(int a, int b, int c) { return calculate.add(a, calculate.add(b, c)); // 做到对功能的复用 }}","text":"一、介绍概念:一个类的接口转换成客户端希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。即根据已有的接口,生成想要的接口。 先看下面这个小例子体会一下适配器模式: 123456789101112131415161718192021222324252627282930313233public class AppTest { /** * * 符合开闭原则,源代码没有进行修改 * 也符合组合优于继承原则(只是进行复用) */ public static void main(String[] args) { Calculate calculate = new Calculate(); // 原来的接口是接两个 CalcAdapter calcAdapter = new CalcAdapter(calculate); // 做个适配器,可以接收三个 int add = calcAdapter.add(1, 2, 3); System.out.println(add); }}// 基本计算器class Calculate { public int add(int a, int b) { return a + b; }}// 变化来了,客户想要计算三个数的和 - 采用适配器模式的方式,将接口转换成我们需要的形式class CalcAdapter { private Calculate calculate; // 复用其功能,采用组合的方式。组合需要被是适配的类 public CalcAdapter(Calculate calculate) { this.calculate = calculate; } public int add(int a, int b, int c) { return calculate.add(a, calculate.add(b, c)); // 做到对功能的复用 }} 二、实例该章节中对适配器模式的讲解,采用Java编程思想的实例,进行介绍。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253class Processor { public String name() { return getClass().getSimpleName(); } Object process(Object input) { return input; }}class Upcase extends Processor { @Override String process(Object input) { // 重写中,返回值可以进行向上转型 return ((String) input).toUpperCase(); }}class DownCase extends Processor { @Override String process(Object input) { // 重写中,返回值可以进行向上转型 return ((String) input).toLowerCase(); }}class Splitter extends Processor { @Override String process(Object input) { // 重写中,返回值可以进行向上转型 return Arrays.toString(((String) input).split(\" \")); }}class Apply { public static void process(Processor p, Object s) { System.out.println(\"Using Process: \" + p.name()); System.out.println(p.process(s)); }}public class AppTest { public static void main(String[] args) { String s = \"How are you\"; Apply.process(new Upcase(), s); Apply.process(new DownCase(), s); Apply.process(new Splitter(), s); }}=== 结果 ===Using Process: UpcaseHOW ARE YOUUsing Process: DownCasehow are youUsing Process: Splitter[How are you] 该段代码中,主要是为了复用Processor类中的process方法,对process的方法采用三种不同实现方式,即设计三个不同的子类。 现在又发现了一个跟process功能相似的类: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455class Waveform { private static long counter; private final long id = counter++; @Override public String toString() { return \"waveform\" + id; }}class Filter { public String name() { return getClass().getSimpleName(); } public Waveform process(Waveform input) { return input; }}class LowPass extends Filter { double cutoff; public LowPass(double cutoff) { this.cutoff = cutoff; } public Waveform process(Waveform input) { return input; // 对该input的 Dummy processing }}class HighPass extends Filter { double cutoff; public HighPass(double cutoff) { this.cutoff = cutoff; } public Waveform process(Waveform input) { return input; // 对该input的 Dummy processing }}class BandPass extends Filter { double cutoff; public BandPass(double cutoff) { this.cutoff = cutoff; } public Waveform process(Waveform input) { return input; // 对该input的 Dummy processing }} 从这段代码中,我们可以看到Filter和前面我们自己定义的Processor类似,同样的有三个子类也同Processor的子类。 此时,我们如果想要复用Apply的方法,参数却无法传入。因为,Apply参数接收的是Processor类及其子类。这个时候,你肯定想,那就直接将Filter继承Processor类,让其成为子类不就好了?但是,不要忘记,Filter代码是别人的代码,我们无法对其进行修改。同时,即使进行了修改,那也违反了开闭原则。 所以我们需要用接口的方式,让Apply接收的是接口: 12345678910111213141516171819202122232425262728293031323334interface Processor { String name(); Object process(Object input);}abstract class AbstractProcess implements Processor { @Override public String name() { return getClass().getSimpleName(); }}class Upcase extends AbstractProcess { @Override public String process(Object input) { // 重写中,返回值可以进行向上转型 return ((String) input).toUpperCase(); }}class DownCase extends AbstractProcess { @Override public String process(Object input) { // 重写中,返回值可以进行向上转型 return ((String) input).toLowerCase(); }}class Splitter extends AbstractProcess { @Override public String process(Object input) { // 重写中,返回值可以进行向上转型 return Arrays.toString(((String) input).split(\" \")); }} 在上面中,我们将Process设计成了接口,并且使用了一个AbstractProcess抽象类,实现这个接口,再用子类去继承这个抽象类。设计这个抽象类的原因是,子类如果直接实现Process接口,那么name()这个方法都是重复实现,所以直接使用抽象类进行该方法的实现,即对一些固定功能进行实现,再设计子类直接继承这个抽象类对process方法进行自己所需要的实现。所以也能看见,抽象类是对接口中固定功能的实现进行抽取,即定义出一些模板方法,这样子类就不需要进行重复实现了。 现在继续看Apply类: 123456class Apply { public static void process(Processor p, Object s) { System.out.println(\"Using Process: \" + p.name()); System.out.println(p.process(s)); }} 此时接收的参数Process是接口,表明只要是实现了该接口的类及其实现类的子类都能被传入。那么我们该如何传入这个参数呢?即采用适配器模式。设计一个适配器去实现该接口,将被适配的类和适配器进行关联,而适配器作为接口的具体的实现,这样适配器就能够被作为参数进行传入,而具体使用的是被适配类的方法。代码如下: 1234567891011121314151617181920class FilterAdapter implements Processor { private Filter filter; // 和被适配的对象产生关联关系 public FilterAdapter(Filter filter) { this.filter = filter; } @Override public String name() { return filter.name(); } @Override public Object process(Object input) { Waveform process = filter.process((Waveform) input); // 真正调用的process方法,还是所发现的类中的process方法;目的是让发现的类的process能够适配我们自己的process接口,目的是让接口复用;而不用再写一个接口 return process; }} 此时我们就可以直接复用process代码: 12345678910111213141516public class AppTest { public static void main(String[] args) { String s = \"How are you\"; // 当调用process方法时,传入的是适配器,调用适配的process方法,最终是调用的Filter的process方法,而调用p.name(),最终也是调用的filter的filter.name()方法,这样就能让我们所要适配的类去使用之前已经实现的代码了 Apply.process(new FilterAdapter(new LowPass(1)), new Waveform()); Apply.process(new FilterAdapter(new HighPass(1)), new Waveform()); Apply.process(new FilterAdapter(new BandPass(1)), new Waveform()); }}===结果===Using Process: LowPasswaveform: [0]Using Process: HighPasswaveform: [1]Using Process: BandPasswaveform: [2] 适配器实现接口而没有继承类的原因是,如果使用继承,后面就不能再继承其它类了,而接口可以进行多实现,也可以看出接口比继承有着更宽松的规则(不仅可以是接口的实现类,其实现类的子类也能够作为参数进行传递)。 如果,我们不使用适配器,而是直接使用这个类会发生什么? 123456789101112public class AppTest { public static void main(String[] args) { Filter lowPass = new LowPass(1); System.out.println(lowPass.name()); System.out.println(lowPass.process(new Waveform())); Filter highPass = new HighPass(1); System.out.println(highPass.name()); System.out.println(highPass.process(new Waveform())); Filter bandPass= new BandPass(1); System.out.println(bandPass.name()); System.out.println(bandPass.process(new Waveform()));} 无法使用Apply类提供的方法,明明所实现的都是一样的,可还需要多写一套代码,使用适配器,将需要被使用类的接口,转换成能够被适配的接口。 三、使用步骤自定义适配器类,适配器继承被适配的接口,自定义的适配器类中组合被适配的类,在实现的方法中调用被适配类的方法。","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"适配器模式","slug":"适配器模式","permalink":"https://chemlez.github.io/tags/%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F/"}]},{"title":"设计模式之开闭原则及迪米特法则","slug":"设计模式之开闭原则","date":"2021-07-10T05:51:17.000Z","updated":"2021-08-30T06:34:51.447Z","comments":true,"path":"2021/07/10/设计模式之开闭原则/","link":"","permalink":"https://chemlez.github.io/2021/07/10/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E5%BC%80%E9%97%AD%E5%8E%9F%E5%88%99/","excerpt":"一、开闭原则1.1 概念开闭原则是设计模式中最重要也是最基本的原则,可以说其它的原则都是为了开闭原则进行服务的。 核心概念: 一个软件实体如类,模块和函数应该对扩展开放(对提供方),对修改关闭(对使用方)。对抽象构建框架,用实现扩展细节。 当软件需要实现时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。 编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。 综合:对扩展新功能是开放的,对修改原有功能是关闭的。也就说,在对功能进行扩展时,是对原有的类上进行扩展(继承、接口设计等等)","text":"一、开闭原则1.1 概念开闭原则是设计模式中最重要也是最基本的原则,可以说其它的原则都是为了开闭原则进行服务的。 核心概念: 一个软件实体如类,模块和函数应该对扩展开放(对提供方),对修改关闭(对使用方)。对抽象构建框架,用实现扩展细节。 当软件需要实现时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。 编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。 综合:对扩展新功能是开放的,对修改原有功能是关闭的。也就说,在对功能进行扩展时,是对原有的类上进行扩展(继承、接口设计等等) 1.2 实例演示需求:商家可以卖不同类型的车以及各种促销活动。 对Car的类设计: 1234567public class Car { private String brand; private double price; private String color; private boolean louYou; // 省略getter和setter及toString()方法} 现在对该类的正常使用,表明正常构造的一辆车。 12345678910public class AppTest { public static void main(String[] args) { Car car = new Car(); car.setBrand(\"奔驰\"); car.setColor(\"black\"); car.setLouYou(true); car.setPrice(6666666); System.out.println(car); }} 现在变化出现了。如果商家需要对,所有车进行打折、促销活动,在原价格的基础上*0.8。 此时,要符合开闭原则,就是对修改关闭,对扩展开放。所以,我们不能在源代码上进行修改,而是在对源代码不改变的基础上,进行类功能的扩展。所以,我们可以扩展出一个DiscountCar来继承Car,即对Car的功能的扩展。 123456public class DiscountCar extends Car { @Override public double getPrice() { return super.getPrice()*0.8; }} 此时: 12345678910public class AppTest { public static void main(String[] args) { Car car = new DiscountCar(); // 向上转型,调用方法,只和new 的对象有关 car.setBrand(\"奔驰\"); car.setColor(\"black\"); car.setLouYou(true); car.setPrice(6666666); System.out.println(car); }} 这里的DiscountCar是对源代码的扩展,而没有修改源代码,所以符合开闭原则。 二、迪米特法则2.1 介绍迪米特法则也叫最少知道原则。即一个类,对于其他类,要知道的越少越好,只和朋友进行通信。 一个对象应该对其他对象保持最少的了解 类与类的关系越密切,耦合度越大 迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public 方法,不对外泄露任何信息。 直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。 具体看一看什么不是朋友。 1234567891011121314151617181920public class Demo { public void test(B b) { A a = b.getA(); // 此时a是局部变量就不是test方法中的朋友,不能直接使用a的方法 // 如果要想使用a,该怎么办? f1(a);// 即将对其使用的逻辑再进行封装成一个方法,将其作为朋友传入。此时a就是f1中的朋友,可以对其直接使用 } public void f1(A a){ // 处理逻辑 此时a是f1的朋友,可以任意调用其方法 }}class A { }class B { public A getA() { return new A(); }} 2.2 实例需求:给用户生产一台电脑供用户使用(只实现简单的关机功能)。 看下面这段代码: 12345678910111213141516171819202122232425262728293031323334public class AppTest {}class Computer { public void saveDate() { System.out.println(\"保存数据\"); } public void killProcess() { System.out.println(\"关闭程序\"); } public void closeScreen() { System.out.println(\"关闭屏幕\"); } public void powerOff() { System.out.println(\"断电\"); }}class Person { // private Computer computer = new Computer(); // 此时,这个Person对于Computer的细节就知道的太多了。实际意义是:让用户关个机,他自己需要做那么多步骤才能够关机 // 对于Person,只需要知道,关机按钮在哪里就行了,不需要知道如何保存数据,如果关闭程序... // 这样的话,代码的复杂度就提升了,万一用户操作错误,就会出现错误 public void shutDown() { computer.saveDate(); computer.killProcess(); computer.closeScreen(); computer.powerOff(); }} 将生产的电脑交给用户使用,但是对于用户来说,它知道的太多了,就是说这个用户拿到电脑时,要想对电脑关机,需要按下四个按钮才能将电脑关闭。 正确的设计,就是将上面的四个功能封装到一个接口中,交给用户这一个接口使用就可以了。 123456789101112131415161718192021222324252627282930313233343536public class AppTest { }class Computer { public void saveDate() { System.out.println(\"保存数据\"); } public void killProcess() { System.out.println(\"关闭程序\"); } public void closeScreen() { System.out.println(\"关闭屏幕\"); } public void powerOff() { System.out.println(\"断电\"); } // 直接将上面的关机步骤进行封装 public void shutDown() { saveDate(); killProcess(); closeScreen(); powerOff(); }}class Person { // private Computer computer = new Computer(); public void shutDown() { computer.shutDown(); // 直接点击关机按钮即可 }} 2.3 注意事项和细节 迪米特法则的核心是降低类之间的耦合。 但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系, 并不是要求完全没有依赖关系。 其实迪米特法则讲究的就是一个封装。","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"开闭原则","slug":"开闭原则","permalink":"https://chemlez.github.io/tags/%E5%BC%80%E9%97%AD%E5%8E%9F%E5%88%99/"}]},{"title":"设计模式之工厂模式","slug":"设计模式之工厂模式","date":"2021-05-26T13:21:47.000Z","updated":"2021-08-30T07:42:19.949Z","comments":true,"path":"2021/05/26/设计模式之工厂模式/","link":"","permalink":"https://chemlez.github.io/2021/05/26/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F/","excerpt":"本文用来记录工厂模式中,简单工厂、工厂方法和抽象工厂。 一、前置知识 上图中,每个功能可以看成一个模块,每个模块要想能正常、更好的使用,一定也会依赖其他的模块,所以需要满足: 每个模块负责自己的职责(单一职责),各个模块之间通过接口隔离原则对外暴露功能的使用(接口隔离原则)。 每个模块都应该”承诺”自己对外暴露的接口是不变的。当模块内部发生变化时,其他模块是不需要知道的。这便是依赖于抽象而不依赖于实现(依赖倒置原则) 上层模块只需要知道下层模块暴露出的接口即可,至于实现细节不需要也不应该知道。(迪米特法则) 为了对下面的实例代码进行演示,先明确两个概念: 产品:对应着类。 抽象产品:抽象类或接口 需求: 设计一个食物的项目,便于食物种类的扩展,且便于维护。 食物存在各种各样的种类 客户端可以对其进行扩展自己所需要的食物。","text":"本文用来记录工厂模式中,简单工厂、工厂方法和抽象工厂。 一、前置知识 上图中,每个功能可以看成一个模块,每个模块要想能正常、更好的使用,一定也会依赖其他的模块,所以需要满足: 每个模块负责自己的职责(单一职责),各个模块之间通过接口隔离原则对外暴露功能的使用(接口隔离原则)。 每个模块都应该”承诺”自己对外暴露的接口是不变的。当模块内部发生变化时,其他模块是不需要知道的。这便是依赖于抽象而不依赖于实现(依赖倒置原则) 上层模块只需要知道下层模块暴露出的接口即可,至于实现细节不需要也不应该知道。(迪米特法则) 为了对下面的实例代码进行演示,先明确两个概念: 产品:对应着类。 抽象产品:抽象类或接口 需求: 设计一个食物的项目,便于食物种类的扩展,且便于维护。 食物存在各种各样的种类 客户端可以对其进行扩展自己所需要的食物。 二、简单工厂123456789101112131415161718192021// 抽象产品interface Food { void eat();}// 具体产品class Hamburger implements Food { @Override public void eat() { System.out.println(\"吃汉堡包...\"); }}// ==================================服务端/客户端==========================================public class AppTest { public static void main(String[] args) { Food f = new Hamburger(); f.eat(); }} 上面中,存在抽象产品(Food),而具体产品依赖于抽象产品(对其进行实现)。抽象产品和具体产品都是由服务端提供,而客户端就是直接对其使用。 现在考虑如果服务端代码设计成这样会出现什么问题: 上面的代码中,如果服务端作者,修改了具体产品的产品名(Hamburger -> Hamburger2),那么客户端也需要将所有的名称进行修改。 这种设计相当脆弱!因为,作者(服务端)修改了具体产品的类名,那么客户端代码,也要随之一起改变。这样服务器端代码,和客户端代码就是耦合的,这违反了迪米特法则! 我们希望的效果是,无论服务器端代码如何修改,客户端代码都应该不知道,不用修改客户端的代码! 针对上面的问题,服务端一旦修改,客户端代码也要跟着修改!因此,服务单修改代码如下,使用简单工厂模式。 123456789101112131415161718192021222324252627282930313233343536373839404142434445// 抽象产品interface Food { void eat();}// 具体产品1class Hamburger implements Food { @Override public void eat() { System.out.println(\"吃汉堡包...\"); }}// 具体产品2class RiceNoodle implements Food { @Override public void eat() { System.out.println(\"吃过桥米线...\"); }}// 工厂class FoodFactory { public static Food getFood(int n) { Food food = null; switch (n) { case 1: food = new Hamburger(); break; case 2: food = new RiceNoodle(); } return food; }}// ==============================服务端/客户端==============================================public class AppTest { public static void main(String[] args) { Food f = FoodFactory.getFood(1); f.eat(); }} 采用这种方法,客户端获取具体的产品时,是通过工厂的获得的,即使服务端将产品的名字修改了,而客户端并不用进行修改,这样就达到一个解耦的目的。 简单工厂的优点: 把具体产品的类型,从客户端代码中,解耦出来。 服务器端,如果修改了具体产品的类名,客户端不用关心。 这符合了”面向接口编程”的思想,客户端对服务端暴露的接口进行使用(接口是趋于稳定的,不会随便修改名字之类的,前置中提到过每个模块都应该”承诺”自己对外暴露的接口是不变的)。 简单工厂的缺点: 客户端不得不死记硬背那些常量与具体产品的映射,比如:1对应汉堡包,2对应米线。 如果具体产品特别多,则简单工厂,就会变得十分臃肿。比如有100个具体产品,则需要在简单工厂的swich中写出100个case!。 最重要的是,变化来了:客户端需要扩展具体产品的时候,势必要修改简单工厂中的代码(修改工厂中的映射关系),这样便违反了”开闭原则”。 三、工厂方法根据简单工厂中所提出的确定,接下来采用工厂方法。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384// 抽象产品interface Food { void eat();}// 具体产品class Hamburger implements Food { @Override public void eat() { System.out.println(\"吃汉堡包...\"); }}class RiceNoodle implements Food { @Override public void eat() { System.out.println(\"吃过桥米线...\"); }}interface FoodFactory { Food getFood();}class HamburgerFactory implements FoodFactory { @Override public Food getFood() { return new Hamburger(); }}class RiceNoodleFactory implements FoodFactory { @Override public Food getFood() { return new RiceNoodle(); }}class Bussiness { public static void taste(FoodFactory foodFactory) { Food food = foodFactory.getFood(); System.out.println(\"评委1,品尝\"); food.eat(); System.out.println(\"评委2,品尝\"); food.eat(); System.out.println(\"评委3,品尝\"); food.eat(); }}// ==============================服务端/客户端==============================================// 从这里扩展出了新的功能(食物种类)class Lp implements Food { @Override public void eat() { System.out.println(\"吃凉皮...\"); }}// 新功能对应的工厂 - 通过工厂获得具体产品(该新功能对应的工厂,用于生产具体产品)class LpFactory implements FoodFactory { @Override public Food getFood() { return new Lp(); }}public class AppTest { public static void main(String[] args) { // 以下进行了替换,符合开闭原则 FoodFactory ff = new LpFactory(); Food food = ff.getFood(); food.eat(); Bussiness.taste(ff); }} 上面代码,工厂方法的思路: 每个抽象产品对应一个工厂接口,每个具体产品都有其对应的具体工厂的实现类对其进行返回。总的思路,工厂用于生产用户所需的产品,每个具体工厂实现抽象工厂的接口,即每个产品对应一个工厂实现,由这个具体的工厂实现返回所需的产品。 优点: 仍然具有简单工厂的优点,服务器端修改了具体产品的类名以后,客户端不知道 当客户端需要扩展一个新的产品时,不需要修改作者原来的代码,只是扩展一个新的工厂而已。 吐槽点: 我们已经知道,简单工厂也好,工厂方法也好,都有一个优点,就是服务器端的具体产品类名变换了以后,客户端不知道,但是,反观我们现在的代码,客户端依然依赖于具体的工厂的类名啊!此时,如果服务器端修改了具体工厂的类名,那么客户端也要随之一起修改。 解释: 工厂的名字,是视为接口的。作者有责任、义务,保证工厂的名字是稳定的。也就是说,虽然客户端依赖于工厂的具体类名,可是IT业内,所有工厂的名字都是趋向于稳定的(并不是说100%不会变的)。至少工厂类的名字,要比具体产品类的名字更加稳定! 既然产品是我们自己客户端扩展出来的,那为什么不直接自己实例化呢?毕竟这个扩展出来的Lp这个产品,我们自己就是作者。我们想怎么改类名自己都能把控!为什么还要为自己制作的产品做工厂呢? 解释: 因为,作者在开发功能时,不仅仅只会开发一些抽象产品、具体产品、对应的工厂,还会配套搭配一些提前做好的框架。比如: 在服务端有这么一个业务代码: 1234567891011class Bussiness { public static void taste(FoodFactory foodFactory) { Food food = foodFactory.getFood(); System.out.println(\"评委1,品尝\"); food.eat(); System.out.println(\"评委2,品尝\"); food.eat(); System.out.println(\"评委3,品尝\"); food.eat(); }} 这里的框架中,只能传入工厂,因此在客户端我们对代码进行扩展时,也要给出对应的工厂对象,才能使用这个功能,同时我们扩展的功能可能也会被作为服务器端拿给别人使用。 123456789101112131415161718192021222324252627282930313233// === 客户端 扩展出来的功能=== // 从这里扩展出了新的功能class Lp implements Food { @Override public void eat() { System.out.println(\"吃凉皮...\"); }}// 新功能对应的工厂 - 通过工厂获得具体产品class LpFactory implements FoodFactory { @Override public Food getFood() { return new Lp(); }}public class AppTest { public static void main(String[] args) { // FoodFactory ff = new RiceNoodleFactory(); // Food food = ff.getFood(); // food.eat(); // 以下进行了替换,符合开闭原则 FoodFactory ff = new LpFactory(); Food food = ff.getFood(); food.eat(); Bussiness.taste(ff); }} 现在制作出LpFatory,是为了能把LpFactory传入给Bussiness.taste方法,所以,必须定义这个LpFactory。那么为什么不从一开始,就让Bussiness.taste方法就直接接受Food参数呢?而不是现在的FoodFactory作为参数呢? 解释: 如果是直接传入食物(具体产品),那么在客户端是直接new出食物的对象传入,那么就又会回到,类名修改,违反迪米特法则,客户端也要跟着一起进行修改。 缺点: 如果有多个产品等级,那么工厂类的数量,就会爆炸式增长。(并且每个产品等级对应一个抽象产品和抽象工厂,一个具体产品又对应一个工厂类) 抽象工厂针对工厂方法的问题,当有多个产品等级时(食物、饮料、甜品…),工厂类就会有很多。 例如: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119// 抽象产品 - 食物产品等级interface Food { void eat();}// 具体产品class Hamburger implements Food { @Override public void eat() { System.out.println(\"吃汉堡包...\"); }}class RiceNoodle implements Food { @Override public void eat() { System.out.println(\"吃过桥米线...\"); }}interface FoodFactory { Food getFood();}class HamburgerFactory implements FoodFactory { @Override public Food getFood() { return new Hamburger(); }}class RiceNoodleFactory implements FoodFactory { @Override public Food getFood() { return new RiceNoodle(); }}// 饮料产品等级interface Drink { void drink();}class Cola implements Drink { @Override public void drink() { System.out.println(\"喝可乐...\"); }}class IcePeak implements Drink { @Override public void drink() { System.out.println(\"冰峰饮料...\"); }}interface DrinkFactory { Drink getDrink();}class ColaFactory implements DrinkFactory { @Override public Drink getDrink() { return new Cola(); }}public class IcePeakFactory implements DrinkFactory { @Override public Drink getDrink() { return new IcePeak(); }}class Bussiness { public static void taste(FoodFactory foodFactory) { Food food = foodFactory.getFood(); System.out.println(\"评委1,品尝\"); food.eat(); System.out.println(\"评委2,品尝\"); food.eat(); System.out.println(\"评委3,品尝\"); food.eat(); }}// ==============================服务端/客户端==============================================// 从这里扩展出了新的功能class Lp implements Food { @Override public void eat() { System.out.println(\"吃凉皮...\"); }}// 新功能对应的工厂 - 通过工厂获得具体产品class LpFactory implements FoodFactory { @Override public Food getFood() { return new Lp(); }}public class AppTest { public static void main(String[] args) { }} 这里多了一个Drink的产品等级,那么就需要多出具体产品的工厂实现类。 面对产品簇,使用工厂方法设计模式会造成类爆炸。上面的类图中存在两个产品等级,一个食物产品等级,一个饮料产品等级;其中一个产品等级对应一个工厂,一个工厂又存在多个产品工厂的实现类,用于生产产品。多一个产品等级,就会多出很多的类出来。 采用抽象工厂,将上面特定的工厂,进行抽象。即一个工厂能够生产多个产品等级(既能生产饮料,又能食物)。 代码改进如下: 服务端 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576// 抽象产品 - 食物产品等级interface Food { void eat();}// 具体产品class Hamburger implements Food { @Override public void eat() { System.out.println(\"吃汉堡包...\"); }}class RiceNoodle implements Food { @Override public void eat() { System.out.println(\"吃过桥米线...\"); }}interface Factory { Food getFood(); Drink getDrink();}class KFCFactory implements Factory { // 一个抽象工厂的实现类对应一个产品簇 @Override public Food getFood() { return new Hamburger(); } @Override public Drink getDrink() { return new Cola(); }}// 饮料产品等级interface Drink { void drink();}// 具体产品class Cola implements Drink { @Override public void drink() { System.out.println(\"喝可乐...\"); }}class IcePeak implements Drink { @Override public void drink() { System.out.println(\"冰峰饮料...\"); }}class SanQiFactory implements Factory { @Override public Food getFood() { return new RiceNoodle(); } @Override public Drink getDrink() { return new IcePeak(); }} 这里将Food和Drink进行抽象成一个工厂。而不是单纯是只生产一个产品的工厂。这里将食物工厂和饮料工厂进行了合并,从而减少了类。即:一个工厂生成多个产品等级,而不是一个产品等级。 优点: 仍然有简单工厂和工厂方法的优点 更重要的是,抽象工厂把工厂类的数量减少了!无论多少个产品等级,工厂就一套。从下面的抽象工厂的UML图中,可以看出,再多一个产品等级,工厂的数量却不会发生改变,多的就是那个产品等级自身的类,跟工厂数量无关。 吐槽点: 为什么三秦工厂时,就必须是米线搭配冰峰呢 解释: 抽象工厂中,可以生产多个产品,这多个产品之间,必须有内在的联系。同一个工厂中的产品都属于同一个产品簇!不能把不同产品簇的产品进行混合使用(该设计模式的特点)。 缺点: 当产品等级发生变化时(增加产品等级、删除产品等级),都要引起所有以前工厂代码的修改,这就违反了”开闭原则”。 在上面的图中,6mm螺丝和8mm螺母就形成了一个产品簇,而螺丝和螺母为各自的产品等级。这里6mmFactory只能生产6mm螺丝和6mm螺母。所以,他们之间的多个产品之间存在着必要的内在联系。 产品簇:多个有内在联系,或者是有逻辑关系的产品,这里Food和Drink可以组成一组产品簇。 产品等级:产品簇中的每一类产品,这里Food是一个产品等级,Drink是一个产品等级。 由下图说明: 产品簇存在内在联系,这里中一个工厂5个get方法,分别返回洗衣机…笔记本。一个工厂有五个实现类,分别是格力、海尔、美的、华为、腾讯5个工厂实现类。 从图上也能看出,产品簇可以无限扩展,符合开闭原则,但是如果增加产品等级,就会去修改源代码,这样违反了开闭原则。也就是说,当产品等级发生变化时,就会引起原来抽象工厂的内部变化,这样就破坏了开闭原则。 结论: 当产品等级比较固定时,可以考虑使用抽象工厂,否则不建议使用。 解决方法:通过Spring,动态工厂加反射。","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"工厂模式","slug":"工厂模式","permalink":"https://chemlez.github.io/tags/%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F/"}]},{"title":"设计模式之单例模式","slug":"设计模式之单例模式","date":"2021-05-26T06:14:09.000Z","updated":"2021-05-26T12:39:25.941Z","comments":true,"path":"2021/05/26/设计模式之单例模式/","link":"","permalink":"https://chemlez.github.io/2021/05/26/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/","excerpt":"本文用来介绍设计模式中的单例模式。 一、基本介绍所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例, 并且该类只提供一个取得其对象实例的方法(静态方法)。 并且常见的单例设计模式共有八种: 饿汉式(静态常量) 饿汉式(静态代码块的方式) 懒汉式(线程不安全) 懒汉式(线程安全,同步方法) 懒汉式(线程安全,同步代码块) 双重检查 静态内部类 枚举 接下来,我们将逐一介绍这八种单例设计模式。 单例模式设计步骤如下: 构造器私有化(防止new) 类的内部创建对象 向外暴露一个静态的公共方法。","text":"本文用来介绍设计模式中的单例模式。 一、基本介绍所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例, 并且该类只提供一个取得其对象实例的方法(静态方法)。 并且常见的单例设计模式共有八种: 饿汉式(静态常量) 饿汉式(静态代码块的方式) 懒汉式(线程不安全) 懒汉式(线程安全,同步方法) 懒汉式(线程安全,同步代码块) 双重检查 静态内部类 枚举 接下来,我们将逐一介绍这八种单例设计模式。 单例模式设计步骤如下: 构造器私有化(防止new) 类的内部创建对象 向外暴露一个静态的公共方法。 二、饿汉式2.1 饿汉式(静态常量)代码示例: 123456789101112131415161718192021222324252627public class SingletonTest01 { public static void main(String[] args) { // 测试 Singleton instance = Singleton.getInstance(); Singleton instance1 = Singleton.getInstance(); System.out.println(instance == instance1); System.out.println(\"instance.hashCode=\" + instance.hashCode()); System.out.println(\"instance1.hashCode=\" + instance1.hashCode()); }}class Singleton { private static Singleton singleton = new Singleton(); // 内部创建的对象 private Singleton() { // 构造器私有化 } public static Singleton getInstance() { // 外部调用的静态方法 return singleton; }}====输出====trueinstance.hashCode=1554874502instance1.hashCode=1554874502 总结: 优点:这种写法比较简单,就是在类装载的时候就完成实例化,避免了线程同步问题。 缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。 这种方式基于classloder机制避免了多线程的同步问题(若多个线程同时调用getInstance()方法时,就本例来说,如果不存在SingleTon实例对象,则会触发类的初始化。已经存在类初始化,则直接会去调用),不过,instance在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法, 但是导致类装载的原因有很多种,可能是通过其它的方式不小心触发了类的加载从而造成了对象的创建。因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 lazy loading 的效果。 因此,这种单例模式可用,可能造成内存浪费。 关于classloder机制避免了多线程的同步问题解释:当调用一个类的静态方法时,就会触发类加载(如果类未加载过则会对类进行加载),而在类加载阶段中的初始化阶段,JVM负责对类的静态变量赋值(程序员设定的值)操作(执行<clinit>()方法的过程)。而执行<clinit>()方法时,JVM必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的<clinit>(),其他线程都需要阻塞等待,直到活动线程执行完毕<clinit>()方法。所以在 1private static Singleton singleton = new Singleton(); // 内部创建的对象 类加载时,singleton只会被赋值一次。 2.2 饿汉式(静态代码块)采用静态代码的方式,是将new的方法直接方法在静态代码块中完成。 12345678910111213141516171819202122232425262728public class SingleTonTest02 { public static void main(String[] args) { // 测试 SingleTon instance = SingleTon.getInstance(); SingleTon instance1 = SingleTon.getInstance(); System.out.println(instance == instance1); System.out.println(\"instance.hashCode=\" + instance.hashCode()); System.out.println(\"instance1.hashCode=\" + instance1.hashCode()); }}class SingleTon { private SingleTon() { } private static SingleTon singleton; static { singleton = new SingleTon(); } public static SingleTon getInstance() { return singleton; }} 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候(<clinit()>方法同时也执行static静态代码块),就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。 结论:这种单例模式可用,但是可能造成内存浪费。 三、懒汉式3.1 懒汉式——线程不安全实现方式1234567891011121314class SingleTon { private static SingleTon singleTon; private SingleTon() { } public static SingleTon getInstance() { if (singleTon == null) { singleTon = new SingleTon(); } return singleTon; }} 总结: 1) 起到了 Lazy Loading 的效果,但是只能在单线程下使用。 2) 如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。 结论:在实际开发中,不要使用这种方式。 3.2 懒汉式——线程安全 加同步方法123456789101112131415class Singleton { private static Singleton singleton; private Singleton() { } public static synchronized Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; }} 总结: 解决了线程安全问题 效率太低了,每个线程在想获得类的实例时候,执行 getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。方法进行同步效率太低 结论:在实际开发中,不推荐使用这种方式。 3.3 懒汉式——线程安全 同步代码块错误演示: 1234567891011121314151617class Singleton { private static Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { singleton = new Singleton(); } } return singleton; }} 当一个线程进入到singleton==null还未进行实例化对象时,另外一个线程可能也进入条件singleton==null,这样实例对象就会被覆盖,同时该种方式也是线程不安全的。 正确演示: 1234567891011121314151617class Singleton { private static Singleton singleton; private Singleton() { } public static Singleton getInstance() { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } return singleton; }} 上面这个同步代码块是无论singleTon有没有被实例化,都需要进入代码块才能确定,因此这种方式效率也较低,不推荐。 结论:不推荐使用。 四、双重检查其实双重检查就是懒汉式线程不安全中同步代码块的变种,本质也是懒汉式。 12345678910111213141516171819class Singleton { private static volatile Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }} Double-Check 概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if (singleton == null)检查,这样就可以保证线程安全了。 这样,实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接 return 实例化对象,也避免的反复进行方法同步。 线程安全;延迟加载;效率较高。 结论:在实际开发中,推荐使用这种单例设计模式 关于使用valatile的原因,参见博客。双重检查锁单例模式为什么要用volatile关键字?。主要原因: 指令重排序;2. new操作非原子操作,因此发生指令重排时,产生线程不安全的问题。 new的关键字非原子操作: 12341 memory=allocate();// 分配内存 相当于c的malloc2 ctorInstanc(memory) //初始化对象3 instance=memory //设置instance指向刚分配的地址上面的代码在编译器运行时,可能会出现重排序 从1-2-3 排序为1-3-2 比如线程A获取到锁,进入到同步代码块时,并且执行字节码文件中的对象创建时,执行的顺序是1-3-2,执行了1-3,即还未对对象进行初始化操作;而此时线程B刚进入方法执行到if (singleton == null),那么此时由于线程A执行了3操作,有了地址,但是该对象是空的,而线程A拿到这个空对象进行返回,那这就发生了线程不安全的问题。 五、静态内部类1234567891011121314151617class SingleTon { private static volatile SingleTon singleTon; private SingleTon() { } // 静态内部类,该类中有一个静态属性 SingleTon private static class SingleTonInstance { // 该类的加载 通过 getInstance方法进行加载 private static final SingleTon INSTANCE = new SingleTon(); } public static SingleTon getInstance() { return SingleTonInstance.INSTANCE; }} 总结: 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。 静态内部类方式在SingleTon类被加载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletoInstance类,从而完成SingleTon的实例化。 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高 结论:推荐使用。 静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonInstance,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonInstance类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。 六、枚举方式借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。 12345678910111213141516public class SingleToTest { public static void main(String[] args) { SingleTon instance = SingleTon.INSTANCE; SingleTon instance1 = SingleTon.INSTANCE; System.out.println(instance == instance1); System.out.println(instance.hashCode()); System.out.println(instance1.hashCode()); }}enum SingleTon { INSTANCE; // 属性} 推荐使用。 七、总结 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new。 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session 工厂等)","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"单例模式","slug":"单例模式","permalink":"https://chemlez.github.io/tags/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/"}]},{"title":"设计模式-接口隔离原则","slug":"设计模式-接口隔离原则","date":"2021-05-25T02:32:00.000Z","updated":"2021-05-25T05:13:36.552Z","comments":true,"path":"2021/05/25/设计模式-接口隔离原则/","link":"","permalink":"https://chemlez.github.io/2021/05/25/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E6%8E%A5%E5%8F%A3%E9%9A%94%E7%A6%BB%E5%8E%9F%E5%88%99/","excerpt":"本章用来叙述设计模式中的接口隔离原则。叙述方式同样采用实例演示的方式。 一、基本介绍接口隔离原则的核心:客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。 二、实例介绍我们现在设计一个接口,接口中有5个功能,分别让B,D类实现这个接口;然后,我们再设计A,C类,其中A,C类分别依赖B,D类;A使用B类中的option1,option2,option3方法;C使用D类中option1,option4,option5方法。","text":"本章用来叙述设计模式中的接口隔离原则。叙述方式同样采用实例演示的方式。 一、基本介绍接口隔离原则的核心:客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。 二、实例介绍我们现在设计一个接口,接口中有5个功能,分别让B,D类实现这个接口;然后,我们再设计A,C类,其中A,C类分别依赖B,D类;A使用B类中的option1,option2,option3方法;C使用D类中option1,option4,option5方法。 将以上的情景展示成代码,就是: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110// 接口功能interface Interface1 { void option1(); void option2(); void option3(); void option4(); void option5();}// 实现类Bclass B implements Interface1 { @Override public void option1() { System.out.println(\"B...o1\"); } @Override public void option2() { System.out.println(\"B...o2\"); } @Override public void option3() { System.out.println(\"B...o3\"); } @Override public void option4() { System.out.println(\"B...o4\"); } @Override public void option5() { System.out.println(\"B...o5\"); }}// 实现类Dclass D implements Interface1 { @Override public void option1() { System.out.println(\"A...o1\"); } @Override public void option2() { System.out.println(\"A...o1\"); } @Override public void option3() { System.out.println(\"A...o1\"); } @Override public void option4() { System.out.println(\"A...o1\"); } @Override public void option5() { System.out.println(\"A...o1\"); }}// 使用类Aclass A { public void depend1(Interface1 interface1) { interface1.option1(); } public void depend2(Interface1 interface1) { interface1.option2(); } public void depend3(Interface1 interface1) { interface1.option3(); }}// 使用类Cclass C { public void depend1(Interface1 interface1) { interface1.option1(); } public void depend4(Interface1 interface1) { interface1.option4(); } public void depend5(Interface1 interface1) { interface1.option5(); }} 从上面的代码中,我们可以看到类A只用到了接口中的1,2,3方法,但是上面的接口实现,却全部实现了;同样C类只使用了1,4,5方法,而依赖的D也实现了接口的全部方法。 现在将以上的类和接口呈现出UML图展示: 通过上面的图可以看到, 类 A 通过接口 Interface1 依赖类 B,类 C 通过接口 Interface1 依赖类 D,如果接口 Interface1 对于类 A 和类 C来说不是最小接口,那么类 B 和类 D 必须去实现他们不需要的方法。其中B必须去实现4,5方法;D必须去实现2,3方法。 三、接口隔离原则按照接口隔离原则应当这样处理: 将接口 Interface1拆分为独立的几个接口(这里我们拆分成3个接口),类 A 和类 C 分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。将A,C公共使用的接口方法抽象成一个接口,各自不相交的部分再抽象各自成一个接口。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182interface Interface1 { void option1();}interface Interface2 { void option2(); void option3();}interface Interface3 { void option4(); void option5();}class B implements Interface1, Interface2 { @Override public void option1() { System.out.println(\"A...o1\"); } @Override public void option2() { System.out.println(\"A...o2\"); } @Override public void option3() { System.out.println(\"A...o3\"); }}class D implements Interface1, Interface3 { @Override public void option1() { System.out.println(\"B...o1\"); } @Override public void option4() { System.out.println(\"B...o4\"); } @Override public void option5() { System.out.println(\"B...o5\"); }}class A { public void depend1(Interface1 interface1) { interface1.option1(); } public void depend2(Interface2 interface2) { interface2.option2(); } public void depend3(Interface2 interface2) { interface2.option3(); }}class C { public void depend1(Interface1 interface1) { interface1.option1(); } public void depend4(Interface3 interface3) { interface3.option4(); } public void depend5(Interface3 interface3) { interface3.option5(); }} 其UML类图: 从类图中看出,A依赖使用到的方法只有1,2,3;C依赖使用到的方法只有1,4,5。其中A不用依赖接口2,C不用依赖接口2。 四、总结在实例介绍代码一中,根据以上类图的改进原则: 类 A 通过接口 Interface1 依赖类 B,类 C 通过接口 Interface1 依赖类 D,如果接口 Interface1 对于类 A 和类 C来说不是最小接口,那么类 B 和类 D 必须去实现他们不需要的方法。 将接口 Interface1 拆分为独立的几个接口,类 A 和类 C 分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。 接口 Interface1 中出现的方法,根据实际情况拆分为三个接口。","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"接口隔离原则","slug":"接口隔离原则","permalink":"https://chemlez.github.io/tags/%E6%8E%A5%E5%8F%A3%E9%9A%94%E7%A6%BB%E5%8E%9F%E5%88%99/"}]},{"title":"设计模式之原型模式","slug":"设计模式之原型设计模式","date":"2021-05-18T11:54:20.000Z","updated":"2021-05-19T05:56:56.586Z","comments":true,"path":"2021/05/18/设计模式之原型设计模式/","link":"","permalink":"https://chemlez.github.io/2021/05/18/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E5%8E%9F%E5%9E%8B%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/","excerpt":"本章用来介绍设计模式中的原型模式,该模式也是设计模式中较为简单、也是比较常见的一种设计模式。同样给出场景及相应代码层层推进的方式来学习原型模式。","text":"本章用来介绍设计模式中的原型模式,该模式也是设计模式中较为简单、也是比较常见的一种设计模式。同样给出场景及相应代码层层推进的方式来学习原型模式。 一、场景假设现在公司内部要开发一款周报提交系统。周报需要填写的表单设计如下: 比如第一周我们填写了一份表单如下: 现在过了第二周,我们再填写一份表单,如下: 仔细看这两份表单,有什么问题?是不是主要产生变化的,只有表单中被加粗的这三项(本周总结、下周计划、提交日期),其它的几项是不是都不会怎么变动?如果,我们每周填写周报都要填写全部的项,是不是显得很虎?所以,我们的需求是能够保留上周填写的周报,在其基础上进行修改。 下面开始进行代码的模拟及步步推进。 二、代码模拟为了方便模式的讲解,我们用一个WeekReport类,来模拟这份表单。代码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142public class AppTest { public static void main(String[] args) throws CloneNotSupportedException, InterruptedException { WeekReport reportFirst = new WeekReport(\"Chemlez\", \"IT部门\", \"看了一本书\", \"再看一本书\", \"Nothing\", new Date()); WeekReport reportSecond = new WeekReport(\"Chemlez\", \"IT部门\", \"又看完了一本书\", \"看两本书\", \"Nothing\", new Date(new Date().getTime() + 7 * 24 * 3600 * 1000)); System.out.println(reportFirst); System.out.println(reportSecond); }}/** * 该类用来模拟周报 不使用Cloneable接口就要重复输入很多内容 * 下面是使用Cloneable接口进行原型模式 */class WeekReport { // 该方式是浅拷贝,如果其中包含引用,将引用一并进行拷贝 private static int id = 0; private String emp; private String summary; private String plain; private String suggestion; private Date date; public WeekReport() { ++id; } public WeekReport( String emp, String summary, String plain, String suggestion, Date date) { ++id; this.emp = emp; this.summary = summary; this.plain = plain; this.suggestion = suggestion; this.date = date; } // 省略setter和getter方法,以及toString方法}===输出===WeekReport{name='Chemlez', dept='IT部门', summary='看了一本书', plain='再看一本书', suggestion='Nothing', date=Tue May 18 21:18:38 CST 2021}WeekReport{name='Chemlez', dept='IT部门', summary='又看完了一本书', plain='看两本书', suggestion='Nothing', date=Tue May 25 21:18:38 CST 2021} 我们看到第二周周报,尽管第二周周报的大部分内容与第一周周报内容一致,但是仍然需要重复设置。 此时,我们便可以通过使用”原型模式”来解决这个问题。 市容 2.1 代码一现在,我需要保留上周需要填写的内容。从面向对象的角度出发,就是我们能够”克隆”出原来的对象,然后复用这个对象的属性,直接在”克隆”出的对象上进行修改我们需要修改的部分即可,不用修改的部分就不去改动它即可。 这里使用Java中提供的接口,Cloneable接口。 12public interface Cloneable {} 可以看到这个接口没有定义任何的方法,其实这只是一个标记接口,类似于Serializable这个接口。只是告诉JVM该类实例化的对象是可以被”克隆”的。 要想达到这个上面”克隆”的效果,我们需要重写该接口。即: 12345678910111213141516171819202122232425262728293031class WeekReport implements Cloneable { // 该方式是浅拷贝,如果其中包含引用,将引用一并进行拷贝 private static int id = 0; private String name; private String dept; private String summary; private String plain; private String suggestion; private Date date; public WeekReport() { ++id; } public WeekReport(String name, String dept, String summary, String plain, String suggestion, Date date) { ++id; this.name = name; this.dept = dept; this.summary = summary; this.plain = plain; this.suggestion = suggestion; this.date = date; } @Override public Object clone() throws CloneNotSupportedException { // 将修饰符提升到public,可以在任何类下进行使用 return super.clone(); } // 省略getter、setter以及toString()方法} clone方法是直接复制内存中的二进制(重新开辟的一块内容空间用来存储),效率更高。 这个时候我们就可以直接进行对象的克隆,使用如下: 1234567891011121314151617181920public class AppTest { public static void main(String[] args) throws CloneNotSupportedException,InterruptedException { WeekReport reportFirst = new WeekReport(\"Chemlez\", \"IT部门\", \"看了一本书\", \"再看一本书\", \"Nothing\", new Date()); WeekReport reportSecond = (WeekReport) reportFirst.clone(); reportSecond.setSummary(\"又看了一本书\"); reportSecond.setPlain(\"看两本书\"); Date date = reportSecond.getDate(); date.setTime(date.getTime() + 7*24*3600*1000); // 模拟过了一周 System.out.println(reportFirst); System.out.println(reportSecond); }}====输出====WeekReport{name='Chemlez', dept='IT部门', summary='看了一本书', plain='再看一本书', suggestion='Nothing', date=Tue May 25 21:19:35 CST 2021}WeekReport{name='Chemlez', dept='IT部门', summary='又看了一本书', plain='看两本书', suggestion='Nothing', date=Tue May 25 21:19:35 CST 2021} 可以看出,我们将源对象进行克隆,在现有对象上修改我们想要修改(通过set和get方法)的属性即可,是不是就满足了我们所提出的需求? 不过细心的读着可能看出了,为什么我们的周报一的时间和周报二的时间是一样的了?难道克隆出来的对象和源对象指向同一个地址? 我们可以来测试一下,比较两个对象是否指向同一个地址。 123System.out.println(reportFirst==reportSecond);===输出===false 结果为false,所以可以看到两个对象并非指向同一个地址。其实,在上面我们也写到了,clone方法是直接复制内存中的二进制(重新开辟的一块内容空间用来存储),所以两个对象并非指向同一片地址。而这种拷贝的方式是一种浅拷贝方式。请看下图: 一图胜千言。对象是两个对象,但是对象中的属性,如果是引用类型,那么在拷贝的时候就会指向同一片堆内存空间,也就是说他们的引用类型的属性指向的是同一个对象,所以改动其中一个属性也会改动另外一个对象的属性。 那么朝着这个目的,我们让其引用类型属性指向对象也是各自的对象,从而互不干扰。 2.2 代码二我们改造的思路是,将其中的引用类型属性通过get获取后,再进行clone,然后再通过set方法传回到原来clone对象中。这么说有点绕,直接看代码。 12345678910111213141516171819202122232425public class AppTest { public static void main(String[] args) throws CloneNotSupportedException{ WeekReport reportFirst = new WeekReport(\"Chemlez\", \"IT部门\", \"看了一本书\", \"再看一本书\", \"Nothing\", new Date()); WeekReport reportSecond = (WeekReport) reportFirst.clone(); Date date = reportSecond.getDate(); Date dateCloned = (Date) date.clone(); reportSecond.setDate(dateCloned); // 保证我们可以保留原有的值,并且在后续的改动中,不会干扰到原对象 // 设置新值 reportSecond.setSummary(\"又读完了一本书\"); reportSecond.setPlain(\"看两本书\"); Date dateC = reportSecond.getDate(); dateC.setTime(dateC.getTime() + 7 * 24 * 3600 * 1000); System.out.println(reportFirst); System.out.println(reportSecond); }}====输出====WeekReport{id=1, name='Chemlez', dept='IT部门', summary='看了一本书', plain='再看一本书', suggestion='Nothing', date=Tue May 18 21:31:29 CST 2021}WeekReport{id=1, name='Chemlez', dept='IT部门', summary='又读完了一本书', plain='看两本书', suggestion='Nothing', date=Tue May 25 21:31:29 CST 2021} 通过将date进行克隆,返回的对象也是二进制复制,并且是新的内存空间。如下图: 一图胜千言。在代码: 12Date dateCloned = (Date) date.clone(); // 对象clone,返回新的对象,即新开辟的内存空间reportSecond.setDate(dateCloned); // 保证我们可以保留原有的值,并且在后续的改动中,不会干扰到原对象 第二句通过set,给reportSecond重新设置了一个Date对象,并且保证了,其内容值与原来的值一样,这样既能够保证我们可以复用原来的属性值,也可保证我们修改其属性,不会影响到过去的属性值。 到了这里,代码的改动似乎已经很完美了,但是肯定有读者有会产生这么一个疑问,你现在对象中只有一个引用类型的属性,如果我给你成百上千的引用类型的属性你该怎么办,难道还去一个一个clone么?如果你属性中引用类型的属性中中又存在引用类型的属性怎么办?层层嵌套的引用类型,用这种逐个属性克隆再赋值的方式,岂不是要累死。 因此,就需要对其继续进行改进。 3.3 代码三代码二遗留下的来的问题是:如果一个类中包含的引用型对象过多,即如果对象的深度比较深,则深拷贝实现起来较为繁琐。 进一步的改进,我们还是需要实现Cloneable接口,重写clone克隆方法。即:在这里就需要修改WeekReport的clone方法。不过,在这里我们还需要将类去实现Serializable接口。目的:是将对象进行序列化,然后再将对象进行反序列化,当反序列化为对象时,此时得到的对象就是一种天然的深拷贝方式得来的对象。 对象中无论存在多少的层级关系(引用关系),将这个对象序列化到硬盘上(二进制,序列化就是天生的深拷贝),在序列化的同时,也会将这种层级关系进行序列化。 实现方式一根据上面的思路,现在我们将对象进行序列化,然后再将对象进行反序列化,从而达到深拷贝的方式。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374public class AppTest { public static void main(String[] args) throws CloneNotSupportedException { // 第一周 WeekReport reportFirst = new WeekReport(\"Chemlez\", \"IT部门\", \"看了一本书\", \"再看一本书\", \"Nothing\", new Date()); // 第二周 WeekReport reportSecond = (WeekReport) reportFirst.clone(); // 改写值 Date date = reportSecond.getDate(); date.setTime(date.getTime() + 7 * 24 * 3600 * 1000); // 日期修改 reportSecond.setSummary(\"又对了一本书\"); reportSecond.setPlain(\"再读两本书\"); System.out.println(reportFirst); System.out.println(reportSecond); }}class WeekReport implements Cloneable, Serializable { private static int id = 0; private String name; private String dept; private String summary; private String plain; private String suggestion; private Date date; public WeekReport() { ++id; } public WeekReport(String name, String dept, String summary, String plain, String suggestion, Date date) { ++id; this.name = name; this.dept = dept; this.summary = summary; this.plain = plain; this.suggestion = suggestion; this.date = date; } @Override public Object clone() throws CloneNotSupportedException { try { // 序列化 OutputStream os = new FileOutputStream(\"a.txt\"); // 输出流 ObjectOutputStream oos = new ObjectOutputStream(os); // 对象的输出流 oos.writeObject(this); // 将对象输出 oos.close(); // 反序列化 InputStream fis = new FileInputStream(\"a.txt\"); ObjectInputStream ois = new ObjectInputStream(fis); Object clone = ois.readObject(); // 需要返回的对象,该对象 \"深拷贝\" ois.close(); return clone; // 将反序列化的对象进行返回 } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return null;}====结果输出====WeekReport{name='Chemlez', dept='IT部门', summary='看了一本书', plain='再看一本书', suggestion='Nothing', date=Wed May 19 13:33:51 CST 2021}WeekReport{name='Chemlez', dept='IT部门', summary='又对了一本书', plain='再读两本书', suggestion='Nothing', date=Wed May 26 13:33:51 CST 2021} 从结果看出,我们达到了所要的要求。 但是从代码中,我们又看到了一种问题,就是我们将序列化的路径给写死了,这里又产生了耦合。又有读着肯定想到,那我们干脆使用当前路径下的相对路径不就好了?但是,你有没有想过在Linux和Windows的路径设置有很大的区别,其中Linux中就不存在盘符的级别。如果设定写死的路径,那么就根本没有达到跨平台的目的。那么,有什么好的方法呢? 答案是肯定有的。我们可以将上面序列化到硬盘,再从硬盘反序列化到内存的操作,全部放到内存层面上进行操作。即:将对象序列化到内存上,再从内存上进行反序列化操作。 实现方式二这里就只给出clone方法,其他方法都一样就不再重复写出了。 12345678910111213141516171819202122232425@Overridepublic Object clone() throws CloneNotSupportedException { try { // 序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream();// 内存层面上输出流 ObjectOutputStream oos = new ObjectOutputStream(bos); // 对象的输出流 oos.writeObject(this); // 序列化对象,对象的所有属性层级关系会被序列化进行自动处理 oos.close(); byte[] bytes = bos.toByteArray(); // 从内存中取出数据 // 反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bis); Object clone = ois.readObject(); // 需要返回的对象,该对象 \"深拷贝\" ois.close(); return clone; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null;} 至此,代码已经完美解决场景提出的问题。 三、总结通过原型模式,创建对象新的方式,即用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。创建出的对象,和原对象属性值相同,但是修改不会影响源对象。 又由于原型模式是通过clone的方式,所以它创建对象的方式会很快。所以当直接创建对象的代价比较大时,则采用这种模式。","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"原型模式","slug":"原型模式","permalink":"https://chemlez.github.io/tags/%E5%8E%9F%E5%9E%8B%E6%A8%A1%E5%BC%8F/"}]},{"title":"设计模式-依赖倒置原则","slug":"设计模式-依赖倒置原则","date":"2021-05-16T13:38:38.000Z","updated":"2021-05-19T01:14:44.928Z","comments":true,"path":"2021/05/16/设计模式-依赖倒置原则/","link":"","permalink":"https://chemlez.github.io/2021/05/16/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E4%BE%9D%E8%B5%96%E5%80%92%E7%BD%AE%E5%8E%9F%E5%88%99/","excerpt":"本文用来讲述设计模式中的依赖倒置原则。通过介绍,场景,代码推进的方式进行一步步的讲解。 一、介绍依赖倒置原则(Dependence Inversion Principle)是指: 高层模块不应该依赖底层模块,二者都应该依赖其抽象。这里调用者就是上层,被调用者就是下层。 抽象不应该依赖细节,细节应该依赖抽象。(抽象是指接口或者抽象类,细节是指具体实现及使用) 依赖倒置的中心思想是面向接口编程。 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在 java 中,抽象指的是接口或抽象类,细节就是具体的实现类 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成","text":"本文用来讲述设计模式中的依赖倒置原则。通过介绍,场景,代码推进的方式进行一步步的讲解。 一、介绍依赖倒置原则(Dependence Inversion Principle)是指: 高层模块不应该依赖底层模块,二者都应该依赖其抽象。这里调用者就是上层,被调用者就是下层。 抽象不应该依赖细节,细节应该依赖抽象。(抽象是指接口或者抽象类,细节是指具体实现及使用) 依赖倒置的中心思想是面向接口编程。 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在 java 中,抽象指的是接口或抽象类,细节就是具体的实现类 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成 二、场景介绍实现场景,一个人通过不同的通讯方式进行通信。例如,可以通过邮箱,通过微信,通过手机等等方式进行通信。 三、代码实现3.1 实现代码假设现在我们需要通过微信,短信,邮箱进行信息的传递。代码都较为简单,请先直接看代码,再进行代码的讲解。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546public class DependenceInversionPrinciple { public static void main(String[] args) { Person person = new Person(); person.getInfoByEmail(new Email()); person.getInfoByMessage(new Message()); person.getInfoByWeChat(new WeChat()); }}class Person { public void getInfoByMessage(Message message) { message.getInfo(); } public void getInfoByEmail(Email email) { email.getInfo(); } public void getInfoByWeChat(WeChat weChat) { weChat.getInfo(); }}class Message { public void getInfo() { System.out.println(\"通过 短信 传递信息\"); }}class Email { public void getInfo() { System.out.println(\"通过 邮箱 传递信息\"); }}class WeChat { public void getInfo() { System.out.println(\"通过 微信 传递信息\"); }} 输出: 123通过 邮箱 传递信息通过 短信 传递信息通过 微信 传递信息 通过上面的代码,我们实现了上面的场景描述。同时下图给出了类之间的关系。 从图中可以看到,Person类依赖WeChat,Message,Email类。 如果我们的需求不变,这样写代码是没有什么问题的。但是如果有一天,我们的需求扩大了,不再满足上面的三种通信方式。此时,我还需要使用钉钉和QQ进行通行呢? 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364public class DependenceInversionPrinciple { public static void main(String[] args) { Person person = new Person(); person.getInfoByEmail(new Email()); person.getInfoByMessage(new Message()); person.getInfoByWeChat(new WeChat()); person.getInfoByWeChat(new QQ()); person.getInfoByWeChat(new DingDing()); }}class Person { public void getInfoByMessage(Message message) { message.getInfo(); } public void getInfoByEmail(Email email) { email.getInfo(); } public void getInfoByWeChat(WeChat weChat) { weChat.getInfo(); } public void getInfoByDingDing(DingDing ding) { weChat.getInfo(); } public void getInfoByWeChat(QQ qq) { weChat.getInfo(); }}class Message { public void getInfo() { System.out.println(\"通过 短信 传递信息\"); }}class Email { public void getInfo() { System.out.println(\"通过 邮箱 传递信息\"); }}class WeChat { public void getInfo() { System.out.println(\"通过 微信 传递信息\"); } class DingDing { public void getInfo() { System.out.println(\"通过 钉钉 传递信息\"); } class QQ { public void getInfo() { System.out.println(\"通过 QQ 传递信息\"); }} 如果按照上面的方式,我们是不是需要新增钉钉和QQ类,同时需要在Person类中去新增方法,那么这里就违反了设计模式中的开闭原则,当我们改动服务端代码时,客户端代码也不得不改动。而我们增加需求所要做到的就是,就在客户端不需要进行结构上的变化,达到对其功能的扩充。不然,以后是不是每新增一个需求就需要把客户端的代码拿来进行大刀阔斧一番? 从上图中,我们看到上层模块(Person)依赖下层模块(WeChat,Message,Email)。如果我们在其中开辟一个中间层,让上层Person,无法感知到下层的存在,是不是要好些呢?这样说有些抽象,我们继续看代码改动。 3.2 改进代码现在对3.1中的代码进行改进。 123456789101112131415161718192021222324252627282930313233343536373839404142public class DependenceInversionPrinciple { public static void main(String[] args) { IReceiver message = new Message(); IReceiver WeChat = new WeChat(); IReceiver Email = new Email(); Person person = new Person(); person.getInfoByMessage(message); person.getInfoByMessage(WeChat); person.getInfoByMessage(Email); }}class Person { public void getInfoByMessage(IReceiver iReceiver) { iReceiver.getInfo(); }}interface IReceiver { void getInfo();}class Message implements IReceiver { public void getInfo() { System.out.println(\"通过 短信 传递信息\"); }}class Email implements IReceiver { public void getInfo() { System.out.println(\"通过 邮箱 传递信息\"); }}class WeChat implements IReceiver { public void getInfo() { System.out.println(\"通过 微信 传递信息\"); }} 输出: 123通过 短信 传递信息通过 微信 传递信息通过 邮箱 传递信息 继续看此时类之间的关系图: 我们看到Person从过去直接依赖WeChat、Message、Email现在只依赖IReceiver,它根本就感知不到其下层的模块变化。 如果,此时我们再新增钉钉,QQ的通信方式,只需要让这两个类去实现IReceiver接口,而Person类根本就不需要改动,就能实现新增的通信方式。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869public class DependenceInversionPrinciple { public static void main(String[] args) { IReceiver message = new Message(); IReceiver weChat = new WeChat(); IReceiver email = new Email(); IReceiver qq = new QQ(); IReceiver ding = new DingDing(); Person person = new Person(); person.getInfoByMessage(message); person.getInfoByMessage(weChat); person.getInfoByMessage(email); person.getInfoByMessage(qq); person.getInfoByMessage(ding); }}class Person { public void getInfoByMessage(IReceiver iReceiver) { iReceiver.getInfo(); }}interface IReceiver { void getInfo();}class Message implements IReceiver { public void getInfo() { System.out.println(\"通过 短信 传递信息\"); }}class Email implements IReceiver { public void getInfo() { System.out.println(\"通过 邮箱 传递信息\"); }}class WeChat implements IReceiver { public void getInfo() { System.out.println(\"通过 微信 传递信息\"); }}class DingDing implements IReceiver { @Override public void getInfo() { System.out.println(\"通过 钉钉 传递信息\"); }}class QQ implements IReceiver { @Override public void getInfo() { System.out.println(\"通过 QQ 传递信息\"); }}=====输出======通过 短信 传递信息通过 微信 传递信息通过 邮箱 传递信息通过 QQ 传递信息通过 钉钉 传递信息 我们服务端对需求的扩展根本就没有影响到客户端代码(Person),这里的Person根本就没有增删代码。实现了上层和下层模块的解耦.这里的上层Person根本就没有感知到下层模块WeChat,…,QQ等。 四、总结现在我们再来看一看依赖倒置的定义: 现在再看是不是高层模块(Person)和底层模块(WeChat,..,QQ)都依赖其抽象,而不是直接的高层模块依赖其底层模块。这里细节是具体实现,它依赖着抽象(接口)。 核心是面向接口面层,这里的高层模块依赖的就是IRecevier接口。 如果违反了该原则,下层模块改动就要带动上层模块一起跟着动(3.1节中的实例代码)。 使用及注意事项: 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好.即子类上层应具有抽象类或接口 变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化。 继承时遵循里氏替换原则。 依赖倒置体现在:在代码一中,是不是Person都依赖着WeChat,…,Email,箭头向下的;经过代码的优化处理,采取依赖倒置原则,是不是原来的箭头向下,即下层模块被依赖,而现在全都去实现其上层接口,即依赖着其接口,箭头向上反转了,所以依赖倒置由此而得名。","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"依赖倒置原则","slug":"依赖倒置原则","permalink":"https://chemlez.github.io/tags/%E4%BE%9D%E8%B5%96%E5%80%92%E7%BD%AE%E5%8E%9F%E5%88%99/"}]},{"title":"设计模式-组合优于继承原则","slug":"设计模式-组合优于继承原则","date":"2021-05-16T07:28:15.000Z","updated":"2021-05-16T08:29:08.547Z","comments":true,"path":"2021/05/16/设计模式-组合优于继承原则/","link":"","permalink":"https://chemlez.github.io/2021/05/16/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E7%BB%84%E5%90%88%E4%BC%98%E4%BA%8E%E7%BB%A7%E6%89%BF%E5%8E%9F%E5%88%99/","excerpt":"本章用来讲述设计模式中的另一个原则——组合优于继承原则,又称为合成复用原则。 一、基本介绍该原则是尽量使用合成/聚合的方式,而不是使用继承。 上面一句话就是对该原则的核心,但单单看这么一句话还是显得过于枯燥与不知所云,下面我们还是结合具体的场景进行代码推进,对该原则进行讲解。 二、场景假设我们需要设计这样一个集合,每次向里面加入元素时,count都加一。例如: 最初集合是空集合,此时我们向里面加入"a",此时集合为{"a"},那么此时count = 1;当加入"b","c"两个元素时,集合为{"a","b","c"},此时count=3;此时再删除"a","c"两个元素,集合为{"b"},count仍然等于3;最后再加入"d",集合为{"b","d"},count=4。 所以该场景是,不论中间是否删除元素,我们只统计加入到集合中的元素的次数,进行返回。","text":"本章用来讲述设计模式中的另一个原则——组合优于继承原则,又称为合成复用原则。 一、基本介绍该原则是尽量使用合成/聚合的方式,而不是使用继承。 上面一句话就是对该原则的核心,但单单看这么一句话还是显得过于枯燥与不知所云,下面我们还是结合具体的场景进行代码推进,对该原则进行讲解。 二、场景假设我们需要设计这样一个集合,每次向里面加入元素时,count都加一。例如: 最初集合是空集合,此时我们向里面加入"a",此时集合为{"a"},那么此时count = 1;当加入"b","c"两个元素时,集合为{"a","b","c"},此时count=3;此时再删除"a","c"两个元素,集合为{"b"},count仍然等于3;最后再加入"d",集合为{"b","d"},count=4。 所以该场景是,不论中间是否删除元素,我们只统计加入到集合中的元素的次数,进行返回。 三、代码演进根据上面的场景,我们开始一步步编写代码。 3.1 代码一1234567891011121314151617181920212223public class MySet01<E> extends HashSet<E>{ private int count = 0; @Override public boolean add(E e) { ++count; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { boolean modified = false; for (E e : c) if (add(e)) modified = true; return modified; } public int getCount() { return count; }} 此时,HashSet源码中,所有add的入口就是该方法add方法。 但是如果这么设计,出现的问题有: 如果在新的jdk版本中,HahsSet突然多了一个元素加入集合的入口方法,例如addSome,这个方法是我们不可预知的。我们的MySet01根本没有重写新版本中出现的addSome方法。这样,在新版本中,我们的MySet也继承了addSome方法,当使用addSome方法添加元素时,根本不会去统计元素的数量。 我们重写了addAll方法,和add方法。在HashSet的所有方法中,难免有一些其他方法,会依赖addAll方法和add方法。如果我们就这样随便重写了HashSet类中的某些方法,就会导致其他依赖于这些方法的方法,容易出现问题,不好排查。 那么为了避免以上的问题,肯定就会有人想到那我们自己定义两个方法,不重写HashSet中的这两个方法不就好了,说得很对,我们继续改写来看一看。 3.2 代码二1234567891011121314151617181920public class MySet02<E> extends HashSet<E> { private int count; public boolean add2(E e) { ++count; return super.add(e); } public boolean addAll2(Collection<? extends E> c) { boolean modified = false; for (E e : c) if (add(e)) modified = true; return modified; } public int getCount() { return count; }} 从上面的代码中,我们自己定义了两个方法—— add2和addAll2。此时这段代码看起来是解决了我们上面一段代码所遗留的问题,但是该问题又产生了两个新问题。 首先,如果用户需要用到该计数功能时,我们需要提供一份文档给用户,告诉他你需要调用add2和addAll2才能使用该功能,这种方式未免对用户太过于苛刻了。 以上这个问题还不是最致命的。现在就将产生的问题做一个总结: 目前这种情况对用户要求过于苛刻,用户必须看类的API文档,看完了还要使用add2和addAll2这两个方法,不能出错。 更加致命的是,如果在未来的jdk版本中,HashSet恰恰多两个API,叫add2和addAll2,那么就又出现了代码一种的两个问题了。 因此,继承应该就已经走到绝境了。这个时候,我们就要考虑到组合大于继承的原则了。 3.3 代码三针对代码二出现的问题,先做出如下改进: 我们的MySet,再也不要去继承HashSet了。 取而代之,我们让MySet和HashSet发生关联关系(组合)。 代码如下: 1234567891011121314151617181920public class MySet03<E> { private int count = 0; private Set<E> set = new HashSet<>(); public boolean add(E e) { ++count; return set.add(e); } public boolean addAll(Collection<? extends E> e) { count += e.size(); return set.addAll(e); } public int getCount() { return count; }} 该段代码中,对于用户来说,完全隐藏了add和addAll细节,用户只管调用它们即可。哪怕在未来的JDK中,HashSet又增加了其他的add方法,但是用户只能调用我提供这两个add方法。所以,是不是此时组合的优势就体现出来了。 四、总结通过代码演进这一节,我们看出了组合的优势。这时有人肯定会产生怀疑,组合既然这么好,我们干嘛还要继承呢? 难道以后都不能使用继承了吗? 难道以后都不能进行方法重写了吗? 非也!请看以下的情况。 如果父类作者,和子类的作者,不是同一个人。那么就不要使用继承,因为你不知道父类作者以后会对代码改动时,做出啥事,你两又沟通不上。因为,父类作者不知道,未来的子类,会重写自己的哪个方法;那么子类作者不知道,未来的父类,会加入什么新方法(和自己的产生冲突)。 如果父类作者和子类作者是同一个人,那么就可以随意使用继承了。因为,自己当然知道,每个方法都是什么作用,作者可以很好的控制父类和子类。 我们自己写代码,继承,重写,随便使用;如果我们仅仅是为了复用代码,而继承别人的类,难免会出现“沟通”上的问题,所以谨慎使用继承。","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"组合优于继承原则","slug":"组合优于继承原则","permalink":"https://chemlez.github.io/tags/%E7%BB%84%E5%90%88%E4%BC%98%E4%BA%8E%E7%BB%A7%E6%89%BF%E5%8E%9F%E5%88%99/"}]},{"title":"设计模式-单一职责原则","slug":"设计模式-单一职责原则","date":"2021-05-16T02:06:12.000Z","updated":"2021-05-16T08:28:51.246Z","comments":true,"path":"2021/05/16/设计模式-单一职责原则/","link":"","permalink":"https://chemlez.github.io/2021/05/16/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E5%8D%95%E4%B8%80%E8%81%8C%E8%B4%A3%E5%8E%9F%E5%88%99/","excerpt":"在对设计模式的学习中,首先需要了解、掌握设计模式的七大原则,这样后续对设计模式的学习才能够更加的轻松与透彻。本章用于总结设计模式中的单一职责原则,该原则也是比较容易理解的。 一、基本介绍对类来说的,即一个类应该只负责一项职责。如类 A 负责两个不同职责:职责 1,职责 2。当职责 1 需求变更而改变 A 时,可能造成职责 2 执行错误,所以需要将类 A 的粒度分解为 A1,A2。 二、场景应用2.1 场景一该场景模拟交通工具使用场景。 目的:模拟交通工具的运输形式。 反例代码12345678910111213141516public class SingleExample { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); vehicle.run(\"汽车\"); vehicle.run(\"摩托车\"); vehicle.run(\"飞机\"); }}class Vehicle { public void run(String vehicle) { System.out.println(vehicle + \"在公路上运行\"); }}","text":"在对设计模式的学习中,首先需要了解、掌握设计模式的七大原则,这样后续对设计模式的学习才能够更加的轻松与透彻。本章用于总结设计模式中的单一职责原则,该原则也是比较容易理解的。 一、基本介绍对类来说的,即一个类应该只负责一项职责。如类 A 负责两个不同职责:职责 1,职责 2。当职责 1 需求变更而改变 A 时,可能造成职责 2 执行错误,所以需要将类 A 的粒度分解为 A1,A2。 二、场景应用2.1 场景一该场景模拟交通工具使用场景。 目的:模拟交通工具的运输形式。 反例代码12345678910111213141516public class SingleExample { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); vehicle.run(\"汽车\"); vehicle.run(\"摩托车\"); vehicle.run(\"飞机\"); }}class Vehicle { public void run(String vehicle) { System.out.println(vehicle + \"在公路上运行\"); }} 输出: 123汽车在公路上运行摩托车在公路上运行飞机在公路上运行 该方式run方法中,违反了单一职责原则(飞机也在公路上跑了)。 解决方案:根据不同的交通工具运行方法不同,分解成不同类即可。 修改代码一123456789101112131415161718192021222324252627282930public class SingleExample02 { public static void main(String[] args) { RoadVehicle roadVehicle = new RoadVehicle(); roadVehicle.run(\"汽车\"); WaterVehicle waterVehicle = new WaterVehicle(); waterVehicle.run(\"轮船\"); AirVehicle airVehicle = new AirVehicle(); airVehicle.run(\"飞机\"); }}class RoadVehicle { public void run(String vehicle) { System.out.println(vehicle + \"公路运行\"); }}class WaterVehicle { public void run(String vehicle) { System.out.println(vehicle + \"水中运行\"); }}class AirVehicle { public void run(String vehicle) { System.out.println(vehicle + \"天空运行\"); }} 输出: 123汽车公路运行轮船水中运行飞机天空运行 该方案严格遵守单一职责原则(每个类中只负责一项职能),但是这样做改动较大,同时对客户端(Main)也进行了大改动。在功能、业务逻辑较为简单时,我们可以缩小单一职责原则的范围,可以将该职责落在方法上。 修改代码二123456789101112131415161718192021222324252627public class SingleExample { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); vehicle.runRoad(\"汽车\"); vehicle.runWater(\"轮船\"); vehicle.runAir(\"飞机\"); }}class Vehicle { public void runRoad(String vehicle) { // TODO:公路交通的相关逻辑 System.out.println(vehicle + \"在公路上运行\"); } public void runWater(String vehicle) { // TODO:水中交通的相关逻辑 System.out.println(vehicle + \"在公路上运行\"); } public void runAir(String vehicle) { // TODO:空中交通的相关逻辑 System.out.println(vehicle + \"在公路上运行\"); }} 这种修改方法没有对原来的类做大的修改,只是增加方法,这里虽然没有在类这个级别上遵守单一职责原则,但是在方法级别上,仍然是遵守单一职责。 三、总结——单一职责原则注意事项和细节 降低类的复杂度,一个类只负责一项职责 提高类的可读性,可维护性 降低变更引起的风险 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,可以在方法级别保持单一职责原则(例如修改代码二)","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"设计模式","slug":"设计模式","permalink":"https://chemlez.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"单一职责原则","slug":"单一职责原则","permalink":"https://chemlez.github.io/tags/%E5%8D%95%E4%B8%80%E8%81%8C%E8%B4%A3%E5%8E%9F%E5%88%99/"}]},{"title":"Nginx学习笔记及牛刀小试","slug":"Nginx学习笔记及牛刀小试","date":"2021-05-10T13:25:02.000Z","updated":"2021-05-10T13:26:23.864Z","comments":true,"path":"2021/05/10/Nginx学习笔记及牛刀小试/","link":"","permalink":"https://chemlez.github.io/2021/05/10/Nginx%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E5%8F%8A%E7%89%9B%E5%88%80%E5%B0%8F%E8%AF%95/","excerpt":"一、Nginx的安装本次安装的环境为centos7.0的版本。 1.1 pcre依赖包的相关安装 安装pcre压缩包中的依赖 1wget http://downloads.sourceforge.net/project/pcre/pcre/8.37/pcre-8.37.tar.gz 压缩包解压并进入解压后的文件夹 1tar -zxvf pcre-8.37.tar.gz C++方面的依赖(openssl、zlib、gcc等依赖) 因为Nginx依赖C++的编译环境,故需要安装C++方面的依赖。 1yum -y install make zlib zlib-devel gcc-c++ libtool openssl openssl-devel ./configure 作用:是用来检测你的安装平台的目标特征的。比如它会检测你是不是有CC或GCC,并不是需要CC或GCC,一般用来生成 Makefile,为下一步的编译做准备 make && make install 进行编译与安装","text":"一、Nginx的安装本次安装的环境为centos7.0的版本。 1.1 pcre依赖包的相关安装 安装pcre压缩包中的依赖 1wget http://downloads.sourceforge.net/project/pcre/pcre/8.37/pcre-8.37.tar.gz 压缩包解压并进入解压后的文件夹 1tar -zxvf pcre-8.37.tar.gz C++方面的依赖(openssl、zlib、gcc等依赖) 因为Nginx依赖C++的编译环境,故需要安装C++方面的依赖。 1yum -y install make zlib zlib-devel gcc-c++ libtool openssl openssl-devel ./configure 作用:是用来检测你的安装平台的目标特征的。比如它会检测你是不是有CC或GCC,并不是需要CC或GCC,一般用来生成 Makefile,为下一步的编译做准备 make && make install 进行编译与安装 1.2 安装Nginx下载Nginx安装包后(为方便管理安装目录同pcre),其安装步骤同上面的1,2,4,5;一路安装下来若没有报错则表明此时安装完成。安装完成后的目录,默认在/usr/local/nginx下。 之后进入/usr/local/nginx/sbin/启动服务启动服务,启用命令./nginx。几个常用的命令: 12345678# 启动./nginx# 关闭./nginx -s stop# 不重启nginx服务器,重加载配置文件./nginx -s reload# 查看版本号./nginx -v 1.3 测试因为Nginx默认监听的是80端口,故可以直接访问http:ip即可,例如装载在本机Linux系统中,则直接访问http:localhost,即可。若出现以下画面,则表明安装成功。 二、配置文件说明Nginx的配置文件在/usr/local/nginx/conf/nginx.conf中。配置文件中的内容包含三部分: 全局块:配置服务器整体运行的配置指令。例如:worker_processes 1;处理并发数的配置,数字越大,并发处理量就越大(会受到硬件、软件等设备的制约)。 从配置文件开始到 events 块之间的内容,主要会设置一些影响 nginx 服务器整体运行的配置指令,主要包括配置运行 Nginx 服务器的用户(组)、允许生成的 worker process 数,进程 PID 存放路径、日志存放路径和类型以及配置文件的引入等。 events块:影响Nginx服务器与用户的网络连接。例如:worker_connections 1024;支持的最大连接数为1024。 events 块涉及的指令主要影响 Nginx 服务器与用户的网络连接,常用的设置包括是否开启对多 work process 下的网络连接进行序列化,是否允许同时接收多个网络连接,选取哪种事件驱动模型来处理连接请求,每个 word process 可以同时支持的最大连接数等。 http块:包含两部分,分别为http全局块和server块。这算是 Nginx 服务器配置中最频繁的部分,代理、缓存和日志定义等绝大多数功能和第三方模块的配置都在这里。 http全局块 http 全局块配置的指令包括文件引入、MIME-TYPE 定义、日志自定义、连接超时时间、单链接请求数上限等。 server块:这块和虚拟主机有密切关系,虚拟主机从用户角度看,和一台独立的硬件主机是完全一样的,该技术的产生是为了节省互联网服务器硬件成本。 每个 http 块可以包括多个 server 块,而每个 server 块就相当于一个虚拟主机。 而每个 server 块也分为全局 server 块,以及可以同时包含多个 locaton 块。 全局server块:最常见的配置是本虚拟机主机的监听配置和本虚拟主机的名称或 IP 配置。 location块:一个 server 块可以配置多个 location 块。 这块的主要作用是基于 Nginx 服务器接收到的请求字符串(例如 server_name/uri-string),对虚拟主机名称(也可以是 IP 别名)之外的字符串(例如 前面的 /uri-string)进行匹配,对特定的请求进行处理。地址定向、数据缓存和应答控制等功能,还有许多第三方模块的配置也在这里进行,进行负载均衡、反向代理以及动静分类等配置都涉及到location块。 三、反向代理Nginx最重要的功能之一就是反向代理,这一节会实操如何实现Nginx的反向代理操作,即对Nginx进行相关配置。 3.1 正向代理Nginx不仅可以做反向代理,实现负载均衡。还能用作正向代理来进行上网等功能。 正向代理:如果把局域网外的 Internet 想象成一个巨大的资源库,则局域网中的客户端要访问Internet,则需要通过代理服务器来访问,这种代理服务就称为正向代理。即:需要在客户端配置代理服务进行指定网站访问。 如上图,客户端需要访问谷歌网站时,需要在客户端(浏览器端)进行配置,去访问代理服务器,再由代理服务器进行谷歌网站的访问。 3.2 反向代理反向代理,其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,再返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器 IP 地址。即:对于用户而言,只知道代理服务器,而无法感知到真实服务器。暴露的是代理服务器地址,隐藏了真实服务器IP地址。 3.3 Nginx配置反向代理实验一实现效果:使用nginx反向代理,访问106.15.65.210(代理服务器地址,Nginx服务器地址),跳转到106.12.65.210:8999真实服务器上。 首先,在未配置反向代理前,访问106.15.65.210 则上面直接访问的是nginx服务器地址(即为代理服务器)。 访问106.15.65.210:8999 现在看到访问路径(106.15.65.210)下,展示的nginx页面。 这里的使用反向代理能够做到的是: 访问106.15.65.210,真实访问的是106.15.65.210:8999,这里省略了域名操作,假设我们给106.15.65.210绑定了www.123.com,则直接访问的是106.15.65.210:8999.如下图: 这里省略域名的绑定操作,我们直接访问106.15.65.210代理服务器地址,转发到后面真实tomcat服务器中。 在nginx进行请求转发的配置(反向代理配置) 只需要配置代理服务器地址(域名),以及真实转发地址即可。 访问106.15.65.210,显示: 可以看到跳转到我们所配置的tomcat服务器,106.15.65.210:8999。如果没有生效,检查下是否是浏览器缓存的原因。 配置: 3.4 Nginx配置反向代理实验二实现效果:使用Nginx反向代理,根据访问的路径跳转到不同端口的服务器中。这里Nginx端口为默认的80端口。 访问http://106.15.65.210/edu/ 直接跳转到http://106.15.65.210:8080 访问http://106.15.65.210/vod/直接跳转到http://106.15.65.210:8999 根据不同的访问路径,最终来到不同的服务器请求数据。 准备工作: 准备两个tomcat服务器,一个8080端口,一个是8999端口。(如果用的是云服务器,开头端口权限以及防火墙设置) 创建文件夹和测试页面(只是用于区别根据访问路径的不同,请求的服务器不同) 即在8080端口的tomcat服务器下的webapps中创建一个edu文件夹,创建一个index.html页面,内容 1<h1>tomcat8.5.9</h1> 在在8999端口的tomcat服务器下的webapps中创建一个vod文件夹,创建一个index.html页面,内容 1<h1>tomcat9</h1> 此时,分别访问106.15.65.210:8080/edu/和106.15.65.210:8080/vod/时,页面分别显示 tomcat8.5.9及tomcat9 Nginx配置: 注意:在配置该项方向代理实验之前,需要备份之前的配置文件。 当Nginx代理服务器中,存在edu路径时,跳转到8080的服务上请求相应的服务;存在vod路径时,跳转到8999的服务器上请求相应的服务。 效果如下图: 请求:http://106.15.65.210/edu/ 请求:http://106.15.65.210/vod/ 四、负载均衡4.1 介绍增加服务器的数量,然后将请求分发到各个服务器上,将原先请求集中到单个服务器上的情况改为将请求分发到多个服务器上,将负载分发到不同的服务器,也就是我们所说的负载均衡.即:将原来一台服务器做的事情,现在增加到多台服务器来完成(每台服务器的功能模块相同,即存放的资源相同)。 4.2 实验实现效果: 请求访问http://106.15.65.210/edu/a.html,负载均衡效果,平均到8080和8999端口中。 准备工作: 准备两台tomcat服务器,一台8080,一台8081 在两台tomcat里面webapps目录中创建名称为edu的文件夹,在edu文件夹中创建页面a.html,内容用于标识两台服务即可,不做具体要求了,只用作测试罢了。 配置: 4.3 Nginx分配服务器策略 第一种:轮询(默认) 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,则自动剔除。 第二种:weight,即设置权重访问 weight代表权重默认为1,权重越高被分配的客户端越多。 第三种:ip_hash 每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器 第四种:fair(第三方) 按后端服务器的响应时间来分配请求,响应时间短的优先分配。 注意:当使用docker启动多个tomcat服务器时,做端口映射时,不需要修改默认端口8080,只需要映射端口时,选用不同的端口即可。 五、动静分离Nginx 动静分离简单来说就是把动态跟静态请求分开,不能理解成只是单纯的把动态页面和静态页面物理分离。严格意义上说应该是动态请求跟静态请求分开,可以理解成使用 Nginx 处理静态页面,Tomcat 处理动态页面。动静分离从目前实现角度来讲大致分为两种,一种是纯粹把静态文件独立成单独的域名,放在独立的服务器上,也是目前主流推崇的方案;另外一种方法就是动态跟静态文件混合在一起发布,通过 nginx 来分开。 通过location指定不同的后缀名实现不同的请求转发。通过 expires 参数设置,可以使浏览器缓存过期时间,减少与服务器之前的请求和流量。具体 Expires 定义:是给一个资源设定一个过期时间,也就是说无需去服务端验证,直接通过浏览器自身确认是否过期即可,所以不会产生额外的流量。此种方法非常适合不经常变动的资源。(如果经常更新的文件,不建议使用 Expires 来缓存),我这里设置 3d,表示在这 3 天之内访问这个 URL,发送一个请求,比对服务器该文件最后更新时间没有变化,则不会从服务器抓取,返回状态码304,如果有修改,则直接从服务器重新下载,返回状态码 200。 从上图中也可以看到,当发送请求时,静态资源请求静态资源服务器,动态资源请求动态服务器。 六、Nginx工作原理初探ps -ef | grep nginx 首先发送请求,由Nginx中的master进程进行接收;随后,master将请求分发给worker进程,由worker进程进行”争抢”,获取到任务的worker通过反向代理转发请求到tomcat,进行处理。 一个master和多个worker机制的好处: worker设置的数量:worker数量和cpu数量设置相等。 连接数worker_connection:2个或者4个;如果采用的动态分离机制,静态请求和动态请求是分别请求,各占用两个connection。这里是指,发送一个请求,占用的worker的连接数是多少。 问:Nginx有一个master,有四个worker,每个worker支持最大的连接数据为1024(worker_connections),支持的最大并发数是多少?(最大并发数可以理解为,能够承受的最大请求数量) 答:一共四个worker,则全部worker最大支持的连接数为:1024*4;因为一个请求占用两个连接或四个连接,则最大并发数(请求)为1024*4/2或者1024*4/4. 附:docker常用命令因为,上述实验需要配置两台tomcat服务器,这里我选择使用的docker配合完成,附上一些docker常用命令。 docker主机(Host):安装了Docker程序的机器(Docker直接安装在操作系统之上); docker客户端(Client):连接docker主机进行操作; docker仓库(Registry):用来保存各种打包好的软件镜像; docker镜像(Images):软件打包好的镜像;放在docker仓库中; docker容器(Container):镜像启动后的实例称为一个容器;容器是独立运行的一个或一组应用 首先在Linux中安装docker及相关命令: 12345678910111213141、检查内核版本,必须是3.10及以上uname -r2、安装dockeryum install docker3、输入y确认安装4、启动docker[root@localhost ~]# systemctl start docker[root@localhost ~]# docker -vDocker version 1.12.6, build 3e8e77d/1.12.65、开机启动docker[root@localhost ~]# systemctl enable dockerCreated symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.6、停止dockersystemctl stop docker 镜像操作 操作 命令 说明 检索 docker search 关键字 eg:docker search redis 我们经常去docker hub上检索镜像的详细信息,如镜像的TAG。 拉取 docker pull 镜像名:tag :tag是可选的,tag表示标签,多为软件的版本,默认是latest 列表 docker images 查看所有本地镜像 删除 docker rm image-id 删除指定的本地镜像 容器操作 123456789101112131415161718192021222324252627282930311、搜索镜像[root@localhost ~]# docker search tomcat2、拉取镜像[root@localhost ~]# docker pull tomcat3、根据镜像启动容器docker run --name mytomcat -d tomcat:latest4、docker ps 查看运行中的容器5、 停止运行中的容器docker stop 容器的id6、查看所有的容器docker ps -a7、启动容器docker start 容器id8、删除一个容器 docker rm 容器id9、启动一个做了端口映射的tomcat[root@localhost ~]# docker run -d -p 8888:8080 tomcat-d:后台运行-p: 将主机的端口映射到容器的一个端口 主机端口:容器内部的端口10、为了演示简单关闭了linux的防火墙service firewalld status ;查看防火墙状态service firewalld stop:关闭防火墙11、查看容器的日志docker logs container-name/container-id12、进入容器终端进行操作docker exec -it container-id /bin/bash更多命令参看https://docs.docker.com/engine/reference/commandline/docker/可以参考每一个镜像的文档","categories":[{"name":"Nginx","slug":"Nginx","permalink":"https://chemlez.github.io/categories/Nginx/"}],"tags":[{"name":"Nginx","slug":"Nginx","permalink":"https://chemlez.github.io/tags/Nginx/"}]},{"title":"数组中第K大问题之堆排序","slug":"数组中第K大问题之堆排序","date":"2021-03-18T02:22:41.000Z","updated":"2021-03-18T03:32:37.976Z","comments":true,"path":"2021/03/18/数组中第K大问题之堆排序/","link":"","permalink":"https://chemlez.github.io/2021/03/18/%E6%95%B0%E7%BB%84%E4%B8%AD%E7%AC%ACK%E5%A4%A7%E9%97%AE%E9%A2%98%E4%B9%8B%E5%A0%86%E6%8E%92%E5%BA%8F/","excerpt":"这次在刷Leetcode时,在求解数组中的第K大问题时,想到了使用堆排序,因此本篇文章用于巩固对堆排序的学习以及代码实现。 题目描述:在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。 输入:[3,2,3,1,2,4,5,5,6] 和 k = 4 输出:4 当时看到此题时,第一反应就是想到使用大顶堆来求解,在第K次调堆后,就能够得到最K大元素。后续以大顶堆进行为例。 先来简单回顾堆排序:堆是一棵完全二叉树。如果是一个大顶堆,则根节点递归的大于其左右孩子节点的值。 以大顶堆为例:(3,2,3,1,2,4,5,5,6),对该待排序列进行堆排序。","text":"这次在刷Leetcode时,在求解数组中的第K大问题时,想到了使用堆排序,因此本篇文章用于巩固对堆排序的学习以及代码实现。 题目描述:在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。 输入:[3,2,3,1,2,4,5,5,6] 和 k = 4 输出:4 当时看到此题时,第一反应就是想到使用大顶堆来求解,在第K次调堆后,就能够得到最K大元素。后续以大顶堆进行为例。 先来简单回顾堆排序:堆是一棵完全二叉树。如果是一个大顶堆,则根节点递归的大于其左右孩子节点的值。 以大顶堆为例:(3,2,3,1,2,4,5,5,6),对该待排序列进行堆排序。 首先,将该序列建立一棵完全二叉树。(这里可以假定建立完全二叉树,可以以数组的位序模拟对二叉树的操作)。 从第一个(从右向左,从下向上看起)非叶子节点为根节点的子树开始,将其调整为大根堆。因为6>5,所以6和1进行交换。 开始调整第二个非叶子节点作为根节点的子树,这里第二个非叶子节点为3。因为5>4,所以根节点3和5进行交换。 接下来,来到非叶节点2,因为 6 > 2,所以根节点2与6进行交换。此时,该子树因为根的调整,以2为根节点的子树不满足大顶堆的性质,因此需要递归的调整子树,将5和2进行调换。 此时来到最后一个非叶节点,即根节点。因为6 > 5,所以3和6进行调换,同时需要递归修改此时以3为根节点的子树进行调整为大顶堆。 至此,最后得到的一个二叉树就为大顶堆,每个节点的值都大于其左右子树的值。其中,根节点为最大值,就是第一大元素。 将根节点输出,以最后一个叶子节点将其补上,然后重复上述的步骤。 备注:当左、右节点值相同时,替换哪一个依据代码的具体形式。 该题解的具体代码: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980class Solution { public int findKthLargest(int[] nums, int k) { return maxHeapMethod(nums,k); } public int maxHeapMethod(int[] nums,int k) { /** 采用堆排序该方法 利用数组进行建堆: 如果根为 i,则其左子树为 i*2+1,则其右子树为 i*2+2 建堆的过程为: 从下至上进行建堆,一边建堆,一边调整堆 调堆的过程为: 其左右孩子节点,依次和其父亲节点的值进行比较;如果大于父亲节点则和父亲节点的值进行交换。 调堆的整个过程为递归过程 模拟完全二叉树 这里用来建立最大堆. **/ int heapLength = nums.length; generateHeap(nums); // 建堆 for(int i = nums.length - 1;i > nums.length - k;--i){ // 调整K次堆以后,则此时数组中的第一个元素,即为第K个最大元素 swapNums(nums,0,i); heapLength -= 1; adjustHeap(nums,0,heapLength); } return nums[0]; } private void generateHeap(int[] splitNums){ /** 该函数用于建堆: 如果根为 i,则其左子树为 i*2+1,则其右子树为 i*2+2 建堆的过程为: 从下至上进行建堆,一边建堆,一边调整堆 从最后一个非叶节(最近)点依次向上进行建堆并且调整 **/ int heapLength = splitNums.length; for(int i = heapLength / 2;i >= 0;--i) { // 从下至上进行建堆,最后一个非叶节点开始。 adjustHeap(splitNums,i,heapLength); } } private void adjustHeap(int[] splitNums,int rootIndex,int heapLength) { /** 从下至上进行调堆 比较root和左右孩子的大小, 如果是调换的root和左孩子,调换完以后。那么再递归该左孩子(以左孩子为root)的调堆。 **/ int leftIndex = rootIndex * 2 + 1; // 左孩子 int rightIndex = rootIndex * 2 + 2; // 右孩子 int maxIndex = rootIndex; // 以下两个条件,将左右孩子中的最大值与根节点做比较,使得根为最大值 if(leftIndex < heapLength && splitNums[leftIndex] > splitNums[maxIndex]) { maxIndex = leftIndex; } if(rightIndex < heapLength && splitNums[rightIndex] > splitNums[maxIndex]) { maxIndex = rightIndex; } if(maxIndex != rootIndex) { // 递归对堆进行调整 swapNums(splitNums,rootIndex,maxIndex); // 将左、右孩子中的最大一个与父节点值进行交换 adjustHeap(splitNums,maxIndex,heapLength); // 以调换的孩子节点为root节点,继续递归调整堆 } } private void swapNums(int[] nums,int indexA,int indexB) { int temp = nums[indexA]; nums[indexA] = nums[indexB]; nums[indexB] = temp; }}","categories":[{"name":"数据结构与算法","slug":"数据结构与算法","permalink":"https://chemlez.github.io/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/"}],"tags":[{"name":"堆排序","slug":"堆排序","permalink":"https://chemlez.github.io/tags/%E5%A0%86%E6%8E%92%E5%BA%8F/"}]},{"title":"初探并查集","slug":"初探并查集","date":"2021-03-17T09:34:11.000Z","updated":"2021-03-20T05:12:58.932Z","comments":true,"path":"2021/03/17/初探并查集/","link":"","permalink":"https://chemlez.github.io/2021/03/17/%E5%88%9D%E6%8E%A2%E5%B9%B6%E6%9F%A5%E9%9B%86/","excerpt":"本文用来记录对并查集的学习与总结,并通过leetcode的两道题目来加深对其的理论与实战学习(实现代码Java)。学习一种数据结构,最高效的方式,就是学以致用,所以这里,以leetcode的题目为例。 给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:”a==b” 或 “a!=b”。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。 只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。 示例1: 输入:["a==b","b!=a"]输出:false解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。 示例2: 输入:["b==a","a==b"]输出:true解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。","text":"本文用来记录对并查集的学习与总结,并通过leetcode的两道题目来加深对其的理论与实战学习(实现代码Java)。学习一种数据结构,最高效的方式,就是学以致用,所以这里,以leetcode的题目为例。 给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:”a==b” 或 “a!=b”。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。 只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。 示例1: 输入:["a==b","b!=a"]输出:false解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。 示例2: 输入:["b==a","a==b"]输出:true解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。 一、并查集介绍并查集:并查集支持查找和合并两种操作的数据结构。并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。主要用于解决一些元素分组的问题。管理一系列不相交的集合。 本质:用集合中的某个元素来代表整个集合,该元素称为集合的代表元。 操作: 合并(Union):把两个不相交的集合合并为一个集合。 查询(Find):查询两个元素是否在一个集合中。 并查集的基本使用场景: 相等传递(例如:等式判断的连通性)。由于相等关系具有传递性,所有相等的变量属于一个集合中。 只关心连通性,不关心距离。 具有以上的条件,就可以考虑并查集。 特点: 并查集用于判断一个元素是否相连,它们的关系是动态添加的,这一类问题叫做动态连通性问题。 主要支持合并与查询是否在同一个集合的操作。 底层结构是数组或者哈希表,用于表示节点指向的父节点,初始化时指向自己。 合并就是把一个集合的根节点指向另一个集合的根节点,只表示在同一个集合里。 这种表示不相交集合的方法称为代表元法,以每个节点的根节点作为一个集合的代表元。 典型应用:最小生成树、Kruskal算法。 优化:采用压缩算法。路径压缩,按秩压缩。 以上是理论部分: 结合:算法学习笔记:并查集,简单易懂,强烈推荐。 二、题解针对题一: ==看作是连接两个节点的边。变量看作是图中的一个节点。所有相等的变量属于同一个连通分量。 首先遍历所有的等式,构造并查集。同一个等式中的两个变量属于同一个连通分量,因此将两个变量进行合并。 遍历所有的不等式。同一个不相等的两个变量不能属于同一个连通分量,因此对两个变量分别查找其所在的连通分量,如果两个变量在同一个连通分量中,则产生矛盾,返回false。 实现方式:使用一个数组parent存储每个变量的连通分量信息,其中的每个元素表示当前变量所在的连通分量的父节点信息,如果父节点是自身,说明该变量为所在的连通分量的根节点。一开始所有变量的父节点都是它们自身。对于合并操作,我们将第一个变量的根节点的父节点指向第二个变量的根节点;对于查找操作,我们沿着当前变量的父节点一路向上查找,直到找到根节点。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849public boolean unionSetMethod(String[] equations){ /** 思路:采用并查集。 1.首先遍历所有的等式,将等式中的character添加到同一个集合中;如果出现相交情况就将两个集合进行合并。 2.其次对所有的不等式进行遍历,对不等式两边的字母进行查询,如果属于同一个集合,就返回false; 3.当遍历完所有的不等式后,如果未返回false,则表示所有的不等式\\等式成立。 注意:字符串的长度为 定长4. **/ int[] union = new int[26]; // 代表26个字符; 0->a,1->b,2->c,3->d,....,25->z // 初始化并查集,当前每个元素的根节点就是其自身 for(int i = 0;i < union.length;++i){ union[i] = i; } // 遍历全部的等式,添加到union中 for(int i = 0;i < equations.length;++i){ String currentString = equations[i]; if(currentString.charAt(1) == '=') { // 当前式子 是等式 其中 char 是基本类型,直接使用 == 比较即可,对等式两边进行merge操作 int indexOne = currentString.charAt(0) - 'a'; int indexTwo = currentString.charAt(3) - 'a'; merge(indexOne,indexTwo,union); } } for(int i = 0;i < equations.length;++i){ String currentString = equations[i]; if(currentString.charAt(1) == '!') { // 当前等式是 不等式,对不等式两边进行查询操作,判断两变量是否在同一个集合中 int indexOne = currentString.charAt(0) - 'a'; int indexTwo = currentString.charAt(3) - 'a'; if(find(indexOne,union) == find(indexTwo,union)) { return false; // 同属于同一个集合中,矛盾。返回 false. } } } return true;}// 返回当前节点的根节点public int find(int x,int[] array){ return array[x] == x ? x : (array[x] = find(array[x],array)); // 路径压缩 边查询 边将 当前节点 接入到根节点上}public void merge(int i,int j,int[] array){ // 第一个节点根节点 连接到 第二个节点的根节点上 array[find(i,array)] = find(j,array);} 题二: 给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。 另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。 返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。 注意:输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。 示例一: 输入:equations = [[“a”,”b”],[“b”,”c”]], values = [2.0,3.0], queries = [[“a”,”c”],[“b”,”a”],[“a”,”e”],[“a”,”a”],[“x”,”x”]]输出:[6.00000,0.50000,-1.00000,1.00000,-1.00000]解释:条件:a / b = 2.0, b / c = 3.0问题:a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?结果:[6.0, 0.5, -1.0, 1.0, -1.0 ] 实例二: 输入:equations = [[“a”,”b”],[“b”,”c”],[“bc”,”cd”]], values = [1.5,2.5,5.0], queries = [[“a”,”c”],[“c”,”b”],[“bc”,”cd”],[“cd”,”bc”]]输出:[3.75000,0.40000,5.00000,0.20000] 实现核心:变量和变量之间具有倍数关系。由于变量之间的倍数关系具有传递性,处理具有传递性关系的问题,可以使用并查集.在并查集的合并和查询操作中维护这些变量之间的倍数关系。构建方式:采用带权的有向图(其中一个边对应一个权值,每个点都有自己的权值,初始化时,默认的权值是1);并且,在维护该并查集时,除了根节点以外,所有同在一个连通分量中的父亲节点均为根节点。路径压缩:在查询一个结点a的根节点同时,把节点a到根节点的沿途所有节点的父亲节点都指向根节点。这样,除了根结点以外,所有结点的父亲结点都指向了根结点。结果:两个同在一个连通分量中的不同的变量,它们分别到根结点(父亲结点)的权值的比值,就是题目的要求的结果。即先判断两个节点是否在同一个集合中,如果在同一个集合中,就分别取出其对应的weight值,然后求出相应的比值就是结果。细节点:并查集的查询操作会执行路径压缩。并查集的特点:一边查询,一边修改节点指向是并查集的特色。(修改的方式:采用路径压缩算法。具体的实现:采用递归的方式)。tips:处理数字比处理字母方便的多,因此将变量的值与id进行唯一映射;此后就可以用该id唯一表示这个变量。 具体的实现步骤: 遍历每个等式,并传入相应的value值。 将该等式中的两个value传入相应的集合中。 查询给定的queries中的值,将结果添加到结果集中。 返回结果 UinonFindSet类的设计模式。 初始化构造方法的编写。 union,合并集合的方法。 查找方法。边查找,边调整并查集。(采用递归的方式) 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485public double[] unionSetMethod(List<List<String>> equations, double[] values, List<List<String>> queries) { int id = 0; double[] res = new double[queries.size()]; Map<String,Integer> map = new HashMap<>(); // 用于做出字母和id的映射 UnionSet unionSet = new UnionSet(equations.size() * 2); // 并查集的创建,这里是其最大的长度 // 做字母和id的映射 for(int i = 0;i < equations.size();++i){ String oString = equations.get(i).get(0); String tString = equations.get(i).get(1); if(!map.containsKey(oString)){ map.put(oString,id); ++id; } if(!map.containsKey(tString)) { map.put(tString,id); ++id; } unionSet.union(map.get(oString),map.get(tString),values[i]); } for(int i = 0;i < queries.size();++i){ Integer x = map.get(queries.get(i).get(0)); Integer y = map.get(queries.get(i).get(1)); res[i] = unionSet.isConnection(x,y); } return res;}// 对该并查集类的设计private class UnionSet { int[] parent; // 并查集实现 -- 采用数组的形式实现 double[] weight; // 每个点的权值 初始化1.0 public UnionSet(int count) { this.parent = new int[count]; this.weight = new double[count]; for(int i = 0;i < count;++i){ this.parent[i] = i; // 当前节点的父节点是其自身 this.weight[i] = 1.0d; // 权重初始值为 1.0d } } // find Method 查询出当前节点的根节点,并且查询时,采用路径压缩算法,对沿途的节点全指向根节点(根节点除外) public int find(int x){ if(x == parent[x]){ return x; }else { int origin = parent[x]; // 记录该层的父节点 -- 便于利用当前父节点的权值 parent[x] = find(parent[x]); // 路径压缩 weight[x] *= weight[origin]; // 更新权值 return parent[x]; } } // merge Method 合并两个集合 public void union(int x,int y,double value){ if(find(x) == find(y)){ // 当前两个元素已经在同一个集合中 return; } int rootX = find(x); // x 的根节点 int rootY = find(y); // y 的根节点 weight[rootX] = value * weight[y] / weight[x]; // 更新 rootX 的权值 parent[rootX] = rootY; // 连接两个根节点 } // 结果求解 public double isConnection(Integer x,Integer y){ if(x == null || y == null){ // 不在原等式中的字母 return -1.0d; }else if(find(x) != find(y)) { // 当前两个节点 不在同一个集合中,返回false(-1.0d) return -1.0d; }else { return weight[x] / weight[y]; // 返回结果 } }} 三、总结初始化: 12345public void init(int n){ for (int i = 1; i <= n; ++i) fa[i] = i;} 此时每个节点的父节点都是自身。 查询操作: 1234567public int find(int x){ if(fa[x] == x) return x; else return find(fa[x]);} 递归查询,每个节点都指向其父节点,只有查询到当前集合的根节点时,其父节点是指向其自身,那么就返回当前集合的代表元。即:一层一层访问父节点,直至根节点(根节点的标志就是父节点是本身)。要判断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可。 合并操作: 1234public void merge(int i, int j){ fa[find(i)] = find(j);} 合并操作也是很简单的,先找到两个集合的代表元素,然后将前者的父节点设为后者即可。当然也可以将后者的父节点设为前者,这里暂时不重要。这里没有采用压缩算法,最终造成的结果是树的深度很长,形成一条直链,导致查询效率较低。 路径压缩:把沿途的每个节点的父节点都设为根节点。 123456789public int find(int x){ if(x == fa[x]) return x; else{ fa[x] = find(fa[x]); //父节点设为根节点 return fa[x]; //返回父节点 }} 该算法的核心是,边查找边进行压缩。这里是将每个节点都直接指向该集合中的代表元(通过递归的方式)。 按秩合并:将简单的树往复杂的树上合并,因为这样合并后,到根节点距离变长的节点个数比较少。(这里简单和复杂是指树的高度,高度越高越为复杂) 初始化:(按秩合并) 12345678public void init(int n){ for (int i = 1; i <= n; ++i) { fa[i] = i; rank[i] = 1; }} 合并(按秩合并) 12345678910public void merge(int i, int j){ int x = find(i), y = find(j); //先找到两个根节点 if (rank[x] <= rank[y]) fa[x] = y; else fa[y] = x; if (rank[x] == rank[y] && x != y) rank[y]++; //如果深度相同且根节点不同,则新的根节点的深度+1}","categories":[{"name":"数据结构与算法","slug":"数据结构与算法","permalink":"https://chemlez.github.io/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/"}],"tags":[{"name":"并查集","slug":"并查集","permalink":"https://chemlez.github.io/tags/%E5%B9%B6%E6%9F%A5%E9%9B%86/"}]},{"title":"Java比较器之Comparator和Comparable","slug":"Java比较器之Comparator和Comparable","date":"2021-03-11T02:50:10.000Z","updated":"2021-03-17T06:03:33.302Z","comments":true,"path":"2021/03/11/Java比较器之Comparator和Comparable/","link":"","permalink":"https://chemlez.github.io/2021/03/11/Java%E6%AF%94%E8%BE%83%E5%99%A8%E4%B9%8BComparator%E5%92%8CComparable/","excerpt":"本文用来简单记录Java中Comparator和Comparable接口特点与使用。 首先,分别查看官方对这两个接口的描述定义: 一、Comparator接口 This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference. A comparison function, which imposes a total ordering on some collection of objects. 可以看出官方对该接口的描述是,它是一种比较函数,用于对一些集合中的对象进行总的排序。即:对集合中的元素进行排序。通过其源码可以看出,除了compare(T o1,T o2)方法,其他方法都给了默认实现。 1int compare(T o1, T o2); 所以要想使用该接口,就得实现该接口的此方法。那么,该接口的作用是什么呢?","text":"本文用来简单记录Java中Comparator和Comparable接口特点与使用。 首先,分别查看官方对这两个接口的描述定义: 一、Comparator接口 This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference. A comparison function, which imposes a total ordering on some collection of objects. 可以看出官方对该接口的描述是,它是一种比较函数,用于对一些集合中的对象进行总的排序。即:对集合中的元素进行排序。通过其源码可以看出,除了compare(T o1,T o2)方法,其他方法都给了默认实现。 1int compare(T o1, T o2); 所以要想使用该接口,就得实现该接口的此方法。那么,该接口的作用是什么呢? 实现接口Comparator<T>类型的任何类都必须要有一个compare的方法,该方法有两个泛型类型(AnyType)的参数 并返回一个int型的量,遵守和compareTo(Comparable接口中需要实现的方法)相同的约定。 简而言之,是对集合中的元素,进行比较。该接口用来返回int类型的结果。当返回是一个正数时,表明o1在集合中的排列在o2之前(注意:这里只是说o1排列在o2之前,并没有说o1大于/小于o2,因此这里的比较,只是用来确定元素在集合中的排列顺序,说大小的比较其实并不太准确);当返回是一个负数时,表明o2在集合中排列在o1之前;如果返回0,那么o1和o2的排列先后顺序没有关系,简单理解成两个对象”相等”。 我们以最简单的例子,来对上面的说法进行实践: Java中的java.util.Arrays中的sort方法如果对int类型的数组进行排序,默认是升序排序,这里我们自定义一个Comparator接口来实现降序排序。 自定义Comparator接口 123456789101112public class MyComparator implements Comparator<Integer> { @Override public int compare(Integer o1, Integer o2) { if (o1 - o2 > 0) { // 该条件表明o1大于o2,o1排列在o2之后 return -1; } else if (o1 - o2 < 0) { // 该条件表明o1小于o2,o1排列在o2之前 return 1; } else { // 顺序无关 return 0; } }} 使用自定的接口 12345678910111213141516public class MyComparatorTest { public static void main(String[] args) { Random random = new Random(); Integer[] res = new Integer[20]; for (int i = 0; i < 20; ++i) { res[i] = random.nextInt(50) + 50; // 随机生成 50 到 100 之间的随机整数 } Arrays.sort(res, new MyComparator()); System.out.println(Arrays.toString(res)); }}=====结果=====[96, 87, 87, 85, 84, 80, 80, 78, 78, 74, 68, 65, 65, 65, 64, 63, 53, 53, 53, 51] 所以,Comparator用于指定集合中对象的排列方式,并且我们在使用Comparator接口时,是专门定义类(工具类)去实现该接口,使得该工具类与被比较对象的类进行了分离。如果对被需要比较的对象修改了比较策略,那么只需要修改我们的工具类即可,因此大大降低了对象之间的耦合性(与Comparable接口最大的不同之处),更具有灵活性。 二、Comparable接口 This interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class’s natural ordering, and the class’s compareTo method is referred to as its natural comparison method. 从上面的实现中看出,对实现该接口的对象进行一个”总”的排序。即:如果想要使用该接口的排列策略,让需要排列的对象的类实现该接口。 其源码: 123456/** * Compares this object with the specified object for order. Returns a * negative integer, zero, or a positive integer as this object is less * than, equal to, or greater than the specified object. **/public int compareTo(T o); 使用方式同Comparator接口,只是这里是实现compareTo接口。 这里我们自定义一个People类: 12345678910111213141516171819202122232425262728293031323334353637public class People implements Comparable<Object>{ private int age; private String name; private double salary; public People() { } public People(int age, String name, double salary) { this.age = age; this.name = name; this.salary = salary; } /** getter / setter / toString 方法的省略 **/ @Override public int compareTo(Object o) { People tempPeople = (People) o; if (this.age > tempPeople.getAge()) { return 1; } else if (this.age < tempPeople.getAge()) { return -1; } if (this.salary > tempPeople.getSalary()) { return 1; } else if (this.salary < tempPeople.getSalary()) { return -1; } return 0; }} 我们看出我们自定义的People类实现了Comparable接口,实现的比较规则是,先按照年龄的大小进行排序,如果年龄大小相同,按照工资的大小进行排序,否则最终返回0.(代表此时两个对象”相等”)。 接下来来对该类进行测试: 1234567891011121314@Testpublic void test() { People p1 = new People(24, \"Lisa\", 4000d); People p2 = new People(24, \"Lisa\", 5000d); People p3 = new People(25, \"Lisa\", 5000d); People[] people = {p1, p2, p3}; Arrays.sort(people); System.out.println(Arrays.toString(people));}====结果====[People{age=24, name='Lisa', salary=4000.0}, People{age=24, name='Lisa', salary=5000.0}, People{age=25, name='Lisa', salary=5000.0}] 三、总结 Comparable接口和Comparator接口区别 Comparator位于包Java.util下,而Comparable位于包Java.lang包下。 Comparable接口将比较代码嵌入需要进行比较的自身代码中,而Comparator接口在一个独立的类中实现比较。 Comparable接口需要实现重写comparaTo方法,同时Comparable是排序接口,若一个类实现了Comparable接口,该类支持排序,相当于内部比较器,Comparator相当于外部比较器。Comparable接口强制进行自然排序,Comparator不强制,可以指定排序。 Comparator更适用于Java提供的类使用;Comparable适用我们自定义的类使用(嵌入在自定义的类中)","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[]},{"title":"从Servlet到HttpServlet都经历了什么","slug":"从Servlet到HttpServlet都经历了什么","date":"2020-11-29T08:08:00.000Z","updated":"2021-05-17T06:37:40.496Z","comments":true,"path":"2020/11/29/从Servlet到HttpServlet都经历了什么/","link":"","permalink":"https://chemlez.github.io/2020/11/29/%E4%BB%8EServlet%E5%88%B0HttpServlet%E9%83%BD%E7%BB%8F%E5%8E%86%E4%BA%86%E4%BB%80%E4%B9%88/","excerpt":"在之前的文章Servlet执行原理浅谈中对Servlet的整个原理做了大概介绍。我们知道客户端发送的请求是交给Servlet中的service方法进行处理。而在实际使用时,并没有直接重写service方法,而是继承了HttpServlet,重写了doGet、doPost等方法,而这期间又发生了什么呢。 首先,我们观察Servlet这个接口: 1234567891011public interface Servlet { void init(ServletConfig var1) throws ServletException; ServletConfig getServletConfig(); void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; String getServletInfo(); void destroy();} Servlet是一个接口,其中包含5个方法,我们大多真正使用的是service方法,其他的几个方法并不常用。因此,就出现了以下两个实现类:","text":"在之前的文章Servlet执行原理浅谈中对Servlet的整个原理做了大概介绍。我们知道客户端发送的请求是交给Servlet中的service方法进行处理。而在实际使用时,并没有直接重写service方法,而是继承了HttpServlet,重写了doGet、doPost等方法,而这期间又发生了什么呢。 首先,我们观察Servlet这个接口: 1234567891011public interface Servlet { void init(ServletConfig var1) throws ServletException; ServletConfig getServletConfig(); void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; String getServletInfo(); void destroy();} Servlet是一个接口,其中包含5个方法,我们大多真正使用的是service方法,其他的几个方法并不常用。因此,就出现了以下两个实现类: 首先我们看GenericServlet类: 123456789101112131415161718192021222324252627public abstract class GenericServlet implements Servlet, ServletConfig, Serializable { private static final long serialVersionUID = 1L; private transient ServletConfig config; public void destroy() { } public ServletConfig getServletConfig() { return this.config; } public String getServletInfo() { return \"\"; } public void init(ServletConfig config) throws ServletException { this.config = config; this.init(); } public void init() throws ServletException { } public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; // 其他方法省略} 为方便展示,这里只罗列出了其实现的父类方法,自身的方法未列出。 从其源码中可以看出,除了service方法(改写成抽象方法),其他四个方法都具体实现了(有的只有return,也是实现)。因此,当我们使用继承GenericServlet类时,只需要具体实现service方法即可。从上面的类图上看出,Servlet容器帮我们设计好了继承类——HttpServlet。 我们继续看HttpServlet: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273public abstract class HttpServlet extends GenericServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String msg = lStrings.getString(\"http.method_get_not_supported\"); this.sendMethodNotAllowed(req, resp, msg); } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String msg = lStrings.getString(\"http.method_post_not_supported\"); this.sendMethodNotAllowed(req, resp, msg); } protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); long lastModified; if (method.equals(\"GET\")) { lastModified = this.getLastModified(req); if (lastModified == -1L) { this.doGet(req, resp); } else { long ifModifiedSince; try { ifModifiedSince = req.getDateHeader(\"If-Modified-Since\"); } catch (IllegalArgumentException var9) { ifModifiedSince = -1L; } if (ifModifiedSince < lastModified / 1000L * 1000L) { this.maybeSetLastModified(resp, lastModified); this.doGet(req, resp); } else { resp.setStatus(304); } } } else if (method.equals(\"HEAD\")) { lastModified = this.getLastModified(req); this.maybeSetLastModified(resp, lastModified); this.doHead(req, resp); } else if (method.equals(\"POST\")) { this.doPost(req, resp); } else if (method.equals(\"PUT\")) { this.doPut(req, resp); } else if (method.equals(\"DELETE\")) { this.doDelete(req, resp); } else if (method.equals(\"OPTIONS\")) { this.doOptions(req, resp); } else if (method.equals(\"TRACE\")) { this.doTrace(req, resp); } else { String errMsg = lStrings.getString(\"http.method_not_implemented\"); Object[] errArgs = new Object[]{method}; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(501, errMsg); } } public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request; HttpServletResponse response; try { request = (HttpServletRequest)req; response = (HttpServletResponse)res; } catch (ClassCastException var6) { throw new ServletException(lStrings.getString(\"http.non_http\")); } this.service(request, response); } // 其他方法省略} 为了方便展示,我们只保留了doGet、doPost以及service方法。 从HttpServlet中的源码可以看出,它不仅实现了service方法,还增加了重载形式。在之前的原理讲解中,我们知道客户端发送的请求首先交给Servlet中的service方法进行处理。 在这里,首先发送的请求来到第一个service方法: 123456789101112public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request; HttpServletResponse response; try { request = (HttpServletRequest)req; response = (HttpServletResponse)res; } catch (ClassCastException var6) { throw new ServletException(lStrings.getString(\"http.non_http\")); } this.service(request, response);} 里面的参数req就包含着我们的请求信息(请求头、请求体、请求参数…)。在其方法体中,首先将请求(req)与响应(res)进行转型 –> Http。最后,调用了其重载方法this.service(request, response); 具体再看该段重载方法: 12345678910111213141516171819202122232425262728293031323334353637383940414243protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); long lastModified; if (method.equals(\"GET\")) { lastModified = this.getLastModified(req); if (lastModified == -1L) { this.doGet(req, resp); } else { long ifModifiedSince; try { ifModifiedSince = req.getDateHeader(\"If-Modified-Since\"); } catch (IllegalArgumentException var9) { ifModifiedSince = -1L; } if (ifModifiedSince < lastModified / 1000L * 1000L) { this.maybeSetLastModified(resp, lastModified); this.doGet(req, resp); } else { resp.setStatus(304); } } } else if (method.equals(\"HEAD\")) { lastModified = this.getLastModified(req); this.maybeSetLastModified(resp, lastModified); this.doHead(req, resp); } else if (method.equals(\"POST\")) { this.doPost(req, resp); } else if (method.equals(\"PUT\")) { this.doPut(req, resp); } else if (method.equals(\"DELETE\")) { this.doDelete(req, resp); } else if (method.equals(\"OPTIONS\")) { this.doOptions(req, resp); } else if (method.equals(\"TRACE\")) { this.doTrace(req, resp); } else { String errMsg = lStrings.getString(\"http.method_not_implemented\"); Object[] errArgs = new Object[]{method}; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(501, errMsg); }} 它对获得的请求req进行方法的请求判断,如果是method=GET,就调用doGet方法;如果method=POST,就调用doPost方法,如果method等于其他5种类型之一,就调用其相应的方法。 这里我们继续看doGet、doPost方法: 12345678910protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String msg = lStrings.getString(\"http.method_get_not_supported\"); this.sendMethodNotAllowed(req, resp, msg);}protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String msg = lStrings.getString(\"http.method_post_not_supported\"); this.sendMethodNotAllowed(req, resp, msg);} 这是不是我们在实际应用中,经常重写的两个方法。我们对请求的处理和响应的设置是不是都在这里面。所以归根结底,我们的请求还是交给了service方法进行处理,是在service中又继而调用更加具体的方法(doGet、doPost…)来为我们处理请求。 现在对整个Servlet处理请求做个流程图总结:","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"Servlet","slug":"Servlet","permalink":"https://chemlez.github.io/tags/Servlet/"},{"name":"JavaWeb","slug":"JavaWeb","permalink":"https://chemlez.github.io/tags/JavaWeb/"},{"name":"源码","slug":"源码","permalink":"https://chemlez.github.io/tags/%E6%BA%90%E7%A0%81/"}]},{"title":"Servlet执行原理浅谈","slug":"Servlet执行原理浅谈","date":"2020-11-27T14:44:41.000Z","updated":"2020-12-23T07:18:35.899Z","comments":true,"path":"2020/11/27/Servlet执行原理浅谈/","link":"","permalink":"https://chemlez.github.io/2020/11/27/Servlet%E6%89%A7%E8%A1%8C%E5%8E%9F%E7%90%86%E6%B5%85%E8%B0%88/","excerpt":"在介绍Servlet之前,简单对web方面的知识做个小结。 一、Web知识小结1.1 软件架构这里的软件架构,指服务器软件工作的两种方式: C/S:客户端/服务器端 B/S:浏览器/服务器端 1.2 网络通信三要素 IP:电子设备(计算机)在网络中的唯一标识。作用:用于定位到具体的电子设备,这里指具体的一台计算机。 port(端口):应用程序在计算机中的唯一标识,其范围在0~65536。作用:用于定位计算机中的具体应用程序(每个应用程序都在监听着具体的端口号)。 传输协议:规定了数据传输的规则(该如何发送数据,又该如何接受数据,最后该对接受到的数据如何解析)。 基础协议 tcp:安全协议,三次握手,速度稍慢。 udp:不安全协议,速度较快。","text":"在介绍Servlet之前,简单对web方面的知识做个小结。 一、Web知识小结1.1 软件架构这里的软件架构,指服务器软件工作的两种方式: C/S:客户端/服务器端 B/S:浏览器/服务器端 1.2 网络通信三要素 IP:电子设备(计算机)在网络中的唯一标识。作用:用于定位到具体的电子设备,这里指具体的一台计算机。 port(端口):应用程序在计算机中的唯一标识,其范围在0~65536。作用:用于定位计算机中的具体应用程序(每个应用程序都在监听着具体的端口号)。 传输协议:规定了数据传输的规则(该如何发送数据,又该如何接受数据,最后该对接受到的数据如何解析)。 基础协议 tcp:安全协议,三次握手,速度稍慢。 udp:不安全协议,速度较快。 1.2 资源分类 静态资源:所有用户访问后,得到的结果都是一样的,称为静态资源,静态资源可以直接被浏览器解析。 如:html、css、JavaScript... 动态资源:每个用户访问相同资源后,得到的结果可能不一样。称为动态资源。动态资源被访问后,需要先转换为静态资源,在返回给浏览器。 如:servlet/jsp、php、asp... 客户端像服务器端请求的形式如下图: 浏览器通过具体的ip:port向服务器端发送请求,当请求的资源是静态资源时,服务器将请求的资源返回(响应)给客户端,浏览器对静态资源进行解析,展示给用户;当请求的资源时动态资源时,服务器内部先将动态资源转换为静态资源,再将该资源响应给浏览器,最后由浏览器对资源进行解析,展示给用户。 二、Servlet相关介绍 概念:Servlet是J2EE众多规范中的一种,是运行在服务器端的小程序。其中,Servlet就是一个接口,定义了Java类被浏览器访问到(tomcat识别)的规则。以后,只要我们自定义一个类,实现Servlet接口,复写其方法,就可进行Web开发。 从上图中我们可以看到,浏览器端对服务器发送请求,请求动态资源时,是由服务器内部的服务器软件(这里假设是Tomcat服务器)对该请求进行处理。其中我们定义的Java类必须遵守一定的规范(Servlet/JSP规范),这个类才能被Tomcat识别,进而对请求进行处理。 2.1 执行原理首先创建JavaEE项目,定义一个类,实现Servlet接口,并实现其中的service抽象方法。 1234567public class ServletDemo1 implements Servlet{ @Override public void service(){ System.out.println(\"hello world\"); }} 然后配置Servlet,即web.xml文件 123456789101112<!--配置Servlet --><servlet> <!-- 类标识名 --> <servlet-name>demo1</servlet-name> <servlet-class>cn.itcast.web.servlet.ServletDemo1</servlet-class></servlet><servlet-mapping> <servlet-name>demo1</servlet-name> <!-- 访问路径 --> <url-pattern>/demo1</url-pattern> </servlet-mapping> 首先客户端发送请求,请求来到Tomcat服务器,查询web.xml配置文件,由于请求路径中包含demo1资源路径,对应着<url-pattern>中的值,继而就找到了<servlet-name>标签中的值(因为两者同属于<servlet-mapping>标签)——demo1,然后通过demo1定位到servlet标签中的<servlet-name>,继而找到<servlet-class>中的全类名,通过反射将全类名对应的字节码文件加载进内存,创建对象,由于必须符合serlvet规范,所以调用能够service方法。 执行原理总结: 当服务器接受到客户端浏览器的请求后,会解析请求URL路径,获取访问的Servlet的资源路径; 查找web.xml文件,是否有对应的<url-pattern>标签体内容; 如果有,则在找到对应的<servlet-class>全类名; tomcat会将字节码文件加载进内存,并且创建其对象; 调用其方法。 2.2 实现原理Servlet具体原理见下图: 首先客户端将发送http请求到服务器中(Web容器,这里是Tomcat),由Tomcat创建Servlet容器,调用其service方法对请求进行处理,处理的逻辑是由我们自己进行编写。将处理的结果交给Response,最终由web容器将Response响应给客户端。 因为Servlet是J2EE中的规范之一,其中处理请求的方法servcice是事先约定好的,我们只需要重写servcie方法,由Tomcat创建的Servlet容器自行帮我们处理请求。 2.4 Mapping详解123456789101112<!--配置Servlet --><servlet> <!-- 类标识名 --> <servlet-name>demo1</servlet-name> <servlet-class>cn.itcast.web.servlet.ServletDemo1</servlet-class></servlet><servlet-mapping> <servlet-name>demo1</servlet-name> <!-- 访问路径 --> <url-pattern>/demo1</url-pattern> </servlet-mapping> mapping是请求路径的映射,可以将Mapping想象成一个value,而servlet是一个key,我们通过value来映射key,所以我们的访问路径可以设置多组,由访问路径找到servlet-class,最终将class加载进内存由Servlet容器对其进行处理。因此,我们可以设置: 12345678910111213141516171819202122<!--配置Servlet --><servlet> <!-- 类标识名 --> <servlet-name>demo1</servlet-name> <servlet-class>cn.itcast.web.servlet.ServletDemo1</servlet-class></servlet><servlet-mapping> <servlet-name>demo1</servlet-name> <!-- 访问路径 --> <url-pattern>/demo1</url-pattern> </servlet-mapping><servlet-mapping> <servlet-name>demo1</servlet-name> <!-- 访问路径 --> <url-pattern>/demo2</url-pattern> </servlet-mapping><servlet-mapping> <servlet-name>demo1</servlet-name> <!-- 访问路径 --> <url-pattern>/demo3</url-pattern> </servlet-mapping> 其中,访问路径demo1,demo2,demo3都可以映射到<servlet-class>cn.itcast.web.servlet.ServletDemo1</servlet-class>,对其servlet进行处理。 同时也可以使用通配符: 123456789101112131415161718<!--配置Servlet --><servlet> <!-- 类标识名 --> <servlet-name>demo1</servlet-name> <servlet-class>cn.itcast.web.servlet.ServletDemo1</servlet-class></servlet><servlet-mapping> <servlet-name>demo1</servlet-name> <!-- 访问路径 --> <url-pattern>/*</url-pattern> <!-- 任何请求 --></servlet-mapping><servlet-mapping> <servlet-name>demo1</servlet-name> <!-- 访问路径 --> <url-pattern>/*.do</url-pattern> <!-- 以.do结尾的请求 --></servlet-mapping>","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"servlet","slug":"servlet","permalink":"https://chemlez.github.io/tags/servlet/"},{"name":"JavaWeb","slug":"JavaWeb","permalink":"https://chemlez.github.io/tags/JavaWeb/"},{"name":"源码","slug":"源码","permalink":"https://chemlez.github.io/tags/%E6%BA%90%E7%A0%81/"}]},{"title":"简化Mybatis的使用——通用Mapper","slug":"简化Mybatis的使用——通用Mapper","date":"2020-11-22T11:58:36.000Z","updated":"2020-12-23T07:18:43.222Z","comments":true,"path":"2020/11/22/简化Mybatis的使用——通用Mapper/","link":"","permalink":"https://chemlez.github.io/2020/11/22/%E7%AE%80%E5%8C%96Mybatis%E7%9A%84%E4%BD%BF%E7%94%A8%E2%80%94%E2%80%94%E9%80%9A%E7%94%A8Mapper/","excerpt":"使用通用Mapper的目的是为了替我们生成常用增删改查操作的SQL语句,并能够简化对于Mybatis的操作。 一、快速入门1.1 数据库表的创建12345678910111213141516171819202122CREATE TABLE `tabple_emp` ( `emp_id` INT NOT NULL AUTO_INCREMENT, `emp_name` VARCHAR ( 500 ) NULL, `emp_salary` DOUBLE ( 15, 5 ) NULL, `emp_age` INT NULL, PRIMARY KEY ( `emp_id` ) );INSERT INTO `tabple_emp` ( `emp_name`, `emp_salary`, `emp_age` )VALUES ( 'tom', '1254.37', '27' );INSERT INTO `tabple_emp` ( `emp_name`, `emp_salary`, `emp_age` )VALUES ( 'jerry', '6635.42', '38' );INSERT INTO `tabple_emp` ( `emp_name`, `emp_salary`, `emp_age` )VALUES ( 'bob', '5560.11', '40' );INSERT INTO `tabple_emp` ( `emp_name`, `emp_salary`, `emp_age` )VALUES ( 'kate', '2209.11', '22' );INSERT INTO `tabple_emp` ( `emp_name`, `emp_salary`, `emp_age` )VALUES ( 'justin', '4203.15', '30' );","text":"使用通用Mapper的目的是为了替我们生成常用增删改查操作的SQL语句,并能够简化对于Mybatis的操作。 一、快速入门1.1 数据库表的创建12345678910111213141516171819202122CREATE TABLE `tabple_emp` ( `emp_id` INT NOT NULL AUTO_INCREMENT, `emp_name` VARCHAR ( 500 ) NULL, `emp_salary` DOUBLE ( 15, 5 ) NULL, `emp_age` INT NULL, PRIMARY KEY ( `emp_id` ) );INSERT INTO `tabple_emp` ( `emp_name`, `emp_salary`, `emp_age` )VALUES ( 'tom', '1254.37', '27' );INSERT INTO `tabple_emp` ( `emp_name`, `emp_salary`, `emp_age` )VALUES ( 'jerry', '6635.42', '38' );INSERT INTO `tabple_emp` ( `emp_name`, `emp_salary`, `emp_age` )VALUES ( 'bob', '5560.11', '40' );INSERT INTO `tabple_emp` ( `emp_name`, `emp_salary`, `emp_age` )VALUES ( 'kate', '2209.11', '22' );INSERT INTO `tabple_emp` ( `emp_name`, `emp_salary`, `emp_age` )VALUES ( 'justin', '4203.15', '30' ); 1.2 对应实体类的创建基本数据类型在Java类中都有默认值,会导致Mybatis在执行相关操作时很难判断当前字段是否为Null。因此,在Mybatis环境下使用Java实体类时尽量不要使用基本数据类型,都使用对应的包装类型。 123456789101112131415161718public class Employee implements Serializable { private Integer empId; private String empName; private Double empSalary; private Integer empAge; public Employee() { } public Employee(Integer empId, String empName, Double empSalary, Integer empAge) { this.empId = empId; this.empName = empName; this.empSalary = empSalary; this.empAge = empAge; } // 省略了getter、setter以及toString()方法的展示} 1.3 Spring-SpringMVC-Mybatis的整合整合步骤见此文ssm框架的整合。 二、通用Mapper的MBG原生的MBG和通用的MBG做对比。 通用Mapper的逆向工程,通过其特点的插件,同样的生成Java实体类对象,带有注解(@Id、@Column等注解);在dao接口层,即mapper接口继承通用Mapper中核心的接口Mapper<T>;生成的实体类Mapper文件(XXxMapper文件)没有SQL语句标签。 当通用Mapper与Spring或SpringBoot整合完以后,通用Mapper的MBG可参考官方文档使用Maven执行MBG的方式。 2.1 自定义Mapper接口 其自己的Mapper<T>接口层次结构如上所示。 作用,根据我们自身的需要,继承上方的层级结构中的mapper接口,供我们自身开发。 举例: 自定义接口: 自定义的Mapper不能和原有的实体类Mapper放在同一级的目录下。 12public interface MyInterface<T> extends BaseMapper<T>, ExampleMapper<T> {} 123@Repositorypublic interface EmployeeMapper extends MyMapper<Employee> {} 配置MapperScannerConfigurer注册MyMapper<T>,或者在我们自定义的Mapper接口中加入注解@RegisterMapper 12345678910!-- 配置扫描器,将mybatis接口的实现加入到ioc容器中 --><bean class=\"tk.mybatis.spring.mapper.MapperScannerConfigurer\"> <!--扫描所有dao接口的实现,加入到ioc容器中 --> <property name=\"basePackage\" value=\"cn.lizhi.dao\"></property> <property name=\"properties\"> <value> mapper=cn.lizhi.myInterface.MyMapper </value> </property></bean> 其中value值默认的是原生mapper的值。 2.2 通用Mapper接口扩展其扩展用来指增加通用Mapper中没有提供的功能。 示例:批量更新。 思路:当我们写SQL语句时,如何能做到批量更新呢?即用;分割我们需要更新的SQL语句。 12345UPDATE table_emp SET emp_name=?,emp_age=?,emp_salary=? WHERE emp_id=?;UPDATE table_emp SET emp_name=?,emp_age=?,emp_salary=? WHERE emp_id=?;UPDATE table_emp SET emp_name=?,emp_age=?,emp_salary=? WHERE emp_id=?;UPDATE table_emp SET emp_name=?,emp_age=?,emp_salary=? WHERE emp_id=?;... 那么Mybatis又是如何做到上面这种形式的呢?即,通过foreach标签达到语句的拼接。 12345678<foreach collection='list' item='record' separator=';'> UPDATE table_emp SET emp_name=#{record.empName}, emp_age=#{record.empAge}, emp_salay=#{record.empSalary} WhERE emp_id=#{record.empId}</foreach> 即我们需要使用通用Mapper能够做到动态的生成上面的SQL语句,供我们使用,即可做到接口的扩展。 2.2.1 需要提供的接口和实现类 在我们自定义的MyMapper<T>接口中除了需要继承Mapper<T>中下方层次结构的接口,它还需要继承我们自己自定义功能的Mapper接口,这里是MyBatchUpdateProvider。 其中MyBatchUpdateProvider是我们自己编写的类(需要继承模板),用于解析xml的SQL语句。 代码示例: 首先编写我们自定义的接口MyBatchUpdateMapper。 123456@RegisterMapperpublic interface MyBatchUpdateMapper<T> { @UpdateProvider(type=MyBatchUpdateProvider.class, method=\"dynamicSQL\") void batchUpdateMapper(List<T> list);} 这里的batchUpdateMapper就是我们后续需要生成模板代码的方法。 编写MyBatchUpdateProvider类,继承MapperTemplate 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950public class MyBatchUpdateProvider extends MapperTemplate { public MyBatchUpdateProvider(Class<?> mapperClass, MapperHelper mapperHelper) { super(mapperClass, mapperHelper); } /** 下方的函数目的是为了拼接此字符串,但是需要能做到通用性,又不仅仅局限于下面的单一情况 * <foreach collection='list' item='record' separator=';'> UPDATE table_emp <SET> emp_name=#{record.empName}, emp_age=#{record.empAge}, emp_salay=#{record.empSalary}, WhERE emp_id=#{record.empId}, </SET> </foreach> */ public String batchUpdateMapper(MappedStatement ms) { final Class<?> entityClass = super.getEntityClass(ms); // 用于获取实体类对象 final String tableName = super.tableName(entityClass); // 用于获取实体类对应的表名 // 修改返回值类型为实体类型 super.setResultType(ms, entityClass); // 拼接动态SQL语句 StringBuilder sql = new StringBuilder(); // 用于生成最终的SQL语句 sql.append(\"<foreach collection='list' item='record' separator=';'>\"); // foreach的开标签 String updateClause = SqlHelper.updateTable(entityClass, tableName); // 设置实体类对象、表的映射 sql.append(updateClause); // 生成 UPDATE 部分 sql.append(\"<set>\"); Set<EntityColumn> columns = EntityHelper.getColumns(entityClass); // 获取实体属性对象 String Id_column = null; String Id_columnHolder = null; for (EntityColumn entityColumn : columns) { boolean flag = entityColumn.isId(); if (flag) { // 判断是否是主键 Id_column = entityColumn.getColumn(); // 主键实体类名 Id_columnHolder = entityColumn.getColumnHolder(\"record\"); } else { String column = entityColumn.getColumn(); // 对应属性的名称 String columnHolder = entityColumn.getColumnHolder(\"record\"); // 通过record进行引用,和foreach中相同 sql.append(column).append(\"=\").append(columnHolder).append(\",\"); } } sql.append(\"</set>\"); sql.append(\"where \").append(Id_column).append(\"=\").append(Id_columnHolder); sql.append(\"</foreach>\"); // foreach的闭标签 return sql.toString(); }} 这里通用代码编写的方法要和我们前面接口中定义的方法名相同,这个方法就是最后我们使用接口时,需要使用的方法。 最后,编写我们自定义的Mapper。 123@RegisterMapperpublic interface MyMapper<T> extends Mapper<T>,MyBatchUpdateMapper<T> {} 在使用时,我们实体类Mapper接口中的用法为: 1234@Repositorypublic interface EmployeeMapper extends MyMapper<Employee> {} 即,只需要继承我们自定义的Mapper即可。 测试类编写: 12345678910111213141516171819202122@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = \"classpath:applicationContext.xml\")public class MapperTest { @Autowired private EmployeeService employeeService; @Test public void batchUpdateEmployeeTest() { List<Employee> list = new ArrayList<Employee>(); Employee emp01 = new Employee(1, \"小明\", 120000d, 18); Employee emp02 = new Employee(2, \"小红\", 130000d, 19); Employee emp03 = new Employee(3, \"小黑\", 140000d, 20); Employee emp04 = new Employee(4, \"小娜\", 150000d, 21); list.add(emp01); list.add(emp02); list.add(emp03); list.add(emp04); employeeService.batchUpdateEmployee(list); }} 主要需要在dbConfig.xml中的url里配置上批量查询的请求参数,即: 1jdbc.url=jdbc:mysql://localhost:3306/mybatis_mapper?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true 2.3 通用Mapper的二级缓存方式对同一内容查询两次,其查询两次数据库,默认并没有将第一次查询的内容进行缓存。 1234567891011121314@Testpublic void findAll() { List<Employee> employees = employeeService.findAll(); for (Employee employee : employees) { System.out.println(employee); } System.out.println(\"----\"); List<Employee> employeeList = employeeService.findAll(); for (Employee employee : employeeList) { System.out.println(employee); }} 加入二级缓存方式: 在Mybatis全局配置文件mybatis-config.xml中开启二级缓存。 123456789<!DOCTYPE configuration PUBLIC \"-//mybatis.org//DTD Config 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-config.dtd\"><configuration> <settings> <setting name=\"cacheEnabled\" value=\"true\"/> </settings> <!-- 其他 --></configuration> 实体类的Mapper接口加入@CacheNamespace注解 1234@Repository@CacheNamespacepublic interface EmployeeMapper extends MyMapper<Employee> {} 2.4 实体类中含有复杂类型的注入2.4.1 简单类型和复杂类型 基本数据类型:byte、char、short、int、float、double、boolean 引用类型:类、接口、数据、枚举... 简单类型:只有一个值的类型 复杂类型:多个简单类型组合起来 2.4.2 准备工作 —— 相关类的创建创建复杂类型的类。即创建一张表table_user,表的每个字段对应下面User实体类的属性,并没有进行主从表的建设,而是直接使用一张表进行操作。其对应的实体类如下: 123456789101112@Table(name=\"table_user\")public class User { @Id @Column(name = \"user_id\") private Integer userId; private String userName; private Address address; private SeasonEnum season; // 省略个无参数、有参数构造器以及getter、setter和toString方法} Address类: 123456789101112131415161718public class Address { private String province; private String city; private String street; public Address() { // TODO Auto-generated constructor stub } public Address(String province, String city, String street) { this.province = province; this.city = city; this.street = street; } // 省略getter、setter以及toString()方法} SeasonEnum类: 12345678910111213141516171819public enum SeasonEnum { SPRING(\"spring @_@\"),SUMMER(\"summer @_@\"),AUTUMN(\"autumn @_@\"),WINTER(\"winter @_@\"); private String seasonName; private SeasonEnum(String seasonName) { this.seasonName = seasonName; } public String getSeasonName() { return this.seasonName; } public String toString() { return this.seasonName; }} 数据库表的建立: 12345678DROP TABLE if EXISTS table_user;CREATE TABLE table_user( user_id INT NOT NULL AUTO_INCREMENT, user_name VARCHAR(32) NULL, address VARCHAR(32) NULL, season ENUM("summer @_@","spring @_@","autumn @_@","winter @_@") NULL, PRIMARY KEY (user_id)) 当使用通用mapper对其进行表的查询时,例如: 123456@Testpublic void testQueryUser() { Integer userId = 1; User user = userService.findById(userId); System.out.println(user);} 返回结果: 1User [userId=1, userName=Justin, address=null, season=null] 自动忽略复杂类型的属性注入。对复杂类型不进行”从类到表”的映射。 解决办法:采用typeHandler。设定一种规则,实现复杂类型中的字段和实体类属性的映射。即自定义类型转换器。这里举例,针对Address对象。 首先顶级接口:TypeHandler,其实现接口为: public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T>是一个抽象类,其抽象方法: 1234567891011// 将parameter对象转换为字符串存入到ps对象的i位置上,此方法对应从Address转换为字符串public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;// 从结果集中获取数据库中对应查询结果;分别从列名、列索引、CallableStatement中获取// 将字符串还原为原始的T类型对象// 此三种方法对应从字符串转换为Address对象public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException; 2.4.3 自定义类型处理器的编写接下来编写AddressHandler转换器的编写 —— 各个值之间使用,分开 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465public class AddressHandler extends BaseTypeHandler<Address> { @Override public void setNonNullParameter(PreparedStatement ps, int i, Address parameter, JdbcType jdbcType) throws SQLException { // 对象为空则直接返回 if (parameter == null) { return; } // 定义以 , 进行分割、拼接字符串 StringBuilder builder = new StringBuilder(); String province = parameter.getProvince(); String city = parameter.getCity(); String street = parameter.getStreet(); builder.append(province) .append(\",\") .append(city) .append(\",\") .append(street); ps.setString(i, builder.toString()); } @Override public Address getNullableResult(ResultSet rs, String columnName) throws SQLException { String parameter = rs.getString(columnName); // Address字段中不含值或者没有按规则存放,则返回Null if (parameter == null || parameter.length() == 0 || !parameter.contains(\",\")) { return null; } Address address = new Address(); address.setProvince(parameter.split(\",\")[0]); address.setCity(parameter.split(\",\")[1]); address.setStreet(parameter.split(\",\")[2]); return address; } @Override public Address getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String parameter = rs.getString(columnIndex); // Address字段中不含值或者没有按规则存放,则返回Null if (parameter == null || parameter.length() == 0 || !parameter.contains(\",\")) { return null; } Address address = new Address(); address.setProvince(parameter.split(\",\")[0]); address.setCity(parameter.split(\",\")[1]); address.setStreet(parameter.split(\",\")[2]); return address; } @Override public Address getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String parameter = cs.getString(columnIndex); // Address字段中不含值或者没有按规则存放,则返回Null if (parameter == null || parameter.length() == 0 || !parameter.contains(\",\")) { return null; } Address address = new Address(); address.setProvince(parameter.split(\",\")[0]); address.setCity(parameter.split(\",\")[1]); address.setStreet(parameter.split(\",\")[2]); return address; }} 2.4.4 注册自定义类型处理器2.4.4.1 方法一、字段级别:@ColumnType注解即在对应的实体类中的属性上加入@ColumnType(typeHandler=AddressTypeHandler.class)注解进行标定。 这里是在User中的Address属性上加入此注解。 2.4.4.2 方法二、全局级别:在Mybatis配置文件中配置typeHandlers123<typeHandlers> <typeHandler handler=\"cn.lizhi.Handler.AddressHandler\" javaType=\"cn.lizhi.domain.Address\"/></typeHandlers> 此时对Address类复杂类型的注入进行测试,查询的返回结果: 123456789@Testpublic void testQueryUser() { Integer userId = 1; User user = userService.findById(userId); System.out.println(user);}/**User [userId=1, userName=Justin, address=Address{province='aaa', city='bbb', street='ccc'}, season=null]**/ 2.4.5 枚举类型的转换方法一:让通用Mapper把枚举类型作为简单类型处理 增加一个通用mapper的配置项,即在通用mapper的配置项中配置enumAsSimpleType=true,其本质是使用了EnumTypeHandler处理器。 方法二:为枚举类型配置对应的类型处理器 思路同Address转换为String,和String转化为Address思路相同。可以将枚举对象和String相互转换。 配置类型处理器 内置 org.apache.ibatis.type.EnumTypeHandler:在数据库中配置的是枚举值本身 org.apache.ibatis.type.EnumOrdinalTypeHandler:在数据库中存的是枚举类型的索引值(因为在枚举类型中,值是固定的) 自定义 内置处理器使用说明 不能使用@ColumnType注解注册Mybatis原生注解;只能在Mybatis全局配置文件中进行属性配置,并在属性上使用@Column注解。如: 1<typeHandler handler=\"org.apache.ibatis.type.EnumTypeHandler\" javaType=\"cn.lizhi.domain.SeasonEnum\"/> 附 通用Mapper官方文档","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"Mybatis","slug":"Mybatis","permalink":"https://chemlez.github.io/tags/Mybatis/"},{"name":"Mapper","slug":"Mapper","permalink":"https://chemlez.github.io/tags/Mapper/"},{"name":"MBG","slug":"MBG","permalink":"https://chemlez.github.io/tags/MBG/"}]},{"title":"ssm框架的整合","slug":"ssm框架的整合","date":"2020-11-18T08:17:10.000Z","updated":"2021-05-26T12:49:21.510Z","comments":true,"path":"2020/11/18/ssm框架的整合/","link":"","permalink":"https://chemlez.github.io/2020/11/18/ssm%E6%A1%86%E6%9E%B6%E7%9A%84%E6%95%B4%E5%90%88/","excerpt":"初学Spring、SpringMVC以及Mybatis时,将其整合时步骤繁多,新手容易不理解,面对繁多的XML配置,往往也不易跑通代码,这里用于记录一次整合的配置。 整合的目的:通过Spring的IoC和AOP对组件进行管理。即:通过IoC解决组件间的动态依赖注入;通过AOP来对事务进行控制,即通过Spring来整合SpringMVC及Mybatis。 想法:对Mybatis的整合是,在Service层调用dao层的接口时,使其自动装配。 首先:一张数据库表对应一个实体类,一个实体类对应一张Mapper.xml配置文件。在resources文件夹下创建一个mapper文件夹,用于存放实体类的Mapper文件。这里创建EmployeeMapper.xml配置文件。","text":"初学Spring、SpringMVC以及Mybatis时,将其整合时步骤繁多,新手容易不理解,面对繁多的XML配置,往往也不易跑通代码,这里用于记录一次整合的配置。 整合的目的:通过Spring的IoC和AOP对组件进行管理。即:通过IoC解决组件间的动态依赖注入;通过AOP来对事务进行控制,即通过Spring来整合SpringMVC及Mybatis。 想法:对Mybatis的整合是,在Service层调用dao层的接口时,使其自动装配。 首先:一张数据库表对应一个实体类,一个实体类对应一张Mapper.xml配置文件。在resources文件夹下创建一个mapper文件夹,用于存放实体类的Mapper文件。这里创建EmployeeMapper.xml配置文件。 一、pom.xml相关配置1.1 pom.xml中properties版本控制12345678910<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.version>5.0.2.RELEASE</spring.version> <slf4j.version>1.6.6</slf4j.version> <log4j.version>1.2.12</log4j.version> <mysql.version>5.1.6</mysql.version> <mybatis.version>3.4.5</mybatis.version></properties> 1.2 Spring相关依赖导入12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849<!-- spring --><!-- 切入点表达式 --><dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.8</version></dependency><!-- springAOP AOP核心功能,例如代理工厂等 --><dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version></dependency><!-- springIOC --><dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version></dependency><!-- spring的web依赖 --><dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version></dependency><!-- SpringMVC --><dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version></dependency><!-- spring整合junit --><dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version></dependency><!-- 事务控制 --><dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version></dependency><!-- SpringJDBC --><dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version></dependency> 1.3 Mybatis及Mybatis-Spring适配包依赖导入123456789101112<!-- Mybatis --><dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version></dependency><!-- Mybatis与Spring整合的中间适配包 --><dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.0</version></dependency> 1.4 数据库相关1234567891011121314<!-- 数据库连接池 --><dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> <type>jar</type> <scope>compile</scope></dependency><!-- mysql驱动 --><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version></dependency> 1.5 其他相关12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273<!-- servlet、jsp、jstl表达式 --><dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> <scope>provided</scope></dependency><dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> <scope>provided</scope></dependency><dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version></dependency><!-- log start --><dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version></dependency><dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version></dependency><dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${slf4j.version}</version></dependency><!-- log end --><!--引入pageHelper分页插件 --><dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.0.0</version></dependency><!-- MBG --><!-- https://mvnrepository.com/artifact/org.mybatis.generator/mybatis-generator-core --><dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.5</version></dependency><!--JSR303数据校验支持;tomcat7及以上的服务器, tomcat7以下的服务器:el表达式。额外给服务器的lib包中替换新的标准的el --><!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator --><dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.4.1.Final</version></dependency><!-- 对json处理的包,即能够使用@ResponseBody --><dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.0</version></dependency><dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.9.0</version></dependency><dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.9.0</version></dependency> 二、Mybatis相关配置文件2.1 Mybatis-config.xml配置文件创建conf文件夹,在其下方创建Mybatis-config.xml。 12345678910111213141516171819202122232425262728293031<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE configuration PUBLIC \"-//mybatis.org//DTD Config 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-config.dtd\"> <!-- 引入Mybatis的配置声明dtd文件 --><!-- mybatis的主配置文件 --><configuration> <properties resource=\"jdbcConfig.properties\"/> <typeAliases> <typeAlias type=\"cn.lizhi.domain.User\" alias=\"user\"/> <package name=\"cn.lizhi.domain\"/> </typeAliases> <!-- 配置环境 若想让environments环境起作用,下列的标签中的配置都需要起作用 --> <environments default=\"mysql\"> <!-- 配置mysql(default的值)环境 id值等于default的值 --> <environment id=\"mysql\"> <!-- 配置事务的类型 --> <transactionManager type=\"JDBC\"/> <!-- 配置数据源(连接池 -\\- druid、c3p0..) --> <dataSource type=\"POOLED\"> <!-- 配置数据库连接的基本信息 --> <property name=\"driver\" value=\"${jdbc.driver}\"/> <property name=\"url\" value=\"${jdbc.url}\"/> <property name=\"username\" value=\"${jdbc.username}\"/> <property name=\"password\" value=\"${jdbc.password}\"/> </dataSource> </environment> </environments> <mappers> <mapper resource=\"./mappers/EmployeeMapper.xml\"/> </mappers></configuration> 2.2 数据库连接配置文件 —— jdbcConfig.properties1234jdbc.driver=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://url:3306/mybatis?characterEncoding=utf8jdbc.username=rootjdbc.password=root 2.3 实体类对应的映射文件Mapper在conf文件夹下创建mappers文件包,继而用于存放全部的实体类映射文件。这里创建EmployeeMapper.xml配置文件。 12345678910<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\"><mapper namespace=\"cn.lizhi.dao.UserDao\"> <!-- 配置查询所有,resultType的作用就是返回封装的位置。这里是对User对象进行封装 --> <select id=\"findAll\" resultType=\"employee\"> SELECT *FROM employee; </select></mapper> 三、web.xml相关配置文件3.1 Spring的配置文件的加载123456789<!-- 配置Spring的监听器 --><listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><!-- 配置加载类路径的配置文件 --><context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value></context-param> 3.2 前端控制器 —— 对SpringMVC配置文件的加载12345678910111213141516<!-- 配置前端控制器:服务器启动必须加载,需要加载springmvc.xml配置文件 --><servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 配置初始化参数,创建完DispatcherServlet对象,加载springmvc.xml配置文件 --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <!-- 服务器启动的时候,让DispatcherServlet对象创建 --> <load-on-startup>1</load-on-startup></servlet><servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern></servlet-mapping> 3.3 中文乱码过滤器12345678910111213141516<filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param></filter><filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern></filter-mapping> 3.4 Restful风格配置1234567891011121314151617<!-- 4、使用Rest风格的URI,将页面普通的post请求转为指定的delete或者put请求 --><filter> <filter-name>HiddenHttpMethodFilter</filter-name> <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class></filter><filter-mapping> <filter-name>HiddenHttpMethodFilter</filter-name> <url-pattern>/*</url-pattern></filter-mapping><filter> <filter-name>HttpPutFormContentFilter</filter-name> <filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class></filter><filter-mapping> <filter-name>HttpPutFormContentFilter</filter-name> <url-pattern>/*</url-pattern></filter-mapping> 四、SpringMVC配置文件的编写 —— Spring-servlet.xmlSpringMVC只是用来控制网站跳转逻辑。首先导入相关的名称空间。 123456789<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:context=\"http://www.springframework.org/schema/context\" xmlns:mvc=\"http://www.springframework.org/schema/mvc\" xsi:schemaLocation=\"http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd\"></beans> 4.1 自动扫描所有的组件 —— 只对控制器进行扫描采用注解扫描,只扫描控制器。 12345<!--SpringMVC的配置文件,包含网站跳转逻辑的控制,配置 --><context:component-scan base-package=\"cn.lizhi\" use-default-filters=\"false\"> <!--只扫描控制器,采用注解扫描的方式 --> <context:include-filter type=\"annotation\" expression=\"org.springframework.stereotype.Controller\"/></context:component-scan> 4.2 Springmvc处理器的配置(两个基本配置)12345<!--两个标准配置--><!-- 将springmvc不能处理的请求交给tomcat --><mvc:default-servlet-handler/><!-- 能支持springmvc更高级的一些功能,JSR303校验,快捷的ajax...映射动态请求 --><mvc:annotation-driven/> 4.3 视图解析器12345<!--配置视图解析器,方便页面返回 --><bean class=\"org.springframework.web.servlet.view.InternalResourceViewResolver\"> <property name=\"prefix\" value=\"/WEB-INF/views/\"></property> <property name=\"suffix\" value=\".jsp\"></property></bean> 4.4 静态资源的处理1234567<!-- 设置静态资源不过滤 --><mvc:resources location=\"/css/\" mapping=\"/css/**\"/><mvc:resources location=\"/images/\" mapping=\"/images/**\"/><mvc:resources location=\"/static/js/\" mapping=\"/static/js/**\"/><mvc:resources location=\"/js/\" mapping=\"/js/**\"/><mvc:resources location=\"/font/\" mapping=\"/font/**\"/><mvc:resources location=\"/static/\" mapping=\"/static/**\"/> 五、Spring的配置文件 —— applicationContext.xml通过Spring来管理所有的业务逻辑组件。 5.1 名称空间123456789101112131415<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:context=\"http://www.springframework.org/schema/context\" xmlns:aop=\"http://www.springframework.org/schema/aop\" xmlns:tx=\"http://www.springframework.org/schema/tx\" xsi:schemaLocation=\"http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd\"></beans> 5.2 配置注解扫描controller层交由给SpringMVC进行管理控制。 1234<!-- 注解扫描,不扫描controller层,controller层交给SpringMVC进行管理 --><context:component-scan base-package=\"cn.lizhi\"> <context:exclude-filter type=\"annotation\" expression=\"org.springframework.stereotype.Controller\" /></context:component-scan> 5.3 业务逻辑等相关配置Spring用来控制业务逻辑。数据源、事务控制、aop等等都交由Spring进行控制。 数据源配置: 123456789<!-- 引入数据源配置文件 --><context:property-placeholder location=\"classpath:dbconfig.properties\" /><!-- 数据源配置 --><bean id=\"pooledDataSource\" class=\"com.mchange.v2.c3p0.ComboPooledDataSource\"> <property name=\"jdbcUrl\" value=\"${jdbc.jdbcUrl}\"></property> <property name=\"driverClass\" value=\"${jdbc.driverClass}\"></property> <property name=\"user\" value=\"${jdbc.user}\"></property> <property name=\"password\" value=\"${jdbc.password}\"></property></bean> 事务控制: 1234567891011121314151617181920<bean id=\"transactionManager\" class=\"org.springframework.jdbc.datasource.DataSourceTransactionManager\"> <!--控制住数据源 --> <property name=\"dataSource\" ref=\"pooledDataSource\"></property></bean><!--配置事务增强,事务如何切入 --><tx:advice id=\"txAdvice\" transaction-manager=\"transactionManager\"> <tx:attributes> <!-- 所有方法都是事务方法 --> <tx:method name=\"*\" propagation=\"REQUIRED\" read-only=\"false\"/> <!--以get开始的所有方法 只读 --> <tx:method name=\"get*\" propagation=\"SUPPORTS\" read-only=\"true\"/> </tx:attributes></tx:advice><!--开启基于注解的事务,使用xml配置形式的事务(必要、主要的都是使用配置式) --><aop:config> <!-- 切入点表达式 --> <aop:pointcut expression=\"execution(* cn.lizhi.service..*(..))\" id=\"txPoint\"/> <!-- 配置事务增强 --> <aop:advisor advice-ref=\"txAdvice\" pointcut-ref=\"txPoint\"/></aop:config> 整合mybatis: 目的: spring来管理所有组件,即管理mapper的实现类。 service ==》dao @Autowired:自动注入mapper; spring用来管理事务,spring声明式事务。 123456789101112131415161718192021<!-- 创建SqlSessionFactory对象 --><bean id=\"sqlSessionFactory\" class=\"org.mybatis.spring.SqlSessionFactoryBean\"> <!-- 指定mybatis全局配置文件的位置 --> <property name=\"configLocation\" value=\"classpath:mybatis-config.xml\"></property> <!-- 指定数据源 --> <property name=\"dataSource\" ref=\"pooledDataSource\"></property> <!-- 指定mybatis,mapper文件的位置 --> <property name=\"mapperLocations\" value=\"classpath:mapper/*.xml\"></property></bean><!-- 配置扫描器,将mybatis接口的实现加入到ioc容器中 --><bean class=\"org.mybatis.spring.mapper.MapperScannerConfigurer\"> <!--扫描所有dao接口的实现,加入到ioc容器中 --> <property name=\"basePackage\" value=\"cn.lizhi.dao\"></property></bean><!-- 配置一个可以执行批量的sqlSession --><bean id=\"sqlSession\" class=\"org.mybatis.spring.SqlSessionTemplate\"> <constructor-arg name=\"sqlSessionFactory\" ref=\"sqlSessionFactory\"></constructor-arg> <constructor-arg name=\"executorType\" value=\"BATCH\"></constructor-arg></bean> 5.4 重写Mybatis主配置文件当在Spring的配置文件中整合了Mybatis后,需要将Mybatis中主配置文件中多余的信息删除,此时Mybatis主配置文件只用来Mybatis中自身的配置,其余的交给Spring,进行管理。此时配置结果如下: 1234567891011121314<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE configuration PUBLIC \"-//mybatis.org//DTD Config 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-config.dtd\"><!-- 引入Mybatis的配置声明dtd文件 --><!-- mybatis的主配置文件 --><configuration> <!-- 配置别名 --> <typeAliases> <package name=\"cn.lizhi.domain\"/> </typeAliases></configuration> 六 测试6.1 dao层的编写123456@Repositorypublic interface EmployeeDao { Employee findById(Integer id); List<Employee> findAll();} 6.2 service层的编写123456789101112@Servicepublic class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeDao employeeDao; @Override public List<Employee> findAll() { List<Employee> employees = employeeDao.findAll(); return employees; }} 6.3 controller层的编写12345678910111213@Controller@RequestMapping(\"/emps\")public class EmployeeController { @Autowired private EmployeeService employeeService; @RequestMapping(\"/allEmp\") public String findAllEmployee(Model model) { List<Employee> employees = employeeService.findAll(); model.addAttribute(\"employees\", employees); return \"all\"; }} 6.4 EmployeeMapper.xml的编写123456789101112131415161718192021<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\"><mapper namespace=\"cn.lizhi.dao.EmployeeDao\"> <resultMap id=\"empFindAll\" type=\"employee\"> <id property=\"empId\" column=\"emp_id\"></id> <result property=\"empName\" column=\"emp_name\"></result> <result property=\"empSalary\" column=\"emp_salary\"></result> <result property=\"empAge\" column=\"emp_age\"></result> </resultMap> <select id=\"findById\" parameterType=\"int\" resultType=\"employee\"> SELECT * FROM table_emp WHERE id=#{id}; </select> <select id=\"findAll\" resultMap=\"empFindAll\"> SELECT * FROM table_emp; </select></mapper>","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"Mybatis","slug":"Mybatis","permalink":"https://chemlez.github.io/tags/Mybatis/"},{"name":"ssm","slug":"ssm","permalink":"https://chemlez.github.io/tags/ssm/"},{"name":"Spring","slug":"Spring","permalink":"https://chemlez.github.io/tags/Spring/"},{"name":"SpringMVC","slug":"SpringMVC","permalink":"https://chemlez.github.io/tags/SpringMVC/"}]},{"title":"Springboot自定义starter","slug":"Springboot自定义starter","date":"2020-11-15T12:09:57.000Z","updated":"2020-12-23T07:18:22.949Z","comments":true,"path":"2020/11/15/Springboot自定义starter/","link":"","permalink":"https://chemlez.github.io/2020/11/15/Springboot%E8%87%AA%E5%AE%9A%E4%B9%89starter/","excerpt":"一、介绍与定义本章用于记录自定义Springboot-starter的学习过程。 在我们自定义starter之前,我们首先观察一下SpringBoot自身的starter的形式都是什么样的。我们以spring-boot-starter-web为例。 通过spring-boot-starter-web,可以看出当前引入的依赖是空的JAR文件。它的作用是仅提供辅助依赖管理,这些依赖可用于自动装配或者其他类库。继续点入,可以看见其引入了spring-boot-starter,再进一步点入,又能看见其引入了spring-boot-autoconfigure。 1234567891011121314<!-- 在spring-boot-starter-web中引入了以下依赖 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.4.0</version> <scope>compile</scope></dependency><!-- 在spring-boot-starter中引入了以下依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <version>2.4.0</version> <scope>compile</scope></dependency>","text":"一、介绍与定义本章用于记录自定义Springboot-starter的学习过程。 在我们自定义starter之前,我们首先观察一下SpringBoot自身的starter的形式都是什么样的。我们以spring-boot-starter-web为例。 通过spring-boot-starter-web,可以看出当前引入的依赖是空的JAR文件。它的作用是仅提供辅助依赖管理,这些依赖可用于自动装配或者其他类库。继续点入,可以看见其引入了spring-boot-starter,再进一步点入,又能看见其引入了spring-boot-autoconfigure。 1234567891011121314<!-- 在spring-boot-starter-web中引入了以下依赖 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.4.0</version> <scope>compile</scope></dependency><!-- 在spring-boot-starter中引入了以下依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <version>2.4.0</version> <scope>compile</scope></dependency> 从以上总结出,starter的场景需要以下两个模块: 启动器模块 – xx-starter 自动配置模块 自定义starters,即对自动装配的组件交给SpringBoot管理,继而供我们使用,需满足以下条件: 自动装配Bean 自动装配使用配置类(@Configuration)结合Spring提供的条件判断注解@Conditional,即SpringBoot的派生注解,如:@ConditionOnClass完成; 配置自动装配Bean 将标注@Configuration的自动配置类,放在classpath下META-INF/spring.factories文件中。 同样,以WebMvcAutoConfiguration为例的自动配置编写: 1234567@Configuration //指定这个类是一个配置类@ConditionalOnXXX //在指定条件成立的情况下自动配置类生效@AutoConfigureAfter //指定自动配置类的顺序@Bean //给容器中添加组件@ConfigurationPropertie // 结合相关xxxProperties类来绑定相关的配置@EnableConfigurationProperties //让xxxProperties生效加入到容器中 自动配置类要是能够加载,那么就需要将启动加载的自动配置类,配置在META-INF/spring.factories 123org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\\org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\\ 所有自动配置:首先它是一个自动配置类;设定起作用的条件,再将相关配置加入到容器中。 启动器:启动器模块是一个空的JAR文件,仅提供辅助性依赖管理,这些依赖用于自动装配或者其他类库。 总结starter的使用模式: 首先编写xxx-starter启动器,继续编写一个xxx-starter-autoconfigurer自动配置类,我的启动器则依赖此自动配置类。当需要使用我们的starter时,则只需要直接依赖我们的xxx-starter启动器即可。 命名规范: 官方命名空间 前缀:spring-boot-starter- 模式:spring-boot-starter-模块名 例子:spring-boot-starter-web、spring-boot-starter-actuator、spring-boot-starter-jdbc 自定义命名空间 后缀:-spring-boot-starter 模式:模块-spring-boot-starter 举例:mybatis-spring-boot-starter 自定义starter步骤:创建两个模块。一个作为启动器,一个作为自动配置模块。最终的目的,启动器中包含着自动配置模块,导入依赖时是导入启动器。 二、自定义Starter的编写新建工程,创建两个模块。分别作为启动器模块和自动配置模块。自定义hello的starter。 启动器模块命名:selfdef-spring-boot-starter。 自动配置模块:selfdef-spring-boot-starter-autoconfigurer 启动器模块: 按照之前的描述,启动器模块不写入任何内容,只在pom.xml中引入自动配置模块的依赖。即: 123456789101112131415161718192021<?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>cn.chemlez</groupId> <artifactId>selfdef-springboot-starter</artifactId> <version>1.0-SNAPSHOT</version> <!-- 启动器 --> <dependencies> <!-- 自动配置模块 --> <dependency> <groupId>cn.chemlez.starter</groupId> <artifactId>selfdef-spring-boot-starter-autoconfigurer</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies> </project> 自动配置模块: 12345678910111213141516171819202122232425262728<?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 https://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.4.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>cn.chemlez.starter</groupId> <artifactId>selfdef-spring-boot-starter-autoconfigurer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>sefldef-spring-boot-starter-autoconfigurer</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- 引入spring-boot-starter:所有starter的基本配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies></project> 编写配置类: 1234567891011121314151617181920212223// 绑定属性,以 chemlez.hello 为开头的配置@ConfigurationProperties(prefix = \"chemlez.hello\")public class HelloProperties { private String prefix; private String suffix; public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public String getSuffix() { return suffix; } public void setSuffix(String suffix) { this.suffix = suffix; }} 编写HelloService类: 12345678910111213141516171819public class HelloService { HelloProperties helloProperties; public String sayHello(String name) { return helloProperties.getPrefix() + \"-\" + name + helloProperties.getSuffix(); } public HelloProperties getHelloProperties() { return helloProperties; } public void setHelloProperties(HelloProperties helloProperties) { this.helloProperties = helloProperties; }} 供调用starter的使用者使用的类。最终这个类通过自动配置类,将其添加进容器中,供使用者使用。 编写自动配置类: 12345678910111213141516@Configuration@ConditionalOnWebApplication // web应用才能够生效@EnableConfigurationProperties(HelloProperties.class) // 使属性文件生效public class HelloServiceAutoConfiguration { @Autowired HelloProperties helloProperties; // 将service注入到容器中,共我们使用 @Bean public HelloService helloService() { HelloService service = new HelloService(); service.setHelloProperties(helloProperties); return service; }} 至此,我们编写了一个简单的starter。其作用,就是在页面中返回HelloService中的以下这个函数的返回值: 123public String sayHello(String name) { return helloProperties.getPrefix() + \"-\" + name + helloProperties.getSuffix();} 最后,分别将selfdef-spring-boot-starter、selfdef-spring-boot-starter-autoconfigurer通过Maven中的install,将其打包进Maven仓库供我们使用依赖。 注意:因为,selfdef-spring-boot-starter中引入了selfdef-spring-boot-starter-autoconfigurer,供在打包时,先打包后者,再打包前者。 三、测试创建一个新的web工程的Springboot项目,在其pom.xml文件中,引入上面我们自定义的selfdef-spring-boot-starter依赖,如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849<?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 https://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.4.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>cn.chemlez</groupId> <artifactId>spring-boot-starter-09</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-boot-starter-09</name> <description>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> <!-- 当前依赖就是我们前面自定义的starter --> <dependency> <groupId>cn.chemlez</groupId> <artifactId>seldef-springboot-starter</artifactId> <version>1.0-SNAPSHOT</version> </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> 编写控制器类: 1234567891011121314@Controllerpublic class HelloController { @Autowired private HelloService helloService; @RequestMapping(\"/hello\") @ResponseBody public String hello() { String hello = helloService.sayHello(\"Tom\"); return hello; }} 配置文件的编写。因为我们在第二节中的配置类中,设定了前缀(prefix)和后缀(suffix)。因此,要在配置文件中,将其配置出。 123chemlez.hello: prefix: 张三 suffix: 李四 启动Springboot项目,访问8080端口下的hello。结果: 1张三-Tom李四","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"Springboot-starter","slug":"Springboot-starter","permalink":"https://chemlez.github.io/tags/Springboot-starter/"}]},{"title":"Mongodb的安装配置及基本使用","slug":"Mongodb的安装配置及基本使用","date":"2020-11-09T01:12:53.000Z","updated":"2020-11-09T06:25:45.466Z","comments":true,"path":"2020/11/09/Mongodb的安装配置及基本使用/","link":"","permalink":"https://chemlez.github.io/2020/11/09/Mongodb%E7%9A%84%E5%AE%89%E8%A3%85%E9%85%8D%E7%BD%AE%E5%8F%8A%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8/","excerpt":"本文记载一次在阿里云服务器装载Mongodb并远程连接成功使用的过程记录。 基本安装环境:本次安装环境:CentOS7、Mongodb版本为4.2.10 一、安装通过Mongodb官网,安装Mongodb的社区版本,选择自己需要安装的版本以及依赖的环境。如下图: 在服务器打开终端,应用centos中网络下载的方式下载Mongodb的安装包。 首先,进入服务器端的usr/local,在其路径下创建文件夹: 12mkdir mongodb4cd mongodb4 在此文件夹下远程网络下载Mongodb的安装包: 1wegt https://fastdl.mongodb.org/linux/mongodb-shell-linux-x86_64-rhel70-4.4.1.tgz 解压文件夹: 1tar -zxvf mongodb-linux-x86_64-rhel70-4.2.10.tgz 修改压缩包名称: 1mv mongodb-linux-x86_64-rhel70-4.2.10 mongodb4.2","text":"本文记载一次在阿里云服务器装载Mongodb并远程连接成功使用的过程记录。 基本安装环境:本次安装环境:CentOS7、Mongodb版本为4.2.10 一、安装通过Mongodb官网,安装Mongodb的社区版本,选择自己需要安装的版本以及依赖的环境。如下图: 在服务器打开终端,应用centos中网络下载的方式下载Mongodb的安装包。 首先,进入服务器端的usr/local,在其路径下创建文件夹: 12mkdir mongodb4cd mongodb4 在此文件夹下远程网络下载Mongodb的安装包: 1wegt https://fastdl.mongodb.org/linux/mongodb-shell-linux-x86_64-rhel70-4.4.1.tgz 解压文件夹: 1tar -zxvf mongodb-linux-x86_64-rhel70-4.2.10.tgz 修改压缩包名称: 1mv mongodb-linux-x86_64-rhel70-4.2.10 mongodb4.2 二、环境配置 全局配置 配置系统环境变量: 1vim /etc/profile 写入配置文件: 12# Mongodbexport PATH=\"/usr/local/mongodb4/mongodb4.2/bin:$PATH\" 退出保存后,输入以下命令使环境变量生效。 1source /etc/profile 创建Mongodb数据存放文件夹和日志记录文件夹。 在root的根目录下创建以下文件夹: 12mkdir -p /data/dbmkdir -p /logs 创建Mongodb运行时使用的配置文件。 进入mongodb4文件夹下的bin目录 1cd /usr/local//mongodb4/mongodb4.2/bin/ 此时所在的路径为: 1/usr/local/mongodb4/mongodb4.2/bin 创建MongoDB.conf配置文件: 1vim mongodb.conf 输入以下内容: 123456dbpath = /data/db #数据文件存放目录logpath = /logs/mongodb.log #日志文件存放目录port = 27017 #端口fork = true #以守护程序的方式启用,即在后台运行# auth=true #需要认证。如果放开注释,就必须创建MongoDB的账号,使用账号与密码才可远程访问,第一次安装建议注释bind_ip=0.0.0.0 #允许远程访问,或者直接注释,127.0.0.1是只允许本地访问 加载配置文件,并启动MongoDB服务。 1./mongod -f mongodb.conf 通过以下命令查看端口是否映射成功: 12345netstat -nltp|grep mongod或者:netstat -nltp|grep 27017或者ps -ef | grep mongo 以上命令查看MongoDB是否已经启动以及端口是否成功开启。 三、Mongodb启动及账号创建查看bin目录下的问文件目录,通过ls命令。 目录下存在mongo,启动mongo命令。 1mongo 以上界面是MongoDB启动时,采用的超级权限(即关闭了认证,不需要认证即能登录MongoDB)。 由于第一次使用MongoDB时,MongoDB默认不存在任何用户,所以我们在前面的配置文件中关闭了认证的权限(# auth=true),以方便我们后续自己能够创建一个用户使用。 使用admin数据库(admin数据库用来管理用户权限) 1234567show dbs # 展示目前的所有数据库-----admin 0.000GBconfig 0.000GBhuaJT 0.013GBlocal 0.000GBtest 0.026GB 进入admin数据库: 123use admin----switched to db admin 创建管理员用户: 12345678> use admin> db.createUser( { user:"root", pwd:"123456", roles:[{role:"root",db:"admin"}] } ) 以上字段的含义,创建一个用户,用户名是:root;密码是:123456;赋予的权限是root权限,可以操控的数据库有admin数据库,这种用户的权限较高。 下面创建一个普通用户,供我们后续的实验测试。 123456789101112131415use admindb.createUser( "user" : "user", "pwd" : "123456", "roles" : [ { "role" : "dbOwner", "db" : "test1" }, { "role" : "dbOwner", "db" : "test2" } ]) 此次创建的用户名是:user,密码是:123456;用户角色是:dbOwner,是test1和test2数据库的用户管理者。 给创建的用户赋予权限 1db.auth(\"用户名\",\"密码\") 即: 1234db.auth(\"user\",\"123456\")> 1db.auth(\"root\",\"123456\")> 1 出现1表明操作操作成功。 四、远端连接配置通过以上三步,我们已经安装了Mongodb并配置了Mongodb相关配置文件以及创建了 Mongodb用户。接下来,就需要对Mongodb进行远程连接的配置。 首先开启认证,关闭MongoDB服务端,重新启动,刷新配置文件使其生效。 进入bin目录下,修改mongodb.conf,此时的路径为:/usr/local/mongodb4/mongodb4.2/bin 1vim mongodb.conf 开启用户认证(以下的全部配置): 123456dbpath = /data/db #数据文件存放目录logpath = /logs/mongodb.log #日志文件存放目录port = 27017 #端口fork = true #以守护程序的方式启用,即在后台运行auth = true #需要认证。如果放开注释,就必须创建MongoDB的账号,使用账号与密码才可远程访问,第一次安装建议注释bind_ip=0.0.0.0 #允许远程访问,或者直接注释,127.0.0.1是只允许本地访问 关闭Mongodb服务端 1./mongod --shutdown 重新启动服务器端并刷新配置文件 1./mongod -f mongodb.conf 此时,关于认证的配置已经全部结束,下面我们对Mongodb进行服务器端的使用测试。 登录Mongodb类似于MySQL,采用以下命令。 1mongo -uroot -proot 登录我们之前的创建的user账户: 1mongo -uuser -p123456 由于之前,我们创建user账户时,给予了它对test1、test2数据库的权限;所以我们此时可以创建test1、test2并使用。 首先明确以下概念: Mongodb中的collection对应着Mysql中的表; Mongodb中的document对应着Mysql中的记录。 在Mongodb中创建数据库就直接使用use 数据库名称;例如创建test1数据库,则use test1; 12> use test1switched to db test1 此时系统创建了test1,但只是预创建。如果,我们没有对其进行任何操作,即数据库中不存在任何信息,那么当我们切出这个数据库时,系统就会释放这个数据库的内存信息,就不再存在这个数据库。 向test1数据库中插入collection,并创建document。 1db.firstCollection.save({\"name\":\"Tom\",\"age\":\"18\"}) 这里创建了一个collection-firstCollection。存入了姓名和年龄信息。 12show collections # 展示数据库中的所有collectiondb.firstCollection.find() # 展示firstCollection集合的全部文档信息 至此,Mongodb本地使用的配置已经全部配置完毕,下面是开启远程连接的操作。 登录阿里云,开启服务器端的安全组;因为Mongodb服务器端监听的端口是27017,故需要配置安全组,端口号为27017。 同时配置内网入方向规则和内网出方向规则。 阿里云服务器端开启防火墙,并放行27017端口。 查看防火墙状态: 1systemctl status firewalld 开启防火墙: 1systemctl start firewalld 关闭防火墙: 1systemctl stop firewalld 确认firewalld状态: 1systemctl status firewalld 开放27017端口: 1firewall-cmd --permanent --zone=public --add-port=27017/tcp 至此,通过以上的步骤,我们配置了阿里云的内网规则;服务器端放行了27017端口,允许远程可以访问。 测试连接 打开浏览器,访问自己服务器端的IP地址:端口号。 1IP地址:27017 例如: 1106.65.34.210:27017 若出现以下画面,则说明远程连接访问配置成功。 五、本地通过Pycharm连接Mongodb我们在远程服务器端的一系列配置,就是为了能够在客户端操作Mongodb数据库。我们采用最简单的方式,那就是利用Pycharm的插件Mongo Explorer来帮助我们进行可视化操作。 在Pycharm中的插件下载等工作就不再赘述。下面即对Mongodb的连接的操作: 进入Mongo Explorer的配置,General项的配置为: Label:随便填写,用于标识数据库。 Server url(s):IP地址:端口号。 User Database:连接的数据库名称。 接下来进入第二项Authentication,进行配置。 同Mysql一样,填上Username和Password即可,下方的Auth.mechanism选择第一项。 点击Test Connection进行测试。 大功告成! 至此,我们完成了Mongodb的下载安装以及相关配置,最后到客户端对服务器端的远程连接,完成了可视化操作。 参考文献 [1] Linux安装、运行MongoDB [2] MongoDB用户创建 [3] 解决FirewallD is not running问题","categories":[{"name":"Mongodb","slug":"Mongodb","permalink":"https://chemlez.github.io/categories/Mongodb/"}],"tags":[{"name":"Mongodb","slug":"Mongodb","permalink":"https://chemlez.github.io/tags/Mongodb/"}]},{"title":"Java基础之IO流","slug":"Java基础之IO流","date":"2020-10-14T01:26:17.000Z","updated":"2020-10-14T01:57:52.990Z","comments":true,"path":"2020/10/14/Java基础之IO流/","link":"","permalink":"https://chemlez.github.io/2020/10/14/Java%E5%9F%BA%E7%A1%80%E4%B9%8BIO%E6%B5%81/","excerpt":"一、字节流与字符流 输入流:用于读取数据 – 将数据写入内存进行展示,即将数据从其他设备读取到内存中的流。 输出流:用于数据保存 – 将数据写入磁盘,可持久化存储,即将数据从内存中写出到其他设备上的流。 在字节流(以字节为单位)中,输出数据使用OutStream类完成,输入使用的是InputStream类完成。(所有字节流的父类) 在字符流(以字符为单位)中,输出数据使用Writer类完成,输入使用Reader完成。(所有字符流的父类) 其中,字节流主要操作byte类型数据,以byte数组为准。 如果想对文件进行读写,首先需要创建一个文件对象,如下: 1234567public class FileDemo01 { public static void main(String[] args) { String pathname = \"a.txt\"; File file = new File(pathname); System.out.println(file); // a.txt }} 从上面代码段可以看出,File接收的参数是文件路径,返回的是File对象。但是,直接打印File时,返回的是pathname,即为传入的参数。所以,在File类中,重写了toString方法。后面当我们拿到File对象后,就可以进行后续对当前文件的一系列操作了。","text":"一、字节流与字符流 输入流:用于读取数据 – 将数据写入内存进行展示,即将数据从其他设备读取到内存中的流。 输出流:用于数据保存 – 将数据写入磁盘,可持久化存储,即将数据从内存中写出到其他设备上的流。 在字节流(以字节为单位)中,输出数据使用OutStream类完成,输入使用的是InputStream类完成。(所有字节流的父类) 在字符流(以字符为单位)中,输出数据使用Writer类完成,输入使用Reader完成。(所有字符流的父类) 其中,字节流主要操作byte类型数据,以byte数组为准。 如果想对文件进行读写,首先需要创建一个文件对象,如下: 1234567public class FileDemo01 { public static void main(String[] args) { String pathname = \"a.txt\"; File file = new File(pathname); System.out.println(file); // a.txt }} 从上面代码段可以看出,File接收的参数是文件路径,返回的是File对象。但是,直接打印File时,返回的是pathname,即为传入的参数。所以,在File类中,重写了toString方法。后面当我们拿到File对象后,就可以进行后续对当前文件的一系列操作了。 File对象常见的方法: 123456789101112public class FileDemo01 { public static void main(String[] args) { String pathname = \"in.txt\"; File file = new File(pathname); System.out.println(file); String absolutePath = file.getAbsolutePath(); // 获取in.txt文件的绝对路径 -->String File absoluteFile = file.getAbsoluteFile(); // 获取的是文件对象 String name = absoluteFile.getName(); // 文件名 --> in.txt long length = absoluteFile.length(); // 文件长度 long length1 = file.length(); // 同上 }} 二、字节流的读写操作1.字节流的输出1234<details><summary>字节流的写入</summary> 1234567891011public class FileDemo05 { public static void main(String[] args) throws IOException { File file = new File(\"test1.txt\") FileOutputStream fos = new FileOutputStream(file, true);// 在原有的内容进行追加 byte[] bytes1 = \"问:请再添加一条会怎么样\".getBytes(); // 获取输入的字节数组 byte[] b = \"\\n\".getBytes(); // 换行 fos.write(b); fos.write(bytes1); fos.close(); }} 2.字节流的读取 字节流的读取 123456789101112public class FileDemo05 { public static void main(String[] args) throws IOException { File file = new File(\"test1.txt\") FileInputStream fis = new FileInputStream(file); byte[] bytes = new byte[1024]; // 设置一次读取的字节数组长度 int length = -1; // while ((length = fis.read(bytes)) != -1) { // 循环 System.out.println(new String(bytes, 0, length)); } fis.close(); }} 当一次读取一个字节时,length中存放的是对应字节的ASCII码数值;当一次读取多个字符时,length中记录的是数组的有效长度。 对上图中一次读取多个字节的思考: 上图右边读取多个字符中,在读取时,若没有指定数组的有效长度,会出现重复的情况。例如:这里我们期望读取到是ABCDE,但最终的情况会是ABCDEDED。这里设定的缓冲区读取的数组长度为2。 第一次读取时:缓冲区数组对应的内容是{A,B}(这里对应的其实是ASCII码)。len = 2. 第二次读取时:读取到缓冲区对应的内容是{C,D},len = 2. 第三次读取时:读取到缓冲区对应内容,只有E,但是原来的数组中存储的是{C,D},故将E覆盖C。所以此时缓冲区对应的内容是{E,D},len = 1.故此轮输出是ED。 注意:又因为没有返回-1,所以继续读取。 第四次读取时:文件中没有有效内容,返回-1。但数组中是{E,D},所以再次打印时,还是ED。 因此在读取操作时,需要注意两点: 未读取到有效内容,返回-1时,则停止读取。 while(len != -1){} 读取时,指定数组的有效长度,而数组的有效长度,又可以通过len进行指定。 new String(bytes,0,len) 3.案例 — 统计并打印指定文件夹.java文件思路: 一个文件夹下可能既包含文件,又包含文件夹。所以采取的方式是:遇到文件夹就继续进入,遇到文件则判断文件是否是.java文件,故采用递归的方式。 由于我们需要过滤出.java文件,所以我们有两种方法: 对文件名进行判断.endWith(".java")。 实现FileFilter接口,重写其中的accept方法。 accept接口实现的方式 123456789101112131415public class FileFilterImpl implements FileFilter { @Override public boolean accept(File pathname) { /* 判断: 1.是否是文件 2.文件名后缀是否为.java 不满足返回false,满足则返回true */ if (pathname.isFile() && !pathname.getName().endsWith(".java")) { return false; } return true; }} 主方法的实现 123456789101112131415161718192021public class FindJavaDemo05 { private static int count = 0; // 用于记录.java文件的个数 public static void main(String[] args) { File file = new File(\"../\"); // 选取的文件夹 getAllFile(file); System.out.println(count); } private static void getAllFile(File dir) { File[] files = dir.listFiles(new FileFilterImpl()); // 获取子文件及子文件夹 -- 并采用文件过滤器 for (File f : files) { if (f.isDirectory()) { // 当前对象是文件夹,则递归调用 getAllFile(f); } else { // 否则打印出.java并记录 count++; System.out.println(f); } } }} 对File[] files = dir.listFiles(new FileFilterImpl())的思考: 其中接口实现accept方法,用于过滤满足方法体条件的文件对象。 首先dir.listFiles遍历出每一个File对象,每一个都作为FileFilterImpl中accpet方法的参数进行传入。 然后,调用accpet函数,对当前传入的参数(File对象)进行判断操作。如果满足方法体条件,返回true,否则返回false。 最后,当.listFiles接收到true时,就将当前的File对象添加入File[]数组中,否则就不加入。 三、字符流的读写操作字符流以字符为单位,专门用于处理文本文件。若用字节流读取中文字符时,可能不会显示完整的字符,因为一个中文字符可能占用多个字节存储。 同样在字符输出流中,同样有Reader和Writer两种读取和写入的抽象类。 Reader 用于读取字符流的所有类的超类,可以读取字符信息到内存中。其中,字符输入流的基本共性功能方法有: public void close():关闭此流并释放与此流相关联的任何系统资源。 public void read():从输入流读取一个字符。 public void read(char[] cbuf):从输入流中读取一些字符,并将它们存储到字符数组cbuf中。 Writer 用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。以下为字节输出流的基本共性功能方法。 void write(int c):写入单个字符。 void write(char[] cbuf):写入字符数组。 abstract void write(char[] cbuf,int off,int len):写入字符数组的某一部分,off数组的开始索引,len写的字符个数。 void write(String str):写入字符串。 void write(String str,int off,int len):写入字符串的某一部分,off数组的开始索引,len写的字符个数。 void flush():刷新该流的缓冲。 void close():关闭此流,再次此前先进行刷新。 以上可对OutStream和InStream进行类比。 1.FileReader类注意:读取文件时,构造时使用系统默认的字符编码和默认字节缓冲区。 字符编码:字节与字符的对应规则。idea中默认的是UTF-8 字节缓冲区:一个字节数组,用来临时存储字节数据。 构造方法 FileWriter(File file) FileWriter(String name) 具体使用方法等同FileInputStream,字节输入流。 2.FileWriter其构造时使用系统默认的字符编码和默认字节缓冲区。 构造方法 FileWriter(File file):创建一个新的FileWriter,给定要读取的File对象。 FileWriter(String fileName):同上,传入的参数是文件的名称(路径)。 其使用方法同字节流的使用,需要注意的一点是:在写出数据以后,如果未调用close方法,数据只是保存到了缓冲区,并未写出到文件中。因此,字节流在操作时本身不会用到缓冲区(内存),是文件本身操作的。 字符流在操作时使用了缓冲区,通过缓冲区再操作文件。 因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。当既想写出数据,又想继续使用流,就需要flush方法。 flush:刷新缓冲区,流对象可以继续使用。 close:先刷新缓冲区,然后通知系统释放资源。流对象不可以再使用了。 输出同字节输出流。 1.写出字符数组 string.toCharArray() 2.写出字符串 writer(String str),writer(String str,int off,int len) 四、属性集java.util.Properties 继承于Hashtable ,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时,System.getProperties 方法就是返回一个Properties对象。 4.1 Properties类 构造方法 public Properties() :创建一个空的属性列表。 基本的存储方法 public Object setProperty(String key, String value) : 保存一对属性。 public String getProperty(String key) :使用此属性列表中指定的键搜索属性值。 public Set<String> stringPropertyNames() :所有键的名称的集合。 通常在不知道键值的情况下,获取其键值的方法。 1234567891011121314public static void main(String[] args) throws FileNotFoundException { // 创建属性集对象 Properties properties = new Properties(); // 添加键值对元素 properties.setProperty(\"filename\", \"a.txt\"); properties.setProperty(\"length\", \"209385038\"); properties.setProperty(\"location\", \"D:\\\\a.txt\"); // 遍历属性集,获取所有键的集合 Set<String> strings = properties.stringPropertyNames(); // 打印键值对 for (String key : strings ) { System.out.println(key+\" -- \"+properties.getProperty(key)); }} 4.2 与流相关的方法 public void load(InputStream inStream): 从字节输入流中读取键值对。 参数中使用了字节输入流,通过流对象,可以关联到某文件上,这样就能够加载文本中的数据了。文本数据格式: 123filename=a.txtlength=209385038location=D:\\a.txt 文本中的数据必须是键值对格式,可以冒号,逗号,等号等符号分隔。 加载代码示例: 1234567891011121314151617public class ProDemo2 { public static void main(String[] args) throws FileNotFoundException { // 创建属性集对象 Properties pro = new Properties(); // 加载文本中信息到属性集 pro.load(new FileInputStream(\"read.txt\")); // 遍历集合并打印 Set<String> strings = pro.stringPropertyNames(); for (String key : strings ) { System.out.println(key+\" -- \"+pro.getProperty(key)); } }}输出结果:filename -- a.txtlength -- 209385038location -- D:\\a.txt 五、缓冲流缓冲流,也是高效流,是对4个基本的FileXxx流的增强,所以对应的也是4个流。同理按照数据类型分类: 字节缓冲流:BufferedInputStream、BufferedOutputStream 字符缓冲流:BufferedReader、BufferedWriter 缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。 5.1 字节缓冲流1. 构造方法 public BufferedInputStream(InputStream in) :创建一个新的缓冲输入流,传递字节输入流。 public BufferedOutputStream(OutputStream out): 创建一个新的缓冲输出流,传递字节输出流。 2.用例 12345678910111213141516public class BufferedDemo { public static void main(String[] args) throws IOException { long start = System.currentTimeMillis(); BufferedInputStream bis = new BufferedInputStream(new FileInputStream(\"/navicat120_premium_en.dmg\")); // 创建字节输入缓冲区 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(\"copy.dmg\")); // 字节输出缓冲区 int len; byte[] bytes = new byte[1024]; // 缓冲数组 -- 1024 while ((len = bis.read(bytes)) != -1) { bos.write(bytes, 0, len); // 输出 -- 写入数据 } long end = System.currentTimeMillis(); System.out.println(end - start); }} 5.2 字符缓冲流1.构造方法 public BufferedReader(Reader in) :创建一个新的缓冲输入流。 public BufferedWriter(Writer out): 创建一个新的缓冲输出流。 1234// 创建字符缓冲输入流BufferedReader br = new BufferedReader(new FileReader(\"br.txt\"));// 创建字符缓冲输出流BufferedWriter bw = new BufferedWriter(new FileWriter(\"bw.txt\")); 2.特有方法 字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。 BufferedReader:public String readLine(): 读一行文字。 BufferedWriter:public void newLine(): 写一行分隔符,由系统属性定义符号。 readLine方法演示: 123456789101112131415public class BufferedReaderDemo { public static void main(String[] args) throws IOException { // 创建流对象 BufferedReader br = new BufferedReader(new FileReader(\"in.txt\")); // 定义字符串,保存读取的一行文字 String line = null; // 循环读取,读取到最后返回null,可以把readLine看做是一个指针 while ((line = br.readLine())!=null) { System.out.print(line); System.out.println(\"------\"); } // 释放资源 br.close(); }} newLine方法演示: 123456789101112131415161718192021public class BufferedWriterDemo throws IOException { public static void main(String[] args) throws IOException { // 创建流对象 BufferedWriter bw = new BufferedWriter(new FileWriter(\"out.txt\")); // 写出数据 bw.write(\"Hello\"); // 写出换行 bw.newLine(); bw.write(\"World\"); bw.newLine(); bw.write(\"!\"); bw.newLine(); // 输出换行 // 释放资源 bw.close(); // }}输出效果:HelloWorld! 3.原理 由上图可以看到,当我们使用普通的字节输出流时,由OS进行内存到硬盘的读写时,字符是一个一个读取,这样增加了文件从内存(其中经过缓存)读取到磁盘次数,从而增加了读取的时间;当我们使用缓冲流时,每次操作系统从内存读取到缓冲区的数据就是多个字符,然后再读取到硬盘,由于每次读取的数量多了,那么总的读取次数就肯定减少了,所以就能够减少我们读写的时间。 六、转换流6.1 字符编码和字符集 字符编码 计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。 编码:字符->字节 解码:字节->字符 字符编码Character Encoding : 就是一套自然语言的字符与二进制数之间的对应规则。 编码表:生活中文字和计算机中二进制的对应规则 图片转换流图解-桥梁 字符集 字符集 Charset:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。 计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。 当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。 6.2 InputStreamReader类转换流java.io.InputStreamReader,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。 1.构造方法 InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。 InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。 构造举例,代码如下: 12InputStreamReader isr = new InputStreamReader(new FileInputStream(\"in.txt\"));InputStreamReader isr2 = new InputStreamReader(new FileInputStream(\"in.txt\") , \"GBK\"); 2.指定编码读取 1234567891011121314151617181920212223public class ReaderDemo2 { public static void main(String[] args) throws IOException { // 定义文件路径,文件为gbk编码 String FileName = \"E:\\\\file_gbk.txt\"; // 创建流对象,默认UTF8编码 InputStreamReader isr = new InputStreamReader(new FileInputStream(FileName)); // 创建流对象,指定GBK编码 InputStreamReader isr2 = new InputStreamReader(new FileInputStream(FileName) , \"GBK\"); // 定义变量,保存字符 int read; // 使用默认编码字符流读取,乱码 while ((read = isr.read()) != -1) { System.out.print((char)read); // ��Һ� } isr.close(); // 使用指定编码字符流读取,正常解析 while ((read = isr2.read()) != -1) { System.out.print((char)read);// 大家好 } isr2.close(); }} 6.3 OutputStreamWriter类转换流java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。 1.构造方法 OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。 OutputStreamWriter(OutputStream in, String charsetName): 创建一个指定字符集的字符流。 构造举例,代码如下: 12OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream(\"out.txt\"));OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream(\"out.txt\") , \"GBK\"); 2.指定编码输出 12345678910111213141516171819public class OutputDemo { public static void main(String[] args) throws IOException { // 定义文件路径 String FileName = \"E:\\\\out.txt\"; // 创建流对象,默认UTF8编码 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(FileName)); // 写出数据 osw.write(\"你好\"); // 保存为6个字节 osw.close(); // 定义文件路径 String FileName2 = \"E:\\\\out2.txt\"; // 创建流对象,指定GBK编码 OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream(FileName2),\"GBK\"); // 写出数据 osw2.write(\"你好\");// 保存为4个字节 osw2.close(); }} 6.4 字符流转换实例案例: 输入条件:给定文件中,输入文件指定为gbk格式。 输出条件:将文件以utf-8格式进行输出。 12345678910111213141516171819202122232425public class TransDemo{ public static void main(String[] args){ // 1.定义文件路径 String srcFile = \"file_gbk.txt\"; String destFile = \"file_utf8.txt\"; // 2.创建流对象 // 2.1 转换输入流,指定GBK编码 InputStreamReader isr = new InputStreamReader(new FileInputStream(srcFile) , \"GBK\"); // 2.2 转换输出流,默认utf8编码 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(destFile)); // 3.读写数据 // 3.1 定义数组 char[] cbuf = new char[1024]; // 3.2 定义长度 int len; // 3.3 循环读取 while ((len = isr.read(cbuf))!=‐1) { // 循环写出 osw.write(cbuf,0,len); } // 4.释放资源 osw.close(); isr.close(); } } 七.序列化Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据 、对象的类型和对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。 同样,该字节序列还可以从文件中读取回来,重构对象,对其进行反序列化操作。对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建对象。 序列化:将对象转换为字节。 反序列化:字节重构为对象。 7.1 ObjectOutputStream类java.io.ObjectOutputStream类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。 构造方法: public ObjectOutputStream(OutputStream out) : 创建一个指定OutputStream的ObjectOutputStream。 举例: 12FileOutputStream fileOut = new FileOutputStream(\"employee.txt\");ObjectOutputStream out = new ObjectOutputStream(fileOut); 7.2 序列化操作 一个对象要想序列化,必须满足两个条件: 该类必须实现java.io.Serializable接口,Serializable是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException。 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient关键字修饰。 12345678910public class Employee implements Serializable { private String name; private String address; public transient int age; // 通过transient瞬态修饰成员,不会被序列化 public void addressCheck() { System.out.println(\"Address check:\" + name + \"--\" + address); }} 写出对象方法 public final void writeObject (Object obj):将指定的对象写出。 12345678910111213141516171819202122public class SerializeDemo { public static void main(String[] args) { Employee employee = new Employee(); employee.setAge(21); employee.setAddress(\"江苏\"); employee.setName(\"Tom\"); ObjectOutputStream out = null; try { // 创建序列化对象 out = new ObjectOutputStream(new FileOutputStream(\"employee.txt\")); // 写出对象 out.writeObject(employee); // 释放资源 out.close(); System.out.println(\"Serialized data is saved\"); // 姓名,地址被序列化,年龄没有被序列化 } catch (IOException e) { e.printStackTrace(); } }} 7.3 反序列化操作ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。 构造方法: public ObjectInputStream(InputStream in) : 创建一个指定InputStream的ObjectInputStream。 7.3.1 反序列化操作一通过查找一个对象的class文件,即可将其进行反序列化操作,调用ObjectInputStream读取对象的方法: public final Object readObject () : 读取一个对象。 123456789101112131415161718192021222324public class SerializeDemo01 { public static void main(String[] args) { Employee e = null; try { ObjectInputStream in = new ObjectInputStream(new FileInputStream(\"employee.txt\")); e = (Employee) in.readObject(); in.close(); } catch (IOException ex) { ex.printStackTrace(); } catch (ClassNotFoundException ex) { System.out.println(\"Employee class not found\"); ex.printStackTrace(); return; } System.out.println(\"name:\" + e.getName()); System.out.println(\"address:\" + e.getAddress()); System.out.println(\"age:\"+e.getAge()); }}输出结果:name:Tomaddress:江苏age:0 // age没有被初始化,所以输出为0(Integer类型的初始化值) 由于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个ClassNotFoundException异常。 7.3.2 反序列化操作二当JVM反序列化对象时,能够找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常。发生这个异常的原因如下: 该类的序列版本号与从流中读取的类描述符的版本号不匹配 该类包含未知数据类型 该类没有可访问的无参数构造方法 Serializable接口给需要序列化的类,提供了一个序列号版本号。serialVersionUID该版本号的目的在于验证序列化的对象和对应类是否版本匹配。 12// 加入序列版本号private static final long serialVersionUID = 1L; 7.4 序列化案例 将存有多个自定义对象的集合序列化操作,保存到自定义文件中。 反序列化此文件,并遍历集合,打印对象信息。 分析: 将若干学生对象,保存到集合中。 把集合进行序列化。 反序列化读取时,只需要读取一次,转换为集合类型。 遍历集合,打印所有的学生信息。 首先创建一个student类,再创建集合类的对象,序列化和反序列化的对象都是这个集合对象。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061public class DemoSerialize { public static void main(String[] args) {// serialize();// 对象序列化 rSerialize(); // 对象反序列化 } /** * 对象序列化操作 */ public static void serialize() { Student stu1 = new Student(\"张三\", 12); Student stu2 = new Student(\"李四\", 10); Student stu3 = new Student(\"王五\", 11); StudentList list = new StudentList(); List<Student> students = new ArrayList<>(); students.add(stu1); students.add(stu2); students.add(stu3); list.setStudents(students); ObjectOutputStream os = null; try { os = new ObjectOutputStream(new FileOutputStream(\"students.txt\")); os.writeObject(list); os.close(); System.out.println(\"完成序列化\"); } catch (IOException e) { e.printStackTrace(); } } /** * 对象反序列化操作 */ public static void rSerialize() { StudentList students = null; try { ObjectInputStream in = new ObjectInputStream(new FileInputStream(\"students.txt\")); students = (StudentList) in.readObject(); in.close(); // 资源释放 } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { System.out.println(\"Students class not found\"); e.printStackTrace(); return; } List<Student> studentList = students.getStudents(); System.out.println(studentList); System.out.println(\"-----\"); for (Student student : studentList) { System.out.println(student); } }} 八、打印流print和println方法来自于java.io.PrintStream类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方法。 8.1 PrintStream类构造方法: public PrintStream(String fileName) : 使用指定的文件名创建一个新的打印流。 1PrintStream ps = new PrintStream(\"ps.txt\"); 改变打印流的流向: System.out就是PrintStream类型的,只不过它的流向是系统规定的,打印在控制台上。通过PrintStream类改变打印流流向。 1234567891011public class PrintStreamDemo { public static void main(String[] args) throws FileNotFoundException { System.out.println(97); // 创建打印流,指定文件的名称 PrintStream ps = new PrintStream(\"ps.txt\"); // 设置系统的打印流流向,输出到ps.txt文件上 System.setOut(ps); System.out.println(97); }}","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"IO流","slug":"IO流","permalink":"https://chemlez.github.io/tags/IO%E6%B5%81/"}]},{"title":"SpringMVC学习笔记记录","slug":"SpringMVC学习笔记记录","date":"2020-10-02T07:13:29.000Z","updated":"2021-08-22T12:39:55.667Z","comments":true,"path":"2020/10/02/SpringMVC学习笔记记录/","link":"","permalink":"https://chemlez.github.io/2020/10/02/SpringMVC%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E8%AE%B0%E5%BD%95/","excerpt":"0 、概述服务器端分成三层架构。 一、环境搭建1.1 Maven环境的创建 导入坐标依赖 12345678910111213141516171819202122232425262728293031323334<!-- 版本锁定 --><spring.version>5.0.2.RELEASE</spring.version><!-- 配置依赖 --><dependencies> <!-- spring IOC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <!-- spring web --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency></dependencies> 配置核心的控制器(类似servlet类 – dispatcherServlet) 12345678910111213141516<!DOCTYPE web-app PUBLIC \"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN\" \"http://java.sun.com/dtd/web-app_2_3.dtd\" ><web-app> <display-name>Archetype Created Web Application</display-name> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping></web-app> 当<url-pattern>/</url-pattern>时,默认就是拦截所有路径连静态资源也不能访问。但是Controller中配置@RequestMapping的路径是不会被拦截的,配置了@RequestMapping就相当于在web.xml中注<servlet>。","text":"0 、概述服务器端分成三层架构。 一、环境搭建1.1 Maven环境的创建 导入坐标依赖 12345678910111213141516171819202122232425262728293031323334<!-- 版本锁定 --><spring.version>5.0.2.RELEASE</spring.version><!-- 配置依赖 --><dependencies> <!-- spring IOC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <!-- spring web --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency></dependencies> 配置核心的控制器(类似servlet类 – dispatcherServlet) 12345678910111213141516<!DOCTYPE web-app PUBLIC \"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN\" \"http://java.sun.com/dtd/web-app_2_3.dtd\" ><web-app> <display-name>Archetype Created Web Application</display-name> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping></web-app> 当<url-pattern>/</url-pattern>时,默认就是拦截所有路径连静态资源也不能访问。但是Controller中配置@RequestMapping的路径是不会被拦截的,配置了@RequestMapping就相当于在web.xml中注<servlet>。 1.2 第一个执行程序1.2.1 引入SpringMVC配置文件在resources下新建springMVC.XML配置文件: 12345678<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:mvc=\"http://www.springframework.org/schema/mvc\" xmlns:context=\"http://www.springframework.org/schema/context\" 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.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd\"></beans> 1.2.2 编写控制器12345678910// 控制器@Controllerpublic class HelloController { @RequestMapping(\"/hello\") // 请求的资源路径 public String sayHello() { System.out.println(\"Hello SpringMVC\"); return \"success\"; // 需要跳转的页面,默认jsp文件名 }} 控制器类配置@Controller注解,表明是控制器。 在具体的方法上配置@RequestMapping注解,参数即为访问时的资源路径 return中的字符串,方法执行完需要跳转的页面 1.2.3 完善springMVC.xml配置文件 控制器中加入注解,那么就需要配置需要扫描的包; 配置文件解析器,创建IOC容器对象,由Tomcat负责调用; 配置spring开启注解MVC的支持 123456789101112131415161718192021<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:mvc=\"http://www.springframework.org/schema/mvc\" xmlns:context=\"http://www.springframework.org/schema/context\" 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.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd\"> <!-- 配置spring创建容器时要扫描的包 --> <context:component-scan base-package=\"cn.lizhi\"></context:component-scan> <!-- 配置视图解析器 Ioc容器对象,由tomcat调用--> <bean id=\"viewResolver\" class=\"org.springframework.web.servlet.view.InternalResourceViewResolver\"> <!-- 文件具体所在的目录 --> <property name=\"prefix\" value=\"/WEB-INF/pages/\"></property> <!-- 文件解析的类型(后缀名) --> <property name=\"suffix\" value=\".jsp\"></property> </bean> <!-- 配置spring开启注解mvc的支持 --> <mvc:annotation-driven></mvc:annotation-driven></beans> WEB-INF目录下的内容是对客户端不可见,只对服务端可见,即客户端不能直接对其进行访问。 1.2.4 完善web.xml配置文件由于上方的springMVC.XML配置文件时在resources目录下的,我们启动的是web项目,就需要将该配置文件加载进Tomcat服务器进行读取。 即:配置Servlet的初始化参数,读取springMVC的配置文件,创建spring容器,用于加载配置文件。 当MVC配置文件加载成功,那么其中的扫描就能够成功,继而将控制器中的类加载成对象。 1234567891011121314151617181920212223242526272829<!DOCTYPE web-app PUBLIC \"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN\" \"http://java.sun.com/dtd/web-app_2_3.dtd\" ><web-app> <display-name>Archetype Created Web Application</display-name> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 配置Servlet的初始化参数,读取springMVC的配置文件,创建spring容器,用于加载配置文件 MVC配置文件加载成功,那么其中的扫描就成功,继而到控制器中类加载成对象 --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springMVC.xml</param-value> </init-param> <!-- 服务器在启动时,就加载资源 配置servlet的对象的创建时间点:应用加载时创建。--> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <!-- 配置资源路径/表明所有的资源皆可被访问到 --> <url-pattern>/</url-pattern> <!-- 表明只有hello的资源路径可以被访问到 <url-pattern>/hello</url-pattern> --> </servlet-mapping></web-app> 二、Spring MVC 详解2.1 执行过程及组件分析2.1.1 执行过程 当启动Tomcat服务器的时候,因为配置了load-on-startup标签,所以会创建DispatcherServlet对象,就会加载springmvc.xml配置文件。这里是服务器启动,应用被加载。读取到web.xml中的配置创建spring容器并且初始化容器中的对象。 在springmvc.xml配置文件中开启了注解扫描,那么相应的controller对象(HelloController)对象就会被创建。 从index.jsp发送请求,请求会先到达DispatcherServlet核心控制器,根据配置@RequestMapping注解找到执行的具体方法。浏览器发送请求,被DispatcherServlet捕获,该Servlet并不处理请求,而是把请求转发出去。转发的路径是根据请求URL,匹配@RequestMapping中的内容。 根据执行方法的返回值,再根据配置的视图解析器,去指定的目录下查找指定名称的JSP文件。即:根据方法的返回值,借助InternalResourceViewResolver找到对应的结果视图。 Tomcat服务器渲染页面,做出响应。 2.1.2 组件分析 DispatcherServlet:前端控制器 用户请求到达前端控制器,它就相当于MVC模式中的c,dispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,dispatcherServlet的存在降低了组件之间的耦合性。 HandlerMapping:处理器映射器 HandlderMapping负责根据用户请求找到Handler,即处理器,SpringMVC提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。 Handler:处理器 它就是开发中编写的具体业务控制器。由DispatcherServlet把用户请求转发到Handler。由Handler对具体的用户请求进行处理。 Handler:处理器适配器 通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。 View Resolver:视图解析器 View Resolver负责将处理结果生成View视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。 View:视图 将数据视图展示给客户端,即用户。 2.1.3 <mvc:annotation-driver>说明在SpringMVC的各个组件中,处理器映射器、处理器适配器、视图解析器成为SpringMVC的三大组件。使用<mvc:annotation-driver>自动加载RequestMappingHandlerMapping(处理器映射器)和RequestMappingHandlerAdapter(处理器适配器)。 2.2 常用注解说明2.2.1 RequestMapping作用:用于建立请求URL和处理请求方法之间的对应关系。 2.2.2 RequestParam作用:把请求中指定名称的参数给控制器中的形参赋值。(适用于请求名称与属性名不相同的情况) 属性: value:请求参数的名称。 required:请求参数中是否必须提供此参数。默认值:true。表示必须提供,如果不提供将报错。 12345@RequestMapping(\"/useRequestParam\")public String useRequestParam(@RequestParam(\"name\")String username,@RequestParam(value=\"age\",required=false)Integer age){ System.out.println(username+\",\"+age); return \"success\"; } 2.2.3 RequestBody作用:用于获取请求体内容。直接使用得到是key=value&key=value...结构的数据。get请求方式不适用。 属性: required:是否必须有请求体。默认值是:true。当取值是true时,get请求方式会报错。如果取值为false,get请求得到是null。 12345@RequestMapping(\"/useRequestBody\")public String useRequestBody(@RequestBody(required=false) String body){ System.out.println(body); return \"success\"; } 2.2.4 PathVariable作用:用于绑定url中的占位符。例如:请求url中 /delete/{id},这个{id}就是url占位符。url支持占位符是spring3.0之后加入的。是springmvc支持rest风格URL的一个重要标志。 属性: value:用于指定url中占位符名称。 required:是否必须提供占位符。 重点:restful风格。 12345@RequestMapping(\"/usePathVariable/{id}\")public String usePathVariable(@PathVariable(\"id\") Integer id){ System.out.println(id); return \"success\"; } 2.2.5 RequestHeader作用:用于获取请求消息头 属性: value:提供消息头名称,用于指定获取消息头中的哪一部分。 required:是否必须有此消息头。 123456@RequestMapping(\"/useRequestHeader\")public String useRequestHeader(@RequestHeader(value=\"Accept-Language\",required=false)String requestHeader){ System.out.println(requestHeader); return \"success\"; } 将请求头内容的信息封装到requestHeader参数中。 2.2.6 CookieValue作用:用于把指定cookie名称的值传入控制器方法参数。 属性: value:指定cookie的名称。键值对的形式,通过键来获取到它的值。 required:是否必须有cookie 123456@RequestMapping(\"/useCookieValue\")public String useCookieValue(@CookieValue(value=\"JSESSIONID\",required=false) String cookieValue){ System.out.println(cookieValue); return \"success\"; } 2.2.7 ModelAttribute作用:该注解是SpringMVC4.3版本以后新加入的。它可以用于修饰方法和参数。 出现在方法上,表示当前方法会在控制器的方法执行之前,先执行。它可以修饰没有返回值的方法,也可以修饰有具体返回值的方法。 出现在参数上,获取指定的数据给参数赋值。 属性: value:用于获取数据的key。key可以是POJO的属性名称,也可以是Map结构的key。 应用场景: 当表单提交数据不是完整的实体类数据时,保证没有提交数据的字段使用数据库对象原来的数据。 例如: 当我们在编辑一个用户时,用户有一个创建信息字段,该字段的值是不允许被修改的。在提交表单数据时肯定没有此字段的内容,一旦更新会把该字段内容置为null,此时就可以使用此注解解决问题。 示例1: 12345678910111213@ModelAttributepublic public void showModel(User user) { System.out.println(\"执行了showModel方法\"+user.getUsername());}/*** 接收请求的方法* @param user* @return*/@RequestMapping(\"/testModelAttribute\")public String testModelAttribute(User user) { System.out.println(\"执行了控制器的方法\"+user.getUsername()); return \"success\"; } 即先执行showModel方法,再执行控制器中的方法testModelAttribute。 2.2.7.1 ModelAttribute 修饰方法带返回值需求:修改用户信息,要求用户密码不能修改 前端代码: 12345<form action=\"springmvc/updateUser\" method=\"post\"> 用户名称:<input type=\"text\" name=\"username\" ><br/> 用户年龄:<input type=\"text\" name=\"age\" ><br/> <input type=\"submit\" value=\"保存\"></form> 模拟查询数据库中用户信息 1234567@ModelAttributepublic User showModel(String username) { //模拟去数据库查询 User userByName = findUserByName(username); System.out.println(\"执行了 showModel 方法\"+userByName); return userByName; } 模拟修改用户方法 12345@RequestMapping(\"/updateUser\")public String testModelAttribute(User user) { System.out.println(\"控制器中处理请求的方法:修改用户:\"+user); return \"success\"; } 模拟去数据库查询 12345678private User findUserByName(String username) { // 以下4句用来模拟数据库中的原有对象,即查询出来的对象 User user = new User(); user.setUsername(username); user.setAge(19); user.setPassword(\"123456\"); return user;} 首先通过前端请求,获取username,而在showModel方法中参数,就是由前端请求的参数username。然后showModel方法体中通过数据库查询对象,再将对象进行返回。返回的对象是数据中的相对应的原有对象。(类似过滤器,后续在控制器中将需要修改的值,进行修改,不变的值,就不需要再次改动)。 2.2.7.2 ModelAttribute 修饰方法不带返回值需求:修改用户信息,要求用户的密码不能修改 前端代码 123456<!-- 修改用户信息 --> <form action=\"springmvc/updateUser\" method=\"post\"> 用户名称:<input type=\"text\" name=\"username\" ><br/> 用户年龄:<input type=\"text\" name=\"age\" ><br/> <input type=\"submit\" value=\"保存\"></form> 查询数据库中用户信息– 模拟去数据库查询 1234567@ModelAttributepublic void showModel(String username,Map<String,User> map) { //模拟去数据库查询 User user = findUserByName(username); System.out.println(\"执行了 showModel 方法\"+user); map.put(\"abc\",user);} 模拟修改用户方法 12345@RequestMapping(\"/updateUser\")public String testModelAttribute(@ModelAttribute(\"abc\")User user) { System.out.println(\"控制器中处理请求的方法:修改用户:\"+user); return \"success\"; } 模拟去数据库查询 1234567private User findUserByName(String username) { User user = new User(); user.setUsername(username); user.setAge(19); user.setPassword(\"123456\"); return user; } 第二种不带返回值的方式更容易理解,即将原有的对象存放在map集合中,再通过注解,获取指定(根据map中的键)的数据给参数赋值。最后,将前端请求的参数对应的封装到这个参数中,就能够保证这个参数对象的所有属性都能够有值。 2.2.8 SessionAttributes作用:用于多次执行控制器方法间的参数共享。只能作用在类对象(Class)上。 属性: value:用于指定存入的属性名称 type:用于指定存入的数据类型 前端代码 1234<!-- SessionAttribute 注解的使用 --> <a href=\"springmvc/testPut\">存入 SessionAttribute</a> <hr/><a href=\"springmvc/testGet\">取出 SessionAttribute</a> <hr/><a href=\"springmvc/testClean\">清除 SessionAttribute</a> 控制器中的代码 123456789101112131415161718192021222324252627282930313233@Controller(\"sessionAttributeController\")@RequestMapping(\"/springmvc\")@SessionAttributes(value ={\"username\",\"password\"},types={Integer.class})public class SessionAttributeController { /** * 把数据存入 SessionAttribute * @param model * @return * Model 是 spring 提供的一个接口,该接口有一个实现类 ExtendedModelMap * 该类继承了 ModelMap,而 ModelMap 就是 LinkedHashMap 子类 */ @RequestMapping(\"/testPut\") public String testPut(Model model){ model.addAttribute(\"username\", \"泰斯特\"); model.addAttribute(\"password\",\"123456\"); model.addAttribute(\"age\", 31); //跳转之前将数据保存到 username、password 和 age 中,因为注解@SessionAttribute 中有这几个参数 return \"success\"; } @RequestMapping(\"/testGet\") public String testGet(ModelMap model){ System.out.println(model.get(\"username\")+\";\"+model.get(\"password\")+\";\"+model.get(\"a ge\")); return \"success\"; } @RequestMapping(\"/testClean\") public String complete(SessionStatus sessionStatus){ sessionStatus.setComplete(); return \"success\"; } } 通过model对象进行值的存储操作,底层会将值存储在request域中。而又使用@SessionAttributes注解,具体有@SessionAttributes(value ={"username","password"},types={Integer.class})。此时通过values数组的指定 – username、password,则表明model在存储操作时,不仅会将这两个的值存储在request域对象中,同时也会存储在session域对象中。 取值时,使用的model实现类 – ModelMap。 删除时,使用SessionStatus。 2.3 请求参数的绑定前端向后端进行请求时,表单中请求参数都是基于key=value的。SpringMVC绑定请求参数的过程是通过把表单请求参数,作为控制器中方法参数进行绑定的。 2.3.1 支持的数据类型 基本数据类型 基本数据类型 String类型 POJO类型参数(实现序列化接口) 实体类 关联的实体类 数组和集合类型参数 包括List结构和Map结构的集合(包括数组) SpringMVC绑定请求参数是自动实现的,但是想要使用,必须遵循使用要求。 2.3.2 使用要求 基本数据类型或者是String类型: 要求我们的参数名称必须和控制器中方法的形参名称保持一致。(严格区分大小写) POJO类型或者及其关联对象 如果表单中参数名称和POJO类的属性名称保持一致。并且控制器方法的参数类型是POJO类型。 如果是集合类型 第一种 要求集合类型的请求参数必须在POJO中。在表单中请求参数名称要和POJO中集合属性名称相同。 给List集合中的元素赋值,使用下标。 给Map集合中的元素赋值,使用键值对。 第二种 接收的请求参数是json格式数据。需要借助一个注解实现。 SpringMVC可以实现一些数据类型自动转换。其内置转换器全部都在: org.springframework.core.convert.support包下。 2.3.3 使用实例2.3.3.1 POJO类型作为参数 实体类代码 1234567891011public class Account implements Serializable { private Integer accountId; private String accountName; private Float money; private Address address; // getters and setters 方法 // toString方法} 以上是Account类。其中关联Address类: 1234567891011package cn.lizhi.domain;public class Address { private String provinceName; private String cityName; // getters and setters 方法 // toString方法} 前端代码 1234567<form action=\"account/saveAccount\" method=\"post\"> 账户名称:<input type=\"text\" name=\"name\" ><br/> 账户金额:<input type=\"text\" name=\"money\" ><br/> 账户省份:<input type=\"text\" name=\"address.provinceName\" ><br/> 账户城市:<input type=\"text\" name=\"address.cityName\" ><br/> <input type=\"submit\" value=\"保存\"></form> 通过对象.属性的方式进行赋值。 控制器代码 12345@RequestMapping(\"/saveAccount\")public String saveAccount(Account account) { System.out.println(\"保存了账户。。。。\"+account); return \"success\"; } 2.3.3.2 POJO类中包含集合类型参数 实体类 – User类 12345678910111213141516171819202122232425262728package cn.lizhi.domain;import java.util.Date;import java.util.List;import java.util.Map;public class User { private String username; private String password; private Integer age; private Date date; private List<Account> accounts; private Map<String,Account> accountMap; // getter and setter @Override public String toString() { return \"User{\" + \"username='\" + username + '\\'' + \", password='\" + password + '\\'' + \", age=\" + age + \", date=\" + date + \", accounts=\" + accounts + \", accountMap=\" + accountMap + '}'; }} 前端代码 123456789101112131415<!-- POJO 类包含集合类型演示 --><form action=\"account/updateAccount\" method=\"post\"> 用户名称:<input type=\"text\" name=\"username\" ><br/> 用户密码:<input type=\"password\" name=\"password\" ><br/> 用户年龄:<input type=\"text\" name=\"age\" ><br/> 账户 1 名称:<input type=\"text\" name=\"accounts[0].name\" ><br/> 账户 1 金额:<input type=\"text\" name=\"accounts[0].money\" ><br/> 账户 2 名称:<input type=\"text\" name=\"accounts[1].name\" ><br/> 账户 2 金额:<input type=\"text\" name=\"accounts[1].money\" ><br/> 账户 3 名称:<input type=\"text\" name=\"accountMap['one'].name\" ><br/> 账户 3 金额:<input type=\"text\" name=\"accountMap['one'].money\" ><br/> 账户 4 名称:<input type=\"text\" name=\"accountMap['two'].name\" ><br/> 账户 4 金额:<input type=\"text\" name=\"accountMap['two'].money\" ><br/> <input type=\"submit\" value=\"保存\"></form> 集合、列表赋值的方式。三要素:属性、下标、参数(属性) 控制器代码 12345@RequestMapping(\"/updateAccount\")public String updateAccount(User user) { System.out.println(\"更新了账户。。。。\"+user); return \"success\"; } 2.4 请求参数乱码问题在tomcat8以后get请求方式,中文正常显示;post请求方式,中文会出现乱码问题。 在之前的serlvet学习中,对乱码解决的方式,是通过Filter过滤器。而现在SpringMVC提供好现有的类供我们使用。 首先post请求方式,在web.xml中配置一个过滤器。 1234567891011121314151617181920<!-- 配置解决中文乱码的过滤器 --><filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <!--设置过滤器中的属性值--> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <!-- 启动过滤器 --> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param></filter><!-- 过滤所有请求 --><filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern></filter-mapping> 如果想不过滤静态资源,在 Springmvc 的配置文件中可以配置,静态资源不过滤: 1234<!-- location 表示路径,mapping 表示文件,**表示该目录下的文件以及子目录的文件 --><mvc:resources location=\"/css/\" mapping=\"/css/**\"/><mvc:resources location=\"/images/\" mapping=\"/images/**\"/><mvc:resources location=\"/scripts/\" mapping=\"/javascript/**\"/> get请求方式: tomcat对GET和POST请求处理方式是不同的,GET请求的编码问题,要改tomcat的server.xml配置文件,如下: 12345678<Connector connectionTimeout=\"20000\" port=\"8080\"protocol=\"HTTP/1.1\" redirectPort=\"8443\"/>改为:<Connector connectionTimeout=\"20000\" port=\"8080\"protocol=\"HTTP/1.1\" redirectPort=\"8443\"useBodyEncodingForURI=\"true\"/><!-- 如果遇到 ajax 请求仍然乱码,请把:-->useBodyEncodingForURI=\"true\"改为 URIEncoding=\"UTF-8\" 2.5 自定义类型转换器例如,在前端输入日期格式时: 12<!-- 特殊情况之:类型转换问题 --> <a href=\"account/deleteAccount?date=2018-01-01\">根据日期删除账户</a> 可以看出,请求参数中,date=2018-01-01。 当我们在后端接收到请求时: 12345@RequestMapping(\"/deleteAccount\") public String deleteAccount(String date) { System.out.println(\"删除了账户:\" + date); return \"success\";} 输出:删除了账户:2018-01-01。如果当我们将接收参数的类型改为Date类型时: 12345@RequestMapping(\"/deleteAccount\") public String deleteAccount(Date date) { System.out.println(\"删除了账户:\" + date); return \"success\";} 此时,前端再次请求时,会报400。 下面便是我们自定义类型转化的方式: 首先,定义一个类,实现Convertet接口,该接口有两个泛型。 1234567public interface Converter<S, T> {//S:表示接受的类型,T:表示目标类型 /** * 实现类型转换的方法 */ @Nullable T convert(S source);} 其中,S表示接受的类型,T表示目标类型。 其实现类: 123456789101112131415161718public class StringToDate implements Converter<String, Date> { @Override public Date convert(String source) { if (source == null || source == \"\") { throw new RuntimeException(\"请输入日期\"); } SimpleDateFormat format = new SimpleDateFormat(\"yyyy-mm-dd\"); try { Date data = format.parse(source); return data; } catch (ParseException e) { throw new RuntimeException(\"请输入正确的日期格式\"); } }} 其次,在Spring配置文件中配置类型转换器。 Spring配置类型转换器的机制是,将自定义的转换器注册到类型转换服务中去。 123456789101112<bean id=\"converterService\" class=\"org.springframework.context.support.ConversionServiceFactoryBean\"> <!-- 给工厂注入一个新的类型转换器 --> <property name=\"converters\"> <array> <!-- 配置自定义类型转换器 --> <bean class=\"cn.lizhi.utils.StringToDate\"/> </array> </property></bean><!-- 配置spring开启注解mvc的支持 --><mvc:annotation-driven conversion-service=\"converterService\"></mvc:annotation-driven> 通过查看源码: 1234567891011121314151617181920212223242526272829303132333435public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean { @Nullable private Set<?> converters; @Nullable private GenericConversionService conversionService; public ConversionServiceFactoryBean() { } public void setConverters(Set<?> converters) { this.converters = converters; } public void afterPropertiesSet() { this.conversionService = this.createConversionService(); ConversionServiceFactory.registerConverters(this.converters, this.conversionService); } protected GenericConversionService createConversionService() { return new DefaultConversionService(); } @Nullable public ConversionService getObject() { return this.conversionService; } public Class<? extends ConversionService> getObjectType() { return GenericConversionService.class; } public boolean isSingleton() { return true; }} 其中converters是集合类型,并且有setter方法,通过上方的spring配置文件,给工厂注入一个新的类型转换器(不会覆盖原有的转换器)。 这样,在我们后面的使用中,便能够识别并对应转换成我们相应数据格式。 总之,实现转换器的方法以及对spring的配置,就是在原有的转换器集合中,再加入一种我们自己编写的转换器,以供我们使用。 2.6 响应数据2.6.1 方法返回值的分类2.6.1.1 String 类型Controller方法返回字符串可以指定逻辑视图的名称,根据视图解析器为物理视图的地址。 即为前面所写的常规的返回值为String的方法,返回值为所需要跳转的物理视图的名称(即为跳转页面名称 –>地址)。 123456@RequestMapping(value=\"/hello\") public String sayHello() { System.out.println(\"Hello SpringMVC!!\"); // 跳转到XX页面 -- 跳转到success页面 return \"success\";} 模拟应用 – 模拟对数据库中的数据进行查询 前端代码 index.jsp页面 123<!-- 模拟用户的修改 --><h3>修改用户</h3><a href=\"/user/initUpdate\">模拟用户修改</a> 模拟update.jsp页面 12345678910111213141516<%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" isELIgnored=\"false\" %><html><head> <title>首页</title></head><body><!-- 模拟用户的修改 --><h3>修改用户</h3><form action=\"/user/initUpdate\" method=\"post\"> 姓名:<input type=\"text\" name=\"username\" value=\"${user.username}\"><br> 密码:<input type=\"text\" name=\"password\" value=\"${user.password}\"><br> 金额:<input type=\"text\" name=\"money\" value=\"${user.money}\"><br> <input type=\"submit\" value=\"提交\"></form></body></html> 后端控制器代码 1234567891011121314@Controller(\"userController\")@RequestMapping(\"/user\")public class UserController { @RequestMapping(\"/initUpdate\") public String initUpdate(Model model) { // 模拟从数据库中查询数据 User user = new User(); user.setUsername(\"张三\"); user.setPassword(\"123456\"); user.setMoney(123d); model.addAttribute(\"user\", user); return \"update\"; }} 返回值是update,即最后跳转到update页面。 2.6.1.2 void类型如果控制器的方法返回值是void,在执行程序报404的异常,默认查找jsp页面没有找到。 123Type Status Report消息 /WEB-INF/pages/user/initUpdate.jsp描述 源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的资源表示。 从上面的错误信息可以看出,它会默认跳转到控制器中@RequestMapping("/example")中initUpdate的页面。 在SpringMVC中Servlet原始API可以作为控制器中方法的参数,即在controller方法形参上可以定义request和response,使用request或response指定相应结果。 使用request(即为存储转发)转向页面 12request.getRequestDispatcher(\"/WEB-INF/pages/success.jsp\").forward(request, response); 在存储转发中不需要写虚拟目录,但这里的资源路径需要写webApp中的全路径,没有像前面只写一个success的原因是,前一种是交给了视图解析器进行管理;而这里没有用到视图解析器,是单纯由request进行实现,故需要写出资源的全路径。 使用response进行页面的重定向 1response.sendRedirect(request.getContextPath() + \"/page.jsp\"); 需要加上虚拟目录。 通过response指定响应结果,例如响应json数据: 123response.setCharacterEncoding(\"utf-8\");response.setContentType(\"application/json;charset=utf-8\");response.getWriter().write(\"json串\"); 2.6.1.3 关键字的转发或重定向使用关键字进行转发或重定向时,Spring不会再通过视图解析器帮我们解析路径,需要我们自己手动配置。即就像上一小结中,资源路径需要写全。 Forward转发 controller方法在提供了String类型的返回值之后,默认就是请求转发。当然,我们可以手动进行设置: 12345@RequestMapping(\"/testForward\")public String testForward() { System.out.println(\"test1方法执行了...\"); return \"forward:/WEB-INF/pages/success.jsp\";} 如果用了forward:,则路径必须写成实际视图url,不能写逻辑视图。 它相当于它相当于request.getRequestDispatcher("**url**").forward(request,response)。使用请求转发,既可以转发到jsp(到视图解析器中),也可以转发到其他的控制器方法。 Redirect重定向 controller方法提供了一个String类型返回值之后,它需要在返回值里使用:redirect: 123456 @RequestMapping(\"/testRedirect\") public String testRedirect() { System.out.println(\"testRedirect方法执行了...\");// return \"redirect:testForward\"; // 重定向至另一个servlet地址 return \"redirect:/index.jsp\"; // 重定向至jsp页面 } 它相当于response.sendRedirect(url)。需要注意的是,如果是重定向到jsp页面,则jsp页面不能写在WEB-INF目录中,否则无法找到。 原因:重定向是客户端的,而转发是服务端内部的。重定向是让客户端去访问重定向的地址,而WEB-INF下的文件是不能通过外部访问的! 2.6.1.4 ResponseBody响应json数据作用:该注解用于将Controller的方法返回的对象,通过HttpMessageConverter接口转换为指定格式的数据。如:json、xml等,通过Response响应给客户端。 需求:使用@ResponseBody注解实现将controller方法返回对象转换为json响应给客户端。 前置:SpringMVC默认用MappingJacksonHttpMessageConverter对json数据进行转换,需要加入jackson的包。我们的项目中通过导入依赖的方法进行包的管理。 12345678910111213141516<!-- 对json处理的包,即能够使用@ResponseBody --><dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.0</version></dependency><dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.9.0</version></dependency><dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.9.0</version></dependency> 需要配置前端控制器不拦截静态资源。 DispatcherServlet会拦截到所有的资源,导致一个问题就是静态资源(img、css、js)也会被拦截到,从而不能被使用。解决问题就是需要配置静态资源不进行拦截,在springmvc.xml配置文件添加如下配置: 123456<!-- 设置静态资源不过滤 --><mvc:resources location=\"/css/\" mapping=\"/css/**\"/> <!-- 样式 --> <mvc:resources location=\"/images/\" mapping=\"/images/**\"/><!-- 图片 --> <mvc:resources location=\"/js/\" mapping=\"/js/**\"/> <!-- javascript --> 其中,mvc:resources标签配置不过滤谁。 location元素表示webapp目录下的包下的所有文件. mapping元素表示以/static开头的所有请求路径,如/static/a 或者/static/a/b. 客户端发送ajax的请求,传的是json字符串,后端把json字符串封装到user对象中。 响应:将对象转换成json字符串。 前端部分代码 123456789101112131415161718<input type=\"button\" value=\"异步请求测试\" id=\"testJson\"><script> $(function () { $(\"#testJson\").click(function () { $.ajax({ type: \"post\", url: \"${pageContext.request.contextPath}/user/testResponseBody\", contentType: \"application/json;charset=utf-8\", // 发送信息至服务器时内容编码类型 data: '{\"username\":\"张三\",\"password\":\"123\",\"money\":123}', // 发送的json数据 dataType: \"json\", // 服务器返回的数据类型 -- json success: function (data) { alert(data) } }); }); })</script> 控制器中的代码 12345@RequestMapping(\"/testResponseBody\")public @ResponseBody User testResponseBody(@RequestBody User user) { System.out.println(\"异步请求:\" + user); return user;} 在控制器中的代码,其中@RequestBody将前端发送的ajax请求的json格式数据封装成JavaBean。@ResponseBody将后端返回值JavaBean对象,格式化成json数据类型返回给客户端,直接响应。 总结:首先通过@RequestBody获取请求体数据,将前端请求的json字符穿转换成JavaBean对象,最后使用@ResponseBody注解把JavaBean对面转换成Json字符串,直接响应。 2.6.1.5 ModelAndView对象ModelAndView对象是Spring提供的一个对象,可以用来调整具体的JSP视图,即可以用作控制方法的返回值。该对象中有两个方法: 方法1:addObject 1234public ModelAndView addObject(String attributeName, Object attributeValue) { this.getModelMap().addAttribute(attributeName, attributeValue); return this;} 添加模型到该对象中,通过上面的代码可以看出,和前面所讲的请求参数封装中用到的对象是同一个。即jsp页面中同样可以使用EL表达式从request域中获取值。 方法2:setViewName 123public void setViewName(@Nullable String viewName) { this.view = viewName; } 用于设置逻辑视图名称,视图解析器会根据名称前往指定的视图。这也ModelAndView和Model最大的不同,它可以设置跳转的逻辑视图名称。 使用示例: 12345678@RequestMapping(\"/testModelAndView\")public ModelAndView testModelAndView() { System.out.println(\"testModelAndView...\"); ModelAndView mv = new ModelAndView(); mv.addObject(\"username\", \"张三\"); mv.setViewName(\"success\"); return mv;} 可以将对象存储到request域中。mv.addObject("key",value) 可以存储跳转到哪个页面中。mv.setViewName("success") – 选择视图解析器,进行解析。 前端代码:可以直接通过EL表达式进行值的获取,${username} --> 张三。 ModelAndView和Model两者的不同: Model是每次请求中都存在的默认参数,利用其addAttribute()方法即可将服务器的值传递到jsp页面中; ModelAndView包含model和view两部分,使用时需要自己实例化,利用ModelMap用来传值,也可以设置view的名称。 总而言之,就是ModelAndView除了同Model一样包含存储的键值外,它还存储着跳转页面的名称地址,直接将ModelAndView对象整体作为返回值,其对象中存储的地址自动由SpringMVC进行视图解析。 2.7 SpringMVC实现文件上传文件上传的前提: form表单的enctype取值必须是:multipart/form-data。默认值是:application/x-www-form-urlencoded。enctype:是表单请求正文的类型。 method属性取值必须是POST 提供一个文件选择域<input type="file"/> 文件上传的原理: 1234567891011121314当form 表单的enctype取值不是默认值后,request.getParameter()将失效。 enctype=”application/x-www-form-urlencoded”时,form 表单的正文内容是:key=value&key=value&key=value当form 表单的enctype取值为Mutilpart/form-data时,请求正文内容就变成:每一部分都是 MIME 类型描述的正文-----------------------------7de1a433602ac 分界符Content-Disposition: form-data; name=\"userName\" 协议头aaa 协议的正文-----------------------------7de1a433602acContent-Disposition: form-data; name=\"file\"; filename=\"C:\\Users\\zhy\\Desktop\\fileupload_demofile\\b.txt\"Content-Type: text/plain 协议的类型(MIME 类型)bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-----------------------------7de1a433602ac-- 使用Commons-fileupload组件实现文件上传,需要导入该组件相应的支撑jar包: Commons-fileupload Commons-io Commons-io不属于文件上传组件的开发jar文件,但Commons-fileupload组件从1.1版本开始,它工作时需要commons-io包的支持。 导入依赖: 1234567891011<!-- 文件上传依赖 --><dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.1</version></dependency><dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version></dependency> 前端页面: 1234<form action=\"/user/testUpload\" method=\"post\" enctype=\"multipart/form-data\"> 选择文件:<input type=\"file\" name=\"upload\"/><br> <input type=\"button\" value=\"文件上传\"/></form> 后端页面: 123456789101112131415161718192021222324252627282930313233343536@RequestMapping(\"/testUpload\")public String testUpload(HttpServletRequest request) throws Exception { // 先获取到要上传的文件目录 - 即需要上传的目录设置成uploads String path = request.getSession().getServletContext().getRealPath(\"/uploads\"); // 创建File对象,用于向该路径下上传文件 File file = new File(path); // 判断路径是否存在,若不存在则创建该路径 if (!file.exists()) { file.mkdirs(); } // 创建磁盘文件项工厂 DiskFileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload fileUpload = new ServletFileUpload(factory); // 解析request对象 List<FileItem> list = fileUpload.parseRequest(request); // 遍历 for (FileItem fileItem : list) { // 判断文件项是普通字段,还是上传的文件 if (fileItem.isFormField()) { continue; } else { // 上传文件 // 获取到上传文件的名称 String name = fileItem.getName(); String uuid = UUID.randomUUID().toString().replaceAll(\"-\", \"\").toUpperCase(); // 将文件名唯一化 name = uuid + \"_\" + name; // 上传文件 fileItem.write(new File(file, name)); // 删除临时文件 fileItem.delete(); } } return \"success\";} 备注:request.getSession().getServletContext()获取servlet容器对象(最大的域对象),也可以理解为项目真正部署到tomcat时获得的tomcat(服务器)对象。因为session是服务器端对象。而request.getSession().getServletContext().getRealPath()用于获取指定目录在服务器端所真正部署的路径。 2.7.1 SpringMVC传统方式的文件上传SpringMVC传统方式的文件上传,指的是我们上传的文件和访问的应用存在于同一台服务器上,并且上传完成以后,浏览器可能跳转。 SpringMVC框架提供了MultipartFile对象,该对象表示上传的文件,要求变量名称必须和表单file标签的name属性名称相同。 配置文件解析器 12345<!-- 配置文件解析器对象,要求id名称必须是multipartResolver --> <bean id=\"multipartResolver\" class=\"org.springframework.web.multipart.commons.CommonsMultipartResolver\"> <!-- 配置文件最大上传的byte --> <property name=\"maxUploadSize\" value=\"10485760\"/> </bean> 控制器代码 1234567891011121314151617@RequestMapping(\"/testSpringMVCUpload\")public String testSpringMVCUpload(HttpServletRequest request, MultipartFile upload) throws IOException { // 参数upload要和前端中的name属性相同 System.out.println(\"SpringMVC方式的文件上传..\"); String path = request.getSession().getServletContext().getRealPath(\"/uploads\"); File file = new File(path); if (!file.exists()) { file.mkdirs(); } // 获取到要上传文件的名称 String filename = upload.getOriginalFilename(); String uuid = UUID.randomUUID().toString().replaceAll(\"-\", \"\").toUpperCase(); filename = uuid + \"_\" + filename; // 上传文件 upload.transferTo(new File(path,filename)); return \"success\";} SpringMVC文件上传的主要步骤图: 首先前端请求,经过前端控制器将请求交给配置文件解析器,由相应的处理器进行处理。 2.7.2 SpringMVC跨服务器方式的文件上传在实际开发中,我们可能会处理很多不同功能的服务器。例如: 应用服务器:负责部署我们的应用 数据库服务器:运行我们的数据库 缓存和消息服务器:负责处理大并发访问的缓存和消息 文件服务器:负责存储用户上传文件的服务器 即每个服务器负责每一个功能模块,而不是由一个服务器承担全部。分服务器处理的目的是让服务器各司其职,从而提高我们项目的运行效率。请求都由应用服务器进行处理转发,而需要的功能都分发到每个单独的服务器进行处理。见下图: 现在模拟图片上传: 导入开发需要的Jar包 1234567891011<!-- 跨服务器上传需要的依赖 --> <dependency> <groupId>com.sun.jersey</groupId> <artifactId>jersey-core</artifactId> <version>1.18.1</version> </dependency> <dependency> <groupId>com.sun.jersey</groupId> <artifactId>jersey-client</artifactId> <version>1.18.1</version> </dependency> 需要在图片服务器上的web.xml添加如下配置文件 1234567891011121314151617<servlet> <servlet-name>default</servlet-name> <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class> <init-param> <param-name>debug</param-name> <param-value>0</param-value> </init-param> <init-param> <param-name>listings</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>readonly</param-name> <param-value>false</param-value> </init-param> <load-on-startup>1</load-on-startup></servlet> 即:接收文件的目标服务器可以支持写入操作。 编写应用服务器端的控制器代码 123456789101112131415161718@RequestMapping(\"/testSpringMVCUpload2\")public String testSpringMVCUpload2(MultipartFile upload) throws IOException { System.out.println(\"SpringMVC跨服务器上传...\"); // 定义图片服务器的请求路径 String path = \"http://localhost:8088/picture/uploads/\"; // 获取到上传文件的名称 String filename = upload.getOriginalFilename(); String uuid = UUID.randomUUID().toString().replaceAll(\"-\", \"\").toUpperCase(); filename = uuid + \"_\" + filename; // 向图片服务器上传文件 // 创建客户端对象 Client client = Client.create(); // 连接图片服务器地址 WebResource webResource = client.resource(path + filename); // 上传文件 webResource.put(String.class,upload.getBytes()); return \"success\";} 2.8 SpringMVC的异常处理系统中的异常包括两类:预期异常和运行时异常RuntimeException,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。 系统的dao、service、controller出现都通过throws Exception向上抛出,最后由SpringMVC前端控制器交由异常处理器进行异常处理。SpringMVC异常处理见下图: 控制器中捕获异常,从下向上抛出异常。 其目的是为了在出现异常的时候,跳到另外一个“友好”页面。模拟应用步骤: 自定义一个异常类。(继承Exception) 1234567891011121314151617181920212223package cn.lizhi.other;public class SysException extends Exception { // 用于定义异常信息 private String message; public SysException(String message) { this.message = message; } public SysException() { } @Override public String getMessage() { return message; } public void setMessage(String message) { this.message = message; }} 编写异常处理器类。 1234567891011121314151617181920public class SysExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler, Exception ex) { // 异常信息打印 ex.printStackTrace(); SysException sysException = null; if (ex instanceof SysException) { // 如果抛出的是自定义异常则直接转换 sysException = (SysException) ex; } else { //如果抛出的不是系统自定义异常则重新构造一个系统错误异常。 sysException = new SysException(\"系统错误,请于管理员联系\"); } ModelAndView mv = new ModelAndView(); mv.addObject(\"message\", sysException.getMessage()); mv.setViewName(\"error\"); return mv; }} 即异常处理器,实现接口 HandlerExceptionResolver,其中ex参数表示接收到的异常对象。其中创建的ModelAndView用于存入异常信息,键值对;再设置跳转路径。 这里我们可以通过判断不同的异常类型跳转到不同的页面显示。 bean配置文件中配置异常处理器类。 12<!-- 配置自定义异常处理器 --> <bean id=\"handlerExceptionResolver\" class=\"cn.lizhi.other.SysExceptionResolver\"/> 模拟出现异常 123456@RequestMapping(\"testException\")public String testException() { System.out.println(\"模拟异常处理器的处理方式\"); int i = 3 / 0; return \"success\";} 页面出现结果 –> 系统错误,请于管理员联系 2.9 SpringMVC中的拦截器Spring MVC 的处理器拦截器类似于 Servlet 开发中的过滤器 Filter,用于对处理器进行预处理和后处理。用户可以自己定义一些拦截器来实现特定的功能。 谈到拦截器,还要向大家提一个词——拦截器链(Interceptor Chain)。拦截器链就是将拦截器按一定的顺序联结成一条链。在访问被拦截的方法或字段时,拦截器链中的拦截器就会按其之前定义的顺序被调用。 说到这里,可能大家脑海中有了一个疑问,这不是我们之前学的过滤器吗?是的它和过滤器是有几分相似,但是也有区别,接下来我们就来说说他们的区别: 过滤器是 servlet 规范中的一部分,任何 java web 工程都可以使用。 拦截器是 SpringMVC 框架自己的,只有使用了 SpringMVC 框架的工程才能用。 过滤器在 url-pattern 中配置了/*之后,可以对所有要访问的资源拦截。 拦截器它是只会拦截访问的控制器方法,如果访问的是 jsp,html,css,image 或者 js 是不会进行拦截的。 它也是 AOP 思想的具体应用。 我们要想自定义拦截器, 要求必须实现:HandlerInterceptor 接口。 HandlerInterceptor拦截的是请求地址,所以针对请求地址做一些验证、预处理等操作比较合适。 2.9.1 自定义拦截器步骤 创建类,实现HandlerInterceptor接口,需要重写需要的方法 12345678910111213141516171819public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println(\"拦截器执行了...前\"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println(\"拦截器执行了...后\"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println(\"页面跳转完毕后...\"); }} preHandler 方法是在controller方法执行前,进行拦截的方法;返回结果为true表示放行,返回结果为false表示拦截(配合条件判断使用),因为提供了request、response参数,所以可以使用转发或者重定向直接跳转到指定的页面。 调用:按拦截器定义顺序调用 作用:如果决定该拦截器对请求进行拦截处理后还要调用其他的拦截器,或者是需要业务处理器进行处理,则返回true;如果不需要再调用其他的组件去处理请求,则返回false。 postHandler 后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。 按拦截器定义逆序调用;在拦截器链内所有拦截器返回成功后进行调用。 作用:在业务处理器处理完请求后,但是DispatcherServlet向客户端返回响应前被调用,处理ModelAndView中的值。 afterCompletion 调用顺序:按拦截器定义逆序调用 调用时机:对拦截器链内所有拦截器内返回成功的拦截器才调用它的afterCompletion方法。 作用:在业务处理器处理完请求后,但是DispathcerServlet向客户端返回响应前被调用。可以在该方法中进行一些资源清理的操作。 在view视图渲染完毕以后执行,不能再进行页面的跳转。 一旦在preHandler方法中返回了false(等同于出现了异常),后续的拦截器和controller方法就都不会再执行,也不会再执行postHandler方法,只会倒叙执行afterCompletion方法。 拦截器只能拦截Controller中的方法。 在springmvc.xml配置文件中配置拦截器类 123456789101112<!-- 配置拦截器 --> <mvc:interceptors> <mvc:interceptor> <!-- 哪些方法进行拦截 --> <mvc:mapping path=\"/user/*\"/> <!-- 哪些方法不进行拦截 <mvc:exclude-mapping path=\"\"/> --> <!-- 注册拦截器对象 --> <bean id=\"myInterceptor\" class=\"cn.lizhi.utils.MyInterceptor\"/> </mvc:interceptor> </mvc:interceptors> 可以配置多个拦截器,拦截器执行的顺序就是配置文件中注册的拦截器对象的顺序。 2.9.2 HandlerInterceptor接口中的方法总结 preHandle方法是controller方法执行前拦截的方法 可以使用request或者response跳转到指定的页面 return true放行,执行下一个拦截器,如果没有拦截器,执行controller中的方法。 return false不放行,不会执行controller中的方法。 postHandle是controller方法执行后执行的方法,在JSP视图执行前。 可以使用request或者response跳转到指定的页面 如果指定了跳转的页面,那么controller方法跳转的页面将不会显示。 postHandle方法是在JSP执行后执行 request或者response不能再跳转页面了 三、SpringMVC扩展3.0 启动搭建SpringMVC是Spring的web模块;所有模块的运行都是依赖核心模块(IOC模块) 核心容器模块 web模块 SpringMVC思想是由一个前端控制器能拦截所有请求,并智能派发;这个前端控制器是一个Servlet;应该在web.xml中配置servlet来拦截所有请求。 前端控制器: 123456789101112131415161718192021222324<!-- 前端控制器 --><servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 配置Servlet的初始化参数,读取springMVC的配置文件,创建spring容器,用于加载配置文件 MVC配置文件加载成功,那么其中的扫描就成功,继而到控制器中类加载成对象 --> <init-param> <!-- ContextConfigLocation:指定SpringMVC配置文件位置 --> <param-name>contextConfigLocation</param-name> <param-value>classpath:springMVC.xml</param-value> </init-param> <!-- 服务器在启动时,就加载资源 值越小优先级越高,越先创建对象--> <load-on-startup>1</load-on-startup></servlet><servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <!-- 配置资源路径/表明所有的资源皆可被访问到;/*和/都是拦截所有请求,/*的范围更大;还会拦截到*.jsp请求;一旦拦截,页面就无法显示了 --> <url-pattern>/</url-pattern> <!-- 表明只有hello的资源路径可以被访问到(拦截到) <url-pattern>/hello</url-pattern> --></servlet-mapping> 然后在springMVC.xml中配置扫描组件。 配置视图解析器,能帮我们拼接页面地址,即进行地址映射。方式,通过前缀和后缀的方式进行映射。 123456789<!-- 配置spring创建容器时要扫描的包 --><context:component-scan base-package=\"cn.lizhi\"></context:component-scan><!-- 配置视图解析器 Ioc容器对象,由Tomcat调用--><bean id=\"viewResolver\" class=\"org.springframework.web.servlet.view.InternalResourceViewResolver\"> <!-- 文件具体所在的目录 --> <property name=\"prefix\" value=\"/WEB-INF/pages/\"></property> <!-- 文件解析的类型(后缀名) --> <property name=\"suffix\" value=\".jsp\"></property></bean> SpringMVC中的HelloWorld的运行流程: 运行流程: 客户端点击链接发送请求 来到tomcat服务器 SpringMVC的前端控制器收到所有请求 来看请求地址和@RequestMapping标配的哪个匹配,来找到到底使用哪个类的哪个方法 前端控制器找到了目标处理器类和目标方法,直接利用返回执行目标方法; 方法执行完成以后会有一个返回值;SpringMVC认为这个返回值就是要去的页面地址 拿到方法返回值以后;用视图解析器进行拼串得到完整的页面地址; 拿到页面地址,前端控制器帮我们转发到页面; @RequestMapping:就是告诉SpringMVC,这个方法用来处理什么请求。 如果不指定配置文件位置: /WEB-INF/springDispatcherServlet-serlvet.xml 如果不指定也会默认去找一个文件;/WEB-INF/xxx-serlvet.xml,其中xxx是指定的前端控制器的名称,因此也可以创建这么一个xml文件作为SpringMVC的配置文件,直接放在web下。 3.1 前端控制器路径问题SpringMVC中关于前端控制器路径配置的问题: 在整个Tomcat服务器中,服务器大web.xml中有一个DefaultServlet是url-pattern=/,我们自定义配置中的前端控制器也设置的url-pattern=/,如果访问静态资源,就会来到DispatcherServlet(前端控制器)看那个方法的RequestMapping是这个index.html。 在Tomcat中的DefaultServlet是Tomcat中处理静态资源的。除过Servlet和jsp都是静态资源;我们的前端控制设置了/禁用了tomcat服务器中的DefaultServlet。 为什么jsp又能访问;因为我们没有覆盖服务器中的JspServlet的配置。 关于上面这一块可以看下Tomcat总的web.xml文件,里面配置了jsp的servlet,静态资源的servlet,动态资源servlet,它们都设定了各种路径,来映射到这些对应个的servlet上进行处理。但是,一旦我们的DispatcherServlet进行了配置,会优先交给它进行拦截请求,到Controller中进行处理,其实本质上DispatcherServlet也是servlet,只是它将所有拦截到的请求都映射到给它自己来进行请求,交给Controller进行处理。 所以/*直接就是拦截所有请求;我们写/;也是为了迎合后面的Rest风格的URL地址。 3.2 字符编码在web.xml中配置字符过滤。 123456789101112131415<!-- 配置解决中文乱码的过滤器 --><filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <!--设置过滤器中的属性值,指定POST请求乱码--> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param></filter><!-- 过滤所有请求 --><filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern></filter-mapping> GET的请求编码,设置在服务器端,在server.xml的8080处添加URIEncoding=”UTF-8” 字符编码Filter一定要在其它Filter之前。 12345678910111213protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String encoding = this.getEncoding(); if (encoding != null) { if (this.isForceRequestEncoding() || request.getCharacterEncoding() == null) { request.setCharacterEncoding(encoding); // 在这里设定字符编码,也就是在其它过滤器还没有获取到请求参数前,在这里给请求参数设定编码,否则其它filter都已经获取到参数进行使用了,再进行设置就没有意义了啊 } if (this.isForceResponseEncoding()) { response.setCharacterEncoding(encoding); } } filterChain.doFilter(request, response);} 3.3 SpringMVC数据传递思考:SpringMVC除过在方法行传入原生的request和session外还能怎么样在数据带给页面? 可以在方法处传入Map、或者Model或者ModelMap。给这些参数里面保存的所有数据都会放在域中。可以在页面获取。 方式1: 12345@RequestMapping(\"/handle01\")public String handle01(Map<String, Object> map) { map.put(\"msg\", \"你好\"); return \"success\";} 这样前端,可以通过在request域中获取到这个map中存储的值。 方式2/3:采用Model和ModelMap 1234567891011@RequestMapping(\"/handle02\")public String handle02(Model model) { model.addAttribute(\"msg\", \"你好model\"); return \"success\";}@RequestMapping(\"/handle03\")public String handle02(ModelMap model) { model.addAttribute(\"msg\", \"你好modelMap\"); return \"success\";} 同方式一一样,都可以通过在前端中的request域中获取到值。 通过三种的类型打印: 1class org.springframework.validation.support.BindingAwareModelMap 都是同一种类型。 所以,Map、Model,ModelMap,最终都是BindingAwareModelMap工作。相当于给BindingAwareModel中保存的东西都会被放在请求域中。 通过源码分析,他们之间的关系: 方法的返回值可以变为ModelAndView类型: 12345678@RequestMapping(\"/handle04\")public ModelAndView handle04() { // 之前的返回值我们就叫视图名;视图名,是通过视图解析器 最终拼串得到页面的真实地址; ModelAndView mv = new ModelAndView(\"success\"); mv.addObject(\"msg\", \"您好\"); return mv;} 这种方式,同时将view和model数据进行返回;而且数据是放在请求域中。可以为页面携带数据。 SpringMVC提供了一种可以临时给Session域中保存数据的方式;使用注解:@SessionAttributes。只能使用在类上。例如:@SessionAttributes(value = {"msg"},types={String.class});给BindingAwareModelMap中保存的数据,或者ModelAndView中的数据。同时给session中放一份。value指定保存数据时要给session中方的数据的key。分别以指定value和types两种方式进行保存session域。 3.4 ModelAttribute1234@RequestMapping(\"/handle05\")public void handle05(Account account) { System.out.println(account);} 在全字段更新中,首先根据Id从数据库中查询出Account对象,再根据前端的传过来的字段值进行相应的覆盖,而不是创建一个新的对象,对其赋值,否则里面空值就会覆盖原来的数据库中的值。 就是通过ModelAttribute达到这个效果。 思想核心: SpringMVC要封装请求参数的Book对象不应该是自己new出来的。而应该是从数据库中拿到的准备好的对象。 再来使用这个对象封装请求参数。 @ModelAttribute,可以标记在方法和对象上。 标记在方法上: 这个方法就会提前于目标方法先运行,即提前在数据中查询对应的对象信息。 将这个对象信息保存起来(方便下一个方法还能使用),保存的方式,可以通过map进行保存(保存在request域中) 可以告诉SpringMVC要封装的请求参数不应该是自己new出来的,而是从数据库中查询出来的。再使用这个对象封装参数 在接收请求参数的对象地方再标记上@ModelAttribute,其中的value就是前面保存在map中指定的Key,目的是做映射,这样就能在请求参数封装的对象中,取出从数据库中查询到的值。 12345678910111213141516171819@RequestMapping(\"/handle05\")public void handle05(@ModelAttribute(\"account\") Account account) { System.out.println(account);}@ModelAttributepublic void modelAttribute(Map<String, Object> map) { System.out.println(\"ModelAttribute...测试\"); // 下面模拟从数据库中查询到的数据 Account account = new Account(); account.setMoney(100f); account.setAccountId(10); account.setAccountName(\"123\"); Address address = new Address(); address.setCityName(\"beijing\"); address.setProvinceName(\"beijing\"); account.setAddress(address); map.put(\"account\", account);} 会在每个controller执行之前,先执行ModelAttribute目标方法,然后绑定在对应的参数上,这个参数并不是新new出来的,而是map中保存的。 3.4.1 原理这里ModelAttribute中存放的map和Model,都是同一个对象。 3.5 SpringMVC源码解析3.5.1 前端控制器架构 —— DispatcherServlet当请求到来的时候,其执行流程: 再进一步细看doDispatch: ha.handle执行目标方法,即处理(控制)器的方法被调用。 processDispatchResult 总流程: 所有请求过来DispatcherServlet收到请求; 调用doDispatch()方法进行处理 检查是否是文件上传请求 如果是文件上传请求,就包装新的request 通过getHandler(processedResult)根据当前请求地址找到哪个类可以进行处理,即找到处理这个请求的目标处理器,就是找到对应的Controller 根据当前请求在HandlerMapping中找到这个请求的映射信息,获取到目标处理器类 如果没有找到对应的处理器,就抛出异常。 通过getHandlerAdapter(),根据当前处理器你类获取到能执行这个处理器方法的适配器。就是确定当前类下哪个方法能够处理这个请求。即拿到能执行这个类的所有方法的适配器(反射工具)。(这里是注解方式的适配器) 根据当前处理器类,找到当前类的HandlerAdapter(适配器),即具体的方法。 使用适配器执行目标方法,将目标方法执行完成后的返回值作为视图名,并设置到modelAndView中。目标方法无论怎么写,最终适配器执行完成以后都会执行后的信息封装成ModelAndView 如果没有视图名设置一个默认的视图名 根据ModelAndView的信息转发到具体的页面。processDispatchResult()方法;转发到目标页面,根据方法最终执行完成后封装到ModelAndView;转发到对应页面,而且ModelAndView中的数据可以从请求域中获取。 3.5.2 getHandler()细节怎么根据当前请求就能找到哪个类来处理。进一步进行验证,进入该方法进行一探究竟。 该方法会返回目标处理器类的执行链; HandlerMapping:处理器映射:他里面保存了每一个处理器能处理哪些请求的映射信息。 DefaultAnnotationHandlerMapping:下面的handlerMap保存了每个请求能够通过哪个controller进行处理。通过遍历,找到请求对应的处理器。(在IOC容器一启动,就创建controller对象,再进行扫描,每个处理器都能进行处理什么请求,并保存到handlerMap属性中;下一次请求过来,就来看哪个HandlerMapping中有这个请求) 3.5.3 getHandlerAdapter根据前面得到的处理器,找到其对应请求的适配器。 123456789101112131415161718192021222324252627```存在三种方式的适配器,这里是采用注解版本的适配器。寻求目标处理器类的适配器目的:要拿适配器才去执行目标方法。AnnotationMethodHandlerAdapter:能解析注解方法的适配器。处理器类中只要有标了注解的这些方法就可以使用。#### 3.5.4 DispatcherServlet的属性DispatcherServlet中的几个引用类型的属性,就是SpringMVC的九大组件。SpringMVC在工作的时候,就是这九大组件完成的- 文件上传解析器- 区域信息解析器 和国际化相关- 主题解析器;强大的主题效果更换- Handler映射信息- Handler的适配器- SpringMVC强大的异常解析功能;异常解析器- viewNameTranslator- FlashMap+Manager:SpringMVC中运行重定向携带数据的功能(重定向时,也能携带数据的作用)- 视图解析器 -共同点:都是接口,提供规范;提供了规范,进行扩展。IOC容器启动,就会触发initStrategies方法。(onFresh()下的初始化方法)以HandlerMapping的初始化为例:```java 组件的初始化:去容器中找这个组件,如果没有找到就用默认的配置;(有些组件在容器是使用类型找的,有些组件是使用id找的) 3.5.5 ha.handle(…)深化难点是方法执行。该方法是如何通过反射确定程序传入的参数,进行的方法执行? 以下列该方法为例: 1234567891011@RequestMapping(\"/handle05\")@ResponseBodypublic Account handle05(@ModelAttribute(\"account\") Account account, @RequestParam(value = \"Tom\") String name, Map<String,Object> map, HttpServletRequest request) { System.out.println(\"方法执行....\"); System.out.println(account); System.out.println(name); return account;} handler执行,是如何通过反射能够确定上面的参数,并执行该方法的? 该方法内部,最终是通过,invokHandlerMethod(request,response,handler)执行的目标方法。其大致流程: 拿到方法的解析器 方法解析器根据当前请求地址找到真正的目标方法 创建一个方法执行器 包装原生的request,response 创建一个BindingAwareModelMap,创建一个隐含模型 执行目标方法 —— Object result = methodInvoker.invokeHandlerMethod(...);真正执行目标方法;目标方法利用反射执行期间确定参数值,提前执行modelAttribute等所有的操作都是在方法中; 找到所有@ModelAttribute注解标注的方法 args;确定modelAttribute方法执行时要使用的每一个参数的值,当前返回值的这个方法。 创建了一个和参数个数一样长度数组,用于保存 找到目标方法这个参数的所有注解,如果有注解就解析并保存注解的信息; 如果没有找到注解 resolveCommonArgument,解析普通参数值 继而进入,resolveStandarArgument(解析标准参数),即用来确定当前参数,是否是原生API。 查看该参数是否是未解析参数 查看该参数是否是默认值 获取到参数类型,判断该参数类型是否Model旗下,或者参看是否是Map类型旗下。如果是的话,就将隐含模型将其赋值给args[i]参数。 将目标方法参数通过暴力反射,做成可访问的。 attributeMethodToInvoke执行该目标方法。 如果方法上标注的ModelAttribute注解如果有value值,就是attrName的值。如果没设置,其值就会变为返回值类型首字母小写,比如void,或者account等。@ModelAttribute标在方法上的另一个作用;可以把方法运行后的返回值按照方法上@ModelAttribute("value")指定key放到隐含模型中,如果没有设置,就用返回值首字母小写。 把提前运行的ModelAttribute方法的返回值也放在隐含模型中。 对@ModelAttribute方法执行完以后,再次解析目标方法参数是哪些值。 如果参数标注了注解,保存是哪个注解的详细信息, 如果参数有ModelAttribute注解,拿到ModelAttribute注解的值让attrName保存,其保存的是注解的value值,否则是返回值小写。 如果没标注解 先看是否普通参数(是否是原生api) 如果不是,再看是否Model或者Map,如果是就传入隐含模型。如果操作map,使用的隐含模型在前面的@ModelAttribute 如果自定参数没有注解 先看是否是原生API 不是,就再看是否model或者map 再看是否是其他类型的,比如:SessioinStatus、Http 如果不是,就判断是否是简单类型的属性,比如:Integer,String,基本类型等 如果是,paramName=”” 否者,就给attrName=”” 如果是自定类型对象,最终产生两个效果; 如果这个参数标注了ModelAttribute注解就给attrName赋值为这个注解的value值 如果这个参数没有标注ModelAttribute注解就给attrName赋值””; 确定自定义类型参数的值;还要将请求中的每一个参数赋值给这个对象。 WebDataBinder对象,即resolveModelAttribute(…)方法对数据进行绑定。 如果attrName是空串;就将参数类型的首字母小写作为值 确定对象目标值(SpringMVC确定POJO值的三步) 如果隐含模型中有这个key(标了ModelAttribute注解就是注解指定的value,没标就是参数类型的首字母小写)指定的值;如果有讲这个值赋值给bindObject 如果没有,再判断是否是SessionAttributes标注的属性,就从session中拿 如果都不是,就利用反射创建对象。 最后在return处,执行目标方法 总结: 运行流程简单版 确定方法每个参数的值 标了注解:保存注解的信息;最终得到这个注解应该对应解析的值; 没标注解 看是否原生API 看是否Model或者是Map,xxx 都不是,看是否是简单类型;paramName=”” 给attrName赋值;attrName(参数标了@ModelAttribute("")就是指定的,没标就是””) 确定自定义类型参数: attrName使用参数的类型首字母小写;或者使用之前@ModelAttribute("")的值 先看隐含模型中有这个attrName作为key对应的值;如果有就从隐含模型中获取并赋值 看是否@SessionAttributes(value="xxx");标注的属性,如果是从session中拿; 如果存在该注解,但是拿不到,就会报出异常 否者,通过反射创建类型参数对应的对象 拿到之前创建好的对象,使用数据绑定器(WebDataBinder)将请求中的每个数据绑定到对象。 @ModelAttribute标注的方法会提前运行并把方法的运行结果放在隐含模型中; 放的时候会使用一个key; 如果@ModelAttribute("value")指定了,就用指定的value; 如果没有指定,就用返回值类型的首字母小写作为Key; @SessionAttributes(value="xxx")最好不要使用 如果用的话,隐含模型中存在需要的值,这样就不会跳转到这一步 如果隐含模型中不存在,那就需要session域中存在需要的值,否者会报错。 3.5.6 视图和视图解析器上一小节中,能够让模型进行了数据处理,那该如何对视图进行渲染呢。 视图解析器中的拼串,是从当前web项目下的,/WEB-INF/..进行拼接,资源请求。 如果采用转发: 1234@RequestMapping(\"/handle06\")public String handle06() { return \"forward:/index.jsp\";} 采用转发,是从当前项目开始资源请求(\\web项目的root目录下,不加就是相对路径),而不是/WEB-INF/..。 采用这种方式,可以将请求转发给另外一个controller进行请求处理。 12345678910111213141516@RequestMapping(\"/handle05\")@ResponseBodypublic Account handle05(@ModelAttribute(\"account\") Account account, @RequestParam(name= \"name\",defaultValue = \"tom\") String name, Map<String,Object> map, HttpServletRequest request) { System.out.println(\"方法执行....\"); System.out.println(account); System.out.println(name); return account;}@RequestMapping(\"/handle06\")public String handle06() { return \"forward:/handle05\"; // 派发给handle05进行请求处理} foward:前缀的转发,不会由我们配置的视图解析器进行解析。 如果采用重定向: redirect:重定向的路径 原生的Servlet重定向需要加上项目名才能成功。 1response.sendRedirect(\"demoName/hello.jsp\") 而使用SpringMVC会为路径自动的拼接上项目名(写法同转发)。 总结:有前缀的转发和重定向操作,配置的视图解析器就不会进行拼串。 3.5.7 视图解析源码解析 方法执行后的返回值会作为页面地址参考,转发或重定向到页面 视图解析器会进行页面地址的拼串 任何方法的返回值,都会包装成ModelAndView 核心方法processDispatchResult(...)进行页面的渲染,就是将域中的互数据在页面进行展示。下面对该方法的分析: 调用render(…)进行渲染页面。 得到View与ViewResolver; ViewResovler的作用是根据视图名(方法的返回值)得到View对象,具体体现在它的resolverViewName(...)方法上 再进行探究,如何通过这个方法的返回值(视图名)得到View对象: 首先遍历所有的viewResolvers,调用resolverViewName(...)视图解析器根据方法的返回值,得到一个View对象。即:所有配置的视图解析器都来尝试根据视图名(返回值)得到View(视图)对象;如果能得到就返回,得不到就换下一个视图解析器。 resolverViewName(...)的具体实现createView(...) 是否重定向 如果是转发 如果没有前缀,就使用父类默认创建一个View 返回View对象 调用View对象的render方法; 最终方法落地在renderMergeOutputModel(...)方法。 期间方法中exposeModelAsRequestAttributes(...)方法将隐藏模型中的数据设置到request中的请求域中。 结论:视图解析器只是为了得到视图对象;视图对象才能真正的转发(将模型数据全部放在请求域中)或者重定向到页面。视图对象才真正渲染视图。 1<mvc:view-controller path=\"/handle\" view-name=\"login\"/> 配置在mvc下的该标签,进行view时,经过mvc处理(不重要)。但是,只有当前映射的好用,其它的不好使。所以看下面这个高级功能: 1<mvc:annotation-driver/> 3.5.8 自定义视图和视图解析器自定义视图解析器工作的整体流程: 让我们的视图解析器工作 得到我们的视图对象 我们的视图对象自定义渲染逻辑 - render负责渲染 1response.getWriter().write(\"..\") // 自定义视图和视图解析器的步骤 编写自定义的视图及解析器 视图解析器必须放在IOC容器中 为了让自定义的视图解析器先执行,还需要实现Order接口(默认的视图解析器优先级最高) 实现代码: 1234567891011121314151617181920212223242526272829303132333435363738public class MyViewResolver implements ViewResolver, Ordered { private int order = 0; @Override public View resolveViewName(String viewName, Locale locale) throws Exception { if (viewName.startsWith(\"chemlez:\")) { return new MyView(); } return null; } @Override public int getOrder() { return order; } public void setOrder(int order) { this.order = order; }}/** * 自定义视图 */public class MyView implements View { @Override public String getContentType() { return \"text/html\"; // 返回资源响应类型 } @Override public void render(Map<String, ?> map, HttpServletRequest request, HttpServletResponse response) throws Exception { request.setCharacterEncoding(\"utf-8\"); System.out.println(map); response.setContentType(\"text/html\"); response.getWriter().println(\"自定义视图解析生效了...\"); }} 视图解析器装配: 123<bean id=\"myResolver\" class=\"cn.lizhi.view.MyViewResolver\"> <property name=\"order\" value=\"0\"/></bean> 3.5.9 数据转换SpringMVC封装自定义类型对象的时候,是如何封装并绑定请求参数的? JavaBean要和页面提交的数据进行一一绑定的过程。 牵扯到以下操作: 数据绑定期间的数据类型转换(例如:前端传递过来的Key=value都是字符串类型,就需要对其进行类型转换) 数据绑定期间的数据格式化问题(例如:日期格式转化) 数据校验,即我们提交的数据必须是合法的。 核心方法: 1bindRequestParameters(...) // 请求参数解析与绑定 WebDataBinder:数据绑定器负责数据类型转化和数据校验 ConversionService组件:负责数据类型的转换以及格式化功能 Validators:负责数据校验工作 bindingResult:负责保存以及解析数据绑定期间数据校验产生的错误 自定义类型转换器: 不同类型的转换和格式化用它自己的converter,ConversionService存在多个converter ConversionService是一个接口,它里面有converter进行工作; 步骤: 实现Converter接口,写一个自定义的类型转换器; 两个泛型: S:Source:原数据类型 T:Target:需要转换的数据类型 在convert方法中写转换的逻辑 Converter是ConversionService中的组件 将自己编写的converter放进ConversionService中 将WebDataBinder中的ConversionService设置成我们这个加了自定义类型转换器的ConversionService 配置出ConversionService,配置其对应的ConversionServiiceFactory的Bean,通过set注入配置 123456789101112<!-- 告诉SpringMVC别用默认的ConversionService,用我们自定义的ConversionService、这里包含我们自定义的Converter --><!-- 配置spring开启注解mvc的支持 --><mvc:annotation-driven conversion-service=\"converterService\" /> // 1<bean id=\"converterService\" class=\"org.springframework.context.support.ConversionServiceFactoryBean\"> <!-- 给工厂注入一个新的类型转换器,并使用我们自己配置的类型转换组件 --> <property name=\"converters\"> <array> <!-- 配置自定义类型转换器 --> <bean class=\"cn.lizhi.utils.StringToDate\"/> </array> </property></bean> 3.5.10 <mvc:annotation-driven/>标签12<!-- 静态资源能访问,动态映射的请求就不行 自己映射的请求就进行处理,不能处理的直接交给tomcat --><mvc:default-servlet-handler/> // 2 通过: 只有1时,DefaultAnnotationHandlerMapping中的handlerMap中保存了每一个资源的映射信息; 静态资源不能访问:就是handlerMap中没有保存静态资源映射的请求 handlerAdapter:方法执行的适配器; 只有2时,动态映射HandlerMapping对应的映射没有了DefaultAnnotationHandlerMapping没有了;使用SimpleUrlHandlerMapping替换了,他的作用就是将所有请求交给tomcat,而tomcat中,只配置了dispatcherServlet,没有其他映射的servlet,所以动态请求无法处理。 当1,2都添加时,会有RequestMappingHandlerMapping:动态资源可以访问;handlerMethods属性保存了每一个请求用哪个方法来处理。 HandlerAdapters:存在RequestMappingHandlerAdapter,原来的AnnotationMethodHandlerAdapter被换成RequestMappingHandlerAdapter。 四、SSM框架的整合核心思想:通过Spring整合另外两个框架。 整合方式:配置文件加注解的方式。 整合的思路: 搭建整合的环境 Spring的配置搭建完成 Spring整合SpringMVC框架 Spring整合Mybatis框架 4.1 环境搭建 数据库创建 1234567create database ssm;use ssm;create table account( id INTEGER PRIMARY key auto_increment, name VARCHAR(32), money DOUBLE(7,2)); pom.xml依赖导入 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189<?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>cn.lizhi</groupId> <artifactId>SpringMVC_03</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>SpringMVC_03 Maven Webapp</name> <!-- FIXME change it to the project's website --> <url>http://www.example.com</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.version>5.0.2.RELEASE</spring.version> <slf4j.version>1.6.6</slf4j.version> <log4j.version>1.2.12</log4j.version> <mysql.version>5.1.6</mysql.version> <mybatis.version>3.4.5</mybatis.version> </properties> <dependencies> <!-- spring --> <!-- 切入点表达式 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.8</version> </dependency> <!-- springAOP AOP核心功能,例如代理工厂等 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <!-- springIOC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- spring整合junit --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> </dependency> <!-- 事务控制 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <!-- SpringJDBC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- 单元测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>compile</scope> </dependency> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- log start --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${slf4j.version}</version> </dependency> <!-- log end --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.0</version> </dependency> <!-- 数据库连接池 --> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> <type>jar</type> <scope>compile</scope> </dependency> </dependencies> <build> <finalName>SpringMVC_03</finalName> <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> <plugins> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>3.1.0</version> </plugin> <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging --> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> </plugin> <plugin> <artifactId>maven-war-plugin</artifactId> <version>3.2.2</version> </plugin> <plugin> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> </plugin> <plugin> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> <showWarnings>true</showWarnings> </configuration> </plugin> </plugins> </pluginManagement> </build></project> 实体类编写 123456789101112131415161718192021222324252627282930313233343536373839public class Account implements Serializable { private Integer id; private String name; private Double money; 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 Double getMoney() { return money; } public void setMoney(Double money) { this.money = money; } @Override public String toString() { return \"Account{\" + \"id=\" + id + \", name='\" + name + '\\'' + \", money=\" + money + '}'; }} dao接口编写 由于我们是使用Mybatis框架,所以不需要编写其实现类,只需要写接口即可。 123456public interface AccountDao { public void saveAccount(Account account); public List<Account> findAll();} 编写Service接口和实现类 12345678910111213141516171819202122232425public interface AccountService { public void saveAccount(Account account); public List<Account> findAll();}public class AccountServiceImpl implements AccountService { private AccountDao accountDao; @Override public void saveAccount(Account account) { System.out.println(\"对用户进行保存...\"); accountDao.saveAccount(account); } @Override public List<Account> findAll() { System.out.println(\"业务层:查询所有用户\"); List<Account> accounts = accountDao.findAll(); return accounts; }} 4.2 Spring框架代码的编写4.2.1 搭建和测试Spring的开发环境 在项目中创建applicationContext.xml的配置文件,编写具体的配置信息。 123456789101112131415<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:context=\"http://www.springframework.org/schema/context\" xmlns:aop=\"http://www.springframework.org/schema/aop\" xmlns:tx=\"http://www.springframework.org/schema/tx\" xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsdhttp://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd\"> <!-- 开启注解扫描,要扫描的是service层和dao层的注解,要忽略web层注解,因为web层(Controller层)让SpringMVC框架去管理 --> <context:component-scan base-package=\"cn.lizhi\"> <!-- 配置要忽略的注解 --> <context:exclude-filter type=\"annotation\" expression=\"org.springframework.stereotype.Controller\"/> </context:component-scan></beans> 编写测试方法,进行测试 1234567@Testpublic void testSpring() { // 获取Spring容器 ApplicationContext ac = new ClassPathXmlApplicationContext(\"classpath:applicationContext.xml\"); AccountService accountService = ac.getBean(\"accountService\", AccountService.class); accountService.findAll();} 4.3 Spring整合SpringMVC框架 在web.xml中配置DispatcherServlet前端控制器 12345678910111213141516<!-- 配置前端控制器:服务器启动必须加载,需要加载springmvc.xml配置文件 --> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet </servlet-class> <!-- 配置初始化参数,创建完DispatcherServlet对象,加载springmvc.xml配置文件 --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <!-- 服务器启动的时候,让DispatcherServlet对象创建 --> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> 在web.xml中配置DispatcherServlet过滤器解决中文乱码 12345678910111213<!-- 配置解决中文乱码的过滤器 --> <filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> 创建SpringMVC.xml 的配置文件,编写配置文件 123456789101112131415161718192021222324<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:mvc=\"http://www.springframework.org/schema/mvc\" xmlns:context=\"http://www.springframework.org/schema/context\" 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.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd\"> <!-- 扫描Controller(web层)的注解,别的不扫描 --> <context:component-scan base-package=\"cn.lizhi\"> <context:include-filter type=\"annotation\" expression=\"org.springframework.stereotype.Controller\"/> </context:component-scan> <!-- 配置视图解析器 --> <bean id=\"viewResolver\" class=\"org.springframework.web.servlet.view.InternalResourceViewResolver\"> <!-- JSP文件所在的目录 --> <property name=\"prefix\" value=\"/WEB-INF/pages/\"/> <!-- 文件的后缀名 --> <property name=\"suffix\" value=\".jsp\"/> </bean> <!-- 设置静态资源不过滤 --> <mvc:resources location=\"/css/\" mapping=\"/css/**\"/> <mvc:resources location=\"/images/\" mapping=\"/images/**\"/> <mvc:resources location=\"/js/\" mapping=\"/js/**\"/> <!-- 开启对SpringMVC注解的支持 --> <mvc:annotation-driven/></beans> 测试SpringMVC的框架搭建是否成功 编写index.jsp和list.jsp前端页面 1234<!-- index页面 --><a href=\"/account/findAll\" >查询所有</a><!-- list页面 --><h3>查询所有</h3> 编写控制器方法 123456789@Controller(\"accountController\")@RequestMapping(\"/account\")public class AccountController { @RequestMapping(\"/findAll\") public String findAll() { System.out.println(\"表面层:查询所有用户...\"); return \"list\"; }} 结果:查询所有 。 Spring整合SpringMVC框架 目的:在Controller层中能成功调用service对象中的方法。 如果想在服务器启动的时候,获取到Spring的容器,那么就需要在项目启动的时候就去加载applicationContext.xml的配置文件。在web.xml中配置ContextLoaderListener监听器(该监听器只能加载WEB-INF目录下的applicationContext.xml的配置文件)。 监听器的作用:监听器的作用是监听一些事件的发生从而进行一些操作,比如监听ServletContext,HttpSession的创建,销毁,从而执行一些初始化加载配置文件的操作,当Web容器启动后,Spring的监听器会启动监听,监听是否创建ServletContext的对象,如果发生了创建ServletContext对象这个事件(当web容器启动后一定会生成一个ServletContext对象,所以监听事件一定会发生),ContextLoaderListener类会实例化并且执行初始化方法,将Spring的配置文件中配置的bean注册到Spring容器中,监听的操作是读取WEB-INF/applicationContext.xml,但是我们可以在web.xml中配置多个需要读取的配置文件,如下方所示,读取完成后所有的配置文件中的bean都会注册到spring容器中。 1234567891011121314<context-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/config/application-context.xml /WEB-INF/config/cache-context.xml /WEB-INF/config/captcha-context.xml /WEB-INF/config/jeecms/jeecore-context.xml /WEB-INF/config/jeecms/jeecms-context.xml /WEB-INF/config/shiro-context.xml /WEB-INF/config/plug/**/*-context.xml /WEB-INF/config/quartz-task.xml /WEB-INF/config/zxw/zxw-context.xml </param-value></context-param> web.xml中对监听器的配置: 123456789<!-- 配置Spring的监听器 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 配置加载类路径的配置文件 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> 在Controller中注入service对象,调用service对象的方法进行测试 1234567891011121314@Controller(\"accountController\")@RequestMapping(\"/account\")public class AccountController { @Autowired private AccountService accountService; @RequestMapping(\"/findAll\") public String findAll() { List<Account> accounts = accountService.findAll(); System.out.println(\"表现层:查询所有用户...\"); return \"list\"; }} 输出结果: 业务层:查询所有用户表现层:查询所有用户… 4.4 Spring整合Mybatis框架4.4.1 搭建和测试MyBatis的环境 在web项目中编写SqlMapConfig.xml的配置文件,编写核心配置文件 12345678910111213141516171819202122<?xml version=\"1.0\" encoding=\"UTF-8\"?> <!DOCTYPE configuration PUBLIC \"-//mybatis.org//DTD Config 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-config.dtd\"><configuration> <environments default=\"mysql\"> <environment id=\"mysql\"> <transactionManager type=\"JDBC\"/> <dataSource type=\"POOLED\"> <property name=\"driver\" value=\"com.mysql.jdbc.Driver\"/> <property name=\"url\" value=\"jdbc:mysql:///ssm?useUnicode=true&amp;characterEncoding=utf8\"/> <property name=\"username\" value=\"root\"/> <property name=\"password\" value=\"root\"/> </dataSource> </environment> </environments> <!-- 使用的是注解 --> <mappers> <!-- <mapper class=\"cn.itcast.dao.AccountDao\"/> --> <!-- 该包下所有的dao接口都可以使用 --> <package name=\"cn.itcast.dao\"/> </mappers></configuration> 在AccountDao接口的方法上添加注解,编写SQL语句 12345678public interface AccountDao { @Insert(\"insert into account (name,money) values(#{name},#{money})\") public void saveAccount(Account account); @Select(\"select * from account\") public List<Account> findAll();} 编写测试方法 12345678910111213141516171819202122@Testpublic void saveAccount() throws IOException { Account account = new Account(); account.setName(\"小黑\"); account.setMoney(234d); // 加载配置文件 InputStream is = Resources.getResourceAsStream(\"SqlMapConfig.xml\"); // 创建工厂 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(is); // 获取sqlSession对象 SqlSession session = factory.openSession(); // 常见代理对象 AccountDao accountDao = session.getMapper(AccountDao.class); accountDao.saveAccount(account); // 提交事务 session.commit(); // 释放资源 session.close(); is.close();} 4.4.2 Spring整合MyBatis框架目的:把SqlMapConfig.xml配置文件中的内容配置到applicationContext.xml配置文件中。由Spring为我们进行对象的管理。从上面的测试文件中,可以看出我们需要将工厂对象,session对象代理对象交由Spring容器进行管理。即:把Mybatis配置文件(SqlMapConfig.xml)中内容配置到spring配置文件中。 注意: 当我们使用的是代理dao的模式,dao具体实现类由Mybatis使用代理方式创建,此时Mybatis配置文件不能删除。 整合Spring和Mybatis时,Mybatis创建的Mapper.xml文件名必须和dao接口文件名一致。 配置文件 123456789101112131415<!-- 配置C3P0的连接池对象 --> <bean id=\"dataSource\" class=\"org.springframework.jdbc.datasource.DriverManagerDataSource\"> <property name=\"driverClassName\" value=\"com.mysql.jdbc.Driver\"/> <property name=\"url\" value=\"jdbc:mysql:///ssm?useUnicode=true&amp;characterEncoding=utf8\"/> <property name=\"username\" value=\"root\"/> <property name=\"password\" value=\"root\"/> </bean> <!-- 配置SqlSession的工厂 --> <bean id=\"sqlSessionFactory\" class=\"org.mybatis.spring.SqlSessionFactoryBean\"> <property name=\"dataSource\" ref=\"dataSource\"/> </bean> <!-- 配置扫描dao的包 --> <bean id=\"mapperScanner\" class=\"org.mybatis.spring.mapper.MapperScannerConfigurer\"> <property name=\"basePackage\" value=\"cn.itcast.dao\"/> </bean> 可以删除SqlSessionMap配置文件。 给dao接口加上注解@Repository 在service中注入dao对象,进行测试。 配置Spring框架声明式事务管理 配置事务管理器 1234<!-- 配置Spring的声明式事务管理 --> <bean id=\"transactionManager\" class=\"org.springframework.jdbc.datasource.DataSourceTransactionManager\"> <property name=\"dataSource\" ref=\"dataSource\"></property> </bean> 配置事务通知 1234567<!-- 配置事务的通知 --> <tx:advice id=\"txAdvice\" transaction-manager=\"transactionManager\"> <tx:attributes> <tx:method name=\"*\" propagation=\"REQUIRED\" read-only=\"false\"/> <tx:method name=\"find*\" propagation=\"SUPPORTS\" read-only=\"true\"/> </tx:attributes> </tx:advice> 配置AOP增强 1234567<!-- 配置 aop --><aop:config> <!-- 配置切入点表达式 --> <aop:pointcut expression=\"execution(* cn.lizhi.service.impl.*.*(..))\" id=\"pt1\"/> <!-- 建立通知和切入点表达式的关系 --> <aop:advisor advice-ref=\"txAdvice\" pointcut-ref=\"pt1\"/></aop:config> 五、附常用配置文件5.1 web.xml配置文件12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667<!DOCTYPE web-app PUBLIC \"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN\" \"http://java.sun.com/dtd/web-app_2_3.dtd\" ><web-app> <display-name>Archetype Created Web Application</display-name> <!-- 配置前端控制器:服务器启动必须加载,需要加载springmvc.xml配置文件 --> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 配置初始化参数,创建完DispatcherServlet对象,加载springmvc.xml配置文件 --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <!-- 服务器启动的时候,让DispatcherServlet对象创建 --> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!-- 配置解决中文乱码的过滤器 --> <filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 配置Spring的监听器 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 配置加载类路径的配置文件 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <!-- 4、使用Rest风格的URI,将页面普通的post请求转为指定的delete或者put请求 --> <filter> <filter-name>HiddenHttpMethodFilter</filter-name> <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class> </filter> <filter-mapping> <filter-name>HiddenHttpMethodFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>HttpPutFormContentFilter</filter-name> <filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class> </filter> <filter-mapping> <filter-name>HttpPutFormContentFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping></web-app> 5.2 pom.xml配置文件123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234<?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>cn.lizhi</groupId> <artifactId>SpringMVC_03</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>SpringMVC_03 Maven Webapp</name> <!-- FIXME change it to the project's website --> <url>http://www.example.com</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.version>5.0.2.RELEASE</spring.version> <slf4j.version>1.6.6</slf4j.version> <log4j.version>1.2.12</log4j.version> <mysql.version>5.1.6</mysql.version> <mybatis.version>3.4.5</mybatis.version> </properties> <dependencies> <!-- spring --> <!-- 切入点表达式 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.8</version> </dependency> <!-- springAOP AOP核心功能,例如代理工厂等 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <!-- springIOC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- spring整合junit --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> </dependency> <!-- 事务控制 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <!-- SpringJDBC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- 单元测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>compile</scope> </dependency> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- log start --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${slf4j.version}</version> </dependency> <!-- log end --> <!-- Mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.0</version> </dependency> <!-- 数据库连接池 --> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> <type>jar</type> <scope>compile</scope> </dependency> <!--引入pageHelper分页插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.0.0</version> </dependency> <!-- MBG --> <!-- https://mvnrepository.com/artifact/org.mybatis.generator/mybatis-generator-core --> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.5</version> </dependency> <!--JSR303数据校验支持;tomcat7及以上的服务器, tomcat7以下的服务器:el表达式。额外给服务器的lib包中替换新的标准的el --> <!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.4.1.Final</version> </dependency> <!-- 对json处理的包,即能够使用@ResponseBody --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.9.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.9.0</version> </dependency> </dependencies> <build> <finalName>ssm_crud</finalName> <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> <plugins> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>3.1.0</version> </plugin> <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging --> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> </plugin> <plugin> <artifactId>maven-war-plugin</artifactId> <version>3.2.2</version> </plugin> <plugin> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> </plugin> <plugin> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> <showWarnings>true</showWarnings> </configuration> </plugin> </plugins> </pluginManagement> </build></project> 5.3 SpringMVC配置文件12345678910111213141516171819202122232425262728<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:context=\"http://www.springframework.org/schema/context\" xmlns:mvc=\"http://www.springframework.org/schema/mvc\" xsi:schemaLocation=\"http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd\"> <!--SpringMVC的配置文件,包含网站跳转逻辑的控制,配置 --> <context:component-scan base-package=\"cn.lizhi\" use-default-filters=\"false\"> <!--只扫描控制器。 --> <context:include-filter type=\"annotation\" expression=\"org.springframework.stereotype.Controller\"/> </context:component-scan> <!--配置视图解析器,方便页面返回 --> <bean class=\"org.springframework.web.servlet.view.InternalResourceViewResolver\"> <property name=\"prefix\" value=\"/WEB-INF/views/\"></property> <property name=\"suffix\" value=\".jsp\"></property> </bean> <!--两个标准配置 --> <!-- 将springmvc不能处理的请求交给tomcat --> <mvc:default-servlet-handler/> <!-- 能支持springmvc更高级的一些功能,JSR303校验,快捷的ajax...映射动态请求 --> <mvc:annotation-driven/></beans> 5.4 Spring配置文件 – applicationContext.xml12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:context=\"http://www.springframework.org/schema/context\" xmlns:aop=\"http://www.springframework.org/schema/aop\" xmlns:tx=\"http://www.springframework.org/schema/tx\" xsi:schemaLocation=\"http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd\"> <!-- 注解扫描,不扫描controller层,controller层交给SpringMVC进行管理 --> <context:component-scan base-package=\"cn.lizhi\"> <context:exclude-filter type=\"annotation\" expression=\"org.springframework.stereotype.Controller\" /> </context:component-scan> <!-- Spring的配置文件,这里主要配置和业务逻辑有关的 --> <!--=================== 数据源,事务控制,xxx ================--> <!-- 引入数据源配置文件 --> <context:property-placeholder location=\"classpath:dbconfig.properties\" /> <!-- 数据源配置 --> <bean id=\"pooledDataSource\" class=\"com.mchange.v2.c3p0.ComboPooledDataSource\"> <property name=\"jdbcUrl\" value=\"${jdbc.jdbcUrl}\"></property> <property name=\"driverClass\" value=\"${jdbc.driverClass}\"></property> <property name=\"user\" value=\"${jdbc.user}\"></property> <property name=\"password\" value=\"${jdbc.password}\"></property> </bean> <!--================== 配置和MyBatis的整合=============== --> <bean id=\"sqlSessionFactory\" class=\"org.mybatis.spring.SqlSessionFactoryBean\"> <!-- 指定mybatis全局配置文件的位置 --> <property name=\"configLocation\" value=\"classpath:mybatis-config.xml\"></property> <property name=\"dataSource\" ref=\"pooledDataSource\"></property> <!-- 指定mybatis,mapper文件的位置 --> <property name=\"mapperLocations\" value=\"classpath:mapper/*.xml\"></property> </bean> <!-- 配置扫描器,将mybatis接口的实现加入到ioc容器中 --> <bean class=\"org.mybatis.spring.mapper.MapperScannerConfigurer\"> <!--扫描所有dao接口的实现,加入到ioc容器中 --> <property name=\"basePackage\" value=\"cn.lizhi.dao\"></property> </bean> <!-- 配置一个可以执行批量的sqlSession --> <bean id=\"sqlSession\" class=\"org.mybatis.spring.SqlSessionTemplate\"> <constructor-arg name=\"sqlSessionFactory\" ref=\"sqlSessionFactory\"></constructor-arg> <constructor-arg name=\"executorType\" value=\"BATCH\"></constructor-arg> </bean> <!--============================================= --> <!-- ===============事务控制的配置 ================--> <bean id=\"transactionManager\" class=\"org.springframework.jdbc.datasource.DataSourceTransactionManager\"> <!--控制住数据源 --> <property name=\"dataSource\" ref=\"pooledDataSource\"></property> </bean> <!--开启基于注解的事务,使用xml配置形式的事务(必要主要的都是使用配置式) --> <aop:config> <!-- 切入点表达式 --> <aop:pointcut expression=\"execution(* cn.lizhi.service..*(..))\" id=\"txPoint\"/> <!-- 配置事务增强 --> <aop:advisor advice-ref=\"txAdvice\" pointcut-ref=\"txPoint\"/> </aop:config> <!--配置事务增强,事务如何切入 --> <tx:advice id=\"txAdvice\" transaction-manager=\"transactionManager\"> <tx:attributes> <!-- 所有方法都是事务方法 --> <tx:method name=\"*\"/> <!--以get开始的所有方法 --> <tx:method name=\"get*\" read-only=\"true\"/> </tx:attributes> </tx:advice> <!-- Spring配置文件的核心点(数据源、与mybatis的整合,事务控制) --></beans> 5.5 Mybatis全局配置文件 – mybatis-config.xml1234567891011121314151617181920<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE configuration PUBLIC \"-//mybatis.org//DTD Config 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-config.dtd\"><configuration> <settings> <setting name=\"mapUnderscoreToCamelCase\" value=\"true\"/> </settings> <typeAliases> <package name=\"cn.lizhi.bean\"/> </typeAliases> <plugins> <plugin interceptor=\"com.github.pagehelper.PageInterceptor\"> <!--分页参数合理化 --> <property name=\"reasonable\" value=\"true\"/> </plugin> </plugins></configuration> 5.6 Mybatis逆向工程的配置文件 – mbg.xml123456789101112131415161718192021222324252627282930313233343536373839404142434445<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE generatorConfiguration PUBLIC \"-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN\" \"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd\"><generatorConfiguration> <context id=\"DB2Tables\" targetRuntime=\"MyBatis3\"> <!-- 关闭注释 --> <commentGenerator> <property name=\"suppressAllComments\" value=\"true\" /> </commentGenerator> <!-- 配置数据库连接 --> <jdbcConnection driverClass=\"com.mysql.jdbc.Driver\" connectionURL=\"jdbc:mysql://url:3306/ssm?useUnicode=true&amp;characterEncoding=utf8\" userId=\"username\" password=\"password\"> </jdbcConnection> <javaTypeResolver > <property name=\"forceBigDecimals\" value=\"false\" /> </javaTypeResolver> <!-- 指定javaBean生成的位置 targetPackage为包的路径,targetProject为项目路径,即两个能够连接在一起 --> <javaModelGenerator targetPackage=\"cn.lizhi.bean\" targetProject=\".\\src\\main\\java\"> <property name=\"enableSubPackages\" value=\"true\" /> <property name=\"trimStrings\" value=\"true\" /> </javaModelGenerator> <!-- 指定sql映射文件生成的位置 --> <sqlMapGenerator targetPackage=\"mapper\" targetProject=\"./src/main/resources\"> <property name=\"enableSubPackages\" value=\"true\" /> </sqlMapGenerator> <!-- 指定dao接口生成的位置,mapper接口 --> <javaClientGenerator type=\"XMLMAPPER\" targetPackage=\"cn.lizhi.dao\" targetProject=\"./src/main/java\"> <property name=\"enableSubPackages\" value=\"true\" /> </javaClientGenerator> <!-- table指定每个表的生成策略 --> <table tableName=\"tbl_emp\" domainObjectName=\"Employee\"></table> <table tableName=\"tbl_dept\" domainObjectName=\"Department\"></table> </context></generatorConfiguration> 5.7 日志配置文件 – log4j.properties123456789101112131415161718# Set root category priority to INFO and its only appender to CONSOLE.#log4j.rootCategory=INFO, CONSOLE debug info warn error fatallog4j.rootCategory=info, CONSOLE, LOGFILE# Set the enterprise logger category to FATAL and its only appender to CONSOLE.log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE# CONSOLE is set to be a ConsoleAppender using a PatternLayout.log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppenderlog4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayoutlog4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\\n# LOGFILE is set to be a File appender using a PatternLayout.log4j.appender.LOGFILE=org.apache.log4j.FileAppenderlog4j.appender.LOGFILE.File=axis.loglog4j.appender.LOGFILE.Append=truelog4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayoutlog4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\\n 5.8 数据库连接池 – dbconfig.properties1234jdbc.driverClass=com.mysql.jdbc.Driverjdbc.jdbcUrl=jdbc:mysql://url:3306/ssm?useUnicode=true&characterEncoding=utf8jdbc.user=usernamejdbc.password=password","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"SpringMVC","slug":"SpringMVC","permalink":"https://chemlez.github.io/tags/SpringMVC/"}]},{"title":"Spring学习笔记(全)","slug":"Spring学习笔记(全)","date":"2020-09-22T15:06:48.000Z","updated":"2021-06-18T02:32:43.078Z","comments":true,"path":"2020/09/22/Spring学习笔记(全)/","link":"","permalink":"https://chemlez.github.io/2020/09/22/Spring%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0(%E5%85%A8)/","excerpt":"0、基本介绍Spring是对业务层的操作,同时可以整合Mybatis框架和Spring MVC框架。下图是MVC结构:","text":"0、基本介绍Spring是对业务层的操作,同时可以整合Mybatis框架和Spring MVC框架。下图是MVC结构: 耦合:简单理解为程序间的依赖关系 类之间的依赖 方法间的依赖 解耦:降低程序间的依赖关系 实际开发中编译期不依赖,运行时才依赖。 解耦的思路: 第一步:使用反射来创建对象,而避免使用new关键字。 第二步:通过读取配置文件来获取要创建的对象全限定类名。 一个创建Bean对象的工厂。 Bean:含有可重用组件的含义。 JavaBean:用Java语言编写的可重用组件。 JavaBean > 实体类 JavaBean就是创建service和dao对象的。 第一个:需要一个配置文件来配置我们的service和dao配置的内容:唯一标识=全限定类名(key=value) 第二个:通过读取配置文件中配置的内容,反射创建对象。 配置文件可以是xml,也可以是properties。 使用步骤: 创建Properties对象,读取配置文件。 通过类加载器读取流。 以上两步通过static静态代码块加载。 加载文件代码块: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091/** * 模拟工厂进行解耦,一个创建Bean对象的工厂 */public class BeanFactory { //定义一个Properties对象 private static Properties props; //定义一个Map,用于存放我们要创建的对象。我们把它称之为容器 private static Map<String,Object> beans; //使用静态代码块为Properties对象赋值 static { try { //实例化对象 props = new Properties(); //获取properties文件的流对象 InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream(\"bean.properties\"); props.load(in); //实例化容器 beans = new HashMap<String,Object>(); //取出配置文件中所有的Key Enumeration keys = props.keys(); //遍历枚举 -- 用来获取全限定类名的唯一标识符(key) while (keys.hasMoreElements()){ //取出每个Key String key = keys.nextElement().toString(); //根据key获取value -- 全限定类名 String beanPath = props.getProperty(key); //反射创建对象 Object value = Class.forName(beanPath).newInstance(); //把key和value存入容器中 beans.put(key,value); } }catch(Exception e){ throw new ExceptionInInitializerError(\"初始化properties失败!\"); } } /** * 根据bean的名称获取对象 * @param beanName * @return */ public static Object getBean(String beanName){ return beans.get(beanName); }// private static Properties pro; //// private static Map<String, Object> beans;// 定义一个map,用于存放我们要创建的对象,将之称之为容器。//// static {// try {// pro = new Properties();// InputStream is = BeanFactory.class.getClassLoader().getResourceAsStream(\"bean.properties\"); // 配置文件加载进内存// pro.load(is);// beans = new HashMap<String, Object>();// // 取出配置文件中的所有的key// Enumeration keys = pro.keys();// // 将全限定类名和反射创建的对象组成key-value,存放到集合中,这样我们对同一个类对象,就是只是从bean的集合中获取,始终获取的都是同一个对象。// while (keys.hasMoreElements()) {// String key = keys.nextElement().toString();// String beanPath = pro.getProperty(key);// Object value = Class.forName(beanPath).newInstance();// beans.put(key, value);// }// } catch (IOException e) {// e.printStackTrace();// } catch (IllegalAccessException e) {// e.printStackTrace();// } catch (InstantiationException e) {// e.printStackTrace();// } catch (ClassNotFoundException e) {// e.printStackTrace();// }//// }//// /**// * 根据bean的名称获取对象// *// * @param beanName// * @return// */// public static Object getBean(String beanName) {// Object bean = null; // 创建一个对象引用// bean = beans.get(beanName);// return bean;// }//} 单例对象:从始至终只有一个对象。 只被创建一次,从而类中的成员也就只会初始化一次。 多例对象:对象被创建多次,执行效率没有单例对象高。 目的:我们需要创建单例对象,只会初始化一次。 在创建工厂时,我们创建一个容器,将配置文件中的所有对象都提前创建好,存入到容器中,这样我们每次获取对象时,都是从这个容器中获取对象,保证我们始终获取的都是同一个对象。这里的容器就可以是Map集合。 在BeanFactory中,以容器装载对象。 – 当获取对象时,是从容器中获取对象 获取对象,使用工厂获取对象,避免了new关键字创建出的对象,并由于工厂中,对象存储在容器中,保证了这里的对象的单例的。 为了降低Java开发的复杂性,Spring采取了以下4种关键策略: 基于POJO的轻量级和最小侵入性编程 通过依赖注入和面向接口实现松耦合 基于切面和惯例进行声明式编程 通过切面和模板减少样板式代码 Spring与很多框架不同的一个关键点在于:很多框继承通过强迫应用继承它们的类或实现它们的接口从而导致应用与框架绑死。而Spring不会强迫你实现Spring规范的接口或接口或继承Spring规范的类。 一、IOC – 控制反转将new的自主控制权交给了工厂。工厂再通过全限定类名决定得到获取到的对象。此时类无法再确定所获得到对象是否是自己所需要的(降低了耦合)。 使用步骤一: 创建配置文件 12345678910<?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.xsd\"> <!-- 将对象的创建交给spring来管理 --> <bean id=\"accountService\" class=\"cn.lihzi.service.impl.AccountServiceImpl\"></bean> <bean id=\"accountDao\" class=\"cn.lihzi.dao.impl.AccountDaoImpl\"></bean></beans> 获取容器 – spring的核心容器,并根据id获取对象 1234567891011121314151617181920public class Client { /** * 获取spring的IOC核心容器,并根据id获取对象 * * @param args * @throws IllegalAccessException * @throws InstantiationException * @throws ClassNotFoundException */ public static void main(String[] args) { // 1. 获取核心容器对象 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext(\"bean.xml\"); // 2. 根据id获取Bean对象 AccountService service = (AccountService) ac.getBean(\"accountService\"); AccountDao dao = ac.getBean(\"accountDao\", AccountDao.class); System.out.println(service); System.out.println(dao);// service.save(); }} 1.1 ApplicationContext的三个常用实现类: ClassPathXmlApplicationContext 它可以加载类路径下的配置文件,要求配置文件必须在类路径下。不在的话,加载不了。 FileSystemXmlApplicationContext 它可以加载磁盘任意路径下的配置文件(必须有访问权限) AnnotationConfigApplicationContext 它用于读取注解创建的容器 1.2 核心容器的两个接口引发出的问题 ApplicationContext 它在构建核心容器时,创建对象采取的策略是采用立即加载的方法。也就是说,只要一读取完配置文件马上就创建配置文件中配置的对象。 试用情况:单例对象适用,更多采用此接口 BeanFactory 它在构建核心容器时,创建对象采取的策略是采用延迟加载的方式。也就是说,什么时候根据id获取对象,什么时候才是真正的创建对象。 试用情况:多例对象适用。 1.3 Bean对象的细节 创建Bean的三种方式: 第一种方式 使用默认构造函数创建。在spring的配置文件中使用bean标签,配以id和class属性之后,且没有其他属性和标签时。采用的就是默认构造函数创建bean对象,此时如果类中没有默认构造函数,则对象无法创建。 12<bean id=\"accountService\" class=\"cn.lihzi.service.impl.AccountServiceImpl\"></bean><bean id=\"accountDao\" class=\"cn.lihzi.dao.impl.AccountDaoImpl\"></bean> 第二种方式 使用工厂中的普通方法创建对象(使用某个类中的方法创建对象,并存入spring容器) 12<bean id=\"instanceFactory\" class=\"cn.lihzi.factory.InstanceFactory\"></bean><bean id=\"accountService\" factory-bean=\"instanceFactory\" factory-method=\"getAccountService\"/> 通过factory-bean找到id得到其对象,再通过factory-method得到其方法对象。而其中的id="accountService"是对应到容器中的key。 第三种方式 使用工厂中的静态方法创建对象(使用某个类中的静态方法创建对象,并存入spring容器)。 1<bean id=\"accountService\" class=\"cn.lihzi.factory.StaticInstanceFactory\" factory-method=\"getAccountService\"/> 其中,第二种、第三种方式可以用来获取到jar包中的方法对象。 Bean的作用范围 默认的作用范围是单例。 bean标签的scope属性: 作用:用于指定bean的作用范围 取值:常用的就是单例的和多例的 singleto:单例的(默认值) prototype:多例的 request:作用于web应用的请求范围 session:作用于web应用的会话范围 global-session:作用于集群环境的会话范围(全局会话范围),当不会集群环境时,它就是session bean对象的生命周期 单例对象 出生:当容器创建时对象出生 活着:只要容器还在,对象一直活着 死亡:容器销毁,对象消亡 总结:单例对象的生命周期和容器相同 多例对象 出生:当我们使用对象时spring框架为我们创建 活着:对象只要是在使用过程中就一直活着 死亡:当对象长时间不用,且没有别的对象引用时,由Java的垃圾回收器回收 bean配置文件中分别是,init-method;destory-method指定初始化和销毁时使用的对象方法。 注:创建应用组件之间协作的行为通常称为装配(wiring)。Spring有多种装配bean的方式,采用XML是很常见的一种装配方式。 Spring通过应用上下文(Application Context)装载Bean的定义并把它们组装起来。Spring应用上下文全权负责对象的创建和组装。Spring自带了多种应用上下文的实现,它们之间主要的区别仅仅在于如何加载配置。 1.4 Spring的依赖注入依赖注入: Dependency Injection。依赖注入会将所依赖的关系自动交给目标对象,而不是让对象自己去获取依赖。通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件(就是容器)在创建对象的时候进行设定,对象无需自行创建或管理它们的依赖关系,依赖关系将被自动注入到需要它们的对象当中去。 在进行测试时,可以使用Mock测试。所谓的Mock测试就是指在测试过程中,模拟出那些不容易获取或者不容易构造出来的对象,比如HttpServletRequest对象需要在Servlet容器中构造出来。 使用mock框架Mockito去创建一个Quest接口的mock实现,通过该mock就可以创建新的所属实例,并通过构造器注入这个mock创建出的实例对象。(即通过构造器注入,来达到松耦合) IOC作用:降低程序间的耦合(依赖关系) 依赖关系的管理 交给spring来维护 在当前类需要用到其他类的对象,由spring为我们提供,我们只需要在配置文件中说明 依赖关系的维护就称之为依赖注入。 依赖注入: 能注入的数据:三类 基本数据类型和String 其他bean类型(在配置文件中或者注解配置过的bean) 复杂类型/集合类型 注入的方式:三种 使用构造函数提供 使用set方法提供 使用注解提供 1.4.1 构造函数注入(构造器注入)123456789101112131415<!-- 构造函数注入: 使用的标签:constructor-arg 标签出现的位置:bean标签的内部 标签中的属性 type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型(当构造函数中同时含有多个相同的数据类型时,就无法分别) index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值。索引的位置是从0开始 name:用于指定给构造函数中指定名称的参数赋值。(最常用,也是最直接的方式) =============以上三个用于指定给构造函数中哪个参数赋值================ value:用于提供基本类型和String类型的数据 ref:用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象。 特点: 在获取bean对象时,注入数据是必须的操作,否则对象无法创建成功。 缺点: 改变了bean对象的实例化方法,使我们在创建对象时,如果用不到这些数据,也必须提供。--> 类的代码: 12345678910111213141516public class AccountServiceImpl implements AccountService { private String name; private Integer age; private Date birthday; public AccountServiceImpl(String name, Integer age, Date birthday) { this.name = name; this.age = age; this.birthday = birthday; } public void save() { System.out.println(\"service方法执行..\"+name+\":\"+age+\":\"+birthday); }} 配置文件: 1234567<bean id=\"accountService\" class=\"cn.lihzi.service.impl.AccountServiceImpl\"> <constructor-arg name=\"name\" value=\"Tom\"></constructor-arg> <constructor-arg name=\"age\" value=\"18\"></constructor-arg> <constructor-arg name=\"birthday\" ref=\"now\"></constructor-arg></bean><!-- 以下是配置一个日期对象 反射方式创建Date的对象,再由id指定赋值--><bean id=\"now\" class=\"java.util.Date\"></bean> 1.4.2 set方法注入 – 适用于存在空参的构造方法(更常用的set方法注入)涉及的标签:property 出现的位置:bean标签的内部 标签的属性 name:用于指定注入时所调用的set方法名称 value:用于提供基本类型和String类型的数据 ref:用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现的bean对象。 优势: 创建对象时没有明确的限制,可以直接使用默认构造函数 弊端: 如果有某个成员必须有值,则获取对象是有可能set方法没有执行。 配置文件: 123456 <!-- set --> <bean id=\"accountService1\" class=\"cn.lihzi.service.impl.AccountServiceImpl1\"> <property name=\"age\" value=\"18\"></property> <property name=\"name\" value=\"Tom\"></property><!-- <property name=\"birthday\" ref=\"now\"></property>--> </bean> 1.4.3 复杂类型的注入/集合类型的注入用于给List结构集合注入的标签:list、array、set 用于给Map结构集合注入的标签:map、props 结构相同,标签可以互换。 实体类: 123456789101112131415161718192021public class AccountServiceImpl2 implements AccountService { private Map<String, String> map; private List<String> list; private Set<String> set; private Properties properties; private String[] str; /* 省略了getter和setter方法 */ public void save() { System.out.println(\"str:\"+Arrays.toString(str)); System.out.println(\"map:\"+map); System.out.println(\"list:\"+list); System.out.println(\"set:\"+set); System.out.println(\"properties:\"+properties); }} Bean配置文件: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647 <!-- 复杂对象的封装使用 --> <bean id=\"accountService2\" class=\"cn.lihzi.service.impl.AccountServiceImpl2\"> <property name=\"list\"> <list> <value>a</value> <value>b</value> <value>c</value> </list> </property> <property name=\"str\"> <list> <value>a</value> <value>b</value> <value>c</value> </list> </property> <property name=\"set\"> <list> <value>a</value> <value>b</value> <value>c</value> </list> </property> <property name=\"map\"> <map> <entry key=\"name\"> <value>Tom</value> </entry> <entry key=\"age\" value=\"18\"></entry> </map> </property> <property name=\"properties\"><!-- <map>--><!-- <entry key=\"name\">--><!-- <value>Tom</value>--><!-- </entry>--><!-- <entry key=\"age\" value=\"18\"></entry>--><!-- --><!-- </map>--> <props> <prop key=\"name\">Tom</prop> <prop key=\"age\">18</prop> </props> </property> </bean> 二、基于注解的方式 – IOCxml起始的配置 123<bean id=\"accountService\" class=\"cn.lihzi.service.impl.AccountServiceImpl\" scope=\"\" init-method=\"\" destory-method=\"\"> <property name=\"\" value=\"\" ref=\"\"></property></bean> 注解类别: 用于创建对象的(创建的对象存放至Spring容器中) 其作用就和在XML配置文件中编写一个<bean>标签实现的功能是一样的 @Component 作用:用于把当前类对象写入spring容器中。 属性: - `value`:用于指定`bean`的`id`。当我们不写时,它的默认值是当前类名,且首字母小写。 @Controller:一般用于表现层 @Service:一般用在业务层 @Respository:一般用于持久层 以上三个注解他们的作用和属性与Component是一模一样的。 他们三个是Spring框架为我们提供明确的三层使用的注解,使我们的三层对象更加清晰。 用于注入数据的(注入的对象是从Spring容器中获取) 其作用就和在xml配置文件中的bean标签中写一个<property>标签的作用是一样的 @Autowired 作用:自动按照类型注入。只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。 出现位置:可以是变量上,也可以是方法上。 细节:在使用注解注入时,set方法就不是必须的了。 如果IOC容器中有多个类型匹配时:先找到同类别的,再根据变量名称进行注入。 通过在Spring容器中符合数据类型的value,并将value值赋给该引用,当出现同类型的引用时,再根据变量名称进行赋值。 @Qualifier 作用:在按照类中注入的基础之上再按照名称注入。它在给类成员注入时不能单独使用。但是在给方法参数注入时可以。(要和@Autowired配合使用) 属性 value:用于指定注入bean的id。 @Resource 作用:直接按照bean的id注入。它可以独立使用。 属性 name:用于指定bean的id。 以上三个注入都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现。 另外集合只能通过xml来实现。 @Value 作用:用于注入基本类型和String类型的数据。 属性: value:用于指定数据的值。它可以使用spring中SpEL(也就是spring的el表达式) SpEL的写法:${表达式}。注意:其表达式写在哪里(JSP、Mybatis、Spring…),就是从哪里获值。 用于改变作用范围的 其作用就和在bean标签中使用scope属性实现的功能是一样的 @Scope 作用:用于指定bean的作用范围 属性: value:指定范围的取值。常用取值:singleton、prototype(默认为singleton) 生命周期相关 其作用就和在bean标签中使用init-method和destory-method的作用是一样的。 @PreDestroy 作用:用于指定销毁方法 @PostConstruct 作用:用于指定初始化方法 注意:需要告知spring在创建容器时要扫描的包、配置所需要的标签不是在bean的约束中,而是一个名称为context名称空间和约束中。其配置文件形式为: 12345678910111213<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:context=\"http://www.springframework.org/schema/context\" xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd\"> <context:annotation-config/> <context:component-scan base-package=\"cn.lihzi\"></context:component-scan> <!-- 扫描这个包下的所有包及其子包 --></beans> 2.1 实例:简单的数据库增删改查 XML方式 service层中,提供setter方法,共xml配置使用。 配置: 业务层对象 持久层对象 JDBC对象 数据库连接池 注意数据源的单例、多例造成的线程混乱问题 1234567891011121314151617181920212223242526272829303132<?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.xsd\"> <!-- 创建service对象,并将service对象中的变量注入数据dao --> <bean id=\"accountService\" class=\"cn.lizhi.service.impl.AccountServiceImpl\"> <property name=\"dao\" ref=\"accountDao\"></property> </bean> <!-- 创建dao对象,并给变量注入数据,runner --> <bean id=\"accountDao\" class=\"cn.lizhi.dao.impl.AccountDaoImpl\"> <!-- 注入runner --> <property name=\"runner\" ref=\"queryRunner\"></property> </bean> <!-- 创建QueryRunner对象 并采用构造方法的方式,注入数据源(这里采用的是有参构造方法,其参数就是dataSource数据源,故进而再创建dataSource对象) --> <!-- QueryRunner对象,是单例对象。为了让多个dao在调用这个对象时,互不干扰,故采取多例的方法 --> <bean id=\"queryRunner\" class=\"org.apache.commons.dbutils.QueryRunner\" scope=\"prototype\"> <!-- 注入数据源,方便sql语句的复用,不用每次都传入数据 --> <constructor-arg name=\"ds\" ref=\"dataSource\"></constructor-arg> </bean> <!-- 配置数据源dataSource,给dataSource对象的变量注入数据--> <bean id=\"dataSource\" class=\"com.mchange.v2.c3p0.ComboPooledDataSource\"> <property name=\"driverClass\" value=\"com.mysql.jdbc.Driver\"></property> <property name=\"jdbcUrl\" value=\"jdbc:mysql://url:3306/draft\"></property> <property name=\"user\" value=\"username\"></property> <property name=\"password\" value=\"password\"></property> </bean></beans> 测试方法: 1234567891011121314151617181920212223242526272829public class AccountServiceTest { // 1. 获取对象 private ApplicationContext ac = new ClassPathXmlApplicationContext(\"bean.xml\"); // 2. 得到业务层对象 private AccountService service = ac.getBean(\"accountService\", AccountService.class); @Test public void findAll() { List<Account> accounts = service.findAll(); for (Account account : accounts) { System.out.println(account); } } @Test public void findById() { Account account = service.findById(1); System.out.println(account); } @Test public void insert() { Account account = new Account(); account.setName(\"Tom\"); account.setMoney(800.8f); service.insert(account); }} 基于注解的IOC配置 注意一点:当用注解方式对变量进行注入时,setter方法就不是必要的了。 xml配置文件: 123456789101112131415161718192021<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:context=\"http://www.springframework.org/schema/context\" xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd\"> <context:component-scan base-package=\"cn.lizhi\"></context:component-scan> <!-- 扫描这个包下的所有包及其子包 --> <!-- service和dao的对象通过注解的方法创建以及注入--> <bean id=\"queryRunner\" class=\"org.apache.commons.dbutils.QueryRunner\" scope=\"prototype\"> <constructor-arg name=\"ds\" ref=\"dataSource\"></constructor-arg> </bean> <bean id=\"dataSource\" class=\"com.mchange.v2.c3p0.ComboPooledDataSource\"> <property name=\"driverClass\" value=\"com.mysql.jdbc.Driver\"></property> <property name=\"jdbcUrl\" value=\"jdbc:mysql://url/draft\"></property> <property name=\"user\" value=\"username\"></property> <property name=\"password\" value=\"password\"></property> </bean></beans> 2.2 使用注解不带有xml配置文件的使用(纯注解) 创建配置类 — config 例如:SpringConfiguration:其作用同bean.xml相同 spring中的新注解: @Configuration 作用:指定当前类是一个配置类 细节:当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写。(原因是参数传递的是配置的Class对象,就能够读取到这个类) @ComponentScan 作用:用于通过注解指定spring在创建容器时要扫描的包 属性: value:它和basePackages的作用是一样的,都是用于指定创建容器时要扫描的包。我们使用此注解就等同于在xml中配置了: <context:component-scan base-package="cn.lizhi"></context:component-scan> @Bean 作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中 属性: name:用于指定bean的id。当不写时,默认值是当前方法的名称 细节: 当我们使用注解配置方法时,如果方法需要传递参数,Spring框架会去容器中查找有没有可用的对应类型的bean对象。查找的方式和Autowired注解的作用是一样的。 @Import 作用:用于导入其他的配置类 属性 value:用于指定其他配置类的字节码。当我们使用import的注解之后,有Import注解的类就是父配置类,而导入的都是子配置类。 在主配置类下配置@import,@import中的参数为value数组,内容填写子配置类。这样也可以不用在子配置类下配置@Configuration注解。 @PropertySource 作用:用于指定properties文件的位置。 属性: value:指定文件的名称和路径。 关键字:classpath,表示类路径下。 例如:@PropertySource(classpath:jdbcConfig.properties) 注解位置在主配置文件下。 xml和注解配置选择问题:自己写的类,选择采用注解的方式;存在于jar包中的选择用xml方式,两者可以配合着使用。 注解类: 123456789101112131415161718192021222324@Configuration@ComponentScan(\"cn.lizhi\")public class SpringConfiguration { @Bean(\"runner\") // 用于将返回值存入容器中 -- 其中id设置为runner @Scope(\"prototype\") // 将数据源设置成多例 public QueryRunner getRunner(DataSource dataSource) { // 获取QueryRunner对象 -- 参数为dataSource,故进一步再得到dataSource对象,见下方 return new QueryRunner(dataSource); } @Bean(\"dataSource\") -- 容器中id为dataSource public DataSource getDataSource() { ComboPooledDataSource cpds = new ComboPooledDataSource(); try { cpds.setDriverClass(\"com.mysql.jdbc.Driver\"); cpds.setJdbcUrl(\"jdbc:mysql://url:3306/draft\"); cpds.setUser(\"username\"); cpds.setPassword(\"password\"); } catch (PropertyVetoException e) { e.printStackTrace(); } return cpds; }} 测试用例: 123456789101112131415161718192021222324252627282930313233343536373839404142public class AccountServiceTest { // 1. 获取对象// private ApplicationContext ac = new ClassPathXmlApplicationContext(\"bean.xml\"); private ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class); // 2. 得到业务层对象 private AccountService service = ac.getBean(\"accountService\", AccountService.class); @Test public void findAll() { List<Account> accounts = service.findAll(); for (Account account : accounts) { System.out.println(account); } } @Test public void findById() { Account account = service.findById(1); System.out.println(account); } @Test public void insert() { Account account = new Account(); account.setName(\"Tom\"); account.setMoney(800.8f); service.insert(account); } @Test public void update() { Account account = service.findById(4); account.setMoney(1000.0f); service.update(account,account.getId()); } @Test public void delete() { service.delete(4); }} 注意:此时接口的实现方法使用的是AnnotationConfigApplicationContext。 抽取子配置类 JdbcConfig - 子配置类 12345678910111213141516171819202122232425262728293031public class JdbcConfig { @Value(\"${jdbc.driver}\") private String driver; @Value(\"${jdbc.url}\") private String url; @Value(\"${jdbc.user}\") private String user; @Value(\"${jdbc.password}\") private String password; @Bean(\"runner\") @Scope(\"prototype\") // 将数据源设置成多例 public QueryRunner getRunner(DataSource dataSource) { return new QueryRunner(dataSource); } @Bean(\"dataSource\") public DataSource getDataSource() { ComboPooledDataSource cpds = new ComboPooledDataSource(); try { cpds.setDriverClass(driver); cpds.setJdbcUrl(url); cpds.setUser(user); cpds.setPassword(password); } catch (PropertyVetoException e) { e.printStackTrace(); } return cpds; }} SpringConfiguration - 父配置类 12345@Import(JdbcConfig.class)@ComponentScan(\"cn.lizhi\")@PropertySource(\"classpath:jdbcConfig.properties\")public class SpringConfiguration {} jdbcConfig.properties - 配置文件 1234jdbc.driver=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://url:3306/draftjdbc.user=usernamejdbc.password=password 2.3 spring整合junit问题整合原因: 应用程序的入口 – main 方法 junit单元测试中,没有main方法也能执行 junit集成了一个main方法 该方法就会判断当前测试类中哪些方法有@Test注解 junit就让有Test注解的方法执行 junit中无法探测出是否存在spring框架 在执行测试方法时,junit无法得知我们是否使用了spring框架 因此也就不会为我们读取配置文件/配置类创建spring核心容器 综上:在执行测试方式时,没有IOC容器,就算谢了Autowired注解,也无法实现注入 Spring整合junit的配置 导入spring整合junit的jar(坐标) 使用junit提供的一个注解把原有的main方法替换了,替换成spring提供的 @RunWith注解配置–SpringJUnit4ClassRunner.class 告知spring的运行器,spring和ioc创建是基于xml还是基于注解的,并且说明位置。 @ContextConfiguration locations:指定xml文件的位置,加上classpath关键字,表示在类路径下。 classes:指定注解所在地位置。å例如:@ContextConfiguration(classes=SpringConfiguration.class) 当我们使用spring 5.x版本的时候,要求junit的jar包必须是4.12以上。 三、AOP – 导读AOP允许将遍布应用各处的功能分离出来形成可重用的组件。即通过AOP,可以使用各种功能层去包裹核心业务层。这些层以声明的方式灵活地应用到系统中,核心应用甚至根本不知道它们的存在,即将安全、事务和日志关注点与核心业务逻辑相分离。 3.1 案例-transfer(银行转账案例)在service接口中定义转账方法(其参数列表为转出用户,转入用户,转账金额),dao接口中定义根据用户名称查找用户的方法。 当在数据库数据进行更新时(转账过程中),如果出现异常,可能就会出现破坏数据库的一致性操作。故下面要进行对事物的控制。 事务控制在service层。 以上的问题引发了一个思考,就是获取的connection应该全部由同一个connection进行控制,要成功就一起成功,如果失败就一起失败。 解决办法:需要使用ThreadLocal对象把Connection与当前线程绑定,从而使一个线程中只有一个能控制事务的对象。 事务控制应该都是在业务层。 注意:在web工程中,当tomcat服务器启动时,会初始化线程池,当我们对tomcat服务器进行访问时,便会从线程池中获取线程,同时当使用数据库连接池时,在获取连接以后,当我们对线程访问完毕以后,需要对线程进行归还,此时归还到线程池中的线程还在绑定着数据库的连接,所以在归还连接(线程)前(无论是线程或数据库连接),都需要将线程与数据库连接进行解绑,否则当我们再获取这个线程时,因为它绑定着数据库连接池中的那个连接,再使用时是无法使用的,因为这个数据连接已经被close(归还)了,需要我们重新获取连接并进行绑定。 通过创建service的代理对象的工厂解决事务上方法的耦合问题。即对service类中的方法进行增强(增强的内容就是加入事务的控制)。 事务解决的整个思路: 创建ConnectionUtils工具类,通过ThreadLocal绑定数据库连接。 12345678910111213141516171819202122232425262728293031323334353637/** * 线程绑定 */public class ConnectionUtils { private ThreadLocal<Connection> tl = new ThreadLocal<Connection>(); // 绑定的对象 -- Connection private DataSource dataSource; // 获取数据源 public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public Connection getThreadConnection() { Connection conn = tl.get(); if (conn == null) { // 如果TreadLocal未绑定有连接,则从连接池中获取连接,并对其进行绑定 try { conn = dataSource.getConnection(); tl.set(conn); } catch (SQLException e) { e.printStackTrace(); } } return conn; } /** * 对其进行解绑 */ public void remove() { Connection conn = tl.get(); if (conn != null) { tl.remove(); } }} 创建事务管理类,用于在业务层对SQL进行事务管理 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657/** * 对事物进行管理的工具类 */public class TransactionManager { private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } /** * 开启事务 */ public void beginTransaction() { try { connectionUtils.getThreadConnection().setAutoCommit(false); // 关闭自动提交 -- 即使用此方法为开启事务(关闭了自动提交事务) } catch (SQLException e) { e.printStackTrace(); } } /** * 提交事务 */ public void commit() { try { connectionUtils.getThreadConnection().commit(); } catch (SQLException e) { e.printStackTrace(); } } /** * 回滚事务 */ public void rollback() { try { connectionUtils.getThreadConnection().rollback(); } catch (SQLException e) { e.printStackTrace(); } } /** * 释放连接 */ public void release() { try { connectionUtils.getThreadConnection().close(); connectionUtils.remove(); // 释放连接时,将线程与数据库连接池中的连接进行解绑 } catch (SQLException e) { e.printStackTrace(); } }} dao层代码 1234567891011121314151617181920212223242526272829303132333435363738394041public class AccountDaoImpl implements AccountDao { // 通过spring配置获取QueryRunner对象 private QueryRunner runner; // 通过spring配置获取连接 private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } public void setRunner(QueryRunner runner) { this.runner = runner; } public void update(Account account, Integer id) { try { runner.update(connectionUtils.getThreadConnection(), \"update account set name=?,money=? where id=?\", account.getName(), account.getMoney(), id); } catch (SQLException e) { e.printStackTrace(); } } public Account findByName(String name) { Account account = null; try { List<Account> accounts = runner.query( \"select * from account where name=?\", new BeanListHandler<Account>(Account.class), name); if (accounts == null || accounts.size() == 0) { throw new RuntimeException(\"该用户不存在\"); } else if (accounts.size() > 1) { throw new RuntimeException(\"用户存在异常,存在两个异常\"); } else { account = accounts.get(0); } } catch (SQLException e) { e.printStackTrace(); } return account; } connectionUtils.getThreadConnection()用于获取连接。 创建工厂类-用于创建service的代理对象的工厂 12345678910111213141516171819202122232425262728293031323334353637383940414243444546public class BeanFactory { private AccountService accountService; private TransactionManager tsManager; public final void setTsManager(TransactionManager tsManager) { this.tsManager = tsManager; } public void setAccountService(AccountService accountService) { this.accountService = accountService; } /** * 获取代理对象,对方法进行增强 * @return */ public AccountService getAccountService() { AccountService proxy_accountService = (AccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() { /** * 增强对事务的控制 **/ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object obj = null; try { // 1. 开启事务 tsManager.beginTransaction(); // 2. 执行语句 obj = method.invoke(accountService, args); // 3. 提交事务 tsManager.commit(); return obj; } catch (Exception e) { // 4. 回滚事务 tsManager.rollback(); throw new RuntimeException(e); } finally { // 5. 关闭连接 tsManager.release(); } } }); return proxy_accountService; }} 这里被代理的对象为accountService,当我们使用它的方法时,我们其实是在使用其代理对象(通过BeanFactory中getAccountService方法创建出的对象)中增强后的方法。其中,创建工厂中普通方法的配置方法: 123456789<!-- 创建工厂类对象 --> <bean id=\"beanFactory\" class=\"cn.lizhi.factory.BeanFactory\"> <!-- 被代理对象 --> <property name=\"accountService\" ref=\"accountService\"></property> <!-- 注入事务管理器 --> <property name=\"tsManager\" ref=\"transaction\"></property> </bean> <!-- 创建工厂类方法对象 --> <bean id=\"proxyAccountService\" factory-bean=\"beanFactory\" factory-method=\"getAccountService\"></bean> 例如被代理对象(AccountService)中的一处方法为: 123456789101112public void transfer(String startName, String endName, Float money) { Account startAccount = dao.findByName(startName); // 出款人账号 -- 会获取连接 Account endAccount = dao.findByName(endName); // 收款人账号 -- 会获取连接 Float startMoney = startAccount.getMoney(); startAccount.setMoney(startMoney - money); Float endMoney = endAccount.getMoney(); endAccount.setMoney(endMoney + money); dao.update(startAccount, startAccount.getId()); // 会获取连接 int a = 3 / 0; dao.update(endAccount, endAccount.getId()); // 会获取连接} 即通过代理对象增强后,对其方法进行了事务管理,即对以下方法的执行,在其上下添加事务的管理。 1obj = method.invoke(accountService, args); 增强的方法等价于: 12345678910111213141516171819202122232425public void transfer(String startName, String endName, Float money) { try { // 1. 开启事务 tsManager.beginTransaction(); // 2. 执行操作 Account startAccount = dao.findByName(startName); // 出款人账号 -- 会获取连接 Account endAccount = dao.findByName(endName); // 收款人账号 -- 会获取连接 Float startMoney = startAccount.getMoney(); startAccount.setMoney(startMoney - money); Float endMoney = endAccount.getMoney(); endAccount.setMoney(endMoney + money); dao.update(startAccount, startAccount.getId()); // 会获取连接 int a = 3 / 0; dao.update(endAccount, endAccount.getId()); // 会获取连接 // 3. 提交事务 tsManager.commit(); } catch (Exception e) { // 4. 回滚事务 tsManager.rollback(); throw new RuntimeException(e); } finally { // 5. 关闭连接 tsManager.release(); }} 由以上的比较,故能够很清晰的看到,通过动态代理的方法,可以简化代码量以及降低方法间的耦合问题。 如果,采用上面一般的方式,即在业务层中所有的方法都要加入事务管理的相关代码,这样就增加了代码的冗余;其次,如果我们对TransactionManager类中关于事务管理的方法名进行修改,那么在业务层中相应调用事务的方法名也都要修改。 如果,采用动态代理的方式: 我们关于事务管理的代码只需要写一次(工厂类中的代理对象)。 TransactionManage类中事务管理相关的方法名修改后,只需要在代理对象中对增强的方法进行修改即可。 最后依赖注入的对象是代理对象 123@Autowired@Qualifier(\"proxyAccountService\")private AccountService service = null; 四、Spring AOP4.1 AOP基本介绍定义: 在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 作用: 在程序运行期间,不修改源码对已有方法进行增强。 优势: 减少重复代码 提高开发效率 维护方便 实现方式: 使用动态代理技术 4.2 Spring中的AOP介绍4.2.1 AOP相关术语介绍及使用介绍 Joinpoint(连接点) 所谓连接点是指那些被拦截的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点。即:在业务逻辑中的全部方法。 Pointcut(切入点) 所谓切入点是指我们要对哪些Joinpoint进行拦截的定义。即:在业务逻辑中,那些被增强的方法。所以,有些方法是连接点,但不是切入点,因为没有被增强。 所以得出:所有的切入点都是连接点,但并不一定所有的连接点都是切入点。(只有被增强的连接点,才是切入点) Advice(通知/增强) 所谓通知,是指拦截到Joinpoint之后所要做的事情就是通知。(即,想要增加的功能,事先定义好,然后在想要用的地方添加通知即可) 通知的类型: 前置通知:在method.invoke()之前执行的方法 后置通知:在method.invoke()之后执行的方法 异常通知:catch中的代码 最终通知:finally中的代码 环绕通知:整个invoke(public Object invoke..)方法在执行就是环绕通知,即在环绕通知中有明确的切入点方法调用。 – 最强大的一个通知 Introduction(引介) 引介是一种特殊的通知在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法或Field。 Target(目标对象) 代理的目标对象。(被代理对象) Weaving(织入) 是指把增强应用到目标对象来创建新的代理对象的过程。 Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。 Proxy(代理) 一个类被AOP织入增强后,就产生一个结果代理类。 Aspect(切面) 是切入点和通知(引介)的结合。 Spring框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类型,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。 将公共代码作为通知(即把通知Bean交给Spring来管理),由AOP配置织入到切入点。 Logger类作为通知: 1234567891011/** * 模拟记录日志的工具类,里面提供了公共的代码 */public class Logger { /** * 用于打印日志,计划让其在切入点方法执行之前执行(这里切入点方法就是业务层方法) */ public void printLog() { System.out.println(\"记录了日志...\"); }} 以Logger类为例,aop相关配置步骤: 配置Spring的IOC,把service对象配置进来。 spring中基于XML的aop配置步骤: 把通知Bean交给spring来管理 使用aop:config标签表明开始AOP的配置 使用aop:aspect标签表明配置切面 id属性:是给切面提供一个唯一的标识 ref属性:是指定通知类bean的Id 在aop:aspect标签的内部使用对应标签来配置通知的类型 在Logger类中的pringLog方法是在切入点方法执行之前,所以是前置通知 aop:before 表示配置前置通知 method属性:用于指定Logger类中哪个方法是前置通知。 pointcut属性:用于指定切入点表达式,该表达式的含义是对业务层中哪些方法增强 切入点表达式的写法: 关键字:execution(表达式) 表达式: 访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表) 根据以上说明,见下方配置: 123456789101112131415161718192021<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:aop=\"http://www.springframework.org/schema/aop\" xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd\"> <!-- 配置Spring的IOC,把Service对象配置进来 --> <bean id=\"accountService\" class=\"cn.lizhi.service.impl.AccountServiceImpl\"></bean> <!-- 配置Logger类(通知) --> <bean id=\"logger\" class=\"cn.lizhi.utlis.Logger\"></bean> <!-- 配置AOP --> <aop:config> <!-- 配置切面 --> <aop:aspect id=\"logAdvice\" ref=\"logger\"> <!-- 配置通知的类型,并且建立通知方法和切入点方法的关联--> <aop:before method=\"printLog\" pointcut=\"execution(public void cn.lizhi.service.impl.AccountServiceImpl.saveAccount())\"></aop:before> </aop:aspect> </aop:config></beans> 切入点表达式的写法 全通配写法 * *..*.*(..) 原因: 访问修饰符可以省略 void cn.lizhi.service.impl.AccountServiceImpl.saveAccount() 返回值可以改成任意类型的返回值 * cn.lizhi.service.impl.AccountServiceImpl.saveAccount() 包名可以使用通配符,表示任意包。但是有几级包,就需要几个*. * *.*.*.*.AccountServiceImpl.saveAccount() 包名可以使用..表示当前包及其子包 * *..AccountServiceImpl.saveAccount() 类名和方法名都可以使用*来实现通配 * *..*.*() 参数列表: 可以直接写数据类型: 基本类型直接写名称 例如:int 引用类型写包名.类名的方式 例如:java.lang.String 可以使用通配符表示任意类型,但是必须有参数 可以使用..表示有无参数均可,有参数可以是任意类型 * *..*.*(..) 实际开发中切入点表达式的通常写法: 切到业务层实现类下的所有方法 * cn.lizhi.service.impl.*.*(..) 总结:以上是正则表达式的应用,正则表达式中*代表0次或无限次扩展;.代表任何单个字符。 4.1.2 四种常用通知类型 前置通知:在切入点方法执行之前执行,类比动态代理中的method.invoke执行之前的开启事务方法 – before 后置通知:在切入点方法正常执行之后执行。它和异常通知永远只能执行一个,类比事务中的commit – after-returning 异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个,类比catch代码块中的rollback。– after-throwing 最终通知:无论切入点方法是否正常执行,它都会在其后面执行,类比finally代码块。 – after 最终配置文件: 12345678910111213141516171819202122232425<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:aop=\"http://www.springframework.org/schema/aop\" xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd\"> <!-- 配置Spring的IOC,把Service对象配置进来 --> <bean id=\"accountService\" class=\"cn.lizhi.service.impl.AccountServiceImpl\"></bean> <!-- 配置Logger类(通知) --> <bean id=\"logger\" class=\"cn.lizhi.utlis.Logger\"></bean> <!-- 配置AOP --> <aop:config> <aop:pointcut id=\"pt1\" expression=\"execution(* cn.lizhi.service.impl.*.*(..))\"/> <!-- 配置切面 --> <aop:aspect id=\"logAdvice\" ref=\"logger\"> <!-- 配置通知的类型,并且建立通知方法和切入点方法的关联--> <aop:before method=\"beforePrintLog\" pointcut-ref=\"pt1\"></aop:before> <aop:after-returning method=\"afterReturningPrintLog\" pointcut-ref=\"pt1\"></aop:after-returning> <aop:after-throwing method=\"afterThrowingPrintLog\" pointcut-ref=\"pt1\"></aop:after-throwing> <aop:after method=\"afterPrintLog\" pointcut-ref=\"pt1\"></aop:after> </aop:aspect> </aop:config></beans> 其中这个切入点表达式的配置 1<aop:pointcut id=\"pt1\" expression=\"execution(* cn.lizhi.service.impl.*.*(..))\"/> id属性用于指定表达式的唯一标识。 expression属性用于指定表达式内容。 同时,此标签写在aop:aspect标签内部只能当前切面使用。 当它写在aop:aspect外面时,此时就变成了所有切面可用。 步骤: 把需要切入的对象,声明一个bean 在<aop:aspect>元素中引用该bean,为了进一步定义切面 声明<aop:before>等标签,即声明前置通知、后置通知 pointcut-ref属性都引用了名字为pt1的切入点。该切入点是在前边的<pointcut>元素中定义的,并配置expression属性来选择所应用的通知。 通过少量的XML配置,就可以把logger声明为一个切面。 这样做的好处是,logger类仍然是一个POJO,代码没有任何的更改,但通过以上的修改,logger可以作为一个切面进行使用了;同时,最重要的是,被切入的对象完全不知道Logger的存在。(注意:需要使用的切面,仍然先需要定义成一个bean) 4.1.3 环绕通知4.1.3.1 问题当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。 4.1.3.2 分析通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码代码中没有。 4.1.3.3 解决Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。 使用: 12345678910111213141516public Object aroundPrintLog(ProceedingJoinPoint pjp) { Object obj = null; try { System.out.println(\"Logger类中aroundPrintLog方法开始记录日志了...前置\"); // 前置通知 Object[] args = pjp.getArgs(); // 得到方法执行所需要的参数 obj = pjp.proceed(args);// 明确调用业务层方法(切入点方法) System.out.println(\"Logger类中aroundPrintLog方法开始记录日志了...后置\"); // 后置通知 return obj; } catch (Throwable throwable) { System.out.println(\"Logger类中aroundPrintLog方法开始记录日志了...异常\"); // 异常通知 throw new RuntimeException(throwable); }finally { System.out.println(\"Logger类中aroundPrintLog方法开始记录日志了...结束\"); // 结束通知 }} 从上面可以看出,环绕通知是Spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。 4.2.2 基于注解的AOP配置 在bean.xml配置Spring创建容器时要扫描的包 1234<!-- 配置扫描的包 --> <context:component-scan base-package=\"cn.lizhi\"></context:component-scan> <!-- 配置Spring开启注解AOP的支持 --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> 在通知类中,开启注解 @Before @AfterReturning @AfterThrowing @After @Around 另外需要额外在通知类中,建立通知方法和切入点的关联,以上的通知注解中的参数就写关联方法的函数名: 1234567@Pointcut(\"execution(* cn.lizhi.service.impl.*.*(..))\") private void pt1() {} @Before(\"pt1()\") public void beforePrintLog() { System.out.println(\"beforePrintLog记录了日志...\"); } 最后需要在配置文件中,开启Spring对注解AOP的支持: 1<aop:aspectj-autoproxy></aop:aspectj-autoproxy> 或者采用纯注解的方式(定义配置类): 12345@Configuration@ComponentScan(basePackages=\"cn.lizhi\")@EnableAspectJAutoProxypublic class springConfiguration(){} 注意:使用注解的方式,操作四个普通类型的通知,可能会带来顺序错误的问题。例如: beforePrintLog记录了日志…前置模拟用户保存了…afterPrintLog记录了日志…最终afterReturningPrintLog记录了日志…后置 注意:这里出现了最终通知在后置通知之前出现的问题。这样会在事务控制中产生一个问题,以上面的银行转账为例,就是最终通知–release操作会在后置通知–commit操作之前执行,那么会造成获取的连接不会是同一个连接(每次调用dao方法时,都会获取连接,由于前面我们使用了ThreadLocal对连接进行了绑定,所以此时获取的连接都是同一个连接,当对其进行release之后,再次获取连接时,那么和之前的连接就都不是同一个连接,因为在release方法中首先对数据库连接进行归还连接池操作,然后再将ThreadLocal和数据库连接进行解绑),就没有对事务进行相应的控制。因为当你commit时,会从连接池中重新获取连接再与线程进行绑定,而此时的连接并没有做任何的操作,所以commit就是一个空的提交。 因此,在使用注解的方式时,尽量使用环绕通知代替以上四个普通通知,因为环绕通知中的代码执行一定是由我们自己控制编写的。 五、Spring事务相关 5.1 Spring中JdbcTemplate 介绍5.1.1 Spring JdbcTemplate的内置数据源 12345678910// 准备数据源,spring的内置数据源DriverManagerDataSource ds = new DriverManagerDataSource();ds.setDriverClassName(\"com.mysql.jdbc.Driver\"); // 注入数据库驱动ds.setUrl(\"jdbc:mysql://url:3306/draft\"); // 连接地址ds.setUsername(\"root\"); // 用户名称ds.setPassword(\"1234\"); // 密码// 1.创建JdbcTemplate对象JdbcTemplate template = new JdbcTemplate();// 注入数据源template.setDataSource(ds); 通过配置文件,利用IOC思想简化以上操作: 123456789101112131415161718<?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.xsd\"> <!-- 创建JdbcTemplate对象 --> <bean id=\"template\" class=\"org.springframework.jdbc.core.JdbcTemplate\"> <property name=\"dataSource\" ref=\"dateSource\"></property> </bean> <!-- dataSource数据源 --> <bean id=\"dateSource\" class=\"org.springframework.jdbc.datasource.DriverManagerDataSource\"> <property name=\"driverClassName\" value=\"com.mysql.jdbc.Driver\"></property> <property name=\"url\" value=\"jdbc:mysql://url/draft\"></property> <property name=\"username\" value=\"username\"></property> <property name=\"password\" value=\"password\"></property> </bean></beans> 代码实现: 12345public static void main(String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext(\"bean.xml\");// 获取容器 JdbcTemplate template = ac.getBean(\"template\", JdbcTemplate.class);// 获取template对象 template.execute(\"insert into account (name,money) values('Lisa',900)\");// 执行操作 } 5.1.2 JdbcTemplate的使用 – CRUD 123456789101112131415 public static void main(String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext(\"bean.xml\"); JdbcTemplate template = ac.getBean(\"template\", JdbcTemplate.class);// template.execute(\"insert into account (name,money) values('Lisa',900)\"); // 1.保存操作 template.update(\"insert into account(name,money) values(?,?)\", \"Ben\", 800f); // 2.更新操作 template.update(\"update account set name=?,money=? where id=?\", \"Gu\", 900f, 5); // 3.删除操作 template.update(\"delete from account where id=?\", 6); // 4.查询所有 List<Account> accounts = template.query(\"select * from account\", new BeanPropertyRowMapper<Account>(Account.class)); // 5.查询单个 Account account = template.queryForObject(\"select * from account where id = ?\", new BeanPropertyRowMapper<Account>(Account.class), 10); } 5.1.3 JdbcDaoSupport的使用可以用于抽取dao层中的重复代码块。 例如: dao层的接口: 12345678public interface AccountDao { //更新数据 void updateAccount(Account account); //查询所有 List<Account> findAll();} 实现类: 1234567891011121314151617181920212223public class AccountDaoImpl implements AccountDao { private JdbcTemplate template = null; public void setTemplate(JdbcTemplate template) { this.template = template; } public void updateAccount(Account account) { template.update(\"update account set name=?,money=? where id = ?\", account.getName(), account.getMoney(), account.getId()); } public List<Account> findAll() { List<Account> lists = null; try { lists = template.query(\"select * from account\", new BeanPropertyRowMapper<Account>(Account.class)); } catch (DataAccessException e) { e.printStackTrace(); } return lists; }} 其配置文件bean.xml中bean配置为: 1234567891011121314151617 <!--配置账户的持久层--> <bean id=\"accountDao\" class=\"cn.lizhi.dao.impl.AccountDaoImpl\"> <property name=\"template\" ref=\"template\"></property> </bean><!-- JdbcTemplate对象 --> <bean id=\"template\" class=\"org.springframework.jdbc.core.JdbcTemplate\"> <property name=\"dataSource\" ref=\"dateSource\"></property> </bean><!-- DataSource数据源 --> <bean id=\"dateSource\" class=\"org.springframework.jdbc.datasource.DriverManagerDataSource\"> <property name=\"driverClassName\" value=\"com.mysql.jdbc.Driver\"></property> <property name=\"url\" value=\"jdbc:mysql:/url:3306/draft\"></property> <property name=\"username\" value=\"username\"></property> <property name=\"password\" value=\"password\"></property> </bean> 当我们有多个dao的实现方法时,那么这里的重复代码块: 1234private JdbcTemplate template = null;public void setTemplate(JdbcTemplate template) { this.template = template;} 所以这里就引发了我们的一个思考,是否可以把这些重复的代码块进行抽取,作为一个单独的类,然后当我们的实现类去继承这个类,进而简化我们的代码。答案是可以的。 现在我们对这个类进行抽取: 123456789101112public class JdbcDaoSupport { private JdbcTemplate jdbcTemplate = null; public JdbcTemplate getJdbcTemplate() { return jdbcTemplate; } public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } } 那么我们原有的实现类就需要继承这个类: 12345678910111213141516 public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao { public void updateAccount(Account account) { super.getJdbcTemplate().update(\"update account set name=?,money=? where id = ?\", account.getName(), account.getMoney(), account.getId()); } public List<Account> findAll() { List<Account> lists = null; try { lists = super.getJdbcTemplate().query(\"select * from account\", new BeanPropertyRowMapper<Account>(Account.class)); } catch (DataAccessException e) { e.printStackTrace(); } return lists; }} 从上面的bean.xml配置文件中,我们看到需要单独对JdbcTemplate进行配置,我们是否可以将JdbcTemplate和DataSource进行统一配置呢。我们将DataSource同样创建在JdbcDaoSupport类中: 1234567891011121314151617181920public class JdbcDaoSupport { private JdbcTemplate jdbcTemplate = null; private DataSource dataSource = null; public JdbcTemplate getJdbcTemplate() { return jdbcTemplate; } public void setDataSource(DataSource dataSource) { if (jdbcTemplate == null) { jdbcTemplate = createJdbcTemplate(dataSource); } } private JdbcTemplate createJdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }} 在这里我们创建了一个DataSource对象,并改写了其set方法。作用:如果我们直接传递template对象,那么template理所应当有值(有set方法);如果我们没有传递template对象,我们可以通过DataSource来获取template对象,由于我们改写了其set方法,我们同样可以使template有值(创建了有参构造函数的template,并向其传递了dataSource)。 此时bean.xml配置文件中bean的配置为: 1234567891011<!--配置账户的持久层--><bean id=\"accountDao\" class=\"cn.lizhi.dao.impl.AccountDaoImpl\"> <property name=\"dataSource\" ref=\"dateSource\"></property></bean><bean id=\"dateSource\" class=\"org.springframework.jdbc.datasource.DriverManagerDataSource\"> <property name=\"driverClassName\" value=\"com.mysql.jdbc.Driver\"></property> <property name=\"url\" value=\"jdbc:mysql://url:3306/draft\"></property> <property name=\"username\" value=\"username\"></property> <property name=\"password\" value=\"password\"></property></bean> 从上面的bean.xml文件也可以看出,在创建accountDao时,我们是向其注入了dataSource,所以进而就直接触发其set方法(因为accountDaoImpl继承了JdbcDaoSupport),判断template是否有值,如果没有值,就调用其父类中的createJdbcTemplate方法,其中传递的参数dataSource是来自于bean.xml配置文件中的id=dataSource的bean依赖注入。 以上的操作其实Spring已经帮我们实现了,不需要我们自己手动构建(也算是自己练习了一下源码)。截取其部分源码: 123456789101112131415161718192021222324public abstract class JdbcDaoSupport extends DaoSupport { @Nullable private JdbcTemplate jdbcTemplate; public JdbcDaoSupport() { } public final void setDataSource(DataSource dataSource) { if (this.jdbcTemplate == null || dataSource != this.jdbcTemplate.getDataSource()) { this.jdbcTemplate = this.createJdbcTemplate(dataSource); this.initTemplateConfig(); } } protected JdbcTemplate createJdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } @Nullable public final JdbcTemplate getJdbcTemplate() { return this.jdbcTemplate; }} 是不是同样的存在createJdbcTemplate(DataSource dataSource)和getJdbcTemplate()方法。从上面的源码中也可以看出Spring在使用set依赖注入时,可以不用定义其变量,只要定义其set属性方法即可。 最后,以上这种继承的方法适用于xml配置文件的方法,不适用于注解开发的方式(因为无法在jar包中的JdbcTemplate上加注解,注入我们的数据类型)。 5.2 Spring中事务控制的API5.2.1 Spring事务控制 JavaEE体系进行分层开发,事务处理处于业务层,Spring提供了分层设计业务层的事务处理解决方案。 Spring框架为我们提供了一组事务控制的接口 Spring的事务控制都是基于AOP的,它既可以使用编程的方式实现,也可以使用配置的方法实现。 依赖导入: 12345<dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.0.2.RELEASE</version></dependency> 5.2.2 Spring中事务控制的API介绍 PlatformTransactionManager 1234567public interface PlatformTransactionManager { TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; void commit(TransactionStatus var1) throws TransactionException; void rollback(TransactionStatus var1) throws TransactionException;} 此接口存在commit、rollback方法,即为通知bean。 PlatformTransactionManager接口提供事务操作的方法,包含有3个具体的操作: 获取事务状态信息 TransactionStatus getTransaction(@Nullable TransactionDefinition var1) 提交事务 void commit(TransactionStatus var1) 回滚事务 void rollback(TransactionStatus var1) 我们需要使用的是其实现类: org.springframework.jdbc.datasource.DataSourceTransactionManager 使用Spring JDBC或iBatis进行持久化数据时使用 org.springframework.orm.hibernate5.HibernateTransactionManager 使用Hibernate版本进行持久化数据时使用 TransactionDefinition 获取事务对象名称 String getName() 获取事务隔离级别:有四个隔离级别,Spring默认使用的是数据库的隔离级别 int getIsolationLevel() 获取事务传播行为:定义什么时候需要用事务(增、删、改),什么时候事务可有可无(查询) int getPropagationBehavior() 获取事务超时时间 int getTimeout() 获取事务是否只读(建议查询方法下为只读) boolean isReadOnly() 读写型事务:增加、删除、修改开启事务 只读型事务:执行查询时,也会开启事务 5.2.3 事务的隔离级别事务隔离级别反映事务提交并发访问时的处理态度 ISOLATION_DEFAULT 默认级别,归属下列某一种 ISOLATION_READ_UNCOMMITTED 可以读取未提交数据 ISOLATION_READ_COMMITTED 只能读取已提交数据,解决脏读问题(Oracle默认级别) ISOLATION_REPEATABLE_READ 是否读取其他事务提交修改后的数据,解决不可重复读问题(MySQL默认级别) ISOLATION_SERIALIZABLE 是否读取其他事务提交添加后的数据,解决幻读问题 5.2.4 事务的隔离级别 REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值) SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务) MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常 REQUERS_NEW:新建事务,如果当前在事务中,把当前事务挂起。 NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 NEVER:以非事务方式运行,如果当前存在事务,抛出异常。 NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行REQUIRED类似的操作。 5.2.5 TransactionStatus接口此接口提供的是事务具体的运行状态,TransacitonStatus接口描述了某个时间点上事务对象的状态信息,包含有6个具体的操作: 刷新事务 void flush() 获取是否存在存储点(相当于回滚的断点,回滚时不是回滚到最初的开始,而是回滚到设置的存储点处) boolean hasSavepoint() 获取事务是否完成 boolean isCompleted() 获取事务是否为新的事务 boolean isNewTransaction() 获取事务是否回滚 boolean isRollbackOnly() 设置事务回滚 void setRollbackOnly() 5.3 Spring中基于XML的声明式事务控制配置步骤 配置事务管理器 123<bean id=\"transactionManager\" class=\"org.springframework.jdbc.datasource.DataSourceTransactionManager\"> <property name=\"dataSource\" ref=\"dateSource\"></property> </bean> 配置事务的通知 此时我们需要导入事务的约束 tx名称空间和约束,同时也需要aop 使用tx:advice标签配置事务通知 属性: id:给事务通知起一个唯一标识 transaction-manager:给事务通知提供一个事务管理器引用 123<!-- 配置事务通知 --> <tx:advice id=\"txAdvice\" transaction-manager=\"transactionManager\"> </tx:advice> 配置AOP中的通用切入点表达式 1234<aop:config> <!-- 通用表达式配置 --> <aop:pointcut id=\"pt1\" expression=\"execution(* cn.lizhi.service.*.*(..))\"/></aop:config> 建立事务通知和切入点表达式的对应关系 12345<aop:config> <!-- 通用表达式配置 --> <aop:pointcut id=\"pt1\" expression=\"execution(* cn.lizhi.service.*.*(..))\"/> <aop:advisor advice-ref=\"txAdvice\" pointcut-ref=\"pt1\"></aop:advisor> </aop:config> 配置事务的属性 – tx:attributes 是在事务的通知tx:advice标签的内部 tx:method name属性:表明业务层中哪个方法需要被事务控制。 配置事务的属性: isolation:用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别。 propagation:用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS。 read-only:用于指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写。 timeout:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。 rollback-for:用于指定一个异常。当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值,表示任何异常都回滚。 no-rollback-for:用于指定一个异常,当产生异常时,事务不回滚,产生其他异常时事务回滚。没有默认值,表示任何异常都回滚。 123456789<!-- 配置事务通知 --> <tx:advice id=\"txAdvice\" transaction-manager=\"transactionManager\"> <tx:attributes> <!-- 增删改事务的控制 --> <tx:method name=\"transfer\" propagation=\"REQUIRED\" read-only=\"false\"/> <!-- 查询方法事务的控制 --> <tx:method name=\"find*\" propagation=\"SUPPORTS\" read-only=\"true\"/> </tx:attributes> </tx:advice> 需要命名规范,通过通配符配置方法名。 最终bean.xml中的配置为: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:aop=\"http://www.springframework.org/schema/aop\" xmlns:tx=\"http://www.springframework.org/schema/tx\" xsi:schemaLocation=\" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd\"> <!--配置账户的持久层--> <bean id=\"accountDao\" class=\"cn.lizhi.dao.impl.AccountDaoImpl\"> <property name=\"dataSource\" ref=\"dateSource\"></property> </bean> <!-- 配置账户的业务层 --> <bean id=\"accountService\" class=\"cn.lizhi.service.impl.AccountServiceImpl\"> <property name=\"dao\" ref=\"accountDao\"/> </bean> <!-- 配置数据源 --> <bean id=\"dateSource\" class=\"org.springframework.jdbc.datasource.DriverManagerDataSource\"> <property name=\"driverClassName\" value=\"com.mysql.jdbc.Driver\"></property> <property name=\"url\" value=\"jdbc:mysql://url:3306/draft\"></property> <property name=\"username\" value=\"username\"></property> <property name=\"password\" value=\"password\"></property> </bean> <!-- 配置事务管理器 --> <bean id=\"transactionManager\" class=\"org.springframework.jdbc.datasource.DataSourceTransactionManager\"> <property name=\"dataSource\" ref=\"dateSource\"></property> </bean> <!-- 配置事务通知 --> <tx:advice id=\"txAdvice\" transaction-manager=\"transactionManager\"> <tx:attributes> <tx:method name=\"transfer\" propagation=\"REQUIRED\" read-only=\"false\"/> <tx:method name=\"find*\" propagation=\"SUPPORTS\" read-only=\"true\"/> </tx:attributes> </tx:advice> <!-- AOP配置 --> <aop:config> <!-- 通用表达式配置 --> <aop:pointcut id=\"pt1\" expression=\"execution(* cn.lizhi.service.*.*(..))\"/> <aop:advisor advice-ref=\"txAdvice\" pointcut-ref=\"pt1\"></aop:advisor> </aop:config></beans> 5.4 Spring中基于注解的声明式事务控制配置步骤 配置事务管理器 1234<!-- 配置事务管理器 --><bean id=\"transactionManager\" class=\"org.springframework.jdbc.datasource.DataSourceTransactionManager\"> <property name=\"dataSource\" ref=\"dateSource\"></property></bean> 开启spring对注解事务的支持 1<tx:annotation-driven transaction-manager=\"transactionManager\"></tx:annotation-driven> 在需要事务支持的地方使用@Transaction注解 以上中的注意事项有:因为是使用注解式配置,所以在dao层,不能通过继承JdbcDaoSupport的方式简化我们的代码,故需要在bean.xml中对SpringJdbcTemplate进行配置。 5.4.1 基于纯注解的声明式事务控制通过配置类的方式实现。 SpringConfig配置类 123456@ComponentScan(\"cn.lizhi\")@Import({JdbcConfig.class, TransactionConfig.class})@PropertySource(\"classpath:jdbcConfig.properties\")@EnableTransactionManagementpublic class SpringConfig {} JdbcConfig配置类 1234567891011121314151617181920212223242526public class JdbcConfig { @Value(\"${driver}\") private String driver; @Value(\"${username}\") private String username; @Value(\"${password}\") private String password; @Value(\"${url}\") private String url; @Bean(\"template\") public JdbcTemplate getJdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } @Bean(\"dataSource\") public DataSource getDataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(driver); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setUrl(url); return dataSource; }} TransactionConfig配置类 123456789public class TransactionConfig { @Bean(\"transactionManager\") public DataSourceTransactionManager getTransactionManager(DataSource dataSource) { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; }} 5.5 Spring编程式事务控制因为我们的事务管理都是由Spring进行控制,所以我们都需要进行事务管理器的配置。 1234<!-- 配置事务管理器 --> <bean id=\"transactionManager\" class=\"org.springframework.jdbc.datasource.DataSourceTransactionManager\"> <property name=\"dataSource\" ref=\"dateSource\"></property> </bean> 当有了事务管理器之后,Spring同样为我们提供了事务模板,供我们具体使用事务控制的相关方法(相当于代理对象,对业务层方法进行增强): 1234<!-- 事务模板对象 --> <bean id=\"transactionTemplate\" class=\"org.springframework.transaction.support.TransactionTemplate\"> <property name=\"transactionManager\" ref=\"transactionManager\"/> </bean> 查看具体的TransactionTemplate源码,其中一段为: 12345678910111213141516171819202122@Nullablepublic <T> T execute(TransactionCallback<T> action) throws TransactionException { Assert.state(this.transactionManager != null, \"No PlatformTransactionManager set\"); if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) { return ((CallbackPreferringPlatformTransactionManager)this.transactionManager).execute(this, action); } else { TransactionStatus status = this.transactionManager.getTransaction(this); Object result; try { result = action.doInTransaction(status); } catch (Error | RuntimeException var5) { this.rollbackOnException(status, var5); throw var5; } catch (Throwable var6) { this.rollbackOnException(status, var6); throw new UndeclaredThrowableException(var6, \"TransactionCallback threw undeclared checked exception\"); } this.transactionManager.commit(status); return result; }} 在上面的异常代码块中,可以看出,当执行不通过时,执行this.rollbackOnException(status, var5),进行事务回滚;当执行通过时,try代码块中执行事务状态result = action.doInTransaction(status),最后执行this.transactionManager.commit(status),进行事务的提交。 具体使用: 模仿TransactionTemplate中的execute方法,在业务层中需要事务控制的方法中,执行该方法,该方法中的参数是一个接口,需要我们自己实现,其内容就填写我们需要控制的业务具体代码块。 123456789101112131415161718 public void transfer(final String startName, final String endName, final Float money) { template.execute(new TransactionCallback<Object>() { public Object doInTransaction(TransactionStatus transactionStatus) { Account startAccount = dao.findByName(startName); // 出款人账号 -- 会获取连接 Account endAccount = dao.findByName(endName); // 收款人账号 -- 会获取连接 Float startMoney = startAccount.getMoney(); startAccount.setMoney(startMoney - money); Float endMoney = endAccount.getMoney(); endAccount.setMoney(endMoney + money); dao.update(startAccount, startAccount.getId()); // 会获取连接// int a = 3 / 0; dao.update(endAccount, endAccount.getId()); // 会获取连接 return null; } }); } 由于编程式的事务控制,又增加了代码的冗余,违背了AOP的初心,所以实际中很少使用这种事务控制方式。 六、Spring整合JavaWeb整合步骤: 导包 —— Spring相关的Jar包 写配置 将所有组件加入容器中,并能正确获取 @Controller:Servlet层;但是不能标注在Servlet层,因为这个对象是Tomcat创建的,而非Spring容器进行创建的。 @Service:业务逻辑层 @Repository:dao层 @Component:其他组件 每个组件之间自动装配 配置出声明式事务 配置数据源。配置数据源之前,可抽离出JDBC配置文件。 JDBCTemplate操作数据库。给其注入数据源。 配置事务管理器 – 让其控制住数据源 注解方式 XML配置方式 IOC容器创建和销毁都要在合适的时机完成; 123456项目启动:{ IOC创建完成}项目销毁:{ IOC销毁} 通过监听器完成这项工作,监听器是Tomcat中的,因此,配置在web.xml中。采用Spring提供的监听器(ContextLoaderListener);容器位置,即为Spring配置文件。 这个监听器创建好的IOC容器在ContextLoader —— 这个属性就是IoC容器 1private WebApplicationContext context; 通过静态方法能获取——getCurrentWebApplicationContext","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"Spring","slug":"Spring","permalink":"https://chemlez.github.io/tags/Spring/"}]},{"title":"Mybatis框架介绍与基本使用笔记","slug":"Mybatis框架介绍与基本使用笔记","date":"2020-09-06T06:31:51.000Z","updated":"2020-11-18T07:31:17.785Z","comments":true,"path":"2020/09/06/Mybatis框架介绍与基本使用笔记/","link":"","permalink":"https://chemlez.github.io/2020/09/06/Mybatis%E6%A1%86%E6%9E%B6%E4%BB%8B%E7%BB%8D%E4%B8%8E%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8%E7%AC%94%E8%AE%B0/","excerpt":"Mybatis框架介绍与基本使用笔记注意:一般的一个Maven工程首先注入的依赖包含数据库驱动依赖,日志依赖,测试依赖 domain中的实体类实现serizlizable接口序列化的原因: 最重要的两个原因是: 1、将对象的状态保存在存储媒体中以便可以在以后重新创建出完全相同的副本; 2、按值将对象从一个应用程序域发送至另一个应用程序域。 实现serializable接口的作用是就是可以把对象存到字节流,然后可以恢复。所以你想如果你的对象没实现序列化怎么才能进行网络传输呢,要网络传输就得转为字节流,所以在分布式应用中,你就得实现序列化,如果你不需要分布式应用,那就没那个必要实现序列. namespace:名称空间;写接口的全类名;相当于告诉Mybatis这个配置文件是实现哪个接口的;","text":"Mybatis框架介绍与基本使用笔记注意:一般的一个Maven工程首先注入的依赖包含数据库驱动依赖,日志依赖,测试依赖 domain中的实体类实现serizlizable接口序列化的原因: 最重要的两个原因是: 1、将对象的状态保存在存储媒体中以便可以在以后重新创建出完全相同的副本; 2、按值将对象从一个应用程序域发送至另一个应用程序域。 实现serializable接口的作用是就是可以把对象存到字节流,然后可以恢复。所以你想如果你的对象没实现序列化怎么才能进行网络传输呢,要网络传输就得转为字节流,所以在分布式应用中,你就得实现序列化,如果你不需要分布式应用,那就没那个必要实现序列. namespace:名称空间;写接口的全类名;相当于告诉Mybatis这个配置文件是实现哪个接口的; 一、Mybatis相关配置 [1] mybatis配置文件中config与mapper的约束 自己预设定的配置文件: 1.1 config(mybatis-config.xml)全局配置文件12345678910111213141516171819202122232425262728293031<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE configuration PUBLIC \"-//mybatis.org//DTD Config 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-config.dtd\"> <!-- 引入Mybatis的配置声明dtd文件 --><!-- mybatis的主配置文件 --><configuration> <!-- 配置环境 若想让environments环境起作用,下列的标签中的配置都需要起作用 --> <environments default=\"mysql\"> <!-- 配置mysql(default的值)环境 id值等于default的值 --> <environment id=\"mysql\"> <!-- 配置事务的类型 --> <transactionManager type=\"JDBC\"/> <!-- 配置数据源(连接池 -\\- druid、c3p0..) --> <dataSource type=\"POOLED\"> <!-- 配置数据库连接的基本信息 --> <property name=\"driver\" value=\"com.mysql.jdbc.Driver\"/> <property name=\"url\" value=\"jdbc:mysql://url:3306/mybatis?characterEncoding=utf8\"/> <property name=\"username\" value=\"root\"/> <property name=\"password\" value=\"root\"/> </dataSource> </environment> </environments> <mappers> <!-- 使用的是注解 指定映射配置文件的位置(使用注解时,不能留下配置文件的方式,否则会冲突),映射配置文件指的是每个dao对立的配置文件--><!-- <mapper class=\"cn.lizhi.mybatis_01.dao.UserDao\"/> --><!-- 该包下所有的dao接口都可以使用 --><!-- <package name=\"cn.lizhi.mybatis_01.dao\"/>--> <!-- 配置文件的指定方式 --> <mapper resource=\"cn/lizhi/mybatis_01/dao/UserDao.xml\"/> </mappers></configuration> 1.1.1 通过properties标签引入外部配置信息(动态配置)12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE configuration PUBLIC \"-//mybatis.org//DTD Config 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-config.dtd\"> <!-- 引入Mybatis的配置声明dtd文件 --><!-- mybatis的主配置文件 --><configuration> <!-- 配置properties 可以在标签内部配置连接数据库的信息。也可以通过属性引用外部配置文件信息 resource属性: 用于指定配置文件的位置,是按照类路径的写法来写,并且必须存在与类路径下 url属性: 是要求按照url的写法来写地址 写法: http://localhost:8080/servlet/demoServlet 协议 主机 端口 URI URI:统一资源标识符。应用中可以唯一定位一个资源。 --> <!-- 方式1.jdbcConfig.properties 是存放在类(resources)路径的根路径下 --> <properties resource=\"jdbcConfig.properties\"/> <!-- 方式2. 这种是在标签内部配置连接数据库信息 <properties> <property name=\"driver\" value=\"com.mysql.jdbc.Driver\"/> <property name=\"url\" value=\"jdbc:mysql://url:3306/mybatis?characterEncoding=utf8\"/> <property name=\"username\" value=\"root\"/> <property name=\"password\" value=\"root\"/> </properties> --> <!-- 配置环境 若想让environments环境起作用,下列的标签中的配置都需要起作用 --> <environments default=\"mysql\"> <!-- 配置mysql(default的值)环境 id值等于default的值 --> <environment id=\"mysql\"> <!-- 配置事务的类型 --> <transactionManager type=\"JDBC\"/> <!-- 配置数据源(连接池 -\\- druid、c3p0..) --> <dataSource type=\"POOLED\"> <!-- 配置数据库连接的基本信息 --> <property name=\"driver\" value=\"${driver}\"/> <property name=\"url\" value=\"${url}\"/> <property name=\"username\" value=\"${username}\"/> <property name=\"password\" value=\"${password}\"/> </dataSource> </environment> </environments> <mappers> <mapper resource=\"cn/lizhi/mybatis_01/dao/UserDao.xml\"/> </mappers></configuration> jdbcConfig.properties 1234jdbc.driver=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://url:3306/mybatis?characterEncoding=utf8jdbc.username=rootjdbc.password=root 1.2 MapperMapper是具体某张表的映射。 1234567891011121314151617181920212223<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\"><mapper namespace=\"com.lizhi.dao.IUserDao\"> <!--配置查询所有,resultType的作用就是返回封装的位置,如果你要是不写的话,最后mybatis是不知道你到底要封装到哪里,会出现错误,我这个是User表,查询的也是这个,最后返回的结果就封装在User类中 id要写对应dao中的方法名称--> <select id=\"findAll\" resultType=\"com.lizhi.domain.User\"> SELECT *FROM user </select> <!--注意在resources中,目录是一级结构,要一个一个创建,而cn.lizhi.mybatis.dao是一个目录。而包是三级结构--></mapper><!-- 使用时直接复制以下的即可 --><?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\"><mapper namespace=\"\"> <select id=\"\" resultType=\"\"> SELECT *FROM user; </select></mapper> 通过实例 – 由Mybatis创建实现Dao的接口,进行数据库的查询时,在mapper中只有id是无法定位到具体数据库访问的方法的,所以要加上namespace,限定住工作空间(即限定住全类名,一个类中,方法名是唯一的)。可以简单理解为namespace是定位到具体的一个dao接口,id是定位到当前这个dao下的具体方法。 1.3 log4j的日志配置文件 – log4j.properties将log4j配置文件放入resource资源目录下,记录日志,对日志的操作。 123456789101112131415161718# Set root category priority to INFO and its only appender to CONSOLE.#log4j.rootCategory=INFO, CONSOLE debug info warn error fatallog4j.rootCategory=debug, CONSOLE, LOGFILE# Set the enterprise logger category to FATAL and its only appender to CONSOLE.log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE# CONSOLE is set to be a ConsoleAppender using a PatternLayout.log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppenderlog4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayoutlog4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\\n# LOGFILE is set to be a File appender using a PatternLayout.log4j.appender.LOGFILE=org.apache.log4j.FileAppenderlog4j.appender.LOGFILE.File=axis.loglog4j.appender.LOGFILE.Append=truelog4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayoutlog4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\\n 1.4 表的初始创建123456789101112131415161718192021222324252627282930313233343536373839404142434445464748create DATABASE mybatis;use mybatis;-- user表的创建DROP TABLE IF EXISTS `user`;CREATE TABLE `user` ( `id` int(11) Not NULL PRIMARY KEY auto_increment, `username` varchar(32) NOT NULL COMMENT '用户名称', `birthday` datetime default NULL COMMENT '生日', `sex` char(1) default NULL COMMENT '性别', `address` varchar(256) default NULL COMMENT '地址') ENGINE=InnoDB DEFAULT CHARSET=utf8;insert into `user`(`id`,`username`,`birthday`,`sex`,`address`) values (41,'老王','2018-02-27 17:47:08','男','北京'),(42,'小二王','2018-03-02 15:09:37','女','北京金燕龙'),(43,'小二王','2018-03-04 11:34:34','女','北京金燕龙'),(45,'传智播客','2018-03-04 12:04:06','男','北京金燕龙'),(46,'老王','2018-03-07 17:37:26','男','北京'),(48,'小马宝莉','2018-03-08 11:44:00','女','北京修正');-- account表的创建DROP TABLE IF EXISTS `account`;CREATE TABLE `account` ( `ID` int(11) NOT NULL COMMENT '编号', `UID` int(11) default NULL COMMENT '用户编号', `MONEY` double default NULL COMMENT '金额', PRIMARY KEY (`ID`), KEY `FK_Reference_8` (`UID`), CONSTRAINT `FK_Reference_8` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;insert into `account`(`ID`,`UID`,`MONEY`) values (1,41,1000),(2,45,1000),(3,41,2000);-- role表的创建DROP TABLE IF EXISTS `role`;CREATE TABLE `role` ( `ID` int(11) NOT NULL COMMENT '编号', `ROLE_NAME` varchar(30) default NULL COMMENT '角色名称', `ROLE_DESC` varchar(60) default NULL COMMENT '角色描述', PRIMARY KEY (`ID`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;insert into `role`(`ID`,`ROLE_NAME`,`ROLE_DESC`) values (1,'院长','管理整个学院'),(2,'总裁','管理整个公司'),(3,'校长','管理整个学校');-- user_role表的创建DROP TABLE IF EXISTS `user_role`;CREATE TABLE `user_role` ( `UID` int(11) NOT NULL COMMENT '用户编号', `RID` int(11) NOT NULL COMMENT '角色编号', PRIMARY KEY (`UID`,`RID`), KEY `FK_Reference_10` (`RID`), CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `role` (`ID`), CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;insert into `user_role`(`UID`,`RID`) values (41,1),(45,1),(41,2); 二、Mybatis的注意事项 两个配置文件的编写 config(全局环境Mybatis配置文件) mapper(映射配置文件) – 要与dao的目录结构相同 select中的,resultType的作用就是返回封装的位置,如果你要是不写的话,最后mybatis是不知道你到底要封装到哪里,会出现错误,我这个是User表,查询的也是这个,最后返回的结果就封装在User类中 id要写对应dao中的方法名称。 三、快速入门 – 实现user表中的全部用户信息的查询3.1 实体类编写12345678910111213141516171819public class User implements Serializable { private Integer id; private String username; private Date birthday; private Character sex; private String address; public User() { } public User(String username, Date birthday, Character sex, String address) { this.username = username; this.birthday = birthday; this.sex = sex; this.address = address; } // 省略getter和setter方法以及toString()方法的展示} 3.2 dao层接口的编写12345678public interface UserDao { /** * 查询所有用户信息 */ List<User> findAll();} 3.3 UserMapper.xml配置文件的创建12345678910<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\"><mapper namespace=\"cn.lizhi.dao.UserDao\"> <!-- 配置查询所有,resultType的作用就是返回封装的位置。这里是对User对象进行封装 --> <select id=\"findAll\" resultType=\"cn.lizhi.domain.User\"> SELECT *FROM user; </select></mapper> 3.4 测试方法1234567891011121314151617181920public class MybatisTest { public static void main(String[] args) throws IOException { // 1. 读取mybatis的全局配置文件 InputStream in = Resources.getResourceAsStream(\"mybatis-config.xml\"); // 2.创建SqlSessionFactory工厂 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(in); // 3.使用工厂生产SqlSession对象 SqlSession session = factory.openSession(); // 4.使用SqlSession创建Dao接口的代理对象 UserDao userDao = session.getMapper(UserDao.class); // 5.使用代理对象执行方法 List<User> users = userDao.findAll(); for (User user : users) { System.out.println(user); } session.close(); in.close(); }} 其中Resources(ibatis)类默认加载resource资源路径下的配置文件。 四、路径问题与代理dao的方式解析 绝对路径:d:/xxx/xxx/xml –> 采用:类加载器,它只能读取类路径的配置文件 相对路径:src/java/main/xxx.xml (在web项目下没有src目录文件) –> 使用ServletContext对象的getRealPath()获取web下真实运行的类的路径。 123// 2.创建SqlSessionFactory工厂SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();SqlSessionFactory factory = builder.build(in); 以上创建工厂使用了构建者模式。 — builder就是构建者。 构建者模式:把对象的创建细节隐藏,是使用者直接调用方法即可拿到对象。 12// 3.使用工厂生产SqlSession对象SqlSession session = factory.openSession(); 通过factory生产SqlSession使用了工厂模式。优势:解耦(降低类之间的依赖关系)。没有用new,不需重新边编译,解决了类之间的依赖关系。通过工厂生产对象。 12// 4.使用SqlSession创建Dao接口的代理对象UserDao userDao = session.getMapper(UserDao.class); 创建Dao接口实现类使用了代理模式。优势:不修改源码的基础上对已有方法增强。 代理dao的方式解析 连接数据库信息 – 用于创建connection对象。 同时在config配置中的有了mapper就有了所需要映射的信息(位置) 映射到Mapper文件,即可查询到全类名,id(接口方法),sql语句。便能获取到PreparedStatement。 对以上进行读取配置文件(用到解析XML的技术) – 此处用到的是dom4j解析xml的技术 继而: 根据配置文件的信息创建Connection对象 注册驱动,获取连接 获取预处理对象PreparedStatement 此时需要SQL语句 –> 从3中的sql语句中获取 执行查询 此节涉及源码,待我理解完了,再填上。 五、Mybatis的增删改查在Mapper配置文件中,增加配置信息,其中id为接口中的方法名. 如果在测试类中,自定义方法上@Before表示在测试方法前执行;@After表示在测试方法之后执行。 sqlSession.commit() – 手动提交事务。resultType的指定是在对数据库的结果进行封装时的返回值类型,告诉Mybatis应该封装到哪里的参数。parameterType指查询的时候查询参数类型 5.1 findAll() – 查找全部用户的信息123<select id=\"findAll\" resultType=\"cn.lizhi.mybatis_01.domain.User\"> SELECT *FROM user;</select> resultType为全类名,表示需要封装到的对象。 5.2 saveUser – 保存用户信息(插入用户)123<insert id=\"saveUser\" parameterType=\"cn.lizhi.mybatis_01.domain.User\"> insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday});</insert> parameterType为提供属性的domain中的对象全类名。values中是写{}是写domain对象中的属性名称。 如果要想获得当前保存的信息的id,其使用方法是: select last_insert_id() – 获取最后一条插入语句的id 1234567<insert id=\"saveUser\" parameterType=\"cn.lizhi.mybatis_01.domain.User\"> <!-- KeyPropery代表要返回的值名称,order:取值为AFTER代表插入后的行为 resultType代表返回值的类型 --> <selectKey keyProperty='id' order='AFTER' resultType=\"java.lang.Integer\"> select last_insert_id(); </selectKey> insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday});</insert> 5.3 updateUser – 更新用户123<update id=\"updateUser\" parameterType=\"cn.lizhi.mybatis_01.domain.User\"> update user set username=#{username},address=#{address},sex=#{sex},birthday=#{birthday} where id=#{id};</update> 5.4 deleteUser – 删除用户123<delete id=\"deleteUser\" parameterType=\"Integer\"> delete from user where id=#{uid}; <!-- 这个id的占位符可以随便写 --></delete> 在删除方法中parameterType只指定id,没有像上面一样指定User对象,是因为,我们这个是只对表进行操作,没有经过User对象的取值或使用(即在delete中,指定了我所要删除的是哪张表,并指定了id,那么我就可以通过这两项信息去定位到我所要删除的那条记录)。 5.5 findById – 根据用户id查询一条记录123<select id=\"findById\" parameterType=\"Integer\" resultType=\"cn.lizhi.mybatis_01.domain.User\"> SELECT *FROM user where id=#{uid};</select> 5.6 findByName – 模糊查询12345<select id=\"findById\" parameterType=\"String\" resultType=\"cn.lizhi.mybatis_01.domain.User\"> SELECT *FROM user where username like #{name}; 或者 SELECT *FROM user where username like '%${value}%'; <!-- value是固定写法 --></select> 在进行MyBatis时的模糊查询时,通配符需要需要设置在参数中,即和查询的参数组成字符串,而不是将通配符写在mapper配置文件中。推荐用第一种,预编译可防止SQL注入。 5.7 findTotal – 聚合函数的使用123<select id=\"findTotal\" resultType=\"int\"> SELECT count(id) FROM user;</select> 六、MyBatis的参数深入6.1 parameterType(查询参数的输入类型) Integer,String等简单类型 OGNL表达式 Object Graphic Navigation Language 通过对象的取值方法来获取数组,在写法上将get省去。 例如获取用户的名称: 类中的写法:user.getUsername(); OGNL表达式写法:user.username; Mybatis中能直接写username,而不用user.的原因是: 因为在parameterType中已经提供了属性所属的类,所以此时不需要写对象名。例如: 123<update id=\"updateUser\" parameterType=\"c\"> update user set username=#{username},address=#{address},sex=#{sex},birthday=#{birthday} where id=#{id};</update> 上面的参数都是直接写属性名称,而没有通过对象.属性的方式进行获取值username=#{username},address=#{address},sex=#{sex},birthday=#{birthday} where id=#{id} 这里的用处:当我们的查询条件被封装成一个对象时,就需要采用OGNL表达式。 例如封装了一个对象QueryVo: 123456789101112public class QueryVo { private User user; public User getUser() { return user; } public void setUser(User user) { this.user = user; }} 此时将QueryVo作为查询对象时,mapper配置文件应该写成: 123<select id=\"findUserByVo\" parameterType=\"cn.lizhi.mybatis_01.domain.QueryVo\" resultType=\"cn.lizhi.mybatis_01.domain.User\"> select * from user where username like #{user.username}</select> {user.username}的解释,根据上面的解释,我们知道user是QueryVo类的一个属性,我们可以直接获取属性,而username是user类的属性,所以可以继而通过对象.属性(继续对ONGL表达式的嵌套使用)的方式获取。 以上这种适用于由多个对象组成的查询条件,实现对数据库的进行查询 传递pojo对象 Mybatis使用ognl表达式解析对象字段的值,#{}或者${}括号中的值为pojo属性名称。 其中对pojo和javaBean两者的区别,参考链接:java对象 POJO和JavaBean的区别。可以简单的把pojo理解为我们定义的实体类。 传递pojo包装对象 开发中通过pojo传递查询条件,查询条件时综合的查询条件,不仅包括用户查询条件还包含其它的查询条件(比如将用户购买商品信息也作为查询条件),这时可以使用包装对象传递输入参数。Pojo类中包含pojo. 例如上面的:根据用户名查询用户信息,查询条件放到QueryVo的user属性中。 6.2 resultType(输出类型) 可以输出Integer、String等简单类型 pojo对象 pojo列表 问题一 在属性和Mysql数据库表中字段不统一时,会无法进行封装。 注意:windows下的MySQL不区分大小写;Linux下的MySQL严格区分大小写。 解决方式: 在Mapper配置文件中的sql语句中对操作字段起别名。别名和pojo中的属性名称相同。 例如: select id as userId,username as userName,address as userAddress,sex as UserSex from user; 这种方式运行效率最高,因为在数据库层面进行了解决,速度更快。 配置查询结果的列名和实体类的属性名的对应关系 123456789<resultMap id=\"userMap\" type=\"cn.lizhi.mybatis_01.domain.User\"> <!-- 主键字段的对应 --> <id property=\"userId\" column=\"id\"></id> <!-- 非主键字段的对应 --> <result property=\"userName\" column=\"username\"></result> <result property=\"userAddress\" column=\"address\"></result> <result property=\"userSex\" column=\"sex\"></result> <result property=\"userBirthday\" column=\"birthday\"></result></resultMap> id:唯一标志 – 随便填写。用于select、insert、delete等配置的类型映射。 type:查询的实体类,所对应的实体类是哪一个 – 全类名 举例使用: 123<select id=\"findAll\" resultMap=\"userMap\"> select * from user;</select> 即将以前的resultType替换成resultMap,值匹配resultMap配置中的id,这样就能够进行映射匹配。 这种方式能够提升开发效率。 6.3 配置别名文件 typeAliases配置别名,用于配置domain中类的别名 – 指定实体类别名 123<typeAliases> <typeAlias type=\"cn.lizhi.mybatis_01.domain.User\" alias=\"user\"></typeAlias></typeAliases> typeAlias用于配置别名。type属性指定的是实体类全限定类名。alias属性指定别名,当指定了别名就不再区分大小写。 package用于指定要配置别名的包,当指定后,该包下的实体类都会注册别名,并且类名就是别名,不再区分大小写。 – 指定实体类别名 123<typeAliases> <package name=\"cn.lizhi.mybatis_01.domain\"></package></typeAliases> 作用于接口,package标签是用于指定dao接口所在的包,当指定了之后,就不需要再写mapper以及resource或者class. 1234<mapper> <!-- package标签是用于指定dao接口所在的包,当指定了之后,就不需要再写mapper以及resource或者class --> <package name=\"cn.lizhi.mybatis_01.dao\"></package></mapper> 七、Mybatis实现Dao层的开发7.1 Dao实现类的使用 由于是自己写实现类,所以就不需要代理对象对我们的方法进行增强。 首先实现类的方法: 123456789101112131415public class UserDaoImpl implements UserDao { private SqlSessionFactory factory; public UserDaoImpl(SqlSessionFactory factory) { // 保证我们的factory中有值,通过构造方法将配置文件读取 this.factory = factory; } public List<User> findAll() { SqlSession session = factory.openSession(); List<User> user = session.selectList(\"cn.lizhi.mybatis_01.dao.UserDao.findAll\");// 配置文件中namespace+id session.close(); return user; } } 测试类: 123456789101112@Test public void findAllTest() throws IOException { InputStream is = Resources.getResourceAsStream(\"SqlMapConfig.xml\"); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(is); UserDaoImpl userDao = new UserDaoImpl(factory); List<User> users = userDao.findAll(); for (User user : users) { System.out.println(user); } is.close(); } 7.2 session中各种操作数据库语句 12345678910// 保存方法session.insert(\"namespace+id\",Object)// 更新操作session.update(\"namespace+id\",Object)// 删除操作session.delete(\"namespace+id\",Integer id)// 单条查询session.selectOne(\"namespace+id\",Integer id)// 聚合函数session.selectOne(\"namespace+id\") 7.3 源码分析 PreparedStatement对象的执行方法 execute:执行CRUD中的任意一种语句。它的返回值是一个boolean类型,表示是否有结果集。有结果集是true,没有结果集是false。 executeUpdate:只能执行CUD语句,查询语句无法执行。他的返回值是影响数据库记录的记录数。 executeQuery:只能执行select语句,无法执行增删改。执行结果封装的结果集ResultSet对象。 以后再补上… 八、Mybatis连接池与事务相关连接池介绍:可以减少我们获取连接所消耗的时间。用于存储连接的一个容器。 容器其实就是一个集合对象,该集合必须是线程安全的,不能两个线程拿到同一连接。该集合还必须实现队列的特性 – 先进先出 8.1 Mybatis连接池配置位置: 主配置文件SqlMapConfig.xml中的dataSource标签,type属性就是表示采用何种连接池方式。 type属性: POOLED:采用传统的javax.sql.DataSource规范中的连接池,Mybatis中有针对规范的实现(每次从池中获取连接,连接完以后归还) UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有实现池的思想(每次创建一个连接来使用) JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样的。 注意:如果不是web或者maven的war工程,是不能使用的。 这里采用dbcp连接池。 Mybaits POOLED的连接池原理: 8.2 事务解决4个问题 什么是事务 事务的四大特性ACID 不考虑隔离性会产生的3个问题 解决办法:四种隔离级别 Mybatis底层实现还是借用JDBC。 九、Mybatis动态sql9.1 动态查询动态查询(组合查询) – 查询条件不确定有没有的情况. 查询条件:可以根据username、gender、age等多条件组合查询 Mapper中的配置: 12345678910111213141516171819202122232425262728293031<!-- 根据条件进行查询 --><select id=\"方法名\" resultType=\"返回值类型\" parameterType=\"查询的参数类型\"> select * from user where 1=1 <if test=\"username!=null(条件1)\"> <!-- username是对应的实体类属性名(判断是否提供了这个查询条件的参数值) --> and username = #{username} <!-- 左边是数据库column字段名称,右边是实体类属性名称 --> </if> <if test=\"(条件2)\"> and sex = #{sex} </if> <if test=\"(条件3)\"> and age = #{age} <!-- 注意属性名和字段名对应问题 --> </if> ...</select><!-- where 标签可以省略 初始条件的 where --><select id=\"方法名\" resultType=\"返回值类型\" parameterType=\"查询的参数类型\"> select * from user <where> <if test=\"(条件1)\"> and username = #{username} </if> <if test=\"(条件2)\"> and sex = #{sex} </if> <if test=\"(条件3)\"> and age = #{age} <!-- 注意属性名和字段名对应问题 --> </if> ... </where></select> 根据可能提供的条件,进行组合查询(这些条件可能存在也可能不存在),例如: 主查询代码: 123456789@Test public void findUserByCondition() throws IOException { User user = new User(); user.setUsername(\"Tom\"); // 查询出所有叫Tom的记录,提供的查询条件只有\"Tom\" List<User> users = userDao.findUserByCondition(user); for (User u : users) { System.out.println(u); } } 对应表的配置文件查询: 1234567891011121314151617181920<select id=\"findUserByCondition\" parameterType=\"cn.lizhi.mybatis_01.domain.User\" resultType=\"cn.lizhi.mybatis_01.domain.User\"> select * from user <where> <if test=\"username != null\"> and username=#{username} </if> <if test=\"sex != null\"> and sex=#{sex} </if> <if test=\"address != null\"> and address=#{address} </if> <if test=\"address != null\"> and address=#{address} </if> <if test=\"birthday != null\"> and birthday=#{birthday} </if> </where></select> 9.2 子查询子查询:根据被封装对象(例如queryvo)的id集合,查询用户信息 123456789101112131415161718<!-- 根据queryvo中的Id集合实现查询用户列表 --><select id=\"findUserInIds\" resultMap=\"userMap\" parameterType=\"queryvo\"> select * from user <where> <if test=\"ids !=null and ids.size()>0\"> <foreach collection(代表一个集合)=\"ids\" open=\"and id in (\" close=\")\" item=\"uid(集合中每个元素的代称)\" separator(每一项的分隔符)=\",\"> <!-- 查询出id在集合中的全部条目 --> #{uid} </foreach> </if> </where></select><!-- 属性解释 --><foreach>:标签用于遍历集合,它的属性:collection:代表要遍历的集合元素,注意编写时不要写#{}open:代表语句的开始部分close:代表结束部分item:代表遍历集合的每个元素,生成的变量名sperator:代表分隔符 十、Mybatis中的多表查询示例:用户和账户 一个用户可以有多个账户(一对多) 一个账户只能属于一个用户 (多个账户也可以属于同一个用户;一对一或者多对一) 步骤: 建立两张表:用户表、账户表 让用户表和账户表之间具备一对多的关系:需要使用外键在账户表中添加 建立两个实体类:用户实体类和账户实体类 让用户和账户的实体类能体现出一对多的关系 建立两个配置文件 用户的配置文件 账户的配置文件 注意:一张数据库表对应一个实体类且对应一个配置文件,并在相应的配置文件中进行配置操作。 实现配置 当我们查询用户时,可以同时得到用户下所包含的账户信息 当我们查询账户时,可以同时得到账户的所属用户信息 一的一方是主表;多的一方是主表。 10.1 Mybatis一对一的查询查询所有账户同时包含用户名和地址信息 方式一:新建子类对象accountUser继承account accountUser实体类中包含需要展示的User实体类中的信息,例如需要展示username,address。那么accountUser定义如下: 12345678910111213141516public class AccountUser extends Account{ private String username; private String address; /* 省略了getter与setter方法 */ @Override public String toString() { return super.toString()+\"AccountUser{\" + \"username='\" + username + '\\'' + \", address='\" + address + '\\'' + '}'; }} mapper中配置的查询信息 1234<!-- 新建子类对象accountUsr继承account --><select id=\"findAllAccount\" resultType=\"accountUser\"> select 所要查询的字段 from 表1,表2 where 表1.字段 = 表2.字段 <!-- 组合查询 --></select> 具体使用,创建AccountMap.xml配置文件。 12345678910<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\"><mapper namespace=\"cn.lizhi.dao.AccountDao\"> <!-- 配置查询所有,resultType的作用就是返回封装的位置。这里是对User对象进行封装 --> <select id=\"findAllAccount\" resultType=\"cn.lizhi.domain.AccountUser\"> SELECT u.*,a.* from user u,account a where u.id=a.UID; </select></mapper> 测试方法: 1234567@Testpublic void findAllAccount() { List<AccountUser> allAccount = accountDao.findAllAccount(); for (AccountUser accountUser : allAccount) { System.out.println(accountUser); }} 结果: 123Account{id=41, uid=41, money=1000.0, user=null}AccountUser{username='老王', address='北京'}Account{id=45, uid=45, money=1000.0, user=null}AccountUser{username='传智播客', address='北京金燕龙'}Account{id=41, uid=41, money=2000.0, user=null}AccountUser{username='老王', address='北京'} 方式二:定义封装account和user的resultMap 例如在从表domain中Account的实体类: 123456789101112131415161718192021public class Account implements Serializable { private Integer id; private Integer uid; private Double money; private User user; // 这里从表中包含了主表实体的对象引用 /* 省略了getter与setter方法 */ @Override public String toString() { return \"Account{\" + \"id=\" + id + \", uid=\" + uid + \", money=\" + money + '}'; }} 在Mybatis中多对一或一对一中,从表的实体类和主表的实体类关系:从表实体应该包含主表实体的对象应用。 定义封装account和user的resultMap 1234567891011121314151617181920<!-- 定义封装account和user的resultMap --><resultMap id=\"accountUserMap\" type=\"account\"> <!-- type表明从表封装的对象 --> <!-- 先写从表的信息 配置account信息的封装--> <!-- 主键信息 --> <id property=\"id\" column=\"aid\"></id> <!-- 非主键信息 --> <result property=\"uid\" column=\"uid\"></result> <result property=\"money\" column=\"money\"></result> <!-- 主表信息 一对一的关系映射:配置封装user的内容 --> <!--多对一的关系, property: 指的是属性的值(account中),column:指通过哪一个字段进行获取属性值,这里选择uid(外键), javaType:指的是属性的类型 --> <association property=\"user\" column=\"uid\" javaType=\"user\"> <!-- javaType表明从表中主表实体需要封装的对象(因为配置了别名,所以这里没有写全类名) --> <!-- 主键信息 --> <id property='id' column='id'></id> <!-- 非主键字段 --> <result column=\"username\" property=\"username\"></result> <result column=\"address\" property=\"address\"></result> <result column=\"sex\" property=\"sex\"></result> <result column=\"birthday\" property=\"birthday\"></result> </association></resultMap> 查询所有,一对一的查询(在查询账户时,也会查询出用户信息) 123<select id=\"findAll\" resultMap=\"accountUserMap\"> select 所要查询的字段 from 表1,表2 where 表1.字段 = 表2.字段 <!-- 组合查询 --></select> 使用左外连接的查询语句,通过此种方式,其输出结果: 123<select id=\"findAll\" resultMap=\"accountUserMap\"> SELECT * from account left outer join user on user.id=account.uid;</select> 10.2 Mybatis一对多的查询一对多映射:主表实体应该包含从表实体的集合引用。 Domain中User加入从表实体的集合引用。 12345678910111213141516private Integer id; private String Username; private Date birthday; private String sex; private String address; private List<Account> accounts; // 加入的从表实体集合 public List<Account> getAccounts() { return accounts; } public void setAccounts(List<Account> accounts) { this.accounts = accounts; }/* 其余属性的getter与setter方法省略,以及toString()方法的省略*/ 对应的mapper配置文件: 123456789101112131415161718<!-- 定义封装User的resultMap --><resultMap id=\"userAccountMap\" type=\"user\"> <!-- type表明主表封装的对象 --> <!-- 主键信息 --> <id property='id' column='id'></id> <!-- 非主键字段 --> <result column=\"username\" property=\"username\"></result> <result column=\"address\" property=\"address\"></result> <result column=\"sex\" property=\"sex\"></result> <result column=\"birthday\" property=\"birthday\"></result> <!-- 配置user对象中accounts集合的映射 --> <collection property=\"accounts\" ofType=\"account\"> <!-- ofType写集合中元素的对象类型(因为配置了别名,所以这里没有写全类名) --> <!-- 主键信息 以下property是java domain的属性,column是数据库列名(可以用别名代替) --> <id property=\"id\" column=\"aid\"></id> <!-- 非主键信息 --> <result property=\"uid\" column=\"uid\"></result> <result property=\"money\" column=\"money\"></result> </collection></resultMap> 123<select id=\"findAll\" resultMap=\"userAccountMap\"> 数据库中的查询语句 <!-- 组合查询 --></select> 在resultMap中,需要注意两个id中是否存在column重名的情况,如果相同将其中一个进行修改(查询数据库时起别名)。查询时,建议将所有字段内容都查询出,便于对对象的封装(对象的封装的内容就是来源于查询的内容返回的内容,property与column对应)。 10.3 Mybatis 多对多查询实例:用户和角色 一个用户可以有多个角色 一个角色可以赋予多个用户 步骤: 建立两张表:用户表、角色表 让用户表和角色表之间具备多对多的关系:需要使用中间表,中间表包含各自的主键,在中间表中是外键。 建立两个实体类:用户实体类和角色实体类 让用户和角色的实体类能体现出多对多的关系 各自包含对方一个集合引用 建立两个配置文件 用户的配置文件 角色的配置文件 实现配置 当我们查询用户时,可以同时得到用户下所包含的角色信息 当我们查询角色时,可以同时得到角色的所赋予的用户信息 两个实体类中各自加入多对多的实体关系映射。 第一种:查询所有角色,同时获取角色的所赋予的用户。即:在查询角色时,同时获取到它的全部用户信息。 以中间表作为连接的媒介。确定出查询角色时,同时获取角色下的全部用户信息(可以简单的看成一对多的查询关系)。这里角色表作为主表(用左外连接,保存主表的全部信息) Mapper配置同一对多。 第二种:查询所有的用户,同时获取用户的所拥有的角色。 思路同第一种 10.4 JNDI补充模仿windows的注册表。 插图 创建Maven的war工程。 在webapp下创建META-INF,将context.xml方入此目录下。 替换原有的SqlMapConfig 123456789101112131415161718192021222324252627<?xml version=\"1.0\" encoding=\"UTF-8\"?><!-- 导入约束 --><!DOCTYPE configuration PUBLIC \"-//mybatis.org//DTD Config 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-config.dtd\"><configuration><typeAliases> <package name=\"com.itheima.domain\"></package> </typeAliases> <!-- 配置mybatis的环境 --> <environments default=\"mysql\"> <!-- 配置mysql的环境 --> <environment id=\"mysql\"> <!-- 配置事务控制的方式 --> <transactionManager type=\"JDBC\"></transactionManager> <!-- 配置连接数据库的必备信息 type属性表示是否使用数据源(连接池)--> <dataSource type=\"JNDI\"> <property name=\"data_source\" value=\"java:comp/env/jdbc/eesy_mybatis\"/> </dataSource> </environment> </environments> <!-- 指定mapper配置文件的位置 --> <mappers> <mapper resource=\"com/itheima/dao/IUserDao.xml\"/> </mappers></configuration> 将之前的测试方法写在jsp的java代码块中。 原因是有tomcat服务器进行,将.jsp翻译成.java再进行编译,运行字节码文件,进而使用tomcat服务器内部的资源连接池再通过Mybatis访问数据库。 十一、Mybatis缓存相关11.1 Mybatis中的延迟加载问题: 在一对多中,当我们有一个用户,它有100个账户。(用户对象-user;accounts(集合,size=100)) 问:在查询用户的时候,要不要把关联的账户查询出来? 答:在查询用户时,用户下的账户信息应该是,什么时候使用,什么时候查询的。 问:在查询账户的时候,要不要把关联的用户查询出来? 答:在查询用户时,账户的所属用户信息应该是随着账户查询时一起查询出来。 延迟加载:在真正使用数据时才发起查询,不用的时候不查询。按需加载(懒加载)。 立即加载:无论是否使用,只要一调用方法,马上发起查询。 在对应的四种表关系中:一对多,多对一,一对一,多对多 一对多,多对多:通常情况下都采用延迟加载。 多对一,一对一:通常情况下采用立即加载。 11.1.1 一对一(association)延迟加载: 全局配置文件需设置 lazyLoadingEnabled:true aggressiveLazyLoading:false 12345<settings> <!-- Mybatis全局延迟加载的开关,true表示打开--> <setting name=\"lazyLoadingEnabled\" value=\"true\"/> <setting name=\"aggressiveLazyLoading\" value=\"false\"/></settings> mapper的配置文件: 12345678<!-- 一对一的查询 --><resultMap id=\"accountUserMap\" type=\"account\"> <id property=\"id\" column=\"id\"></id> <result proerty=\"uid\" column=\"uid\"></result> <result property=\"money\" column=\"money\"></result> <association property=\"user\" column=\"uid\" javaType=\"user\" select=\"cn.lizhi.mybatis_01.dao.UserDao.findById\"> </association></resultMap> select属性:查询用户的唯一标识,这个例子中是关联信息的主键id。例如这里account表中的uid对应着user表中的id。所以我们要使得uid和id进行匹配,故在user中的唯一标识就是id。反之,如果我们通过查询user表同时将其关联的account表的信息一并查出,此时user表中的id对应着account表中的uid,那么uid便是所要查询的关联表的唯一标识。 这里对唯一标识的理解是两个表之间相关联的字段。 column属性:查询用户是根据id查询,而id对应的是account表中的uid,所需要的参数的值(外键)- 根据当前表中的uid去查询user表的记录(对应到user表中的主键)。即,拿着当前表的uid(外键),去user表中匹配主键,查询出记录。 cn.lizhi.mybatis_01.dao.UserDao.findById: 123<select id=\"findById\" parameterType=\"Integer\" resultType=\"cn.lizhi.mybatis_01.domain.User\"> select * from user where id=#{uid};</select> cn.lizhi.mybatis_01.dao.AccountDao.findAllByCount 123<select id=\"findAllByCount\" resultMap=\"accountUserMap\"> select * from account</select> 以上可以实现懒加载。 11.1.2 一对多(Collection)使用方法同上: 1234567891011<!-- 一对多的查询 --> <resultMap id=\"userAccountMap\" type=\"user\"> <id property='id' column='id'></id> <result column=\"username\" property=\"username\"></result> <result column=\"address\" property=\"address\"></result> <result column=\"sex\" property=\"sex\"></result> <result column=\"birthday\" property=\"birthday\"></result> <collection property=\"accounts\" ofType=\"account\" select=\"cn.lizhi.mybatis_01.dao.AccountDao.findAccountByUid\" column=\"id\"> <!-- select中uid是唯一标识(因为user中的id对应这account中的uid)。这里column写id是因为通过用户的id去查询Account表中的信息(对应account表中的uid) --> </collection> </resultMap> 延迟加载的两点: mapper中内容的填写。即sql语句的写法,resultMap中标签的写法。 开启全局配置文件的懒加载 11.2 Mybatis中的一级缓存指的是Mybatis中SqlSession对象的缓存。当我们执行查询之后,查询的结果会同时存入到SqlSession为我们提供一块区域中。该区域的结构是一个Map。当我们再次查询同样的数据,Mybatis会先去SqlSession中查询是否有,有的话直接拿出来用。当SqlSession对象消失时,Mybatis的一级缓存也就消失了。 默认开启了一级缓存。当SqlSession关闭(sqlSession.close())时,缓存会自动消失。 同时sqlSession.clearCache(),也可以清楚缓存。 当数据库中的内容和缓存中数据不同时,Mybatis的做法: 一级缓存是SqlSession范围的缓存,当调用SqlSession的修改,添加,修改,commit(),close()等方法时,就会清空一级缓存,直接从数据库中查询,并将查询的记录添加入缓存当中。 11.3 Mybatis中的二级缓存指的是Mybatis中SqlSessionFactory对象的缓存。由同一个SqlSessionFactory对象创建的SqlSession共享其缓存。 二级缓存的使用步骤: 第一步:让Mybatis框架支持二级缓存(在SqlMapConfig.xml中配置,可以不用配置,默认是开启状态) 123<settings> <setting name=\"cacheEnabled\" value=\"true\"/></settings> 第二步:让当前的映射文件支持二级缓存(在UserDao.xml中配置) 12<!-- 开启user支持二级缓存--><cache/> 第三部:让当前的操作支持二级缓存(在select标签中配置,配置userCache值为true) 123<select id=\"findById\" parameterType=\"Integer\" resultType=\"cn.lizhi.mybatis_01.domain.User\" useCache=\"true\"> select * from user where id=#{uid};</select> 二级缓存中存放的内容是数据(json数据),而不是对象。当发起查询时,创建一个新的对象,然后将二级缓存中的数据填充到对象当中,所以前后两次查询中对象不同。 十二、Mybatis的注解开发<packaging>jar</packaging> 在Mybatis中针对CRUD的四个注解: @Select、@Insert、@Update、@Delete 在对应的Dao方法中直接写上对应的注解,注解中(@Select)写入操作数据库的语句即可。 注意:dao.XML配置文件会和注解产生冲突。故不能混用(dao.xml配置文件存放到其他地方或者不用) 12.1 Mybatis中的属性与字段不一致解决办法使用注解@Results 例如: 123456@Results(id=\"userMap\",value={ @Result(id=true,cloumn=\"id\",property=\"userId\"), @Result(cloumn=\"username\",property=\"userName\"), @Result(cloumn=\"address\",property=\"userAddress\"), @Result(cloumn=\"sex\",property=\"userSex\")}) id用于表示是否是主键,默认值是false。外部的id="userMap",用于指定唯一标识,可以让其他的注解进行复用。 如果需要让当前dao下其他的方法也能使用,指定注解@ResultMap.即:@ResultMap("userMap"),其中为数组类型,可以指定多个,例如:@ResultMap(value={"userMap",...})。可以理解为对应在dao.xml配置文件中的resultMap。 12.2 Mybatis中的多表查询 – 注解方式12.2.1 一对一思想同dao.xml一样。通过配置@Results注解实现。 12345678@Select(\"select * from account\") @Results(id=\"accountMap\",value={ @Result(id=true,cloumn=\"id\",property=\"Id\"), @Result(cloumn=\"uid\",property=\"uid\"), @Result(cloumn=\"money\",property=\"money\"), // 以上完成了对account类对象的封装,下面是完成对User类对象的封装 @Result(property=\"user\",column=\"uid\",one=@One(select=\"全限定类名.方法名\",fetchType = FetchType.EAGER(加载方式)))}) select是指向如何查询封装对象的唯一标识 – 全限定类名.方法名。由于select直接定位到方法名,查询到具体的对象,所以在@Select语句中只查询了account(account中包含了user的属性,且在下方的注解中指定了uid去查询对应的user对象,进行封装) ,注解的方式包含了加载的方式,故采用这种查询方式。 12.2.2 一对多123456789@Select(\"select * from user\")@Results(id=\"userMap\",value={ @Result(id=true,cloumn=\"id\",property=\"id\"), @Result(cloumn=\"username\",property=\"name\"), @Result(cloumn=\"address\",property=\"address\"), @Result(cloumn=\"sex\",property=\"sex\") // 以上完成对user对象的封装,下方是account集合,继而封装account对象 @Result(property=\"accounts\",column=\"id\",many=@Many(select=\"全限定类名.方法名\",fetchType = FetchType.LAZY(加载方式))) }) 以懒加载的形式,去理解这个查询语句及配置语句。 以上中,在@Result属性中需要关注的属性是select以及fetchType。 12.3 Mybatis中的二级缓存 – 注解方式在Mybaits中一级缓存是默认打开的。 注解使用二级缓存的步骤: 二级缓存中,同样在全局配置中开启二级缓存。 在对应的Dao接口上配置全局的注解 – @CacheNamespace(blocking = true)","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"},{"name":"框架","slug":"Java/框架","permalink":"https://chemlez.github.io/categories/Java/%E6%A1%86%E6%9E%B6/"}],"tags":[{"name":"Mybatis","slug":"Mybatis","permalink":"https://chemlez.github.io/tags/Mybatis/"}]},{"title":"Java基础之反射初步理解","slug":"Java基础之反射初步理解","date":"2020-09-05T01:44:28.000Z","updated":"2020-09-05T01:44:12.550Z","comments":true,"path":"2020/09/05/Java基础之反射初步理解/","link":"","permalink":"https://chemlez.github.io/2020/09/05/Java%E5%9F%BA%E7%A1%80%E4%B9%8B%E5%8F%8D%E5%B0%84%E5%88%9D%E6%AD%A5%E7%90%86%E8%A7%A3/","excerpt":"由于在学习框架时,经常会遇到反射,故此篇文章用于对反射的基本学习。 一、概述基本定义:JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。 对其简单的理解就是将类的各个组成部分封装为其他对象,以便我们能够更加细化的使用。同时,我们也都知道,Java中程序是运行在虚拟机中,我们平常用文本编辑器或者是IDE编写的程序都是.java格式的文件,这是最基础的源码,但这类文件是不能直接运行的,必须经过编译成.class字节码文件进而加载进内存供JVM虚拟机执行。要想理解反射,就先需要谈起Java代码在计算机中经历的三个阶段,见下图。","text":"由于在学习框架时,经常会遇到反射,故此篇文章用于对反射的基本学习。 一、概述基本定义:JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。 对其简单的理解就是将类的各个组成部分封装为其他对象,以便我们能够更加细化的使用。同时,我们也都知道,Java中程序是运行在虚拟机中,我们平常用文本编辑器或者是IDE编写的程序都是.java格式的文件,这是最基础的源码,但这类文件是不能直接运行的,必须经过编译成.class字节码文件进而加载进内存供JVM虚拟机执行。要想理解反射,就先需要谈起Java代码在计算机中经历的三个阶段,见下图。 第一个阶段 – 源代码阶段 定义一个了类,继而将一个类进行编译.class文件。 在第一阶段中,可以通过Class.forName("全类名")的方式获取Class类对象。 第二阶段 – 加载进内存阶段(Class对象阶段) 类加载器(ClassLoader):负责将字节码文件加载进内存。 在内存中需要由一个对象来描述加载进内存中这个Class文件 – Class类对象。 Class类对象的基本组成: 成员变量 封装成Field[] fields对象 构造方法 Constructor[] cons 成员方法 Method[] methods 继而通过Class类对象创建真正的对象,供我们使用。 这一阶段可以通过类名.class的方式获取到Class类对象。 第三阶段 – Runtime阶段 通过类的实例化创建出对象。Runtime运行阶段 new 出类的实例化对象。 通过对象.getClass()方式获取到Class类对象。 综上上面的三个阶段对Class类对象的获取:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个(内存中的位置同一个,本质就是一个class文件)。 好处: 可以在程序运行过程中,操作这些对象。(例如:获取一个对象的方法,将一个类进行拆解,对其各个组成部分进行操作,获取到一个类对象在内存中的状态) 可以解耦,提高程序的可扩展性(在不改变原有代码的基础上,对功能进行增强实现)。 二、具体使用Java反射相关的类: 类名 用途 Class类 代表类的实体,在运行的Java应用程序中表示类和接口 Field类 代表类的成员变量(成员变量也称为类的属性) Method类 代表类的方法 Constructor类 代表类的构造方法 2.1 Class对象功能 获取成员变量们 获取构造方法们 获取成员方法们 获取全类名 Field[] getFields() Constructor<?>[] getConstructors() Method[] getMethods() String getName() Field getField(String name) Constructor getConstructor(类<?>… parameterTypes) Method getMethod(String name, 类<?>… parameterTypes) Field[] getDeclaredFields() Constructor getDeclaredConstructor(类<?>… parameterTypes) Method[] getDeclaredMethods() Field getDeclaredField(String name) Constructor<?>[] getDeclaredConstructors() Method getDeclaredMethod(String name, 类<?>… parameterTypes) 其中*Declared*代表忽视权限修饰符的安全检查,可以获取一切权限的(成员变量、构造方法,成员方法);而上方的只能获取到public修饰的(成员变量,构造方法,成员方法)。 在忽视权限修饰符的同时,如果想对其成员变量\\构造方法\\成员方法使用等,可采用暴力反射(不推荐,降低了安全机制)。 setAccessible(true):暴力反射 – 忽略访问权限修饰符的安全检查 成员变量 – Field 1.1 设置值 void set(Object obj, Object value) 1.2 获取值 get(Object obj) 构造方法 – Constructor 2.1 创建对象 T newInstance(Object... initargs) – 非空参时,获取到Class类对象的对应的构造方法,再填入对应参数。 如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法。例如:Object obj = cls.newInstance。 方法对象 – Method 3.1 执行方法 Object invoke(Object obj, Object... args) 3.2 获取方法名称 String getName:获取方法名 ClassLoader class类对象.getClassLoader得到类加载器对象(获取到这个字节码文件对应的类加载器),负责将这个类加载进行内存。ClassLoader对象可以获取到内存中当前类路径下的文件信息。 三、代码实操首先定义一个实体类: Student实体类 123456789101112131415161718192021222324252627282930313233343536373839404142434445public class Student implements Serializable { private String name; private String age; public Student() { } public Student(String name, String age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAge() { return age; } public void setAge(String age) { this.age = age; } public void eat() { System.out.println(\"eat...\"); } public void eat(String food) { System.out.println(\"eat..\" + food + \"...\"); } @Override public String toString() { return \"Student{\" + \"name='\" + name + '\\'' + \", age='\" + age + '\\'' + '}'; }} 以上的Student实体类中,包含: 成员变量:name、age 构造方法: public Student() public Student(String name, String age) 成员方法:getter、setter、public void eat()、 public void eat(String food) 3.1 反射基本方法使用3.1.1 各阶段对Class对象的获取1234567// 1.通过全类名获取class对象 -- 源码阶段Class cls = Class.forName(\"cn.lizhi.domain.Student\");// 2.通过类名的方式获取class对象 -- 加载进行内存后Class cls1 = Student.class;// 3.类的实例化对象获取class对象 -- Runtime阶段Student student = new Student();Class cls2 = student.getClass(); 其中cls、cls1、cls2为同一个对象,内存地址值相等。 3.1.2 Class对象功能使用 – 成员属性实例12345678910Student student = new Student();Field[] fields = cls.getDeclaredFields(); // 获取全部的成员属性for (Field field : fields) {System.out.println(field);}Field name = cls.getDeclaredField(\"name\"); // 获取指定的成员属性name.setAccessible(true); // 暴力反射name.set(student, \"Tom\"); // 设置属性值(可以不改变原代码)String value_name = (String) name1.get(student); // TomSystem.out.println(student); 3.1.3 Class对象功能使用 – 创建对象123456// 有参构造方法Constructor cs = cls.getDeclaredConstructor(String.class, String.class);Object o = cs.newInstance(\"张三\", \"6\"); // 通过获取构造方法创建对象System.out.println(o);// 空参构造方法Object o1 = cls.newInstance(); 3.1.4 Class对象功能使用 – 方法调用12Method eat = cls.getMethod(\"eat\", String.class); // 确定带参的eat()方法eat.invoke(o, \"food\"); // 传入对象与参数,执行方法 3.2 实例需求:不改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法。 定义一个配置文件,配置文件中配置类名、方法名 12className=cn.lizhi.domain.StudentmethodName=eat 代码编写 123456789101112public static void main(String[] args) throws Exception { Properties pro = new Properties(); // 类加载器 ClassLoader负责将这个类加载进内存 InputStream is = InflectDemo01.class.getClassLoader().getResourceAsStream(\"pro.properties\"); pro.load(is); String className = pro.getProperty(\"className\"); String methodName = pro.getProperty(\"methodName\"); Class cls = Class.forName(className); // 获取class对象 Object obj = cls.newInstance(); // 创建对象 Method method = cls.getMethod(methodName,String.class); // 加载重载的方法 method.invoke(obj,\"fish\"); // 执行方法} 当我们需要创建其它类的对象和执行它的方法时,我们只需要修改配置文件即可,方便我们的解耦开发。 参考文献 [1] Java高级特性——反射 [2] 百度百科 [3] 黑马讲义","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"反射","slug":"反射","permalink":"https://chemlez.github.io/tags/%E5%8F%8D%E5%B0%84/"}]},{"title":"Java基础之Filter","slug":"Java基础之Filter","date":"2020-09-03T12:41:57.000Z","updated":"2020-09-17T01:34:09.531Z","comments":true,"path":"2020/09/03/Java基础之Filter/","link":"","permalink":"https://chemlez.github.io/2020/09/03/Java%E5%9F%BA%E7%A1%80%E4%B9%8BFilter/","excerpt":"一、概念Filter是Java Web的三大组件之一。Java Web三大组件分别是Servlet、Filter、Listener。 Filter的作用一般用于完成通用的操作。例如:登录验证、统一编码处理、敏感字符过滤…在实际开发中,过滤器就是对Web资源进行拦截,做一些处理后再交给下一个过滤器或servlet处理,通常都是用来拦截request进行处理,或者对返回的response进行拦截处理。其处理流程见下图: 拦截request可以简单理解为,在客户端向后端发送请求时,我们需要对其请求加一些”修饰”,将”修饰”后的请求带到后端。其中这个”修饰”是需要在这个请求的过程中完成的,这里因为是通用操作,可能是对所有的request进行”修饰”,所以并没有在客户端进行编写,否则当再加入一个request请求时,我们又要编写对应的规则,因此我们借用过滤器在请求过程中,对我们需要改写的request进行”修饰”。 其中,这里的”修饰”就可以理解为在原有的request请求中,再加入一些”修改”。例如:在Servlet中过多字符集编码发生变化需要修改时,你是选择对每个Servlet都进行修改,还是会选择一个通用的”规则”,来自动判断帮我们进行修改呢?而这里通用的”规则”就是Filter,我们可以把这些通用的字符集编码配置等工作放在Filter中,由Filter在请求过程中或返回过程中帮我们来实现。","text":"一、概念Filter是Java Web的三大组件之一。Java Web三大组件分别是Servlet、Filter、Listener。 Filter的作用一般用于完成通用的操作。例如:登录验证、统一编码处理、敏感字符过滤…在实际开发中,过滤器就是对Web资源进行拦截,做一些处理后再交给下一个过滤器或servlet处理,通常都是用来拦截request进行处理,或者对返回的response进行拦截处理。其处理流程见下图: 拦截request可以简单理解为,在客户端向后端发送请求时,我们需要对其请求加一些”修饰”,将”修饰”后的请求带到后端。其中这个”修饰”是需要在这个请求的过程中完成的,这里因为是通用操作,可能是对所有的request进行”修饰”,所以并没有在客户端进行编写,否则当再加入一个request请求时,我们又要编写对应的规则,因此我们借用过滤器在请求过程中,对我们需要改写的request进行”修饰”。 其中,这里的”修饰”就可以理解为在原有的request请求中,再加入一些”修改”。例如:在Servlet中过多字符集编码发生变化需要修改时,你是选择对每个Servlet都进行修改,还是会选择一个通用的”规则”,来自动判断帮我们进行修改呢?而这里通用的”规则”就是Filter,我们可以把这些通用的字符集编码配置等工作放在Filter中,由Filter在请求过程中或返回过程中帮我们来实现。 二、过滤器的快速使用其中过滤增强的方法写在doFilter中。 123456789101112131415161718192021@WebFilter(\"/*\") //访问所有资源之前,都会执行该过滤器public class FilterDemo1 implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { '''对request请求消息进行增强''' filterChain.doFilter(servletRequest,servletResponse); // 修饰完以后进行放行 '''放行回来后,对response响应消息的增强''' } @Override public void destroy() { }} filterChain.doFilter(servletRequest,servletResponse) 用于对我们的请求进行放行。doFilter中的参数就是request,response;带着请求消息和响应消息。 2.1 执行流程在doFilter放行前,对request请求进行增强,然后带着增强后的requeset进入到doFilter;从doFilter方法出来后,将后台返回的response进行增强,返回给前端。 2.2 过滤器生命周期方法 init:在服务器启动后,会创建Filter对象,然后调用init方法。只执行一次。用于加载资源。 doFilter:每一次请求被拦截资源时,会执行。执行多次。 destroy:在服务器关闭后,Filter对象被销毁。如果服务器是正常关闭,则会执行destroy方法。只执行一次。用于释放资源。 三、过滤器配置3.1 web.xml配置123456789<filter> <filter-name>demo1</filter-name> <filter-class>cn.itcast.web.filter.FilterDemo1</filter-class></filter><filter-mapping> <filter-name>demo1</filter-name> <!-- 拦截路径 --> <url-pattern>/*</url-pattern></filter-mapping> 3.2 拦截路径设置 具体资源路径: /index.jsp 只有访问index.jsp资源时,过滤器才会被执行 拦截目录: /user/* 访问/user下的所有资源时,过滤器都会被执行 后缀名拦截: *.jsp 访问所有后缀名为jsp资源时,过滤器都会被执行 拦截所有资源:/* 访问所有资源时,过滤器都会被执行 3.3 注解配置通过设置dispatcherTypes属性,设置拦截方式,即资源被访问的方式。 REQUEST:默认值。浏览器直接请求资源 FORWARD:转发访问资源(只有在转发访问资源时,才会被拦截器所拦截,直接访问反而不会了) INCLUDE:包含访问资源 ERROR:错误跳转资源 ASYNC:异步访问资源 以上是数组的形式,可以同时同时填写多个条件。 在web.xml中的配置在: 设置<dispatcher></dispatcher>标签即可 3.4 过滤器链即同时配置多个过滤器,多个过滤器对同一路径进行拦截时。 类似于栈的形式先进后出,例如有两个过滤器 A和B。 执行顺序即为:过滤器A –> 过滤器B –> 执行资源 –> 过滤器B –> 过滤器A 执行资源前的是对request的增强,执行资源后的是对response的增强 过滤器执行的先后顺序: 注解配置:按照类名的字符串比较规则比较,值小的先执行. 如: AFilter 和 BFilter,AFilter就先执行了。 web.xml配置: <filter-mapping>谁定义在上边,谁先执行. 四、过滤器基本案例实战4.1 登录验证(权限控制)对这一讲中–Java初试MVC及三层架构的登录进行验证。 要求: 如果已经登录,则直接放行。 如果没有登录,则跳转到登录页面,并提示信息。 思路: 设置LoginFilter。判断当前用户是否登录(判断Session中是否有User)。 如果用户已经登录,则对其进行放行 如果没有登录,提示用户进行先进行登录。 注意:先排除是否是登录相关的资源。 如果是,直接放行; 不是,判断是否登录。 代码实现: 登录权限的案例代码实现 123456789101112131415161718192021222324252627282930@WebFilter(\"/*\")public class LoginFilter implements Filter { public void destroy() { } public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { // 强转 -- 转换成子接口HttpServletRequest;也可以将ServletResponse转换成子接口HttpServletResponse HttpServletRequest request = (HttpServletRequest) req; String uri = request.getRequestURI(); // 获取请求路径 // 排除不需要过滤的资源路径 if (uri.contains(\"/login.jsp\") || uri.contains(\"/css/\") || uri.contains(\"/fonts/\") || uri.contains(\"/img/\") || uri.contains(\"/js/\") || uri.contains(\"/checkCode\") || uri.contains(\"/loginUser\")) { chain.doFilter(req, resp); } else { HttpSession session = request.getSession(); // 获取session Object user = session.getAttribute(\"adminUser\"); // 从session中获取用户登录信息 if (user != null) { // 不为空说明已经登录 chain.doFilter(req, resp); } else { // 否则提示用户进行登录,并转发至登录首页 request.setAttribute(\"adminUser_error\", \"请先登录\"); request.getRequestDispatcher(\"/login.jsp\").forward(request, resp); } } } public void init(FilterConfig config) throws ServletException { }} 4.2 敏感字符替换对数据进行敏感词汇过滤,然后用*进行替换。 重点: 将修改完的文字再设置回request域中,放行,继而传递至doFilter中,将请求数据传递给后台。使用新的request对象,图中是蓝色的request对象。通过代理模式实现对象的增强。 4.2.1 代理模式介绍4.2.1.1 设计模式:一些通用的解决固定问题的方式。代理模式: 概念 真实对象:被代理的对象(可以理解为原厂商) 代理对象:代理真实对象的对象。(可以理解为经销商–中间厂商) 代理模式:代理对象代理真实对象,达到增强真实对象功能的目的 实现方式 静态代理:有一个类文件描述代理模式 动态代理:在内存中形成代理类 实现步骤: 代理对象和真实对象实现相同的接口 代理对象 = Proxy.newProxyInstance(); 使用代理对象调用方法 增强方法 增强方式: 增强参数列表:获取具体的参数,对参数进行增强(替换参数等操作) 增强返回值类型:对返回值进行增强(即对返回值的操作) 增强方法体执行逻辑:在方法体中增强具体的逻辑操作 4.2.1.2 动态代理特点:字节码随用随创建,随用随加载 作用:不修改源码的基础上对方法增强 分类: 基于接口的动态代理 基于子类的动态代理 1. 基于接口的动态代理涉及的类:Proxy 提供者:JDK官方 如何创建代理对象: 使用Proxy类中的newProxyInstance方法 创建代理对象的要求: 被代理类最少实现一个接口,如果没有则不能使用 newProxyInstance方法的参数: ClassLoader:类加载器 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。(固定写法) Class[]:字节码数组 它是用于让代理对象和被代理对象有相同方法。(固定写法) InvocationHandler:用于提供增强的代码 它是让我们写如何代理。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。 2. 基于子类的动态代理涉及的类:Enhancer 提供者:第三方cglib库 如何创建代理对象: 使用Enhancer类中的create方法 创建代理对象的要求: 被代理类不能是最终类 create方法的参数: Class:字节码 它是用于指定被代理对象的字节码 Callback:用于提供增强的代码 它是让我们写如何代理。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类。 此接口的实现类都是谁用谁写(我们自己用,那便是我们自己写) 我们一般写的都是该接口的子接口的实现类:MethodInterceptor 4.2.1.3 案例1. 基于接口的动态代理实例定义接口: 1234public interface SaleComputer { String sale(double money); void show();} 定义真实对象: 123456789101112131415/** * 真实类 -- 作为真实对象 */public class Lenovo implements SaleComputer { @Override public String sale(double money) { System.out.println(\"花了\" + money + \"元买了一条电脑\"); return \"联想电脑\"; } @Override public void show() { System.out.println(\"展示电脑\"); }} 代理对象的使用逻辑: 代理对象的使用逻辑 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950public class ProxyTest { public static void main(String[] args) { Lenovo lenovo = new Lenovo(); //2.动态代理增强Lenovo对象 /** * 三个参数: * 1.类加载器:真实对象.getClass().getClassLoader() * 2.接口数组(保证代理对象和真实对象实现相同的接口):真实数组.getClass().getInterfaces() -- 真实对象的接口 * 3.处理器:new InvocationHandler() 这是所关心的,即我们的代理方式 * 4.这里的proxy_lenovo即为代理对象 */ // 创建代理对象 -- 使代理对象和真实对象实现相同的接口(相同接口类型) SaleComputer proxy_lenovo = (SaleComputer) Proxy.newProxyInstance(lenovo.getClass().getClassLoader(), lenovo.getClass().getInterfaces(), new InvocationHandler() { /* 代理逻辑编写的方法:代理对象调用的所有方法都会触发该方法执行 参数: 1.proxy:代理对象 2.method:代理对象调用的方法。被封装为的对象 3.args:代理对象调用的方法时,传递的实际参数(调用方法中的参数列表) */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// System.out.println(\"该方法被执行了...\");// System.out.println(method.getName());// System.out.println(args[0]); // 1.增强参数 if (method.getName().equals(\"sale\")) { double money = (double) args[0]; money = money * 0.85; // 对参数列表进行增强 // 使用真实对象调用该方法 String obj = (String) method.invoke(lenovo, money); // 2. 增强返回值 return obj+\"_鼠标垫\"; } else { Object obj = method.invoke(lenovo, args); return obj; } } }); // 3.调用方法 String computer = proxy_lenovo.sale(8000); // 确定了 方法名 -- sale;参数列表 -- {8000}// System.out.println(computer);// proxy_lenovo.show(); }} 2. 基于子类的动态代理实例 被代理对象类 123456789101112131415161718192021/** * 一个生产者 */public class Producer { /** * 销售 * @param money */ public void saleProduct(float money){ System.out.println(\"销售产品,并拿到钱:\"+money); } /** * 售后 * @param money */ public void afterService(float money){ System.out.println(\"提供售后服务,并拿到钱:\"+money); }} 代理对象类 1234567891011121314151617181920212223242526272829303132/** * 模拟一个消费者 */public class Client { public static void main(String[] args) { final Producer producer = new Producer(); Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() { /** * 执行被代理对象的任何方法都会经过该方法 * @param proxy * @param method * @param args * 以上三个参数和基于接口的动态代理中invoke方法的参数是一样的 * @param methodProxy :当前执行方法的代理对象 * @return * @throws Throwable */ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { //提供增强的代码 Object returnValue = null; //1.获取方法执行的参数 Float money = (Float)args[0]; //2.判断当前方法是不是销售 -- 若是的话,则对方法进行增强 if(\"saleProduct\".equals(method.getName())) { returnValue = method.invoke(producer, money*0.8f); } return returnValue; } }); cglibProducer.saleProduct(12000f); }} 4.2.2 敏感词汇过滤代码逻辑:获取request中带有请求参数的方法,例如:getParameter、getParameterMap、getParameterValue等。对其方法的返回值进行判定,是否存在敏感值。如果存在,则对返回值进行增强。 敏感词汇过滤代码 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687@WebFilter(\"/*\")public class SensitiveWordsFilter implements Filter { public void destroy() { } public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { // 真实对象和代理对象都实现 ServletRequest 接口 ServletRequest request = (ServletRequest) Proxy.newProxyInstance(req.getClass().getClassLoader(), req.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals(\"getParameter\")) { // 存在getParameter方法 String value = (String) method.invoke(req, args); if (value != null) { for (String word : list) { if (value.contains(word)) { value = value.replaceFirst(word, \"**\"); // 获取新的value } } } return value; } else if (method.getName().equals(\"getParameterMap\")) { // 存在getParameterMap方法 Map<String, String[]> parameterMap = (Map<String, String[]>) method.invoke(req, args); if (parameterMap != null && !parameterMap.isEmpty()) { Set<String> keySet = parameterMap.keySet(); for (String key : keySet) { String[] values = parameterMap.get(key); for (int i = 0; i < values.length; i++) { for (String word : list) { if (values[i].contains(word)) { parameterMap.get(key)[i] = values[i].replaceAll(word, \"**\"); } } } } } return parameterMap; } else if (method.getName().equals(\"getParameterValues\")) { String[] values = (String[]) method.invoke(req, args); if (values != null && values.length > 0) { for (int i = 0; i < values.length; i++) { for (String word : list) { if (values[i].contains(word)) { values[i] = values[i].replaceAll(word, \"**\"); } } } } return values; } // 如果不是以上方法,就返回客户端传递的req对象(旧对象)原来的方法返回值即可,而以上方法传递的是新的req对象方法返回值(经过增强后的返回值) -- 对返回值进行了增强 return method.invoke(req, args); } }); chain.doFilter(request, resp); } private List<String> list = new ArrayList<String>(); /* 用于加载敏感词汇表,存放在列表中。init初始时加载 -- 只加载一次 */ public void init(FilterConfig config) throws ServletException { try { // 获取真实路径 ServletContext servletContext = config.getServletContext(); String realPath = servletContext.getRealPath(\"/WEB-INF/classes/word\"); // 读取文件 BufferedReader br = new BufferedReader(new FileReader(realPath)); // 将文件添加到集合当中 String line = null; while ((line = br.readLine()) != null) { list.add(line); } br.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }} 参考文献 [1] Java Web之过滤器(Filter) [2] Java过滤器–百度百科 [3] 黑马讲义","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"Filter","slug":"Filter","permalink":"https://chemlez.github.io/tags/Filter/"},{"name":"动态代理","slug":"动态代理","permalink":"https://chemlez.github.io/tags/%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86/"}]},{"title":"Servlet优化之功能重组成模块","slug":"Servlet优化之功能重组成模块","date":"2020-08-31T12:15:11.000Z","updated":"2020-09-01T01:41:48.317Z","comments":true,"path":"2020/08/31/Servlet优化之功能重组成模块/","link":"","permalink":"https://chemlez.github.io/2020/08/31/Servlet%E4%BC%98%E5%8C%96%E4%B9%8B%E5%8A%9F%E8%83%BD%E9%87%8D%E7%BB%84%E6%88%90%E6%A8%A1%E5%9D%97/","excerpt":"在之前的总结Java初试MVC及三层架构中,查看其目录结构仅仅对于User对象的操作就写了6,7个servlet,而每一个servlet只对应一个功能,但随着以后项目的扩大,业务逻辑的复杂化,我们需要操作的对象不仅仅是一个User类时,换句话说,我们操作数据库中的表不止一张时,那么可想而知我们的servlet需要写多少! 思考:我们能否像service层和dao层一样,将关于User的操作都写在一个类当中,方便我们的使用呢。从而减少Servlet的数量,现在是一个功能一个Servlet,将其优化为一个模块一个Servlet,相当于在数据库中一张表对应一个Servlet,在Servlet中提供不同的方法,完成用户的请求。 我们编写的所有servlet都继承了HttpServlet,与此同时都复写了doGet以及doPost方法。查看HttpServlet源码,可以看见对doGet以及doPost的方法的调用都写在service中。","text":"在之前的总结Java初试MVC及三层架构中,查看其目录结构仅仅对于User对象的操作就写了6,7个servlet,而每一个servlet只对应一个功能,但随着以后项目的扩大,业务逻辑的复杂化,我们需要操作的对象不仅仅是一个User类时,换句话说,我们操作数据库中的表不止一张时,那么可想而知我们的servlet需要写多少! 思考:我们能否像service层和dao层一样,将关于User的操作都写在一个类当中,方便我们的使用呢。从而减少Servlet的数量,现在是一个功能一个Servlet,将其优化为一个模块一个Servlet,相当于在数据库中一张表对应一个Servlet,在Servlet中提供不同的方法,完成用户的请求。 我们编写的所有servlet都继承了HttpServlet,与此同时都复写了doGet以及doPost方法。查看HttpServlet源码,可以看见对doGet以及doPost的方法的调用都写在service中。 HttpServlet中service方法源码 1234567891011121314151617181920212223242526272829303132333435363738394041424344protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); long lastModified; if (method.equals(\"GET\")) { lastModified = this.getLastModified(req); if (lastModified == -1L) { this.doGet(req, resp); } else { long ifModifiedSince; try { ifModifiedSince = req.getDateHeader(\"If-Modified-Since\"); } catch (IllegalArgumentException var9) { ifModifiedSince = -1L; } if (ifModifiedSince < lastModified / 1000L * 1000L) { this.maybeSetLastModified(resp, lastModified); this.doGet(req, resp); } else { resp.setStatus(304); } } } else if (method.equals(\"HEAD\")) { lastModified = this.getLastModified(req); this.maybeSetLastModified(resp, lastModified); this.doHead(req, resp); } else if (method.equals(\"POST\")) { this.doPost(req, resp); } else if (method.equals(\"PUT\")) { this.doPut(req, resp); } else if (method.equals(\"DELETE\")) { this.doDelete(req, resp); } else if (method.equals(\"OPTIONS\")) { this.doOptions(req, resp); } else if (method.equals(\"TRACE\")) { this.doTrace(req, resp); } else { String errMsg = lStrings.getString(\"http.method_not_implemented\"); Object[] errArgs = new Object[]{method}; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(501, errMsg); }} 通过查看以上的源码,我们可以看到service是对当前进入到servlet方法进行判断,然后调用相对应的方法。受到源码的启发,我们是不是可以重写service方法,在里面对我们自己定义的方法进行判断,继而进行使用呢。 基本思路:定义一个BaseServlet类继承HttpServlet,用来重写service方法。然后,我们再定义一个User相关的servlet类继承BaseServlet,我们把之前功能分散的servlet都写在这个类中。 对于重写service方法,而覆盖了原有的doGet,doPost方法,如何对完成请求的疑惑解答。 (当时重写service时,产生了没有了doGet,doPost方法,那么我以后的请求都是如何请求,以及采用哪种请求方式的疑惑,后来查询了相关的博客,现将查询的资料以及自己的理解写在下方,如有不正确请指正) HttpServlet中的service方法是用于转向(get,post,put,delete…对网页的请求方式进行判断)。当重写了service方法时,此时的service就不是用来转向的,而是用来处理业务的,现在不论客户端是用post还是get来请求此servlet,都会执行service方法来调用相应的方法。(可以简单理解为service是客户端向后端传递数据的接口,必须由此进入路径) 简而言之,后端只需要进行方法的调用,不必关心是采用哪种请求方式。是否还记得,在原来的doPost以及doGet中,其中一个方法中,会有这么一句,例如,在doGet方法体中会写this.doPost(request,response),从而达到方法的复用。然而这一句是不是也间接的告诉了我们,两者内部的处理逻辑是一样的。 好了,有了以上的思路以及解答了上面的疑惑,下面我们开始对代码进行重构。 首先给出BaseServlet以及子类User的servlet: BaseServlet的代码 123456789101112131415161718192021public class BaseServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1.获取请求路径 String uri = request.getRequestURI(); // 请求路径(例如:xx/user/add)其中xx代表虚拟目录,而add既包含在请求的资源路径中,也是我们的方法名称 // 2.获取方法名称 String methodName = uri.substring(uri.lastIndexOf('/') + 1); // 通过访问路径,获取方法名称 -- 截取字符串的长度,返回结果 add try { // 3.获取方法对象Method Method method = this.getClass().getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);// 这里的this代表BaseServlet的子类调用者,这里就是UserServlet // 4.执行方法 method.invoke(this, request, response); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }} UserServlet代码 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114/** * 对于UserServlet的优化 * 将单一功能进行模块化 */@WebServlet(\"/user/*\") // user路径下的所有资源都可以通过UserServlet被访问到public class UserServlet extends BaseServlet { /** * 用户增加的方法 * * @param request * @param response * @throws ServletException * @throws IOException */ public void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(\"utf-8\"); User user = new User(); Map<String, String[]> parameterMap = request.getParameterMap(); try { BeanUtils.populate(user, parameterMap); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } System.out.println(user.toString()); UserService service = new UserServiceImpl(); service.addUser(user); HttpSession session = request.getSession(); response.sendRedirect(request.getContextPath() + \"/user/find\"); } /** * 用户删除 * @param request * @param response * @throws ServletException * @throws IOException */ public void delete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String id = request.getParameter(\"id\"); UserService service = new UserServiceImpl(); service.delete(Integer.parseInt(id)); response.sendRedirect(request.getContextPath() + \"/user/find\"); } /** * 查询用户 * @param request * @param response * @throws ServletException * @throws IOException */ public void find(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(\"utf-8\"); String currentPage = request.getParameter(\"currentPage\"); String rows = request.getParameter(\"rows\"); if (currentPage == null || \"\".equals(currentPage)) { currentPage = \"1\"; } if (rows == null || \"\".equals(rows)) { rows = \"5\"; } Map<String, String[]> condition = request.getParameterMap(); UserService service = new UserServiceImpl(); PageBean<User> userByPage = service.findUserByPage(Integer.parseInt(currentPage), Integer.parseInt(rows),condition); request.setAttribute(\"userByPages\", userByPage); request.setAttribute(\"condition\",condition); request.getRequestDispatcher(\"/list.jsp\").forward(request, response); } /** * 用户登录方法 * @param request * @param response * @throws ServletException * @throws IOException */ public void active(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(\"utf-8\");// 设置编码 User loginUser = new User(); Map<String, String[]> parameterMap = request.getParameterMap(); // 获取request全部提交的参数 String[] checkCodes = parameterMap.get(\"verifycode\"); // 取验证码 HttpSession session = request.getSession(); String checkCode_session = (String) session.getAttribute(\"checkCode_session\");// 取生成的真正正确的验证码 session.removeAttribute(\"checkCode_session\"); // 获取验证码后,销毁验证码信息 // 判断验证码是否正确 if (checkCode_session == null || !checkCode_session.equalsIgnoreCase(checkCodes[0])) { // 验证码不正确 -- 存储转发至首页重新登录 request.setAttribute(\"code_error\", \"验证码输入错误\"); request.getRequestDispatcher(\"/login.jsp\").forward(request, response); } else { // 对username和password进行封装 try { BeanUtils.populate(loginUser, parameterMap); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } UserService service = new UserServiceImpl(); User adminUser = service.adminUser(loginUser); // 查询数据库 if (adminUser != null) { // 查询成功 session.setAttribute(\"adminUser\", adminUser); response.sendRedirect(request.getContextPath() + \"/index.jsp\"); } else { // 查询失败 -- 存储转发至首页重新登录 request.setAttribute(\"adminUser_error\", \"用户名或者密码错误\"); request.getRequestDispatcher(\"/login.jsp\").forward(request, response); } } }} 首先UserServlet继承BaseServlet,而请求的资源路径是由子类的UserServlet决定,其注解@WebServlet("/user/*")。其中的通配符*就是用来匹配下方对应的方法名称。这里需要发挥作用的就需要父类BaseServlet中service方法通过反射的技术获取到资源路径,而资源路径中就包含了我们子类UserServlet的方法名,所以就可以通过反射的技术来使用定义在子类中的方法。 还有需要注意的一点是,子类中方法采用public修饰而不用protect修饰是因为作用域的原因。 参考文献 [1] servlet中的service()方法重写与不重写 [2] 黑马讲义笔记","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"Servlet","slug":"Servlet","permalink":"https://chemlez.github.io/tags/Servlet/"},{"name":"反射","slug":"反射","permalink":"https://chemlez.github.io/tags/%E5%8F%8D%E5%B0%84/"}]},{"title":"前端之表单验证","slug":"前端之表单验证","date":"2020-08-27T01:00:35.000Z","updated":"2020-08-31T01:36:06.650Z","comments":true,"path":"2020/08/27/前端之表单验证/","link":"","permalink":"https://chemlez.github.io/2020/08/27/%E5%89%8D%E7%AB%AF%E4%B9%8B%E8%A1%A8%E5%8D%95%E9%AA%8C%E8%AF%81/","excerpt":"一、简概因为,前端经常涉及到表单验证,故此篇博客用于记录前端JS对表单验证的方法。 通过表单验证,当对form表单提交时,可以防止不合法的数据传递至后台(以及判空操作)。这里我们先定义一个表单,作为示例,其它的情况都可以此类推。 前端表单代码 123456789101112131415161718192021222324252627282930<div> <form method=\"get\" action=\"https://www.baidu.com\" id=\"form\"> <table align=\"center\" style=\"margin: 0 auto;\"> <tr> <td><label>用户名</label></td> <td><input type=\"text\" name=\"username\" placeholder=\"请输入用户名\" style=\"width: 150px\" id=\"username\"></td> </tr> <tr> <td><label>邮箱</label></td> <td><input type=\"email\" name=\"email\" placeholder=\"请输入邮箱\" style=\"width: 150px\" id=\"email\"></td> </tr> <tr> <td><label>性别</label></td> <td align=\"left\"> <input type=\"radio\" name=\"male\" value=\"male\" checked>男 <input type=\"radio\" name=\"female\" value=\"female\">女 </td> </tr> <tr> <td><label>出生日期</label></td> <td><input type=\"date\" name=\"birthday\" style=\"width: 150px\" id=\"birthday\"></td> </tr> <tr> <td align=\"center\" colspan=\"2\"> <input type=\"submit\" name=\"register\" value=\"注册\" style=\"margin-top: 5px\"> </td> </tr> </table> </form></div> 前端进行表单验证只需作用在两个地方即可: 绑定当前需要验证的input标签的离焦事件。 对form表单的submit进行全部的input标签判定 当结果全部返回true才能进行提交,否则无法提交。","text":"一、简概因为,前端经常涉及到表单验证,故此篇博客用于记录前端JS对表单验证的方法。 通过表单验证,当对form表单提交时,可以防止不合法的数据传递至后台(以及判空操作)。这里我们先定义一个表单,作为示例,其它的情况都可以此类推。 前端表单代码 123456789101112131415161718192021222324252627282930<div> <form method=\"get\" action=\"https://www.baidu.com\" id=\"form\"> <table align=\"center\" style=\"margin: 0 auto;\"> <tr> <td><label>用户名</label></td> <td><input type=\"text\" name=\"username\" placeholder=\"请输入用户名\" style=\"width: 150px\" id=\"username\"></td> </tr> <tr> <td><label>邮箱</label></td> <td><input type=\"email\" name=\"email\" placeholder=\"请输入邮箱\" style=\"width: 150px\" id=\"email\"></td> </tr> <tr> <td><label>性别</label></td> <td align=\"left\"> <input type=\"radio\" name=\"male\" value=\"male\" checked>男 <input type=\"radio\" name=\"female\" value=\"female\">女 </td> </tr> <tr> <td><label>出生日期</label></td> <td><input type=\"date\" name=\"birthday\" style=\"width: 150px\" id=\"birthday\"></td> </tr> <tr> <td align=\"center\" colspan=\"2\"> <input type=\"submit\" name=\"register\" value=\"注册\" style=\"margin-top: 5px\"> </td> </tr> </table> </form></div> 前端进行表单验证只需作用在两个地方即可: 绑定当前需要验证的input标签的离焦事件。 对form表单的submit进行全部的input标签判定 当结果全部返回true才能进行提交,否则无法提交。 二、代码2.1 单个标签的离焦事件 用户名:限制用户名的长度为3~10位。 邮箱:符合邮箱的规则(xxx+@xx+.xx+)其中的+号代表正则的通配符。 出生日期:非空即可。 1.用户名对username绑定离焦事件 – 采用JS的方式 12345678910111213function checkusername() { var username = $('#username'); var value = username.val(); var regex = /^\\w{3,10}$/; //用户名长度3-10 var flag = regex.test(value); if (flag) { // 如果验证通过 username.css('border', ''); } else { // 如果验证不通过 username.css('border', '2px solid red'); // 验证不通过输入框显示红色 } return flag; }$('#username').blur(checkusername); // 对username绑定离焦事件,触发checkusername函数 示例: 当对用户名输入不合法时,输入框会报红: 符合规定时: 2.邮箱同理,只要写出正确的正则表达式即可。 123456789101112 function checkemail() { var val = $('#email').val(); var regx = /^\\w+@\\w+\\.\\w+$/; // 邮箱的验证规则 var flag = regx.test(val); if (flag) { $('#email').css('border', ''); } else { $('#email').css('border', '2px solid red') } return flag }$('#email').blur(checkemail); 3.出生日期 非空非空的正则表达式:var regex = /\\S/; 12345678910111213 function checkbirthday() { var birthday = $('#birthday'); var value = birthday.val(); var regex = /\\S/; // 非空验证 var flag = regex.test(value); if (flag) { // 如果验证通过 birthday.css('border', ''); } else { // 如果验证不通过 birthday.css('border', '2px solid red'); } return flag; }$('#birthday').blur(checkbirthday); 注意:以上所有的函数返回flag标记,是最后对表单form的submit事件进行判定,只有当以上三个全部返回true时,那么表单才能够进行提交。 2.2 表单提交的验证1234// 当submit接收false时不提交,接收true时才进行提交$('#form').submit(function () { return checkemail() && checkusername() && checkbirthday(); }); 2.3 全部代码 前端全部代码 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107<!DOCTYPE html><html lang=\"en\"><head> <meta charset=\"UTF-8\"> <title>form表单验证</title> <script src=\"js/jquery-3.2.1.min.js\"></script> <link rel=\"stylesheet\" href=\"css/注册样式.css\"/> <style> div { padding-top: 200px; margin: 0 auto; text-align: center; } </style></head><body><div> <form method=\"get\" action=\"https://www.baidu.com\" id=\"form\"> <table align=\"center\" style=\"margin: 0 auto;\"> <tr> <td><label>用户名</label></td> <td><input type=\"text\" name=\"username\" placeholder=\"请输入用户名\" style=\"width: 150px\" id=\"username\"></td> </tr> <tr> <td><label>邮箱</label></td> <td><input type=\"email\" name=\"email\" placeholder=\"请输入邮箱\" style=\"width: 150px\" id=\"email\"></td> </tr> <tr> <td><label>性别</label></td> <td align=\"left\"> <input type=\"radio\" name=\"male\" value=\"male\" checked>男 <input type=\"radio\" name=\"female\" value=\"female\">女 </td> </tr> <tr> <td><label>出生日期</label></td> <td><input type=\"date\" name=\"birthday\" style=\"width: 150px\" id=\"birthday\"></td> </tr> <tr> <td align=\"center\" colspan=\"2\"> <input type=\"submit\" name=\"register\" value=\"注册\" style=\"margin-top: 5px\"> </td> </tr> </table> </form></div><script> function checkusername() { var username = $('#username'); var value = username.val(); var regex = /^\\w{3,10}$/; //用户名长度3-10 var flag = regex.test(value); if (flag) { // 如果验证通过 username.css('border', ''); } else { // 如果验证不通过 username.css('border', '2px solid red'); // 验证不通过输入框显示红色 } return flag; } function checkemail() { var val = $('#email').val(); var regx = /^\\w+@\\w+\\.\\w+$/; var flag = regx.test(val); if (flag) { $('#email').css('border', ''); } else { $('#email').css('border', '2px solid red') } return flag } function checkbirthday() { var birthday = $('#birthday'); var value = birthday.val(); var regex = /\\S/; // 非空验证 var flag = regex.test(value); if (flag) { // 如果验证通过 birthday.css('border', ''); } else { // 如果验证不通过 birthday.css('border', '2px solid red'); } return flag; } $('#email').blur(checkemail); $('#birthday').blur(checkbirthday); $('#username').blur(checkusername); $('#form').submit(function () { return checkemail() && checkusername() && checkbirthday(); });</script></body></html> 三、附 常用表单验证的正则表达式 待续","categories":[{"name":"前端","slug":"前端","permalink":"https://chemlez.github.io/categories/%E5%89%8D%E7%AB%AF/"}],"tags":[{"name":"表单验证","slug":"表单验证","permalink":"https://chemlez.github.io/tags/%E8%A1%A8%E5%8D%95%E9%AA%8C%E8%AF%81/"},{"name":"JavaScript","slug":"JavaScript","permalink":"https://chemlez.github.io/tags/JavaScript/"},{"name":"JQuery","slug":"JQuery","permalink":"https://chemlez.github.io/tags/JQuery/"}]},{"title":"Redis思考及基础案例实战","slug":"Redis基础案例","date":"2020-08-26T01:00:33.000Z","updated":"2020-08-26T12:48:40.035Z","comments":true,"path":"2020/08/26/Redis基础案例/","link":"","permalink":"https://chemlez.github.io/2020/08/26/Redis%E5%9F%BA%E7%A1%80%E6%A1%88%E4%BE%8B/","excerpt":"一、简单介绍redis是一款高性能的NOSQL系列的非关系型数据库。主要用于缓存,可提升数据访问的性能。这里用于做缓存的数据是不经常做改变的数据。核心思想见下图: 使用缓存机制,可以加快我们数据的访问。因为数据是暂存在内存中,直接访问内存的数据可以减少在访问数据库过程中的I/O操作,这样便可以提升系统的性能,查询速度。但是作为缓存也有一定的缺点:数据因为是暂存在内存上的,一旦redis服务端关闭,再次开启时,缓存数据将不复存在。因此在某些场合中,我们需要对redis缓存数据做持久化操作,将其持久化到硬盘上,当再次查询时,可将数据读取到缓存中。 从以上我们看出redis使用时的两点注意事项: 数据不会经常改变。如果,数据持续改变,就不断的访问数据库,再将数据放入到缓存中。 确定持久化操作的条件。不能随时随地的进行持久化(反而增加了IO操作),也不能对缓存中大量改变的数据不做持久化数据(会导致数据大量的丢失)。","text":"一、简单介绍redis是一款高性能的NOSQL系列的非关系型数据库。主要用于缓存,可提升数据访问的性能。这里用于做缓存的数据是不经常做改变的数据。核心思想见下图: 使用缓存机制,可以加快我们数据的访问。因为数据是暂存在内存中,直接访问内存的数据可以减少在访问数据库过程中的I/O操作,这样便可以提升系统的性能,查询速度。但是作为缓存也有一定的缺点:数据因为是暂存在内存上的,一旦redis服务端关闭,再次开启时,缓存数据将不复存在。因此在某些场合中,我们需要对redis缓存数据做持久化操作,将其持久化到硬盘上,当再次查询时,可将数据读取到缓存中。 从以上我们看出redis使用时的两点注意事项: 数据不会经常改变。如果,数据持续改变,就不断的访问数据库,再将数据放入到缓存中。 确定持久化操作的条件。不能随时随地的进行持久化(反而增加了IO操作),也不能对缓存中大量改变的数据不做持久化数据(会导致数据大量的丢失)。 二、案例实操通过对redis的基本介绍,我们做一个小案例。案例需求如下: 提供index.html页面,页面中有一个省份下拉列表 当页面加载完成后,发送ajax请求,加载所有省份 思路: 当接收到请求时,做redis缓存的查询。如果缓存中存在所需要的数据,就将缓存数据进行返回;如果不存在,就进行数据库查询,同时将数据库中的数据加入到缓存中,再将数据进行返回。 前端接收到的数据是序列化后的Json数据,便于数据的读取,进行页面数据的展示。 思考:为什么这里要使用ajax?当我们对页面进行加载时,就需要自动显示后台传递的数据,而没有进行任何链接的操作(form表单提交、点击超链接等操作),而让页面主动对后端进行请求,所以我们这里需要使用ajax,简化我们的操作。 第二点是因为异步请求是为了获取服务器响应的数据,而前端使用的是html,不能够直接从servlet相关的域对象获取值,只能通过ajax获取相应的数据。 以下便是具体实现的代码: dao层中findAll方法的实现 1234567891011public class ProvinceDaoImpl implements ProvinceDao { private JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource()); @Override public List<Province> findAll() { String sql = \"select * from province\"; List<Province> list = template.query(sql, new BeanPropertyRowMapper<Province>(Province.class)); return list; }} service层中findAll方法的实现 123456789101112131415161718192021222324252627public class ProvinceServiceImpl implements ProvinceService { private ProvinceDao provinceDao = new ProvinceDaoImpl(); @Override public String findAllByRedis() { Jedis jedis = JedisUtils.getJedis(); // 创建Jedis对象,用于redis的操作 String province = jedis.get(\"province\"); // 获取需要查询的对象 if (province == null || province.length() == 0) { // 若缓存中不存在则查询数据库 System.out.println(\"缓存中没有,先对数据库进行查询\"); List<Province> list = provinceDao.findAll(); // 调用dao中,数据库查询操作 ObjectMapper mapper = new ObjectMapper(); // 数据序列化 -- Json格式 try { province = mapper.writeValueAsString(list); jedis.set(\"province\", province); } catch (JsonProcessingException e) { e.printStackTrace(); } finally { jedis.close(); } } else { System.out.println(\"查询的数据在缓存中\"); } return province; }} web层中FindAllProvince类 12345678910111213@WebServlet(\"/findAllProvince\")public class FindAllProvince extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(\"application/json;charset=utf-8\");// 设置编码格式 ProvinceService service = new ProvinceServiceImpl(); String json_list = service.findAllByRedis(); // Redis缓存机制 response.getWriter().write(json_list); // Ajax数据返回 } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request, response); }} 2.1 Util包 JDBCUtils工具类 1234567891011121314151617181920212223242526272829public class JDBCUtils { private static DataSource ds; static { InputStream is = JDBCUtils.class.getClassLoader().getResourceAsStream(\"druid.properties\");// 配置文件的字节输入流 Properties properties = new Properties(); try { properties.load(is); // 属性集的加载 -- map,key:value ds = DruidDataSourceFactory.createDataSource(properties);// 初始化数据库连接池 } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } public static DataSource getDataSource() { return ds; } public static Connection getConnection() throws SQLException { // 用于获取连接对象 return ds.getConnection(); }} JedisUtils工具类 123456789101112131415161718192021222324252627282930public class JedisUtils { private static Jedis jedis; static { InputStream is = JedisUtils.class.getClassLoader().getResourceAsStream(\"jedis.properties\"); Properties properties = new Properties(); try { properties.load(is); } catch (IOException e) { e.printStackTrace(); } JedisPoolConfig config = new JedisPoolConfig(); // 配置对象,可对redis进行配置 String password = (String) properties.get(\"password\"); String port = properties.getProperty(\"port\"); String host = (String) properties.get(\"host\"); String maxTotal = (String) properties.get(\"maxTotal\"); String maxIdle = (String) properties.get(\"maxIdle\"); config.setMaxIdle(Integer.parseInt(maxIdle)); config.setMaxTotal(Integer.parseInt(maxTotal)); JedisPool jedisPool = new JedisPool(config, host, Integer.parseInt(port)); jedis = jedisPool.getResource(); jedis.auth(password); } public static Jedis getJedis() { // 返回Jedis对象 return jedis; }} 2.2前端代码 前端代码展示 1234567891011121314151617181920212223<!DOCTYPE html><html lang=\"en\"><head> <meta charset=\"UTF-8\"> <title>省份导入</title> <script src=\"js/jquery-3.3.1.min.js\"></script></head><body><select id=\"province\"> <option>--请选择省份--</option></select><script> $.get(\"findAllProvince\", {}, function (data) { $(data).each(function () { var option = \"<option name=(''+this.id) value=(''+this.id)>\"+this.name+\"</option>\"; // value中的值是被送往服务器作为请求参数,而标签中的值是作为前端的展示值不是作为请求参数。 $province.append(option) }); });</script></body></html> 2.3 注意事项redis是用于缓存一些不经常发生变化的数据。 数据库的数据一旦发生改变,则需要更新缓存。 数据库的表执行增删改的相关操作,需要将对应的redis缓存数据清空,再次存入 在service对应的增删改方法中,将redis数据删除。","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"redis","slug":"redis","permalink":"https://chemlez.github.io/tags/redis/"}]},{"title":"Git的基本操作","slug":"Git的基本使用","date":"2020-08-20T01:43:49.000Z","updated":"2022-05-15T14:00:19.928Z","comments":true,"path":"2020/08/20/Git的基本使用/","link":"","permalink":"https://chemlez.github.io/2020/08/20/Git%E7%9A%84%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8/","excerpt":"本文借鉴廖雪峰老师博客而整理的git相关总结,以方便自己查阅使用。 1.Git用户设定及其配置1.Git配置Git 提供了一个叫做 git config 的工具,专门用来配置或读取相应的工作环境变量。这些环境变量,决定了 Git 在各个环节的具体工作方式和行为。这些变量可以存放在以下三个不同的地方: /etc/gitconfig 文件:系统中对所有用户都普遍适用的配置。若使用 git config 时用 --system 选项,读写的就是这个文件。 ~/.gitconfig 文件:用户目录下的配置文件只适用于该用户。若使用 git config 时用 --global 选项,读写的就是这个文件。 当前项目的 Git 目录中的配置文件(也就是工作目录中的 .git/config 文件):这里的配置仅仅针对当前项目有效。每一个级别的配置都会覆盖上层的相同配置,所以 .git/config 里的配置会覆盖 /etc/gitconfig 中的同名变量。","text":"本文借鉴廖雪峰老师博客而整理的git相关总结,以方便自己查阅使用。 1.Git用户设定及其配置1.Git配置Git 提供了一个叫做 git config 的工具,专门用来配置或读取相应的工作环境变量。这些环境变量,决定了 Git 在各个环节的具体工作方式和行为。这些变量可以存放在以下三个不同的地方: /etc/gitconfig 文件:系统中对所有用户都普遍适用的配置。若使用 git config 时用 --system 选项,读写的就是这个文件。 ~/.gitconfig 文件:用户目录下的配置文件只适用于该用户。若使用 git config 时用 --global 选项,读写的就是这个文件。 当前项目的 Git 目录中的配置文件(也就是工作目录中的 .git/config 文件):这里的配置仅仅针对当前项目有效。每一个级别的配置都会覆盖上层的相同配置,所以 .git/config 里的配置会覆盖 /etc/gitconfig 中的同名变量。 2.用户信息12$ git config --global user.name \"runoob\"$ git config --global user.email [email protected] 如果用了 --global 选项,那么更改的配置文件就是位于你用户主目录下的那个,以后你所有的项目都会默认使用这里配置的用户信息。 如果要在某个特定的项目中使用其他名字或者电邮,只要去掉 --global 选项重新配置即可,新的设定保存在当前项目的 .git/config 文件里。 3.查看配置信息1git config --list 2.版本库的创建1234git init # 初始化一个仓库git add <file1> <file2> ... <filen> # 添加文件,并将文件修改添加到暂存区git commit -m <message> # 添加文件到Git仓库,提交修改,将暂存区的所有内容提交到当前分支git commit -am \"committed message\" # 一次提交所有在暂存区中改动的文件到版本库 3.查看历史12git log # 显示从最近到最远的提交日志 -- 显示最近的3次提交git log --pretty =oneline # 简略输出 4.版本回退在git中HEAD代表的是当前版本,可以将HEAD理解为一个指针(指向版本)。上一个版本是HEAD^,上上版本为HEAD^^…,若向上100个版本则为HEAD^100. 123git reset --hard HEAD^ # 返回至上一个版本git reset --hard 版本号 # 达到指定版本git reflog # 记录着每一次的命令(查看历史命令) 5.工作区与暂存区 工作区:包含整个项目的文件夹(.git隐藏文件夹不包含在工作区内)。 版本库:工作区中的隐藏目录.git,不算工作区,而是Git的版本库。 Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。具体结构分析参考廖雪峰此节。 其中在创建Git版本库时,Git自动为我们创建了唯一一个master分支。 暂存区:英文叫stage, 或index。一般存放在 “.git目录下” 下的index文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)。 1git status # 查看工作区的状态 Git版本控制下的文件状态的三种状态: 已提交(committed):该文件已经被安全地保存在本地数据库中了。 已修改(modified):修改了某个文件,但还没有提交保存。 已暂存(staged):把已修改的文件放在下次提交时要保存的清单中。 6.修改操作注意:Git跟踪管理的是修改而非文件。每次需要将工作区的修改添加入到暂存区中,然后再进行commit,将暂存区中跟踪的修改的进行全部的提交。 其中: 1234git diff HEAD -- <file> # 可以查看工作区和版本库里面最新版本的区别git diff ANode BNode 用于比较项目中任意两个版本(分支)的差异,即A B两个节点之间的差异同理,可以比较两个分支之间的差异git diff --cached # 比较当前索引和上次提交间的差异 以及在--name--status,只看文件列表 几种撤销修改方式: 未添加到暂存区 1git checkout -- <file> # 让这个文件回到最近一次git commit或git add时的状态 添加到暂存区未commit到分支 12git reset HEAD <file> # 将暂存区的修改撤销(unstage) -- 即回到场景1(这里HEAD表示最新的版本)git checkout -- <file> # 同场景1,丢弃工作区的修改 添加到分支中,未推送至远端 版本回退 进行情况2中的两步 7.文件删除在git add后,即添加到版本库之后。见以下操作: 123# 以test.txt文件为例git add test.txt # 提交到版本库rm test.txt # 删除工作区的文件 此时就会出现工作区和版本库不一致的情况。通过git status命令查看工作区的状态。此时有两种选择: git rm test.txt git commit -m \"remove test.txt\" # 提交至分支中 12345 此时,文件就从版本库中被删除。2. ```bash git checkout -- test.txt 误删。但是版本库中存在,可以通过以上命令将其恢复到最新版本。 `git checkout`是用版本库里的版本替换工作区的版本,故无论工作区是修改还是删除,都可以进行还原。 注意:从来没有被添加到版本库就被删除的文件,是无法恢复的。同时恢复文件只能恢复到最新版本(commit的最新一次),会丢失最近一次提交后修改的内容。 8.远程库添加的两种方式前置工作 在本地创建密匙,添加到GitHub上,这样便可以将本地与自己的GitHub账号关联起来。 git-ssh1ssh-keygen -t rsa -C [邮箱] 其中的邮箱对应的是Git设置的邮箱。 【解释】:ssh-agent 是一种控制用来保存公钥身份验证所使用的私钥的程序,其实 ssh-agent 就是一个密钥管理器,运行 ssh-agent 以后,使用 ssh-add 将私钥交给 ssh-agent 保管,其他程序需要身份验证的时候可以将验证申请交给 ssh-agent 来完成整个认证过程,即进行公钥和秘钥的匹配。 1eval \"$(ssh-agent -s)\" 添加生成的SSH key到ssh-agent: 1ssh-add ~/.ssh/id_rsa 最后登录GitHub,添加ssh,将id_rsa.pub(这里注意是添加公钥,不要添加成了秘钥)添加入GitHub设置中的SSH中。至此,本地和GitHub的关联完成。 将本地创建的Git仓库同步至GitHub上。 1git remote add origin git <ssh路径> # 添加成功后远程库的名字就是origin(默认) 1git push -u origin master # 将本地库所有内容推送到远程库上 注意:git push命令实际将当前分支master推送到远程;由于远程库是空的,在第一次推送master分支时,加上-u参数,Git不但会把本地的master分支内容推送到远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时刻简化命令。即: 1git push origin master 直接clone远程库,将 文件添加至到本地,再将本地库推送至远端。 12345git clone <ssh地址> # 带具体仓库/-------------修改完文件以后-----------/git add <file>git commit -m <message>git push origin master 9.分支管理具体的理解与解释,可以参考廖雪峰分支管理一节,这里只做命令的汇总。 9.1创建dev分支,再切换到dev分支123456git checkout -b <分支名> # -b参数代表创建并切换/----等价于以下两句----/git branch devgit checkout dev# 或者git switch -c <name> 9.2 查看分支123git branch # 查看本地工程所有分支git branch -r # 远端服务器分支git branch -a # 远端服务器和本地服务器分支 会列出所有分支,当前分支前面会标有*号。 9.3分支切换123git checkout [分支] # 分支1切换分支2# 或者git switch <name> 9.4合并分支12# 用于合并指定分支到当前分支。git merge <指定分支> 9.5删除分支12git branch -d <分支名>git branch -d -r branch_name # 删除服务端分支 -> 推送该分支到远端服务器 git push origin:branch_name HEAD指向的就是当前分支,每一次提交,master分支都会向前移动一步。 9.6 switch命令(用于切换分支)12git switch -c <分支名> # 创建并切换到新的dev分支上git switch <分支名> # 直接切换到已有的分支上 10.解决冲突12345678910111213* 51af5f6 (HEAD -> master) conflict fixed2|\\ | * 30a56f8 conflict feature1* | 5e4ae6e conflict add fixed* | d4fa5fc conflict fixed|\\| | * a4c77c0 feature11* | cb7d18b & feature11|/ * 44e1c04 feature1* bee796d (origin/master, origin/HEAD) 分支练习* ba3dee6 测试提交* 0caf900 Initial commit 当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。 解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。 查看分支合并图: 12git log --graph --pretty=oneline --abbrev-commit # 详细版git log --graph # 简略版 11.分支管理通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。 如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit。这样,从分支历史上就可以看出分支信息。 123456git merge --no-ff -m \"message\" branch# 例子sh-3.2# git merge --no-ff -m \"merge with no-ff\" devMerge made by the 'recursive' strategy.test.txt | 1 +1 file changed, 1 insertion(+) 因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。 合并分支时,加上--no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward合并就看不出来曾经做过合并。 目的:为了协同开发,保证我们自己一直能够使用自己的分支,不像之前那样合并完后就删除了。需要在哪个分支上创建分支,就先切换到该分支上,创建分支。 保存工作现场: 1git stash 查看工作现场: 1git stash list 回复工作现场: 12345# 1.恢复工作现场 2.删除stash内容git stash applygit stash drop# 2.恢复的同时,删除stash内容git stash pop 复制一个特定的提交到当前分支(bug修复复制): 1git cherry-pick <commit> 12.Feature分支用于新功能的分开,开发一个新模块,最好创建一个feature分支,在此上面开发,完成后,合并,最后删除该feature分支。 强制删除: 1git branch -D feature-vulcan 13. 协作开发查看远程库信息 123git remote# 或git remote -v 推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上: 12git push origin master # 主分支git push origin dev # dev分支 抓取分支 123git clone [email protected]:sshgit branch # 查看分支git checkout -b dev origin/dev # 创建远程orgin的dev分支到本地(为了和远程的dev分支对应),创建本地dev分支 经修改commit后推送: 1git push origin dev 如果产生冲突(最新提交和你试图推送的提交有冲突),先用git pull把最新的提交从origin/dev抓下来,然后,在本地合并,解决冲突,再推送: 1234567891011git pullThere is no tracking information for the current branch.Please specify which branch you want to merge with.See git-pull(1) for details.git pull <remote> <branch>If you wish to set tracking information for this branch you can do so with:git branch --set-upstream-to=origin/<branch> dev 如果,git pull也失败了,原因是没有指定本地dev分支与远程origin/dev分支的链接,根据提示,设置dev和origin/dev的链接: 123git branch --set-upstream-to=origin/dev devBranch 'dev' set up to track remote branch 'dev' from 'origin'. 如果出现冲突,就解决冲突(更新文件)即可。 多人协作工作模式: 首先,可以试图用git push origin <branch-name>推送自己的修改; 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并; 如果合并有冲突,则解决冲突,并在本地提交; 没有冲突或者解决掉冲突后,再用git push origin <branch-name>推送就能成功! 如果git pull提示no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to <branch-name> origin/<branch-name>。 12git pull origin remote_branch:local_branch # 将远端分支拉取到本地,并和本地分支合并,如果远端和本地分支名相同,可省略本地分支名部分git fetch origin remote_branch:local_branch # 相比较git pull,没有将更新同本地合并,而是获取远端的更新 14.标签操作用于新建一个标签,默认为HEAD,也可以指定一个commit id 1git tag <tagname> 指定标签信息 1git tag -a <tagname> -m \"blablabla...\" 查看所有标签 1git tag 查看标签信息: 1git show <tagname> 删除标签: 12git tag -d <tagname>git tag -D <tagname> 标签推送远端: 12git push origin <tagname>git push origin --tags # 一次性推送全部 删除远程标签,先删除本地,再推送远端 1git push origin :refs/tags/<tagname> 15.其他修改最新commit提交的信息: 1git commit --amend 修改多次的信息: 1234git rebase -i commit_id ## 只能修改commit_id 之前的log messagegit rebase -i --root ## 修改第一次及之前的log messagegit rebase -i HEAD~2 ## 修改倒数第二次及之前的log messagegit rebase -i HEAD~1 ## 修改最后一次提交的log message 未完待续…","categories":[{"name":"git","slug":"git","permalink":"https://chemlez.github.io/categories/git/"}],"tags":[{"name":"git","slug":"git","permalink":"https://chemlez.github.io/tags/git/"}]},{"title":"Java初试MVC及三层架构","slug":"Java初试MVC及三层架构","date":"2020-08-16T02:50:50.000Z","updated":"2020-08-26T02:28:04.732Z","comments":true,"path":"2020/08/16/Java初试MVC及三层架构/","link":"","permalink":"https://chemlez.github.io/2020/08/16/Java%E5%88%9D%E8%AF%95MVC%E5%8F%8A%E4%B8%89%E5%B1%82%E6%9E%B6%E6%9E%84/","excerpt":"最近又捡起了对Java的学习,通过对一个实例的介绍,总结下此次对Web开发中MVC三层架构的学习,以便用于日后的复习。 一、 MVC简单的先介绍下MVC模式: M(Model):JavaBean。用于完成具体的业务操作。 JavaBean:Java中特殊的类. JavaBean满足条件: public修饰的类,并提供public无参构造方法 所有的属性都是private修饰 提供getter和setter方法 使用层面: 封装业务逻辑:dao层封装对数据库的底层操作 封装数据:domain层。对数据库中所要查询对象的封装 V(View):视图。用于数据的展示。 页面的展示 与用户的交互 C(Controller):控制器。由Servlet实现控制器。 主要功能: 获取用户的输入 调用模型,将请求交给模型进行处理 将数据交给视图进行展示","text":"最近又捡起了对Java的学习,通过对一个实例的介绍,总结下此次对Web开发中MVC三层架构的学习,以便用于日后的复习。 一、 MVC简单的先介绍下MVC模式: M(Model):JavaBean。用于完成具体的业务操作。 JavaBean:Java中特殊的类. JavaBean满足条件: public修饰的类,并提供public无参构造方法 所有的属性都是private修饰 提供getter和setter方法 使用层面: 封装业务逻辑:dao层封装对数据库的底层操作 封装数据:domain层。对数据库中所要查询对象的封装 V(View):视图。用于数据的展示。 页面的展示 与用户的交互 C(Controller):控制器。由Servlet实现控制器。 主要功能: 获取用户的输入 调用模型,将请求交给模型进行处理 将数据交给视图进行展示 首先浏览器(通过View页面)向服务器端进行请求(可以是表单请求、超链接、AJAX请求等),Controller层获取浏览器请求的数据进行解析,调用模型;模型进行业务逻辑的操作,并将处理结果返回给Controller层;Controller层再将相应的数据交给View层,进行数据展示到客户端。 二、三层架构三层架构:视图层View、服务层Service、与持久层Dao。 View:用于接收用户提交请求的代码。 Service:用于编写系统的业务逻辑。(最重要的一层) Dao:对数据库进行最直接的操作。即:对数据库的增删改查。 dao层中,定义了对数据库的增删改查的接口。而service层中即对数据的具体业务操作,用于组合dao层中的接口方法。web层中则用于对用户数据的接收和发送。上图很好的解释了MVC与三层架构之间的关系。 三、案例通过servlet、jsp、Mysql、JDBCTempleat、Duird、BeanUtils、Tomcat等技术完成用户信息列表展示的实例。此部分着重解释后端代码的实现。 1.查询所有用户信息 当点击前端页面查询按钮时,此时通过对服务器端的请求,到web层中的Controller层,触发FindUserServlet。由Controller调用Service层中的模型,继而Service层通过dao层获取全部的用户信息,封装到List<User>集合中。返回给Service层,再通过Service层将用户信息返回给Controller层。Web层中的Controller将数据进行存储转发给View,进行数据的解析展示,返回给客户端。 findAll方法: dao层中findAll接口的实现 1234567@Overridepublic List<User> findAll() { JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource()); String sql = \"select * from user\"; List<User> users = template.query(sql, new BeanPropertyRowMapper<User>(User.class)); return users;} service层中findAll接口的实现 123456@Overridepublic List<User> findAll() { UserDao userDao = new UserDaoImpl(); List<User> users = userDao.findAll(); return users;} web层中UserListServlet 12345678910111213@WebServlet(\"/userListServlet\")public class UserListServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { UserService userService = new UserServiceImpl(); List<User> users = userService.findAll(); request.setAttribute(\"users\", users); request.getRequestDispatcher(\"/list.jsp\").forward(request, response); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request,response); }} 2.增加用户信息 当客户端点击提交按钮时,此次对服务器端的请求带着表单的数据。根据我们上面对三层架构以及Controller层的介绍可知,由其进行数据的接收。这里即为AddUserServlet对获取的数据进行处理、封装。将封装好的数据传给Service层(Model),即为UerService,进行业务逻辑的操作。再通过dao层对数据库进行相应的访问。 dao层中add接口的实现 123456@Overridepublic void addUser(User user) { JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource()); String sql = \"insert into user values(null,?,?,?,?,?,?,null,null)\"; template.update(sql, user.getName(), user.getGender(), user.getAge(), user.getAddress(), user.getQq(), user.getEmail());} Service层中add接口的实现 12345@Overridepublic void addUser(User user) { UserDao userDao = new UserDaoImpl(); userDao.addUser(user); } web层中addServlet的实现 123456789101112131415161718192021222324@WebServlet(\"/addServlet\")public class AddServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(\"utf-8\"); User user = new User(); Map<String, String[]> parameterMap = request.getParameterMap(); // 将请求参数进行Map集合的封装 try { BeanUtils.populate(user, parameterMap);//将获取到的值封装到User对象中 } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } UserService service = new UserServiceImpl(); service.addUser(user); HttpSession session = request.getSession(); // 再进行重定向之前,将需要添加的User对象设置到session中,以便后续对其的获取使用 session.setAttribute(\"addUser\", user); response.sendRedirect(request.getContextPath() + \"/findUserByPageServlet\");//重定向至View层 } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request, response); }} 思考: 在上一个方法查找所有的用户时,在最后,我们将获取到的数据传给view层,采用的是存储转发,而在这里,对用户的添加,我们却采用的是重定向(实际上,增、删、改都是采用的重定向)。这是为什么呢? 这里先对重定向和存储转发的几个特点进行简单的比较: 存储转发:一种在服务器内部的资源跳转方式。 特点: 浏览器地址栏不会发生变化 只能转发到当前服务器内部资源中 转发是一次请求 最重要的一点便是转发只在当前服务器内部进行,请求也只有一次。这样做带来的一点用处便是,request域中带有的请求是可以在多次资源跳转中进行共享的。在使用时,只需要添加资源路径即可。 重定向:具有多次的请求。其跳转在于客户端与服务器端之间,每次请求都是独立的,存在新的request与response。 特点: 重定向之后地址栏会发生变化 重定向可以访问其他站点(服务器)的资源 重定向是两次请求。故不能使用request域共享数据。 其中,重定向可避免在用户重新加载页面时,两次调用相同的动作。即,访问数据库时,增删改使用重定向。当前我的理解是:转发只有一次请求,故当重新加载页面时,会沿着之前的请求再请求一次(这句话可能比较绕),在页面进行展示全部用户数据之前,又会将数据库添加用户的操作进行一遍,所以会造成表单的重复提交;而重定向中是多次(两次)请求,所以只复用”最近一次“请求(因为每次请求是独立的,之前的请求都不在了),这里只是对页面进行全部用户的展示。 重定向: 存储转发: 3.用户信息更改 思路:用户信息的更改相对前面的用户显示的展示和增加要多了一些步骤。首先,在我们需要对信息进行更改时,需要将原信息展示给我们,在原信息的基础上进行修改,即:是查询到所要修改的用户,将其信息进行回显。其次,当我们对用户信息更改完毕以后,通过对数据库的访问,将对应的数据库信息进行Update操作。最后,通过view层,对列表信息进行展示传送给客户端。这里,每个用户都有一个唯一标识符–id主键,所以当我们拿到主键id时,其实也就获得了对应的数据库中的User对象,后面的操作也就顺理成章了。 FindUserServlet – 查询需要修改的用户,用于信息的回显 web层中FindUserServlet的实现 123456789101112131415@WebServlet(\"/findUserServlet\")public class FindUserServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(\"utf-8\"); String id = request.getParameter(\"id\"); // 获取id UserService service = new UserServiceImpl(); User user = service.findUser(Integer.parseInt(id));// 通过id返回User对象 request.setAttribute(\"user\", user);// 查找操作所以存储-转发,将用户信息封装传至view层 request.getRequestDispatcher(\"/update.jsp\").forward(request,response);// 即传至update.jsp页面进行解析 } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request,response); }} service层中findUser的实现 123456@Overridepublic User findUser(int id) { UserDao userDao = new UserDaoImpl(); User user = userDao.findUser(id); return user;} dao层中findUser接口的实现 1234567@Overridepublic User findUser(int id) { JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource()); String sql = \"select * from user where id = ?\"; User user = template.queryForObject(sql, new BeanPropertyRowMapper<User>(User.class), id); return user;} 以上是获取到需要修改的对象,然后对用户信息进行一个回显操作。 下面,便是对用户信息进行修改。 UpdateUserServlet – 修改用户信息,对数据库数据进行修改 web层中UpdateUserServlet的实现 1234567891011121314151617181920212223@WebServlet(\"/updateUserServlet\")public class UpdateUserServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(\"utf-8\"); Map<String, String[]> parameterMap = request.getParameterMap(); User user = new User(); try { BeanUtils.populate(user, parameterMap); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } int id = user.getId(); UserService service = new UserServiceImpl(); service.updateUser(id, user);// 对用户信息进行修改 response.sendRedirect(request.getContextPath()+\"/findUserByPageServlet\");//View层,信息的展示 } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request,response); }} service层中UpdateUser接口的实现 12345@Overridepublic void updateUser(int id, User user) { UserDao userDao = new UserDaoImpl(); userDao.updateUser(id, user);} dao层中UpdateUser抽象方法的实现 123456@Overridepublic void updateUser(int id, User user) { JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource()); String sql = \"update user set name = ?,age=?,gender=?,address=?,qq=?,email=? where id=?\"; template.update(sql, user.getName(), user.getAge(), user.getGender(), user.getAddress(), user.getQq(), user.getEmail(), user.getId());} 4.用户的删除 用户的删除同用户的更新类似,这里同样是获取用户的id,通过id查询到数据库中相应的数据,然后将数据进行删除。 dao层中deleteUser抽象方法的实现 123456@Overridepublic void deleteUser(int id) { JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource()); String sql = \"delete from user where id = ?\"; template.update(sql, id);} 5.删除选中功能 这里删除选中,是对数据进行批量删除。其中思路同单个用户的删除类似,这里是获取一个id的集合,然后通过循环遍历id删除用户即可。这里的难点在于如何通过前端获取到这些id传递到web层。 1<a class=\"btn btn-primary\" href=\"javascript:void(0);\" onclick=\"select()\">删除选中</a> 定义Javascript方法。 其中: 1234567891011121314151617function select() { var uids = document.getElementsByName(\"uid\");// 获取属性为uid的标签 -- 这里给所有用户指定的class属性值即为uid,所以是为了获取每一个用户的信息 var flag = false; for (var i = 0; i < uids.length; ++i) { var checked = uids[i].checked; if (checked) { flag = true; break; } } if (flag) { // 判断id是否为空 (前端防空的校验) var flag = window.confirm(\"您确认删除么?\"); // 用于确认删除 if (flag) { document.getElementById(\"s_form\").submit(); // 提交表单 } }} 首先获取被选中的uid,判断是否有用户被选中,这里是为了防止传入到后端的值为空,而报空指针异常。 1234567891011121314151617<c:forEach items=\"${userByPages.user}\" var=\"user\" varStatus=\"u\"> <tr> <td><input type=\"checkbox\" name=\"uid\" value=\"${user.id}\"></td> <td>${u.count}</td> <td>${user.name}</td> <td>${user.gender}</td> <td>${user.age}</td> <td>${user.address}</td> <td>${user.qq}</td> <td>${user.email}</td> <td> <a class=\"btn btn-default btn-sm\" href=\"${pageContext.request.contextPath}/findUserServlet?id=${user.id}\">修改</a>&nbsp; <a class=\"btn btn-default btn-sm\" href=\"javascript:give_msg(${user.id})\">删除</a> </td> </tr></c:forEach> 以上便是由前端获取需要删除的用户的id。 web层中DeleteSelectServlet的实现 123456789101112131415@WebServlet(\"/delSelectServlet\")public class DelSelectServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(\"utf-8\"); String[] uids = request.getParameterValues(\"uid\");// 获取用户id的数组 UserService service = new UserServiceImpl(); service.delByIds(uids); response.sendRedirect(request.getContextPath()+\"/findUserByPageServlet\"); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request,response); }} service层中DelByIds接口的实现 1234567891011@Overridepublic void delByIds(String[] ids) { UserDao userDao = new UserDaoImpl(); int id_count; if (ids != null && ids.length != 0) { // 删除选项的后台验证 -- 防止参数为空(后端判空的校验) for (String id : ids) { id_count = Integer.parseInt(id); userDao.deleteUser(id_count); } }} dao层中DeleteUser接口的实现 123456@Overridepublic void deleteUser(int id) { JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource()); String sql = \"delete from user where id = ?\"; template.update(sql, id);} 批量删除的注意事项: id获取的批量方式 删除前的确认操作 防止空参数时的删除操作(前后端都进行校验) 6.分页查询 在客户端对用户列表进行分页展示的基本思路,即需要获取以下的信息: 首先确定数据库中总的条目数 – totalCount; 总的页面数量 – totalPage 获取每页的数据 – List集合 获取当前的页码 – currentPage 每页显示的条数 – rows totalCount:可以借鉴findAll()的思路,统计出总条目数。 1totalCount = select count(*) frow user; rows:有客户端进行传递到参数,即预先设定好的请求参数。 totalPage:totalCount/rows,上取。 currentPage:包含在客户端的请求参数中。 List集合:获取起始索引index以及上面的rows即可确定当前页面需要显示的条目数。 12index = (currentPage - 1)*rowslist = select * from user where limit index,rows 还记得前面所说的Model中操作的是JavaBean对象么,所以这里我们将以上的信息封装成一个PageBean对象。以便我们进行业务逻辑的操作,最终将整个PageBean进行返回交给Web层的Controller,再由Controller传递给View视图,进行展示。逻辑操作见下图: 扩展:对数据的查询进行组合查询,即复杂功能的查询。 web层中FindUserByPageServlet的实现 123456789101112131415161718192021222324252627//findUserByPageServlet?currentPage=2&rows=5@WebServlet(\"/findUserByPageServlet\")public class FindUserByPageServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(\"utf-8\"); String currentPage = request.getParameter(\"currentPage\"); String rows = request.getParameter(\"rows\"); if (currentPage == null || \"\".equals(currentPage)) { // 当首次进入到列表查询页面时,用于判空操作,即给currentPage,rows赋初始值。 currentPage = \"1\"; } if (rows == null || \"\".equals(rows)) { rows = \"5\"; } Map<String, String[]> condition = request.getParameterMap();//用于复杂条件的查询,本小结可暂时忽略 UserService service = new UserServiceImpl(); PageBean<User> userByPage = service.findUserByPage(Integer.parseInt(currentPage), Integer.parseInt(rows),condition); request.setAttribute(\"userByPages\", userByPage); request.setAttribute(\"condition\",condition);// 后续转发时,条件的回显 request.getRequestDispatcher(\"/list.jsp\").forward(request, response); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request,response); }} Service层中findUserByPage抽象方法的实现 12345678910111213141516171819202122232425262728@Overridepublic PageBean<User> findUserByPage(int currentPage, int rows, Map<String, String[]> condition) { PageBean<User> userPageBean = new PageBean<>(); // 页数不能小于1 if (currentPage <= 0) { currentPage = 1; } // 查询总条目数 UserDao dao = new UserDaoImpl(); int totalCount = dao.findTotalCount(condition); // 计算出总页数 int totalPageCount = (totalCount % rows == 0) ? (totalCount / rows) : (totalCount / rows + 1); // 页数不能大于总页数 if (currentPage > totalPageCount) { currentPage = totalPageCount; } // 查询的索引数 int starIndex = (currentPage - 1) * rows; // 返回出所要查询的对象数目 List<User> users = dao.findByPage(starIndex, rows,condition); // 设置对象的属性 userPageBean.setTotalCount(totalCount); userPageBean.setTotalPage(totalPageCount); userPageBean.setUser(users); userPageBean.setCurrentPage(currentPage); // 当前页数 userPageBean.setRows(rows); // 每页展示的条目数 return userPageBean;} dao层中findTotalCount及findByPage抽象方法的实现 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849@Overridepublic int findTotalCount(Map<String, String[]> condition) { // search condition JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource()); String sql = \"select count(*) from user where 1=1 \"; List<Object> params = new ArrayList<Object>(); StringBuffer sb = new StringBuffer(sql); Set<String> keySet = condition.keySet(); // 复杂条件的查询 for (String key : keySet) { if (\"currentPage\".equals(key) || \"rows\".equals(key)) { continue; } String value = condition.get(key)[0]; if (value != null && value != \"\") { sb.append(\" and \" + key + \" like ?\"); params.add(\"%\" + value + \"%\"); } } sql = sb.toString(); Integer count = template.queryForObject(sql, Integer.class, params.toArray()); System.out.println(sql); System.out.println(params); return count;}@Overridepublic List<User> findByPage(int starIndex, int rows, Map<String, String[]> condition) { // 根据索引返回的列表对象 JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource()); String sql = \"select * from user where 1=1\"; List<Object> params = new ArrayList<Object>(); StringBuffer sb = new StringBuffer(sql); Set<String> keySet = condition.keySet(); for (String key : keySet) { // 用于复杂条件的查询 if (\"currentPage\".equals(key) || \"rows\".equals(key)) { continue; } String value = condition.get(key)[0]; if (value != null && value != \"\") { sb.append(\" and \" + key + \" like ?\"); params.add(\"%\" + value + \"%\"); } } params.add(starIndex); params.add(rows); sb.append(\" limit ?,? \"); sql = sb.toString(); List<User> users = template.query(sql, new BeanPropertyRowMapper<User>(User.class), params.toArray()); return users;} 7.复杂条件的分页查询 组合条件的查询,在于SQL语句的编写。 定义初始化SQL语句: select count(*) from user where 1=1再将查询条件进行拼接,再拼接之前先进行判空操作。 web、service、dao层的编写在上一个小节中已包含。 四、前端代码展示 add.jsp 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164<%-- Created by IntelliJ IDEA. User: liz Date: 2020/8/9 Time: 19:17 To change this template use File | Settings | File Templates.--%><%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" %><html lang=\"zh-CN\"><head> <!-- 指定字符集 --> <meta charset=\"utf-8\"> <!-- 使用Edge最新的浏览器的渲染方式 --> <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"> <!-- viewport视口:网页可以根据设置的宽度自动进行适配,在浏览器的内部虚拟一个容器,容器的宽度与设备的宽度相同。 width: 默认宽度与设备的宽度相同 initial-scale: 初始的缩放比,为1:1 --> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"> <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! --> <title>添加用户</title> <!-- 1. 导入CSS的全局样式 --> <link href=\"css/bootstrap.min.css\" rel=\"stylesheet\"> <!-- 2. jQuery导入,建议使用1.9以上的版本 --> <script src=\"js/jquery-2.1.0.min.js\"></script> <!-- 3. 导入bootstrap的js文件 --> <script src=\"js/bootstrap.min.js\"></script> <style> .error { color: red; } </style> <script> window.onload = function () { document.getElementById('form').onsubmit = function () { return checkName() && checkAge() && checkQQ() && checkEmail(); }; document.getElementById(\"name\").onblur = checkName; document.getElementById(\"age\").onblur = checkAge; document.getElementById(\"qq\").onblur = checkQQ; document.getElementById(\"email\").onblur = checkEmail; }; function checkName() { var name = document.getElementById(\"name\").value; var regExp = /(^([a-zA-Z]+\\s)*[a-zA-Z]+$)|(^[\\u4e00-\\u9fa5]+$)/; var flag = regExp.test(name); var s_name = document.getElementById(\"s_name\"); if (flag) { s_name.innerHTML = '<img src=\"img/gou.png\" width=\"35px\" height=\"25px\">'; } else { s_name.innerHTML = \"请输入正确姓名\" } return flag; }; function checkAge() { var age = document.getElementById(\"age\").value; var s_age = document.getElementById(\"s_age\"); var regExp = /^(([0-9]|[1-9][1-9]|1[0-7][0-9])(\\\\.[0-9]+)?|180)$/; var flag = regExp.test(age); if (flag) { s_age.innerHTML = '<img src=\"img/gou.png\" width=\"35px\" height=\"25px\">'; } else { s_age.innerHTML = \"请输入正确格式的年龄\" } return flag; }; function checkQQ() { var qq = document.getElementById(\"qq\").value; var regExp = /^[1-9][0-9]{4,14}$/; var flag = regExp.test(qq); var s_qq = document.getElementById(\"s_qq\"); if (flag) { s_qq.innerHTML = '<img src=\"img/gou.png\" width=\"35px\" height=\"25px\">'; } else { s_qq.innerHTML = '请输入正确格式的qq号' } return flag; }; function checkEmail() { var email = document.getElementById(\"email\").value; var regExp = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\\.[a-zA-Z0-9_-]{2,3}){1,2})$/; var flag = regExp.test(email); var s_email = document.getElementById(\"s_email\"); if (flag) { s_email.innerHTML = '<img src=\"img/gou.png\" width=\"35px\" height=\"25px\">'; } else { s_email.innerHTML = '请输入正确格式的email地址'; } return flag; }; </script></head><body><div class=\"container\"> <center><h3>添加联系人页面</h3></center> <form action=\"${pageContext.request.contextPath}/addServlet\" method=\"post\" id=\"form\"> <div class=\"form-group\"> <label for=\"name\">姓名:</label> <input type=\"text\" class=\"form-control\" id=\"name\" name=\"name\" placeholder=\"请输入姓名\"> <sapn id=\"s_name\" class=\"error\"></sapn> </div> <div class=\"form-group\"> <label>性别:</label> <input type=\"radio\" name=\"gender\" value=\"男\" checked=\"checked\"/>男 <input type=\"radio\" name=\"gender\" value=\"女\"/>女 </div> <div class=\"form-group\"> <label for=\"age\">年龄:</label> <input type=\"text\" class=\"form-control\" id=\"age\" name=\"age\" placeholder=\"请输入年龄\"> <sapn id=\"s_age\" class=\"error\"></sapn> </div> <div class=\"form-group\"> <label for=\"address\">籍贯:</label> <select name=\"address\" class=\"form-control\" id=\"address\"> <option value=\"陕西\">陕西</option> <option value=\"北京\">北京</option> <option value=\"南京\">南京</option> <option value=\"安徽\">安徽</option> <option value=\"上海\">上海</option> </select> </div> <div class=\"form-group\"> <label for=\"qq\">QQ:</label> <input type=\"text\" class=\"form-control\" id=\"qq\" name=\"qq\" placeholder=\"请输入QQ号码\"/> <sapn id=\"s_qq\" class=\"error\"></sapn> </div> <div class=\"form-group\"> <label for=\"email\">Email:</label> <input type=\"text\" class=\"form-control\" id=\"email\" name=\"email\" placeholder=\"请输入邮箱地址\"/> <sapn id=\"s_email\" class=\"error\"></sapn> </div> <div class=\"form-group\" style=\"text-align: center\"> <input class=\"btn btn-primary\" type=\"submit\" value=\"提交\" id=\"submit\"/> <input class=\"btn btn-default\" type=\"reset\" value=\"重置\"/> <input class=\"btn btn-default\" type=\"button\" value=\"返回\"/> </div> </form></div></body></html> index.jsp 123456789101112131415161718192021222324252627282930313233<%-- Created by IntelliJ IDEA. User: liz Date: 2020/8/9 Time: 14:20 To change this template use File | Settings | File Templates.--%><%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" %><html lang=\"zh-CN\"><head> <meta charset=\"utf-8\"/> <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/> <title>首页</title> <!-- 1. 导入CSS的全局样式 --> <link href=\"css/bootstrap.min.css\" rel=\"stylesheet\"> <!-- 2. jQuery导入,建议使用1.9以上的版本 --> <script src=\"js/jquery-2.1.0.min.js\"></script> <!-- 3. 导入bootstrap的js文件 --> <script src=\"js/bootstrap.min.js\"></script> <script type=\"text/javascript\"> </script></head><body><div align=\"center\"> <a href=\"${pageContext.request.contextPath}/findUserByPageServlet\" style=\"text-decoration:none;font-size:33px\">查询所有用户信息 </a></div></body></html> list.jsp 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176<%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" %><%@taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\" %><!DOCTYPE html><!-- 网页使用的语言 --><html lang=\"zh-CN\"><head> <!-- 指定字符集 --> <meta charset=\"utf-8\"> <!-- 使用Edge最新的浏览器的渲染方式 --> <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"> <!-- viewport视口:网页可以根据设置的宽度自动进行适配,在浏览器的内部虚拟一个容器,容器的宽度与设备的宽度相同。 width: 默认宽度与设备的宽度相同 initial-scale: 初始的缩放比,为1:1 --> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"> <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! --> <title>用户信息管理系统</title> <!-- 1. 导入CSS的全局样式 --> <link href=\"css/bootstrap.min.css\" rel=\"stylesheet\"> <!-- 2. jQuery导入,建议使用1.9以上的版本 --> <script src=\"js/jquery-2.1.0.min.js\"></script> <!-- 3. 导入bootstrap的js文件 --> <script src=\"js/bootstrap.min.js\"></script> <style type=\"text/css\"> td, th { text-align: center; } </style> <script> function give_msg(id) { var flag = confirm(\"您确定删除么?\"); if (flag) { location.href = '${pageContext.request.contextPath}/deleteServlet?id=' + id; } } function select() { var uids = document.getElementsByName(\"uid\"); var flag = false; for (var i = 0; i < uids.length; ++i) { var checked = uids[i].checked; if (checked) { flag = true; break; } } if (flag) { var flag = window.confirm(\"您确认删除么?\"); if (flag) { document.getElementById(\"s_form\").submit(); } } } window.onload = function () { document.getElementById(\"selectAll\").onclick = function () { var uids = document.getElementsByName(\"uid\"); for (var i = 0; i < uids.length; ++i) { uids[i].checked = this.checked; // this代表 --> document.getElementById(\"selectAll\")组件 } } }; </script></head><body><div class=\"container\"> <h3 style=\"text-align: center\">用户信息列表</h3> <div> <form class=\"form-inline\" style=\"float: left\" action=\"${pageContext.request.contextPath}/findUserByPageServlet\" method=\"post\"> <div class=\"form-group\"> <label for=\"name\">姓名</label> <input type=\"text\" name=\"name\" class=\"form-control\" id=\"name\" value=\"${condition.name[0]}\"> </div> <div class=\"form-group\"> <label for=\"address\">籍贯</label> <input type=\"text\" name=\"address\" class=\"form-control\" id=\"address\" value=\"${condition.address[0]}\"> </div> <div class=\"form-group\"> <label for=\"email\">Email</label> <input type=\"text\" name=\"email\" class=\"form-control\" id=\"email\" value=\"${condition.email[0]}\"> </div> <button type=\"submit\" name=\"submit\" class=\"btn btn-default\">查询</button> </form> <div style=\"float: right;margin: 3px\"> <a class=\"btn btn-primary\" href=\"${pageContext.request.contextPath}/add.jsp\">添加联系人</a> <a class=\"btn btn-primary\" href=\"javascript:void(0);\" onclick=\"select()\">删除选中</a> </div> </div> <form action=\"${pageContext.request.contextPath}/delSelectServlet\" method=\"post\" id=\"s_form\"> <table border=\"1\" class=\"table table-bordered table-hover\"> <tr class=\"success\"> <th><input type=\"checkbox\" id=\"selectAll\"></th> <th>编号</th> <th>姓名</th> <th>性别</th> <th>年龄</th> <th>籍贯</th> <th>QQ</th> <th>邮箱</th> <th>操作</th> </tr> <c:forEach items=\"${userByPages.user}\" var=\"user\" varStatus=\"u\"> <tr> <td><input type=\"checkbox\" name=\"uid\" value=\"${user.id}\"></td> <td>${u.count}</td> <td>${user.name}</td> <td>${user.gender}</td> <td>${user.age}</td> <td>${user.address}</td> <td>${user.qq}</td> <td>${user.email}</td> <td> <a class=\"btn btn-default btn-sm\" href=\"${pageContext.request.contextPath}/findUserServlet?id=${user.id}\">修改</a>&nbsp; <a class=\"btn btn-default btn-sm\" href=\"javascript:give_msg(${user.id})\">删除</a> </td> </tr> </c:forEach> </table> </form> <div> <nav aria-label=\"Page navigation\"> <ul class=\"pagination\"> <c:if test=\"${userByPages.currentPage - 1==0}\"> <li class=\"disabled\"> </c:if> <c:if test=\"${userByPages.currentPage - 1 > 0}\"> <li> </c:if> <a href=\"${pageContext.request.contextPath}/findUserByPageServlet?currentPage=${userByPages.currentPage - 1}&rows=${5}&name=${condition.name[0]}&address=${condition.address[0]}&email=${condition.email[0]}\" aria-label=\"Previous\"> <span aria-hidden=\"true\">&laquo;</span> </a> </li> <c:forEach begin=\"1\" end=\"${userByPages.totalPage}\" var=\"num\" step=\"1\" varStatus=\"c\"> <c:if test=\"${userByPages.currentPage==num}\"> <li class=\"active\"> <a href=\"${pageContext.request.contextPath}/findUserByPageServlet?currentPage=${num}&rows=${5}&name=${condition.name[0]}&address=${condition.address[0]}&email=${condition.email[0]}\">${num}</a> </li> </c:if> <c:if test=\"${userByPages.currentPage!=num}\"> <li> <a href=\"${pageContext.request.contextPath}/findUserByPageServlet?currentPage=${num}&rows=${5}&name=${condition.name[0]}&address=${condition.address[0]}&email=${condition.email[0]}\">${num}</a> </li> </c:if> </c:forEach> <c:if test=\"${userByPages.currentPage + 1 > userByPages.totalPage}\"> <li class=\"disabled\"> </c:if> <c:if test=\"${userByPages.currentPage + 1 <= userByPages.totalPage}\"> <li> </c:if> <a href=\"${pageContext.request.contextPath}/findUserByPageServlet?currentPage=${userByPages.currentPage + 1}&rows=${5}&name=${condition.name[0]}&address=${condition.address[0]}&email=${condition.email[0]}\" aria-label=\"Next\"> <span aria-hidden=\"true\">&raquo;</span> </a> </li> <span style=\"font-size: 25px;padding-left: 10px\"> 共${userByPages.totalCount}条记录,${userByPages.totalPage}页 </span> </ul> </nav> </div></div></body></html> login.jsp 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970<%-- Created by IntelliJ IDEA. User: liz Date: 2020/8/9 Time: 16:35 To change this template use File | Settings | File Templates.--%><%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" %><%@taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\" %><html lang=\"zh-CN\"><head> <meta charset=\"utf-8\"/> <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/> <title>管理员登录</title> <!-- 1. 导入CSS的全局样式 --> <link href=\"css/bootstrap.min.css\" rel=\"stylesheet\"> <!-- 2. jQuery导入,建议使用1.9以上的版本 --> <script src=\"js/jquery-2.1.0.min.js\"></script> <!-- 3. 导入bootstrap的js文件 --> <script src=\"js/bootstrap.min.js\"></script> <script type=\"text/javascript\"></script> <script> function refreshCode() { var vcode = document.getElementById(\"vcode\"); var date = new Date().getTime(); vcode.src = \"${pageContext.request.contextPath}/checkCode?time=\" + date; } </script></head><body><div class=\"container\" style=\"width: 400px;\"> <h3 style=\"text-align: center;\">管理员登录</h3> <form action=\"${pageContext.request.contextPath}/loginUser\" method=\"post\"> <div class=\"form-group\"> <label for=\"user\">用户名:</label> <input type=\"text\" name=\"username\" class=\"form-control\" id=\"user\" placeholder=\"请输入用户名\"/> </div> <div class=\"form-group\"> <label for=\"password\">密码:</label> <input type=\"password\" name=\"password\" class=\"form-control\" id=\"password\" placeholder=\"请输入密码\"/> </div> <div class=\"form-inline\"> <label for=\"vcode\">验证码:</label> <input type=\"text\" name=\"verifycode\" class=\"form-control\" id=\"verifycode\" placeholder=\"请输入验证码\" style=\"width: 120px;\"/> <a href=\"javascript:refreshCode()\"><img src=\"${pageContext.request.contextPath}/checkCode\" title=\"看不清点击刷新\" id=\"vcode\"/></a> </div> <hr/> <div class=\"form-group\" style=\"text-align: center;\"> <input class=\"btn btn btn-primary\" type=\"submit\" value=\"登录\"> </div> </form> <!-- 出错显示的信息框 --> <div class=\"alert alert-warning alert-dismissible\" role=\"alert\"> <button type=\"button\" class=\"close\" data-dismiss=\"alert\"> <span>&times;</span> </button> <strong>${code_error}</strong> <strong>${adminUser_error}</strong> </div></div></body></html> update.jsp 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124<%-- Created by IntelliJ IDEA. User: liz Date: 2020/8/10 Time: 20:03 To change this template use File | Settings | File Templates.--%><%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" %><%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\" %><html lang=\"zh-CN\"><head> <!-- 指定字符集 --> <meta charset=\"utf-8\"> <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"> <title>修改用户</title> <link href=\"css/bootstrap.min.css\" rel=\"stylesheet\"> <script src=\"js/jquery-2.1.0.min.js\"></script> <script src=\"js/bootstrap.min.js\"></script></head><body><div class=\"container\" style=\"width: 400px;\"> <h3 style=\"text-align: center;\">修改联系人</h3> <form action=\"${pageContext.request.contextPath}/updateUserServlet\" method=\"post\"> <div class=\"form-group\"> <label for=\"name\">姓名:</label> <input type=\"hidden\" name=\"id\" value=\"${user.id}\"> <input type=\"text\" class=\"form-control\" id=\"name\" name=\"name\" readonly=\"readonly\" placeholder=\"请输入姓名\" value=\"${requestScope.user.name}\"/> </div> <div class=\"form-group\"> <label>性别:</label> <c:if test=\"${user.gender=='男'}\"> <input type=\"radio\" name=\"gender\" value=\"男\" checked/>男 <input type=\"radio\" name=\"gender\" value=\"女\"/>女 </c:if> <c:if test=\"${user.gender=='女'}\"> <input type=\"radio\" name=\"gender\" value=\"男\"/>男 <input type=\"radio\" name=\"gender\" value=\"女\" checked/>女 </c:if> </div> <div class=\"form-group\"> <label for=\"age\">年龄:</label> <input type=\"text\" class=\"form-control\" id=\"age\" name=\"age\" placeholder=\"请输入年龄\" value=\"${user.age}\"/> </div> <div class=\"form-group\"> <label for=\"address\">籍贯:</label> <select name=\"address\" id=\"address\" class=\"form-control\"> <c:if test=\"${user.address =='陕西'}\"> <option value=\"陕西\" selected>陕西</option> <option value=\"北京\">北京</option> <option value=\"上海\">上海</option> <option value=\"南京\">南京</option> <option value=\"安徽\">安徽</option> <option value=\"苏州\">苏州</option> </c:if> <c:if test=\"${user.address =='北京'}\"> <option value=\"陕西\">陕西</option> <option value=\"北京\" selected>北京</option> <option value=\"上海\">上海</option> <option value=\"南京\">南京</option> <option value=\"安徽\">安徽</option> <option value=\"苏州\">苏州</option> </c:if> <c:if test=\"${user.address =='上海'}\"> <option value=\"陕西\">陕西</option> <option value=\"北京\">北京</option> <option value=\"上海\" selected>上海</option> <option value=\"南京\">南京</option> <option value=\"安徽\">安徽</option> <option value=\"苏州\">苏州</option> </c:if> <c:if test=\"${user.address =='南京'}\"> <option value=\"陕西\">陕西</option> <option value=\"北京\">北京</option> <option value=\"上海\">上海</option> <option value=\"南京\" selected>南京</option> <option value=\"安徽\">安徽</option> <option value=\"苏州\">苏州</option> </c:if> <c:if test=\"${user.address =='安徽'}\"> <option value=\"陕西\">陕西</option> <option value=\"北京\">北京</option> <option value=\"上海\">上海</option> <option value=\"南京\">南京</option> <option value=\"安徽\" selected>安徽</option> <option value=\"苏州\">苏州</option> </c:if> <c:if test=\"${user.address =='苏州'}\"> <option value=\"陕西\">陕西</option> <option value=\"北京\">北京</option> <option value=\"上海\">上海</option> <option value=\"南京\">南京</option> <option value=\"安徽\">安徽</option> <option value=\"苏州\" selected>苏州</option> </c:if> </select> </div> <div class=\"form-group\"> <label for=\"qq\">QQ:</label> <input type=\"text\" id=\"qq\" class=\"form-control\" name=\"qq\" placeholder=\"请输入QQ号码\" value=\"${user.qq}\"/> </div> <div class=\"form-group\"> <label for=\"email\">Email:</label> <input type=\"text\" id=\"email\" class=\"form-control\" name=\"email\" placeholder=\"请输入邮箱地址\" value=\"${user.email}\"/> </div> <div class=\"form-group\" style=\"text-align: center\"> <input class=\"btn btn-primary\" type=\"submit\" value=\"提交\"/> <input class=\"btn btn-default\" type=\"reset\" value=\"重置\"/> <input class=\"btn btn-default\" type=\"button\" value=\"返回\"/> </div> </form></div></body></html>","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/tags/Java/"},{"name":"MVC","slug":"MVC","permalink":"https://chemlez.github.io/tags/MVC/"},{"name":"Servlet","slug":"Servlet","permalink":"https://chemlez.github.io/tags/Servlet/"}]},{"title":"conda常用命令","slug":"conda常用命令","date":"2020-05-27T03:09:18.000Z","updated":"2020-05-27T03:51:24.698Z","comments":true,"path":"2020/05/27/conda常用命令/","link":"","permalink":"https://chemlez.github.io/2020/05/27/conda%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/","excerpt":"一、设置镜像源conda1234// 以清华镜像为例conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/conda config --set show_channel_urls yes 此时在当前用户的根目录下生成一个名为”.condarc”的配置文件,打开该文件。 将”.condarc”配置文件内容修改如下,此外可以添加更多的下载渠道。 12345678channels: - defaultsshow_channel_urls: truechannel_alias: https://mirrors.tuna.tsinghua.edu.cn/anacondadefault_channels: - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r","text":"一、设置镜像源conda1234// 以清华镜像为例conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/conda config --set show_channel_urls yes 此时在当前用户的根目录下生成一个名为”.condarc”的配置文件,打开该文件。 将”.condarc”配置文件内容修改如下,此外可以添加更多的下载渠道。 12345678channels: - defaultsshow_channel_urls: truechannel_alias: https://mirrors.tuna.tsinghua.edu.cn/anacondadefault_channels: - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r 最后通过 conda info查看当前配置信息。URL如下: 123456channel URLs : https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/Paddle/osx-64 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/Paddle/noarch https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/osx-64 https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/noarch https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/osx-64 https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/noarch pip 临时使用 在使用pip的时候加参数-i https://pypi.tuna.tsinghua.edu.cn/simple 例如:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pandas,即从清华镜像下载pandas库。 永久使用 修改 ~/.pip/pip.conf(如果没有则自行创建),修改index-url至tuna.即: 12[global]index-url = https://pypi.tuna.tsinghua.edu.cn/simple 阿里云镜像https://mirrors.aliyun.com/pypi/simple 二、常用命令2.1 Anaconda环境相关123456conda create --name [env-name] # 建立名为[env-name]的conda环境conda create --name [env-name] python=x.x # 建立名为[env-name]的conda环境并指定python版本conda activate [env-name] # 激活[env-name]环境conda deactivate # 退出当前环境conda env remove --name [env-name] # 删除名为[env-name]的conda环境conda env list # 列出所有的conda虚拟环境 2.2 conda安装包安装名为[package-name]的包 conda install [package-name] 安装名为[package-name]的包并制定版本X.X conda install [package-name]=X.X 更新名为[package-name]的包 conda update [package-name] 删除名为[package-name]的包 conda remove [package-name] 列出名为[package-name]的包在conda源中的所有可用版本 conda search [package-name] 列出当前环境下已安装的所有包 conda list 2.3 pip安装包安装名为[package-name]的包并制定版本X.X pip install [package-name]==X.X 更新名为[package-name]的包 pip install [package-name] --upgrade 删除名为[package-name]的包 pip uninstall [package-name] 2.3 终端启动Jupyterpython3 -m IPython notebook","categories":[],"tags":[{"name":"conda","slug":"conda","permalink":"https://chemlez.github.io/tags/conda/"}]},{"title":"对小木虫考研调剂信息的爬取","slug":"对小木虫考研调剂信息的爬取","date":"2020-04-09T12:15:36.000Z","updated":"2020-08-16T06:28:34.193Z","comments":true,"path":"2020/04/09/对小木虫考研调剂信息的爬取/","link":"","permalink":"https://chemlez.github.io/2020/04/09/%E5%AF%B9%E5%B0%8F%E6%9C%A8%E8%99%AB%E8%80%83%E7%A0%94%E8%B0%83%E5%89%82%E4%BF%A1%E6%81%AF%E7%9A%84%E7%88%AC%E5%8F%96/","excerpt":"一、说明由于国家线快出了,故写了一份爬取小木虫网站调剂信息的爬虫代码,方便信息查看。此代码仅用于学习,不作为任何商业用途。","text":"一、说明由于国家线快出了,故写了一份爬取小木虫网站调剂信息的爬虫代码,方便信息查看。此代码仅用于学习,不作为任何商业用途。 二、代码–单线程 单线程示例 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106#!~/opt/anaconda3/bin/python# -*- coding: utf-8 -*-import requestsfrom bs4 import BeautifulSoupimport reimport pandas as pdimport os# 获取网页def getHTMLText(url): try: r = requests.get(url, timeout=30) r.raise_for_status() r.encoding = r.apparent_encoding return r.text except: return ''# 获取数据def getDataInfo(infoList, url, pre_params, *args): params = [] count = -1 for i in args: count += 1 par_ = pre_params[count] + i params.append(par_) # 根据参数获取访问链接 for param in params: url += param + '&' # print(url) html = getHTMLText(url) soup = BeautifulSoup(html, 'html.parser') # 获取页码数,并处理空页异常 try: pages_tag = soup.find_all('td', 'header')[1].string pages = int(re.split('/', pages_tag)[1]) except: pages = 0 # 判读是否只有一页 if pages == 0: pages += 1 for i in range(pages): # 遍历每一页 page = i + 1 url = url + '&page=' + str(page) html = getHTMLText(url) soup = BeautifulSoup(html, 'html.parser') tbody = soup.find_all('tbody', 'forum_body_manage')[0] trs = tbody.find_all('tr') # 每个学校的全部信息被tr标签包围 for tr in trs: # 遍历每一个学校 dicts = {} href = tr.find_all('a')[0].get('href') # 定位至a标签,提取href的属性值 tds = tr.find_all('td') # 每个学校的各个信息包含在td标签内 lens = len(tds) for i in range(lens): # 将各个学校信息添加至字典中 if i == 0: title = tds[i].find('a').string dicts[i] = title else: dicts[i] = tds[i].string dicts['href'] = href print(dicts) infoList.append(dicts) # 每一个学校的信息,添加至列表def outputCSV(infoList, path): data = pd.DataFrame(infoList) # with open(r'./info.csv','w+',encoding='utf-8') as f: try: data.columns = ['标题', '学校', '门类/专业', '招生人数', '发布时间', '链接'] except: print('没有调剂信息...') try: if not os.path.exists(path): data.to_csv(path) print('保存成功') else: print('路径存在') except: print('保存失败')# 设定查询参数 -- 专业、年份def parameters(pro_='', pro_1='', pro_2='', year=''): paramsList = [pro_, pro_1, pro_2, year] return paramsListdef main(): url = 'http://muchong.com/bbs/kaoyan.php?' path = './2020计算机调剂信息(截止4.09).csv' pre_params = ['r1%5B%5D=', 'r2%5B%5D=', 'r3%5B%5D=', 'year='] params = parameters(pro_='08', pro_1='0812',year='2020') dataList = [] getDataInfo(dataList, url, pre_params, *params) outputCSV(dataList, path)main() 三、代码–多线程 多线程示例 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163#! ~/opt/anaconda3/bin/python# -*- coding: utf-8 -*-import requestsfrom bs4 import BeautifulSoupimport reimport pandas as pdimport osimport timefrom threading import Threadfrom threading import Lockimport timedef getHTMLText(url): \"\"\" 获取网页 \"\"\" try: r = requests.get(url, timeout=30) r.raise_for_status() r.encoding = r.apparent_encoding return r.text except: return ''def getPages(infoList, url, pre_params, *args): \"\"\" 获取当前需要爬取的页面数,及完整链接 \"\"\" params = [] count = -1 for i in args: count += 1 par_ = pre_params[count] + i params.append(par_) for param in params: url += param + '&' # print(url) html = getHTMLText(url) soup = BeautifulSoup(html, 'html.parser') # 处理空页异常 try: pages_tag = soup.find_all('td', 'header')[1].string pages = int(re.split('/', pages_tag)[1]) except: pages = 0 # 判读是否只有一页 if pages == 0: pages += 1 return pages, urlpage = 0lock = Lock()def getDataInfo(infoList, pages, url): \"\"\" 获取数据信息 \"\"\" global page while True: lock.acquire() page += 1 lock.release() if page > pages: break url = url + '&page=' + str(page) time.sleep(1) # lock.acquire() html = getHTMLText(url) soup = BeautifulSoup(html, 'html.parser') tbody = soup.find_all('tbody', 'forum_body_manage')[0] trs = tbody.find_all('tr') # 每个学校的全部信息被tr标签包围 for tr in trs: # 遍历每一个学校 dicts = {} href = tr.find_all('a')[0].get('href') # 定位至a标签,提取href的属性值 tds = tr.find_all('td') # 每个学校的各个信息包含在td标签内 lens = len(tds) for i in range(lens): if i == 0: title = tds[i].find('a').string dicts[i] = title else: dicts[i] = tds[i].string dicts['href'] = href print(dicts) infoList.append(dicts)def outputCSV(infoList, path): \"\"\" 输出文档 \"\"\" data = pd.DataFrame(infoList) # with open(r'./info.csv','w+',encoding='utf-8') as f: try: data.columns = ['标题', '学校', '门类/专业', '招生人数', '发布时间', '链接'] data.sort_values(by='发布时间', ascending=False, inplace=True) data = data.reset_index(drop=True) except: print('没有调剂信息...') return try: if not os.path.exists(path): data.to_csv(path) print('爬取成功') else: print('路径存在') except: print('保存失败')def parameters(pro_='', pro_1='', pro_2='', year=''): \"\"\" 设定查询参数 -- 专业、年份 \"\"\" paramsList = [pro_, pro_1, pro_2, year] return paramsListdef threadingUp(count, infoList, pages, url): \"\"\" 启动多线程 \"\"\" threadList = [] iList = [] for i in range(count): iList.append(i) t = Thread(target=getDataInfo, args=(infoList, pages, url)) t.start() threadList.append(t) for thread in threadList: thread.join()def main(): url = 'http://muchong.com/bbs/kaoyan.php?' path = './08.csv' pre_params = ['r1%5B%5D=', 'r2%5B%5D=', 'r3%5B%5D=', 'year='] params = parameters(pro_='08', year='2020') dataList = [] count = 1000 pages, url_ = getPages(dataList, url, pre_params, *params) start = time.time() threadingUp(count, dataList, pages, url_) # 多线程 # getDataInfo(dataList,pages,url_) # 单线程 outputCSV(dataList, path) end = time.time() print('时间:'+str(end - start))if __name__ == \"__main__\": main() 四、代码使用参数说明123456789101112def parameters(pro_='', pro_1='', pro_2='', year=''): paramsList = [pro_, pro_1, pro_2, year] return paramsListdef main(): url = 'http://muchong.com/bbs/kaoyan.php?' path = './data_info.csv' pre_params = ['r1%5B%5D=', 'r2%5B%5D=', 'r3%5B%5D=', 'year='] params = parameters(pro_='08', pro_1='0801') dataList = [] getDataInfo(dataList, url, pre_params, *params) outputCSV(dataList, path) 主体代码已写完,只需要修改main函数中params中的相关参数,即可使用。 parameters函数主要用于返回查询的参数。默认参数都为空。如果都不填,则是爬取小木虫全部年份,全部专业的所有调剂信息。 params具体参数说明: pro_ 所要查询的学科门类。可查询的见下图: 只要查询填写对应学科门类前的数字即可。例如工学,则:pro_='08' 注意:填写的为字符串格式 pro_1 填写的一级学科代码。如下图: 以电子科学与技术为例,同样只需要填写前面代码即可。如:pro_2='0806' 如果这一项不填,则查询的是前一个填写的整个学科门类所有信息。 pro_2 填写的二级学科代码。如图: 例如查询物理电子学调剂信息,同上。则填:pro_2='080901'。如果不填,则默认查询的是上一级学科下的所有调剂信息。例如,这里就是全部的电子科学与技术的调剂信息。 year 查询年份。例如查询2020年。year='2020'。注意:同样是字符串类型。如果不填,则是查询全部的年份。 其中,main()函数中的保存路径path,可自定义修改。 总结:只需修改params和保存路径url即可。 五、效果图 附小木虫调剂信息网站:http://muchong.com/bbs/kaoyan.php 下载源码","categories":[{"name":"Python","slug":"Python","permalink":"https://chemlez.github.io/categories/Python/"},{"name":"爬虫","slug":"Python/爬虫","permalink":"https://chemlez.github.io/categories/Python/%E7%88%AC%E8%99%AB/"}],"tags":[{"name":"爬虫","slug":"爬虫","permalink":"https://chemlez.github.io/tags/%E7%88%AC%E8%99%AB/"},{"name":"正则表达式","slug":"正则表达式","permalink":"https://chemlez.github.io/tags/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F/"},{"name":"Request","slug":"Request","permalink":"https://chemlez.github.io/tags/Request/"},{"name":"Beautifulsoup","slug":"Beautifulsoup","permalink":"https://chemlez.github.io/tags/Beautifulsoup/"}]},{"title":"机器学习:特征工程之数据预处理","slug":"机器学习-特征工程之数据预处理","date":"2020-04-04T17:21:13.000Z","updated":"2020-04-05T13:57:37.017Z","comments":true,"path":"2020/04/05/机器学习-特征工程之数据预处理/","link":"","permalink":"https://chemlez.github.io/2020/04/05/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0-%E7%89%B9%E5%BE%81%E5%B7%A5%E7%A8%8B%E4%B9%8B%E6%95%B0%E6%8D%AE%E9%A2%84%E5%A4%84%E7%90%86/","excerpt":"在上一节中的泰坦尼克号入门案例的数据预处理过程中,出现了数据不完整、数据的编码(数值转化),即将非结构化文本转化为结构化文本。本文主要用来记录在sklearn中常用的数据预处理基本方法。 数据预处理从数据中检测,纠正或删除损坏,不准确或不适用于模型的记录的过程。 可能面对的问题有:数据类型不同,比如有的是文字,有的是数字,有的含时间序列,有的连续,有的间断。也可能,数据的质量不行,有噪声,有异常,有缺失,数据出错,量纲不一,有重复,数据是偏态,数据量太大或太小。 目的:让数据适应模型,匹配模型的需求。","text":"在上一节中的泰坦尼克号入门案例的数据预处理过程中,出现了数据不完整、数据的编码(数值转化),即将非结构化文本转化为结构化文本。本文主要用来记录在sklearn中常用的数据预处理基本方法。 数据预处理从数据中检测,纠正或删除损坏,不准确或不适用于模型的记录的过程。 可能面对的问题有:数据类型不同,比如有的是文字,有的是数字,有的含时间序列,有的连续,有的间断。也可能,数据的质量不行,有噪声,有异常,有缺失,数据出错,量纲不一,有重复,数据是偏态,数据量太大或太小。 目的:让数据适应模型,匹配模型的需求。 1. 数据无量纲化在机器学习算法实践中,往往有着将不同规格的数据转换到同一规格,或不同分布的数据转换到某个特定分布的需求,这种需求统称为将数据“无量纲化”。 数据的无量纲化包括线性与非线性。其中线性的无量纲化包括:中心化(Zero-centered或Mean-subtraction)处理和缩放处理(Scale)。 中心化 让所有记录减去一个固定值,即让数据的样本数据平移到某个位置。 缩放处理 通过除以一个固定值,将数据固定在某个范围之中,通常采用取对数的方式。 1.1 数据归一化当数据(x)按照最小值中心化后,再按极差(最大值-最小值)缩放,数据移动了最小值个单位,并且会被收敛到[0,1]之间,而这个过程,就叫做数据归一化(Normalization,又称Min-MaxScaling)。公式如下: $$x={x^*-min(x)\\over max(x)-min(x)}$$ 在sklearn中通过preprocessing.MinMaxScaler实现此功能。其中,feature_range可以控制数据压缩的范围,默认为[0,1]。 12345678from sklearn.preprocessing import MinMaxScalerimport pandas as pddata = [[-1,2],[-0.5,6],[0,10],[1,18]]pd.DataFrame(data)# 实现归一化scaler = MinMaxScaler() # 实例化scaler = scaler.fit(data) # 生成min(x),max(x)result = scaler.transform(data) # 导出结果 结果输出: 1234array([[0. , 0. ], [0.25, 0.25], [0.5 , 0.5 ], [1. , 1. ]]) 将所有的数据压缩至[0,1]之间。 123456scaler.inverse_transform(result) #逆转结果Out: array([[-1. , 2. ], [-0.5, 6. ], [ 0. , 10. ], [ 1. , 18. ]]) 采用feature_range将数据范围压缩至[0,5]之间。 12345678910# 使用MinMaxScaler的参数feature_range实现将数据归一化到[0,1]以外的范围中data = [[-1,2],[-0.5,6],[0,10],[1,18]]scaler = MinMaxScaler(feature_range=[5,10]) # 实例化归一化到5~10之间result = scaler.fit_transform(data)resultOut: array([[ 5. , 5. ], [ 6.25, 6.25], [ 7.5 , 7.5 ], [10. , 10. ]]) 采用Numpy实现归一化处理。 123456789101112131415161718192021222324# 使用numpy来实现归一化import numpy as npX = np.array(data)XOut: array([[-1. , 2. ], [-0.5, 6. ], [ 0. , 10. ], [ 1. , 18. ]])X_nor = (X - X.min(axis=0))/(X.max(axis=0) - X.min(axis=0))X_norOut: array([[0. , 0. ], [0.25, 0.25], [0.5 , 0.5 ], [1. , 1. ]])# 还原,即:公式的还原X = X_nor * (X.max(axis=0) - X.min(axis=0)) + X.min(axis=0)XOut: array([[-1. , 2. ], [-0.5, 6. ], [ 0. , 10. ], [ 1. , 18. ]]) 通过以上的实例,将数据压缩至统一的范围内。 1.2 数据标准化当数据(x)按均值(μ)中心化后,再按标准差(σ)缩放,数据就会服从为均值为0,方差为1的正态分布(即标准正态分布),而这个过程,就叫做数据标准化(Standardization,又称Z-scorenormalization),公式如下: $$x^*={x-u\\over \\sigma} $$ sklearn中提供了preprocessing.StandarScaler接口进行使用。 123from sklearn.preprocessing import StandardScalerscaler = StandardScaler() # 实例化scaler.fit(data) # fit,本质用于生成均值和方差 12345# 对每一列向量表示一个特征,故默认对列进行操作scaler.mean_ # 查看均值的属性mean_scaler.var_ # 查看方差的属性var_Out: array([-0.125, 9. ]) 导出结果: 12345678# 导出结果x_std = scaler.transform(data)x_stdOut: array([[-1.18321596, -1.18321596], [-0.50709255, -0.50709255], [ 0.16903085, 0.16903085], [ 1.52127766, 1.52127766]]) 查看其方差与均值 123# 结果均值为0,方差为1的标准正态分布x_std.mean()x_std.std() 逆标准化 123456scaler.inverse_transform(x_std) # 使用inverse_transform逆标准化Out: array([[-1. , 2. ], [-0.5, 6. ], [ 0. , 10. ], [ 1. , 18. ]]) 1.3 小结目的:为了把不同来源的数据(不同特征)统一到同一数量级(一个参考坐标系)下,消除指标之间的量纲影响,解决数据指标简单可比性问题。 优点: 提高精度 可提高梯度下降求最优解的速度 2. 数据缺失值的处理此小节记录对于sklearn中缺失值处理的基本方法。 导入数据: 1234567891011121314import pandas as pddata = pd.read_csv(r'/jupyter-notebook/sklearn/2- Feature Engineering/Narrativedata.csv',index_col=0)data.head()data.info()Out: <class 'pandas.core.frame.DataFrame'> Int64Index: 891 entries, 0 to 890 Data columns (total 4 columns): Age 714 non-null float64 Sex 891 non-null object Embarked 889 non-null object Survived 891 non-null object dtypes: float64(1), object(3) memory usage: 34.8+ KB 从以上结果中可以看出,共有891条数据,其中Age,Embarked皆存在缺失值。sklearn中提供了sklearn.impute.SimpleImputer接口处理缺失值。 首先对Age缺失值处理方式: 12345678910from sklearn.impute import SimpleImputerAge = data.loc[:,'Age'].values.reshape(-1,1)imp_mean = SimpleImputer() # 实例化,默认均值填补imp_median = SimpleImputer(strategy='median') # 采取中位数填补imp_0 = SimpleImputer(strategy='constant',fill_value=0) # 给定常数,以0填补imp_most = SimpleImputer(strategy='most_frequent')#采用众数进行填补,可用于字符串imp_mean = imp_mean.fit(Age)imp_mean = imp_mean.transform(Age)imp_median = imp_median.fit_transform(Age)imp_most = imp_most.fit_transform(Age) 结果输出,取前5个数据。 123456789imp_mean[:5]imp_median[:5]imp_most[:5]Out: # 采用众数进行填补的结果 array([[22.], [38.], [26.], [35.], [35.]]) 将众数作为Age缺失值处理的方式: 12Age = imp_mostdata.loc[:,'Age'] = Age 对Embarked处理的方式: 123456# 采用众数填补EmbarkedEmbarked = data.loc[:,'Embarked'].values.reshape(-1,1)imp_most = SimpleImputer(strategy='most_frequent')imp_most = imp_most.fit_transform(Embarked)Embarked = imp_mostdata.loc[:,'Embarked'] = Embarked 注意:众数的施加对象可以是非数值型。 补充: 采用Pandas和Numpy进行缺失值的填补 12345678910111213141516# 采用平均值填补年龄的缺失值,利用.fillna 在DataFrame里面进行填补data_.loc[:,'Age'] = data_.loc[:,'Age'].fillna(data.loc[:,'Age'].mean())# 删除Embarked缺失的两条记录,dropna(axis=0)删除所有有缺失值的行,.dropna(axis=1) 删除所有有缺失值的列# 当采用删除操作时axis=0是对行操作,axis=1是对列操作;拼接,切片相反data_.dropna(axis=0,inplace=True)data_.info()Out: <class 'pandas.core.frame.DataFrame'> Int64Index: 889 entries, 0 to 890 Data columns (total 4 columns): Age 889 non-null float64 Sex 889 non-null object Embarked 889 non-null object Survived 889 non-null object dtypes: float64(1), object(3) memory usage: 34.7+ KB 3. 编码与哑变量在机器学习中,大多数算法,譬如逻辑回归,支持向量机SVM,k近邻算法等都只能够处理数值型数据,不能处理文字,在sklearn当中,除了专用来处理文字的算法,其他算法在fit的时候全部要求输入数组或矩阵,也不能够导入文字型数据(其实手写决策树和普斯贝叶斯可以处理文字,但是sklearn中规定必须导入数值型)。然而在现实中,许多标签和特征在数据收集完毕的时候,都不是以数字来表现的。比如说,学历的取值可以是[“小学”,“初中”,“高中”,”大学”],付费方式可能包含[“支付宝”,“现金”,“微信”]等等。在这种情况下,为了让数据适应算法和库,我们必须将数据进行编码,即是说,将文字型数据转换为数值型。 3.1 标签的编码preprocessing.LabelEncoder:标签专用,能够将分类转换为分类数值 123456789from sklearn.preprocessing import LabelEncoder # 对标签进行编码y = data.iloc[:,-1] # 取出特征,最后一列,标签允许是一维le = LabelEncoder() # 实例化le = le.fit(y)label = le.transform(y)data.iloc[:,-1] = labelle.classes_ # 查看标签中类别数量Out: array(['No', 'Unknown', 'Yes'], dtype=object) 查看标签Survived这一列: 123456789# 取前5条数据查看data['Survived'][:5]Out: 0 0 1 2 2 2 3 2 4 0 Name: Survived, dtype: int64 3.2 特征的编码preprocessing.OrdinalEncoder:特征专用,能够将分类特征转换为分类数值。 1234567fromsklearn.preprocessingimportOrdinalEncoder #接口categories_对应LabelEncoder的接口classes_,一模一样的功能data_=data.copy()data_.head()OrdinalEncoder().fit(data_.iloc[:,1:-1]).categories_data_.iloc[:,1:-1]=OrdinalEncoder().fit_transform(data_.iloc[:,1:-1])data_.head() 3.3 独热编码——创建哑变量类别OrdinalEncoder可以用来处理有序变量,但对于名义变量,我们只有使用哑变量的方式来处理,才能够尽量向算法传达最准确的信息。 名义变量 判断两变量是否相同。例如:性别,邮编,身份证号等等 有序变量 为数据的相对大小提供信息,但数据之间大小的间隔不是具有固定意义的,不能做加减运算。例如:学历。 有距变量 有距变量之间的间隔是有固定意义的,可做加减运算。例如:日期 从以上定义看出,性别、舱门号等属于有序变量。在之前的编码中,性别简单采用的0\\1区别男\\女。但是,在编码的过程中,想要表达的是男≠女。当被我们转换为[0,1]时,存在着大小关系,即从名义变量的编码转化成为了有距变量的编码。 故:我们采用独热编码(one-hot)的形式进行编码。男:[1,0],女:[0,1]。这样,便能够将男女的编码区别于一般的0、1编码,让算法明白这两取值是没有计算性质的,这种编码即为哑变量。 在sklearn中提供了sklearn.preprocessing.OneHotEncoder接口进行哑变量处理。 1234567891011from sklearn.preprocessing import OneHotEncoderX = data.iloc[:,1:-1] #取特征,即:Sex、Embarked# one-hotenc = OneHotEncoder() # 实例化enc = enc.fit(X)result = enc.transform(X)resultOut: <889x5 sparse matrix of type '<class 'numpy.float64'>' with 1778 stored elements in Compressed Sparse Row format> result中返回的是结果集对象地址。 123456789result.toarray()Out: array([[0., 1., 0., 0., 1.], [1., 0., 1., 0., 0.], [1., 0., 0., 0., 1.], ..., [1., 0., 0., 0., 1.], [0., 1., 1., 0., 0.], [0., 1., 0., 1., 0.]]) 从结果中,看出我们得到5列特征。其中,Sex包含男女两类,Embarked包含S、Q、C三类。故通过One-hot得到了5类特征。 123enc.get_feature_names() # 用于查看特征默认的名称Out: array(['x0_female', 'x0_male', 'x1_C', 'x1_Q', 'x1_S'], dtype=object) 将新得到的特征表示,拼接至原有数据后: 123newdata = pd.concat([data,pd.DataFrame(result)],axis=1)# 将数据进行拼接newdata.drop(['Sex','Embarked'],inplace=True,axis=1) # 删除原来的特征newdata.columns = ['Age','Survived','Female','Male','Embarked_C','Embarked_Q','Embarked_S'] # 列名重命名 4. 连续型特征处理:二值化与分段在上一小节的特征处理中,one-hot处理的是离散型变量。根据阈值将数据二值化(将特征值设置为0或1),用于处理连续型变量。大于阈值的值映射为1,而小于或等于阈值的值映射为0。默认阈值为0时,特征中所有的正值都映射到1。 二值化是对文本计数数据的常见操作,分析人员可以决定仅考虑某种现象的存在与否。它还可以用作考虑布尔随机变量的估计器的预处理步骤(例如,使用贝叶斯设置中的伯努利分布建模)。 sklearn中提供了sklearn.preprocessing.Binarizer用于连续型数据的二值化处理。 12345678910from sklearn.preprocessing import Binarizer # 用于将根阈值将数据二值化,处理连续型变量的工具包data_2 = data.copy()X = data_2.iloc[:,0].values.reshape(-1,1)transformer = Binarizer(threshold=30).fit_transform(X) # threshold=30,即以30作为二值化分段的界限transformer[:4]Out: array([[0], [1], [0], [1]]) 从年龄结果的前4条数据看出,年龄大于30的映射为1,小于等于30的映射为0。 sklearn.preprocessing.KBinsDiscretizer可用于设计连续型变量数据的n分类。 参数解释: 参数 含义&输入 n_bins 每个特征中分箱的个数,默认5,一次会被运用到所有导入的特征 ncode 编码的方式,默认“onehot”“onehot”:做哑变量,之后返回一个稀疏矩阵,每一列是一个特征中的一个类别,含有该类别的样本表示为1,不含的表示为0 “ordinal”:每个特征的每个箱都被编码为一个整数,返回每一列是一个特征,每个特征下含有不同整数编码的箱的矩阵“onehot-dense”:做哑变量,之后返回一个密集数组。 strategy 用来定义箱宽的方式,默认”quantile”“uniform”:表示等宽分箱,即每个特征中的每个箱的最大值之间的差为(特征.max()-特征.min())/(n_bins)“quantile”:表示等位分箱,即每个特征中的每个箱内的样本数量都相同“kmeans”:表示按聚类分箱,每个箱中的值到最近的一维k均值聚类的簇心得距离都相同 123456789101112131415161718from sklearn.preprocessing import KBinsDiscretizerX = data.iloc[:,0].values.reshape(-1,1)# n_bins 为划分的数量,即需要划分多少类。est = KBinsDiscretizer(n_bins=6,encode='ordinal',strategy='uniform')t = est.fit_transform(X)t[:10]Out: array([[1.], [2.], [1.], [2.], [2.], [2.], [4.], [0.], [2.], [1.]])set(t.ravel()) # .ravel() 用于降维,set集合去重,查看类别的数量 5. 源码下载 下载源码","categories":[{"name":"Machine Learning","slug":"Machine-Learning","permalink":"https://chemlez.github.io/categories/Machine-Learning/"},{"name":"sklearn","slug":"Machine-Learning/sklearn","permalink":"https://chemlez.github.io/categories/Machine-Learning/sklearn/"}],"tags":[{"name":"sklearn","slug":"sklearn","permalink":"https://chemlez.github.io/tags/sklearn/"},{"name":"Feature Engineering","slug":"Feature-Engineering","permalink":"https://chemlez.github.io/tags/Feature-Engineering/"},{"name":"Data Preprocessing","slug":"Data-Preprocessing","permalink":"https://chemlez.github.io/tags/Data-Preprocessing/"}]},{"title":"Python爬虫基础入门","slug":"Python爬虫基础入门","date":"2020-03-18T09:30:11.000Z","updated":"2020-04-08T10:57:44.613Z","comments":true,"path":"2020/03/18/Python爬虫基础入门/","link":"","permalink":"https://chemlez.github.io/2020/03/18/Python%E7%88%AC%E8%99%AB%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8/","excerpt":"一、Requests库的7个主要方法 方法 说明 requests.request() 构造一个请求,支撑一下各方法的基础方法 requests.get() 获取HTML网页的主要方法,对应于HTTP的GET requests.head() 获取HTML网页头信息的方法,对应于HTTP的HEAD requests.post() 向HTML网页提交POST请求的方法,对应于HTTP的POST requests.put() 向HTML网页提交PUT请求的方法,对应于HTTP的PUT requests.pathch() 向HTML网页提交局部修改请求,对应于HTTP的PATCH requests.delete() 向HTML页面提交删除请求,对应于HTTP的DELETE 1.requests.getr = requests.get(url) 返回一个包含服务器资源的Response对象,包含爬虫返回的全部内容(内容被封装,返回的是地址信息) 构造一个向服务器请求资源的Request对象 requests.get(url,params=None,**kwargs) url:拟获取网页的url链接 params:url中的额外参数,字典或字节流格式,可选 **kwargs:12个控制访问的参数","text":"一、Requests库的7个主要方法 方法 说明 requests.request() 构造一个请求,支撑一下各方法的基础方法 requests.get() 获取HTML网页的主要方法,对应于HTTP的GET requests.head() 获取HTML网页头信息的方法,对应于HTTP的HEAD requests.post() 向HTML网页提交POST请求的方法,对应于HTTP的POST requests.put() 向HTML网页提交PUT请求的方法,对应于HTTP的PUT requests.pathch() 向HTML网页提交局部修改请求,对应于HTTP的PATCH requests.delete() 向HTML页面提交删除请求,对应于HTTP的DELETE 1.requests.getr = requests.get(url) 返回一个包含服务器资源的Response对象,包含爬虫返回的全部内容(内容被封装,返回的是地址信息) 构造一个向服务器请求资源的Request对象 requests.get(url,params=None,**kwargs) url:拟获取网页的url链接 params:url中的额外参数,字典或字节流格式,可选 **kwargs:12个控制访问的参数 2.Response对象的属性 属性 说明 r.status_code HTTP请求的返回状态,200表示连接成功,404表示失败 r.text HTTP响应内容的字符串形式,即,url对应的页面内容 r.encoding 从HTTP header中猜测的响应内容编码方式 r.apparent_encoding 从内容中分析出的响应内容编码方式(备选编码方式) r.content HTTP响应的二进制形式 r.encoding:如果header中不存在charset,则认为编码为ISO-8859-1 r.apparent_encoding:根据网页内容分析出的编码方式 3.理解Requests库的异常 异常 说明 requests.ConnectionError 网络连接错误异常,如DNS查询失败、拒绝连接等 requests.HTTPError HTTP错误异常 requests.TooManyRedirects 超过最大重定向次数,产生重定向异常 requests.ConnectTimeout 连接远程服务器时异常 requests.URLRequired URL缺失异常 Requests.Timeout 请求URL超时,产生超时异常 异常 说明 r.raise_for_status 如果不是200,产生异常requests.HTTPError 爬取网页的通用代码框架12345678910111213import requestsdef getHTMLText(url): try: r = requests.get(url,timeout=30) r.raise_for_status() # 如果状态不是200,引发HTTPError异常 r.encoding = r.apparent_encoding return r.text except: return '产生异常'if __name__ == '__main__': url = 'http://www.baidu.com' print(getHTMLText(url)) 二、HTTP协议HTTP,Hypertext Transfer Protocol,超文本传输协议。 HTTP是一个基于”请求与响应“模式的、无状态的应用层协议。 无状态:第一次请求与第二次请求无关联 HTTP协议采用URL作为定位网络资源的标识。 URL格式 http://host[:port][path] host:合法的Internet主机域名或IP地址 port:端口号,缺省端口为80 path:请求资源的路径 HTTP URL的理解 URL是通过HTTP协议存取资源的Internet路径,一个URL对应一个数据资源。 1.HTTP协议对资源的操作 方法 说明 GET 请求获取URL位置的资源 HEAD 请求获取URL位置资源的响应消息报告,即获得该资源的头部信息 POST 请求向URL位置的资源后附新的数据 PUT 请求向URL位置存储一个资源,覆盖原URL PATCH 请求局部更新URL位置的资源,即改变该处资源的部分内容 DELETE 请求删除URL位置存储的资源 2.理解PATCH和PUT的区别假设URL位置有一组数据UserInfo,包括UserID、UserName等20个字段。 需求:用户修改了UserName,其他不变。 采用PATCH,仅向URL提交UserName的局部更新请求。 采用PUT,必须将所有20个字段一并提交到URL,未提交字段被删除。 PATCH的最主要好处:节省网络带宽 三、Requests库的7个主要方法解析1.requests.request()requests.request(method,url,**kwargs) method:请求方式。 ‘GET’、’HEAD’、’POST’、’PUT’、’PATCH’、’delete’、’OPTIONS’ **kwargs:控制访问的参数,均为可选项。 params:字典或字节序列,作为参数增加到url中。 data:字典、字节序列对象,重点是向服务器提交资源时使用。 json:JSON格式的数据,作为request的内容。 headers:字典,HTTP定制头。 cookies:字典或CookieJar,Request中的cookie。 auth:元祖,支持HTTP认证功能。 files:字典类型,传输文件。 timeout:设定超时时间,秒为单位。 proxies:字典类型,设定访问代理服务器,可以增加登录认证。 allow_redirects:True/False,默认为True,重定向开关。 stream:True/False,默认为True,获取内容立即下载开关。 verify:True/False,默认为True,认证SSL证书开关。 cert:本地SSL证书路径。 四、Beautiful Soup库使用1.BeautifulSoup 基本使用12from bs4 import BeautifulSoupsoup = BeautifulSoup('<p>data</p>','html.parser') # 第一个参数为html文本内容,对html标签进行解析 2.Beautiful Soup库理解Beautiful Soup库,也叫做 beautifulsoup4或bs4, 是解析、变量、维护”标签树“的功能库。只要提供的文件是标签类型,Beautiful Soup库都可以用来解析。 因为文档和标签树是一一对应的,标签树经过Beautiful Soup,转换为Beautiful Soup类型。故,文档和标签树以及Beautiful Soup是一一对应关系。 123from bs4 import BeautifulSoupsoup = BeautifulSoup('<p>data</p>','html.parser')soup2 = BeautifulSoup(open(\"D://demo.html\",'html.parser') Beautiful Soup对应一个HTML/XML文档的全部内容。 3.Beautiful Soup库解析器 解析器 使用方法 条件 bs的HTML解析器 BeautifulSoup(mk,’html.parser’) 安装bs4库 lxml的HTML解析器 BeautifulSoup(mk,’lxml’) pip install lxml lxml的XML解析器 BeautifulSoup(mk,’xml’) pip install lxml html5lib的解析器 BeautifulSoup(mk,’htlm5lib’) pip install html5lib 4.Beautiful Soup类的基本元素 基本元素 说明 Tag 标签,最基本的信息组织单元,分别用<>和</>表面开头和结尾 Name 标签的名称,…的名字是’p’,格式:.name Attributes 标签的属性,字典形式组织,格式:.attrs NavigableString 标签内非属性字符串,<>…</>中字符串,格式:.string Comment 标签内字符串的注释部分,一种特殊的Comment类型 123456789101112131415161718192021222324252627282930313233343536373839404142434445import requestsr = requests.get('http://python123.io/ws/demo.html')demo = r.text # demo为标签文本>'<html><head><title>This is a python demo page</title></head>\\r\\n<body>\\r\\n<p class=\"title\"><b>The demo python introduces several python courses.</b></p>\\r\\n<p class=\"course\">Python is a wonderful general-purpose programming language. You can learn Python from novice to professional by tracking the following courses:\\r\\n<a href=\"http://www.icourse163.org/course/BIT-268001\" class=\"py1\" id=\"link1\">Basic Python</a> and <a href=\"http://www.icourse163.org/course/BIT-1001870001\" class=\"py2\" id=\"link2\">Advanced Python</a>.</p>\\r\\n</body></html>'# 利用BeautifulSoup 解析成标签树from bs4 import BeautifulSoupsoup = BeautifulSoup(demo,'html.parser')soup>[out]:<html><head><title>This is a python demo page</title></head><body><p class=\"title\"><b>The demo python introduces several python courses.</b></p><p class=\"course\">Python is a wonderful general-purpose programming language. You can learn Python from novice to professional by tracking the following courses:<a class=\"py1\" href=\"http://www.icourse163.org/course/BIT-268001\" id=\"link1\">Basic Python</a> and <a class=\"py2\" href=\"http://www.icourse163.org/course/BIT-1001870001\" id=\"link2\">Advanced Python</a>.</p></body></html>print(soup.prettify()) # 输出标签树 >[out]:<html> <head> <title> This is a python demo page </title> </head> <body> <p class=\"title\"> <b> The demo python introduces several python courses. </b> </p> <p class=\"course\"> Python is a wonderful general-purpose programming language. You can learn Python from novice to professional by tracking the following courses: <a class=\"py1\" href=\"http://www.icourse163.org/course/BIT-268001\" id=\"link1\"> Basic Python </a> and <a class=\"py2\" href=\"http://www.icourse163.org/course/BIT-1001870001\" id=\"link2\"> Advanced Python </a> . </p> </body></html> 实例一 Tag 12345678910111213# 任何标签都可直接用soup.<标签>将其取出,当文本中存在多个相同标签时,其返回的为第一个import requestsr = requests.get('http://python123.io/ws/demo.html')demo = r.text # demo为标签文本from bs4 import BeautifulSoupsoup = BeautifulSoup(demo,'html.parser') # 解析的页面实例soup.title # 标签> <title>This is a python demo page</title>tag = soup.a tag> <a class=\"py1\" href=\"http://www.icourse163.org/course/BIT-268001\" id=\"link1\">Basic Python</a> 实例二 Name 123456789101112# 获取标签名称方法 <tag>.nameimport requestsr = requests.get('http://python123.io/ws/demo.html')demo = r.text # demo为标签文本from bs4 import BeautifulSoupsoup = BeautifulSoup(demo,'html.parser') # 解析的页面实例soup.a.name> 'a'soup.a.parent.name # a的上一层标签,即父标签> 'p' 实例三 Attributes 123456789101112131415161718# 获取标签的属性 <tag>.attrsimport requestsr = requests.get('http://python123.io/ws/demo.html')demo = r.text # demo为标签文本from bs4 import BeautifulSoupsoup = BeautifulSoup(demo,'html.parser') # 解析的页面实例tag = souo.a # 取a标签attrs = soup.attrs # 提取出a标签的属性> {'href': 'http://www.icourse163.org/course/BIT-268001', 'class': ['py1'], 'id': 'link1'}# ,可以从结果看出,是字典的形式,故可直接通过键-值对的形式进一步提取信息内容attrs['id']> 'link1'attrs['href']> 'http://www.icourse163.org/course/BIT-268001' 实例四 NavigableString 123456789101112# 获取标签的属性 <tag>.string 用于取出标签之间的字符串import requestsr = requests.get('http://python123.io/ws/demo.html')demo = r.text # demo为标签文本from bs4 import BeautifulSoupsoup = BeautifulSoup(demo,'html.parser') # 解析的页面实例soup.a.string> 'Basic Python'soup.p.string> 'The demo python introduces several python courses.' (可跨越标签层次) 实例五 判断注释 123456789101112# 获取标签的属性 <tag>.string 用于取出标签之间的字符串soup = BeautifulSoup(\"<b><! --This is a comment--></b><p>This is not a comment</p>\",'html.parser')soup.b.string> 'This is not a comment'type(soup.b.string)> bs4.element.Commentsoup.p.string> 'This is not a comment'type(soup.p.string)> bs4.element.NavigableString# 两者的类型不同,来判断是否为注释 5.基于Beautiful Soup HTML的遍历方法遍历方法:标签树,其为树形结构。 下行遍历 上行遍历 平行遍历 5.1 下行遍历 属性 说明 .contents 子节点的列表,将所有儿子节点存入列表 (返回列表类型) .children 子节点的迭代类型,与.contents类似,用于循环遍历儿子节点 (返回迭代类型) .descendants 子孙节点的迭代类型,包含所有子孙节点,用于循环遍历 (同上) 本小结实例皆以代码作为开头,不再重复写 >代表输出 123456import requestsr = requests.get('http://python123.io/ws/demo.html')demo = r.text # demo为标签文本from bs4 import BeautifulSoupsoup = BeautifulSoup(demo,'html.parser') # 解析的页面实例 实例一 .contents 1234567891011121314151617soup.head.contents # 结果呈现出列表形式> [<title>This is a python demo page</title>]soup.body.contents # 查看Body子节点的列表 > ['\\n', <p class=\"title\"><b>The demo python introduces several python courses.</b></p>, '\\n', <p class=\"course\">Python is a wonderful general-purpose programming language. You can learn Python from novice to professional by tracking the following courses: <a class=\"py1\" href=\"http://www.icourse163.org/course/BIT-268001\" id=\"link1\">Basic Python</a> and <a class=\"py2\" href=\"http://www.icourse163.org/course/BIT-1001870001\" id=\"link2\">Advanced Python</a>.</p>, '\\n']len(soup.body.contents)> 5soup.body.contents[1] # 查看其下行节点的第二个> <p class=\"title\"><b>The demo python introduces several python courses.</b></p> 标签树的下行遍历 遍历儿子节点(只遍历一层) 12for child in soup.body.children: print(child) 遍历子孙节点(所有节点) 12for child in soup.body.descendants: print(child) 5.2 标签树的上行遍历基本代码: 1234567891011soup = BeautifulSoup(demo,\"html.parser\")for parent in soup.a.parents: # 对a标签所有的先辈名字进行打印 if parent is None: print(parent) # 不存在父亲节,则不打印名称 else: print(parent.name) # 存在父亲节点,则打印出先辈节点名称 pbodyhtml[document] 5.3 标签树的平行遍历 属性 说明 .next_sibling 返回按照HTML文本顺序的下一个平行节点标签 .previous_sibling 返回按照HTML文本顺序的上一个平行节点标签 .next_siblings 迭代类型,返回按照HTML文本顺序的后续所有平行节点标签 .previous_siblings 迭代类型,返回按照HTML文本顺序的前续所有平行节点标签 注意:平行遍历发生在同一个父节点下的各节点间** 实例一 1234567891011121314soup.a.next_sibling > ' and 'soup.a.next_sibling.next_sibling > <a class=\"py2\" href=\"http://www.icourse163.org/course/BIT-1001870001\" id=\"link2\">Advanced Python</a>soup.a.previous_sibling > 'Python is a wonderful general-purpose programming language. You can learn Python from novice to professional by tracking the following courses:\\r\\n'soup.a.previous_sibling.previous_sibling # 此时输出为空 >soup.a.parent.name > 'p' 标签树的平行遍历基本代码 遍历后续节点 12for sibling in soup.a.next_siblings: print(sibling) 遍历前续节点 12for sibling in soup.a.previous_siblings: print(sibling) 6.HTML的格式化输出6.1 prettify()方法soup.prettify() # 显示换行符 print(soup.prettify()) # 格式化输出,标签树形式 6.2 bs4库的编码1234567soup = BeautifulSoup('<p>中文</p>','html.parser')soup.p.string> '中文'print(soup.prettify())<p> 中文</p> 五、信息标记的三种形式 XML <name 属性 Attribute(包含标签 Tag)>...</name> **JSON** 有类型的键值对 key:value 12345678 # 一键多值\"name\":[\"value1\",\"value2\",...] # 键值对的嵌套使用\"name\":{ \"key_1\": \"value1\", \"key_2\": \"value2\"} YAML 123456789101112131415name : newName : value oldName : value # 表达并列关系name : - value1 - value2# |表达整块数据,#表示注释key : valuekey: #Comment- value1- value2key : subkey: subvalue 1.三种信息标记形式的比较 XML:Internet上的信息交互与传递。 JSON:移动应用云端和节点的信息通信,无注释。 YAML:各类系统的配置文件,有注释易读 2.信息提取的一般方法 方法一:完整解析信息的标记形式,再提取关键信息。 XML JSON YAML 需要标记解析器 例如:bs4库的标签树遍历 优点:信息解析准确 缺点:提取过程繁琐,速度慢。 方法二:无视标记形式,直接搜索关键信息。 搜索 对信息的文本查找函数即可 优点:提取过程简单,速度较快。 缺点:提取结果准确性与信息内容直接相关。 融合方法 融合方法:结合形式解析与搜索方法,提取关键信息。 XML JSON YMAL 搜索 需要标记解析器及文本查找函数 实例 提取HTML中的所有URL链接 思路: 搜索到所有标签 解析标签格式,提取href后的链接内容 3.基于bs4库的HTML内容查找方法前期工作 12345import requestsr = request.get('http://python123.io/ws/demo.html')demo = r.textfrom bs4 import BeautifulSoupsoup = BeautifulSoup(demo,'html.parser') <>.find_all(name,attrs,recursive,string,**kwargs) 返回一个列表类型,存储查找的结果。 name:对标签名称的检索字符串。 123456789101112131415161718192021222324soup.find_all('a') # 返回a标签的列表,可得其中两个属性> [<a class=\"py1\" href=\"http://www.icourse163.org/course/BIT-268001\" id=\"link1\">Basic Python</a>, <a class=\"py2\" href=\"http://www.icourse163.org/course/BIT-1001870001\" id=\"link2\">Advanced Python</a>]soup.find_all(['a','b']) # 同时查询'a','b'标签,以列表形式返回>Out[65]: [<b>The demo python introduces several python courses.</b>, <a class=\"py1\" href=\"http://www.icourse163.org/course/BIT-268001\" id=\"link1\">Basic Python</a>, <a class=\"py2\" href=\"http://www.icourse163.org/course/BIT-1001870001\" id=\"link2\">Advanced Python</a>]In [67]: for tag in soup.find_all(True): # 查询所有的标签 ...: print(tag.name) ...: > htmlheadtitlebodypbpaa attrs:对标签属性值的检索字符串,可标注属性检索。 1234567891011121314151617soup.find_all('p','course') # 返回p标签中所有的course属性Out[68]: [<p class=\"course\">Python is a wonderful general-purpose programming language. You can learn Python from novice to professional by tracking the following courses: <a class=\"py1\" href=\"http://www.icourse163.org/course/BIT-268001\" id=\"link1\">Basic Python</a> and <a class=\"py2\" href=\"http://www.icourse163.org/course/BIT-1001870001\" id=\"link2\">Advanced Python</a>.</p>]soup.find_all(id='link1') # 返回id='link1'的全部标签信息Out[71]: [<a class=\"py1\" href=\"http://www.icourse163.org/course/BIT-268001\" id=\"link1\">Basic Python</a>]soup.find_all(id='link') Out[75]: []# 查询带有link(查询出所有的link,无论尾号为几),需要借助正则表达式import resoup.find_all(id=re.compile('link')) Out[79]: [<a class=\"py1\" href=\"http://www.icourse163.org/course/BIT-268001\" id=\"link1\">Basic Python</a>, <a class=\"py2\" href=\"http://www.icourse163.org/course/BIT-1001870001\" id=\"link2\">Advanced Python</a>] recursive:是否对子孙全部检索,默认True。 string:<>…</>中字符串区域的检索字符串。 123456789# 只返回检索部分soup.find_all(string='Basic Python') Out[80]: ['Basic Python'] # 利用正则表达式,提取全部soup.find_all(string=re.compile('python')) # 提取全部带Python的过程Out[82]: ['This is a python demo page', 'The demo python introduces several python courses.'] <tag>(...)等价于 <tag>.find_all() soup(...)等价于soup.find_all(...) 4.扩展方法 方法 说明 <>.find() 搜索且只返回一个结果,字符串类型,同.find_all()参数 <>.find_parents() 在先辈节点中搜索,返回列表类型,同.find_all()参数 <>.find_parent() 在先辈节点中返回一个结果,字符串类型,同.find()参数 <>.find_next_siblings() 在后续平行节点中搜索,返回列表类型,同.find_all()参数 <>.find_next_sibling() 在后续平行节点中返回一个结果,字符串类型,同.find() <>.find_previous_siblings() 在前续平行节点中搜索,返回列表类型,同.find_all() <>.find_previous_sibling 在前续平行节点中返回一个结果,字符串类型,同.find() 六、正则表达式1.正则表达式语法1.1正则表达式由字符和操作符构成 操作符 说明 实例 . 表示任何单个字符 [] 字符集,对单个字符给出取值范围 [abc]表示a,b,c;[a-z]表示a到z单个字符 [^] 非字符集,对个单个字符给出排除范围 [^abc]表示非a或b或c的单个字符 * 前一个字符0次或无限次扩展 abc*表示ab、abc、abcc、abcc等 + 前一个字符1次或无限次扩展 abc+表示abc、abcc、abccc等 ? 前一个字符0次或1次扩展 abc?表示ab、abc | 左右表达式任意一个 abc|def表示adc、def {m} 扩展前一个字符m次 ab{2}c表示abbc {m,n} 扩展前一个字符m至n次(含n) ab{1,2}c表示abc、abbc ^ 匹配字符串开头 ^abc表示abc且在一个字符串的开头 $ 匹配字符串结尾 abc$表示abc且在一个字符串的结尾 () 分组标记,内部只能使用|操作符 (abc)表示abc,(abc|def)表示abc、def \\d 数字,等价于[0-9] \\w 单词字符,等价于[A-Za-z0-9_] 举例 正则表达式 对应字符串 P(Y|YT|YTH|YTHO)?N ‘PN’,’PYN’,’PYTN’,’PYTHN’,’PYTHON’ PYTHON+ ‘PYTHON’,’PYTHONN’,’PYTHONNN’… PY[TH]ON ‘PYTON’,’PYHON’ PY[^TH]?ON ‘PYON’,’PYAON’,’PYBON’,… PY{:3}N ‘PN’,’PYYN’,’PYYYN’ 1.2经典正则表达式实例^[A-Za-z]+$ 由26个字母组成的字符串 ^[A-Za-z0-9]+$ 由26个字母和数字组成的字符串 ^-?\\d+$ 整数形式的字符串 - 表示负号 ^[0-9]*[1-9][0-9]*$ 正整数形式的字符串 [1-9]\\d{5} 中国境内邮政编码,6位 [\\u4e00- \\u9fa5] 匹配中文字符utf-8编码 \\d{3}-\\d{8}|\\d{4}-\\d{7} 国内电话号码,010-68913536 1.3匹配IP地址的正则表达式IP地址字符串形式的正则表达式(IP地址分4段,每段0-255) 粗略划分: \\d+.\\d+.\\d+.\\d+ \\d{1,3}.\\d{1,3}.\\d{1,3}.\\{1,3} 精确划分 0-99: [1-9]?\\d 100-199:1\\d{2} 200-249:2[0-4]\\d 250-255:25[0-5] 拼接:(([1-9]?\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}([1-9]?\\d|1\\d{2}|2[0-4]\\d|25[0-5]) 2.Re库2.1 raw string类型(原生字符串类型)re库采用raw string类型表示正则表达式,表示为:r'text' 即:字符串原样输出,不用采用转移字符\\。 2.2 Re库主要功能函数 函数 说明 re.search() 在一个字符串中搜索匹配正则表达式的第一个位置,返回match对象 re.match() 从一个字符串的开始位置起匹配正则表达式,返回match对象 re.findall() 搜索字符串,以列表类型返回全部能匹配的子串 re.split() 将一个字符串按照正则表达式匹配结果进行分割,返回列表类型 re.finditer() 搜索字符串,返回一个匹配结果的迭代类型,每个迭代元素是match对象 re.sub() 在一个字符串中替换所有匹配正则表达式的子串,返回替换后的字符串 re.search(pattern,string,flags=0) 在一个字符串中搜索匹配正则表达式的第一个位置,返回match对象。 pattern:正则表达式的字符串或原生字符串表示 string:待匹配字符串。 flags:正则表达式使用时的控制标记。 flags:正则表达式使用时的控制标记 常用标记 说明 re.I re.IGNORECASE 忽略正则表达式的大小写,[A-Z]能够匹配小写字符 re.M re.MULTILINE 正则表达式中的^操作符能够将给定字符串的每行当做匹配开始 re.S re.DOTALL 正则表达式中的.操作符能够匹配所有字符,默认匹配除换行外的所有字符 12345import rematch = re.search(r'[1-9]\\d{5}','BIT 100081')if match: print(match.group(0))> 100081 re.match(pattern,string,flags=0) 1234import rematch = re.match(r'[1-9]\\d{5}','BIT 100081')match.group(0)> AttributeError: 'NoneType' object has no attribute 'group' re.findall(pattern,string,flags=0) 123ls = re.findall(r'[1-9]\\d{5}','AAA100081 BBBB100084') ls> ['100081', '100084'] re.split(pattern,string,maxsplit,flags=0) maxsplit:最大分割数目,达到数目,剩余部分作为最后一个元素输出。 12re.split(r'[1-9]\\d{5}','AAA100081 BBBB100084') # 将匹配的切割掉> ['AAA', ' BBBB', ''] re.split(pattern,string,flags=0) 123456for m in re.finditer(r'[1-9]\\d{5}','AAA100081 BBBB100084'): # 迭代输出 if m: print(m.group(0))> 100081100084 re.split(pattern,repl,string,count=0,flags=0) repl:替换匹配字符串的字符串 count:匹配的最大替换次数 2.3 Re库的另一种等价用法rst = re.search(r'[1-9]\\d{5}','BIT 100081') 函数式用法:一次性操作 等价于: 123# 面向对象用法:编译后的多次操作pat = re.compile(r'[1-9]\\d{5}') rst = pat.search('BIT 100081') regex = re.compile(pattern,flags=0) 将正则表达式的字符串形式编译成正则表达式对象 pattern:正则表达式的字符串或原生字符串表示 flags:正则表达式使用时的控制标记 2.4 Match对象的属性 属性 说明 .string 待匹配的文本 .re 匹配时使用的pattern对象(正则表达式) .pos 正则表达式搜索文本的开始位置 .endpos 正则表达式搜索文本的结束位置 2.5 Match对象的方法 方法 说明 .group(0) 获取匹配后的字符串 .start() 匹配字符串在原始字符串的开始位置 .end() 匹配字符串在原始字符串的结束位置 .span() 返回(.start(),.end()) 2.6 Re库的贪婪匹配和最小匹配实例: 123match = re.search(r'PY.*N','PYANBNCNDN')match.group(0)Out[101]: 'PYANBNCNDN' Re库默认采用贪婪匹配,即输出匹配最长的子串。 最小匹配 123match = re.search(r'PY.*?N','PYANBNCNDN')match.group(0)Out[102]: 'PYAN' 最小匹配操作符 操作符 说明 *? 前一个字符0次或无限次扩展,最小匹配 +? 前一个字符1次或无限次扩展,最小匹配 ?? 前一个字符0次或1次扩展,最小匹配 {m,n}? 扩展前一个字符m至n次(含n),最小匹配 未完待续….","categories":[{"name":"Python","slug":"Python","permalink":"https://chemlez.github.io/categories/Python/"},{"name":"爬虫","slug":"Python/爬虫","permalink":"https://chemlez.github.io/categories/Python/%E7%88%AC%E8%99%AB/"}],"tags":[{"name":"爬虫","slug":"爬虫","permalink":"https://chemlez.github.io/tags/%E7%88%AC%E8%99%AB/"},{"name":"正则表达式","slug":"正则表达式","permalink":"https://chemlez.github.io/tags/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F/"},{"name":"Request","slug":"Request","permalink":"https://chemlez.github.io/tags/Request/"},{"name":"Beautifulsoup","slug":"Beautifulsoup","permalink":"https://chemlez.github.io/tags/Beautifulsoup/"}]},{"title":"机器学习:决策树入门之泰坦尼克号案例","slug":"机器学习-决策树入门之泰坦尼克号案例","date":"2020-02-08T20:01:22.000Z","updated":"2020-04-04T19:14:00.064Z","comments":true,"path":"2020/02/09/机器学习-决策树入门之泰坦尼克号案例/","link":"","permalink":"https://chemlez.github.io/2020/02/09/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0-%E5%86%B3%E7%AD%96%E6%A0%91%E5%85%A5%E9%97%A8%E4%B9%8B%E6%B3%B0%E5%9D%A6%E5%B0%BC%E5%85%8B%E5%8F%B7%E6%A1%88%E4%BE%8B/","excerpt":"本文用于记录机器学习中的一次入门练习,即:利用决策树进行简单的二分类。同时,结合Kaggle上的经典案例Titanic,来测试实际效果。 一、数据集采用Kaggle中的Titanic的数据集。数据包含分为: 训练集: training set (train.csv) 测试集: test set (test.csv) 提交标准: gender_submission.csv 由于Kaggle涉及到科学上网的操作,所以原始数据集已经下载好放在Gighub上了。 二、数据处理首先导入训练集,查看数据的情况: 123456789from sklearn.tree import DecisionTreeClassifier # 导入模型决策树分类器from sklearn.model_selection import cross_val_score,train_test_split,GridSearchCV # 导入的模型作用分别为交叉验证、训练集与数据集的划分,网格搜索import pandas as pdimport numpy as npimport matplotlib.pyplot as pltdata = pd.read_csv('/Users/liz/code/jupyter-notebook/sklearn/1- DecisionTree/Titanic_train.csv') # 导入数据集data.head() # 显示数据集的前五行[out]:","text":"本文用于记录机器学习中的一次入门练习,即:利用决策树进行简单的二分类。同时,结合Kaggle上的经典案例Titanic,来测试实际效果。 一、数据集采用Kaggle中的Titanic的数据集。数据包含分为: 训练集: training set (train.csv) 测试集: test set (test.csv) 提交标准: gender_submission.csv 由于Kaggle涉及到科学上网的操作,所以原始数据集已经下载好放在Gighub上了。 二、数据处理首先导入训练集,查看数据的情况: 123456789from sklearn.tree import DecisionTreeClassifier # 导入模型决策树分类器from sklearn.model_selection import cross_val_score,train_test_split,GridSearchCV # 导入的模型作用分别为交叉验证、训练集与数据集的划分,网格搜索import pandas as pdimport numpy as npimport matplotlib.pyplot as pltdata = pd.read_csv('/Users/liz/code/jupyter-notebook/sklearn/1- DecisionTree/Titanic_train.csv') # 导入数据集data.head() # 显示数据集的前五行[out]: PassengerId Survived Pclass Name Sex Age SlibSp Parch Ticek Fare Cabin Embarked 0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S 1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C 2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S 3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S 4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S 通过以上的数据所展示的情况,我们所要做的是将Survived作为标签,其余的列作为特征。目标:以所知的特征来预测标签。这份数据集的实际意义是:通过已知数据对乘客的生还情况做一次预测。 12345678910111213141516171819data.info() # 查看整个训练集的情况out: <class 'pandas.core.frame.DataFrame'> RangeIndex: 891 entries, 0 to 890 Data columns (total 12 columns): PassengerId 891 non-null int64 Survived 891 non-null int64 Pclass 891 non-null int64 Name 891 non-null object Sex 891 non-null object Age 714 non-null float64 SibSp 891 non-null int64 Parch 891 non-null int64 Ticket 891 non-null object Fare 891 non-null float64 Cabin 204 non-null object Embarked 889 non-null object dtypes: float64(2), int64(5), object(5) memory usage: 83.7+ KB 数据分析 通过以上的数据展示,共有891条数据,其中具有缺失值的特征有:Age、Cabin、Embarked;非数值型的特征有:Name,Sex,Ticket,Cabin,Embarked。 当我们采用现有的特征对乘客进行生还情况预测时,一些处理较为麻烦且不太重要的特征对可不采用。例如:这里的Name、Ticket可以不采用,因为在实际情况中乘客的名字以及所购的票对于乘客的生还情况作用不大。另外一点原因是这两者皆为非数值型数据,处理成数值形式较为复杂(在计算机中所接受的数据最终都要以数字的形式进行呈现)。 由于Cabin缺失值较多,这里采用删除的方式,理由同上。 虽然性别也为字符型数据,当在实际中性别对于逃生的可能性具有一定的影响,故对其保留。 将缺失值进行填补;将非数值型数据转化为数值型数据。 12345678910111213141516171819202122232425262728293031323334353637383940# 删除Name、Ticket、Cabin特征列data.drop(['Name','Cabin','Ticket'],inplace=True,axis=1)# 缺失值的填补# 对于Age的缺失值填补的一种策略为:以年龄的平均值作为填补data.loc[:,'Age'] = data['Age'].fillna(int(data['Age'].mean()))# Embarked由于只有两条数据具有缺失值,这里采用的方式是删除这两条缺失的数据(缺失两条数据对模型的训练好坏影响不大)data = data.dropna()data = data.reset_index(drop = True) # 删除过后,用于重置索引# 将非数值型数据转化为数值型数据# 性别只有两类,故可用0\\1来表示男女data['Sex'] = (data['Sex'] == 'male').astype(int) # 0表示女,1表示男tags = data['Embarked'].unique().tolist() # tags: ['S', 'C', 'Q']# Embarked只有三类分别以S,C,Q的索引代表他们,0~9均可采用此种方法data.iloc[:,data.columns == 'Embarked'] = data['Embarked'].apply(lambda x : tags.index(x))# 查看数据data.info() # 查看数据信息out:<class 'pandas.core.frame.DataFrame'>RangeIndex: 889 entries, 0 to 888Data columns (total 9 columns):PassengerId 889 non-null int64Survived 889 non-null int64Pclass 889 non-null int64Sex 889 non-null int64Age 889 non-null float64SibSp 889 non-null int64Parch 889 non-null int64Fare 889 non-null float64Embarked 889 non-null int64dtypes: float64(2), int64(7)memory usage: 62.6 KB# 将特征与标签进行分离x = data.iloc[:,data.columns != 'Survived'] # 取出Survived以为的列作为特征xy = data.iloc[:,data.columns == 'Survived'] # 取出Survived列作为特征y 模型训练思路:采用交叉验证来评估我们的模型;同时采用网格搜索来查找决策树中常见的最佳参数。 12345678910111213141516171819202122232425# 网格搜索:能够帮助我们同时调整多个参数的技术,本质是枚举技术。# paramerters:用于确定的参数。parameters = {'splitter':('best','random') ,'criterion':('gini','entropy') ,'max_depth':[*range(1,10)] ,'min_samples_leaf':[*range(1,50,5)] ,'min_impurity_decrease':[*np.linspace(0,0.5,20)] }# 网格搜索实例代码,所需要确定的参数越多,耗时越长clf = DecisionTreeClassifier(random_state=30)GS = GridSearchCV(clf,parameters,cv=10) # cv=10,做10次交叉验证GS = GS.fit(x_train,y_train)# 最佳参数GS.best_params_out: {'criterion': 'gini', 'max_depth': 3, 'min_impurity_decrease': 0.0, 'min_samples_leaf': 1, 'splitter': 'best'} # 最佳得分GS.best_score_ 确定了设置的参数的最佳值,开始训练模型: 12345678# 训练模型,将以上设置参数的最佳值填入模型的实例化中clf_model = DecisionTreeClassifier(criterion='gini' ,max_depth=3 ,min_samples_leaf=1 ,min_impurity_decrease=0 ,splitter='best' )clf_model = clf_model.fit(x,y) 导出模型: 123# 导出模型from sklearn.externals import joblibjoblib.dump(clf_model,'/Users/liz/Code/jupyter-notebook/sklearn/1- DecisionTree/clf_model.m') 测试集的处理: 1234567891011121314151617181920212223242526272829# 导入测试集data_test = pd.read_csv('/Users/liz/code/jupyter-notebook/sklearn/1- DecisionTree/Titanic_test.csv')data_test.info()out: <class 'pandas.core.frame.DataFrame'> RangeIndex: 418 entries, 0 to 417 Data columns (total 11 columns): PassengerId 418 non-null int64 Pclass 418 non-null int64 Name 418 non-null object Sex 418 non-null object Age 332 non-null float64 SibSp 418 non-null int64 Parch 418 non-null int64 Ticket 418 non-null object Fare 417 non-null float64 Cabin 91 non-null object Embarked 418 non-null object dtypes: float64(2), int64(4), object(5) memory usage: 36.0+ KB# 测试集处理的方法同训练集,同时测试集要与训练集保持同样的特征# 由于最后,我们需要将处理结果上传到Kaggle上,所以不能够将数据条目减少,即:需要上传418条测试数据;故这里Fare缺失的一条数目同样采用平均值来填补data_test.drop(['Name','Ticket','Cabin'],inplace=True,axis=1)data_test['Age'] = data_test['Age'].fillna(int(data_test['Age'].mean()))data_test['Fare'] = data_test['Fare'].fillna(int(data_test['Fare'].mean()))data_test.loc[:,'Sex'] = (data_test['Sex'] == 'male').astype(int)tags = data_test['Embarked'].unique().tolist()data_test['Embarked'] = data_test['Embarked'].apply(lambda x : tags.index(x)) 此时测试集数据预处理完毕,导出模型并对数据进行测试: 123456789101112# 导出模型且测试数据集model = joblib.load('/Users/liz/Code/jupyter-notebook/sklearn/1- DecisionTree/clf_model.m')Survived = model.predict(data_test) # 测试结果# 生成数据Survived = pd.DataFrame({'Survived':Survived}) # 将结果转换为字典形式并后续作为csv形式导出PassengerId = data_test.iloc[:,data_test.columns == 'PassengerId'] # 切片,分割出PassengerIdgender_submission = pd.concat([PassengerId,Survived],axis=1)# 将Survived与PassengerId拼接,一一对应#导出数据#导出数据gender_submission.index = np.arange(1, len(gender_submission)+1) # 索引从1开始gender_submission.to_csv('/Users/liz/Code/jupyter-notebook/sklearn/1- DecisionTree/gender_submission.csv',index=False) # index=False,导出时不显示索引 导出文件: PassengerId Survived 0 892 0 1 893 1 2 894 0 3 895 0 4 896 1 ... ... ... 413 1305 0 414 1306 1 415 1307 0 416 1308 0 417 1309 0 418 rows × 2 columns 将结果提交到Kaggle上,最终得分: 最终得分0.77990,分数不高,最高有得满分的,此篇只是作为机器学习及Kaggle的一个入门。 最终的源代码及Kaggle的数据集都会上传到我的Github仓库中,其中也包括一些网络上搬运的相关笔记也都会上传到Github上,此仓库会持续更新… 附 下载源码","categories":[{"name":"Machine Learning","slug":"Machine-Learning","permalink":"https://chemlez.github.io/categories/Machine-Learning/"},{"name":"sklearn","slug":"Machine-Learning/sklearn","permalink":"https://chemlez.github.io/categories/Machine-Learning/sklearn/"}],"tags":[{"name":"DecisionTree","slug":"DecisionTree","permalink":"https://chemlez.github.io/tags/DecisionTree/"},{"name":"Kaggle","slug":"Kaggle","permalink":"https://chemlez.github.io/tags/Kaggle/"},{"name":"sklearn","slug":"sklearn","permalink":"https://chemlez.github.io/tags/sklearn/"}]},{"title":"云端MySQL安装及相关配置","slug":"云端MySQL安装及相关配置","date":"2020-01-22T13:27:59.000Z","updated":"2020-03-14T14:12:12.893Z","comments":true,"path":"2020/01/22/云端MySQL安装及相关配置/","link":"","permalink":"https://chemlez.github.io/2020/01/22/%E4%BA%91%E7%AB%AFMySQL%E5%AE%89%E8%A3%85%E5%8F%8A%E7%9B%B8%E5%85%B3%E9%85%8D%E7%BD%AE/","excerpt":"由于自己的阿里云账号申请不足6个月,能够享受新用户云服务器ECS89元一年的优惠政策,所以就购买了一台云服务器,作为日常学习的使用。本文用来记录此次装载云服务器所遇到的一些问题及MySQL的安装过程。 一、云服务器的选配及配置此次我选购的服务器网址:http://aliyun.langsan.com/?bd_vid=8575091722087683835。下图为此次所购的云服务器配置:","text":"由于自己的阿里云账号申请不足6个月,能够享受新用户云服务器ECS89元一年的优惠政策,所以就购买了一台云服务器,作为日常学习的使用。本文用来记录此次装载云服务器所遇到的一些问题及MySQL的安装过程。 一、云服务器的选配及配置此次我选购的服务器网址:http://aliyun.langsan.com/?bd_vid=8575091722087683835。下图为此次所购的云服务器配置: 后续步骤为:提货券的兑换;地域站点的选取以及系统的选配。这里我选取的为上海的站点(大陆境内站点随便选没什么差别),系统选装的为Centos7(具体到7.x没什么区别)。然后就是阿里云那边的自动配置了。购买及配置较为简单,全部为阿里云的傻瓜一站式操作。 二、安全组的设置第一次服务器的使用,需要进行安全组的设置。进入到自己的控制台-实例与镜像-实例。这个时候就能够看到自己的服务器。勾选此台服务器: 依次设置实例ID、重置实例密码; 在更多选项中选择密码/密匙。重置远程连接的密码。 注:实例密码为操作系统的密码,即为root用户名密码。在实例创建时可选设定,如果没有设定或者遗忘可在阿里云的个人控制台上进行密码重置操作;远程链接密码是通过个人后台控制面板,通过内网形式直接链接到操作系统上,这种链接方式可以绕过安全组拦截,一般用于安全组将远程端口拦截时选择此种方法。 网络与安全组。将此实例加入到安全组里。 设置安全组规则 在安全组规则中,采用快速创建规则。规则方向:入方向/出方向,均可采用,用于控制服务器访入与访出。授权策略:允许/禁止(不解释)。常用端口(TCP):控制端口的访入与访出(根据自己的习惯与用处)。授权对象:默认为0.0.0.0/0。至此服务器基本配置到此结束。回到控制台实例中,点击远程连接。区域即为所选区域(一般默认不变);端口默认为22;用户名默认为root;密码是之前设置的实例密码。 三、本机ssh连接服务器免密设置上一部分中,我们在阿里云的网页实例中,远程连接到我们的服务器。但是,每当我们需要用到服务器时,便要通过阿里云账号登录再来连接就显得比较麻烦。这里,通过ssh的命令在自己电脑终端来远程连接自己的服务器。终端命令:ssh root@ip root:远程连接的用户名;一般默认不变即为root。 ip:自己服务器公网ip。回车后,输入自己的root用户实例密码即可连接。注:Linux、Mac系统终端自带ssh命令;Windows系统不自带ssh命令,需要借助putty或Xshell客户端软件使用。但是,每次我们在自己的电脑连接到服务器都需要通过ip地址,再由密码登录也比较麻烦。所以这里再介绍本机免密码登录服务器的方式。思路:将自己的公有密匙添加到服务器端。 1.在本地生成一对公匙-密匙ssh-keygen -t rsa采用默认目录,不设置密码,一路回车即可。 最终会在~/.ssh目录下生成id_rsa(密匙);id_rsa.pub(公匙)。 2.将公匙部署至服务器上在本地命令执行:方式一:scp ~/.ssh/id_rsa.pub root@公网IP地址:~/.ssh/authorized_keys方式二:ssh-copy-id ~/.ssh/id_rsa.pub root@公网IP地址以上两种方式即将本地公匙内容复制到远程服务器~/.ssh/authorized_keys的文件中。 至此,再次登录服务器只需一句ssh终端命令即可,不需要再输入密码。到这里还不是最简洁的一种登录方式,因为我们还需要输入root用户账号,ip地址。所以后续还有更简洁的方式:本地需要保存ssh登录主机的相关信息,在本地主机用户根目录下的.ssh文件内创建config文件,用于保存ssh登陆主机的相关信息vim config(如果没有vim可以手动到此目下创建config文件)编辑内容: 12345Host name #AAAAA为服务器主机名HostName 39.97.170.231 #写服务器ip地址User root #root为登陆用户名Port 22 #主机端口,默认是22IdentityFile /Users/.ssh/id_rsa #自己生成的私钥的文件路径 注意:Host name是之前服务器设置中设置的实例id/名称实例如下: 3.在服务器设置自动检验的信息打开/etc/ssh/sshd_config文件vim /etc/ssh/sshd_config找到 12PubkeyAuthentication yes AuthorizedKeysFile .ssh/authorized_keys 取消注释。至此,以后在自己本机上只需要采用:ssh liz_es即可登录。 四、 MySQL服务器的安装及相关配置MySQ安装这一部分记录云服务器端安装MySQL及相关配置 下载并安装MySQL官方的Yum Repository[root@localhost ~]# wget -i -c http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm使用上面的命令直接安装Yum Repository[root@localhost ~]# yum -y install mysql57-community-release-el7-10.noarch.rpm 安装MySQL服务器root@localhost ~]# yum -y install mysql-community-server[root@localhost ~]# yum -y remove mysql57-community-release-el7-10.noarch MySQL数据库设置MySQL的启动[root@localhost ~]# systemctl start mysqld.serviceMySQL的关闭systemctl stop mysqld.service查看MySQL运行状态[root@localhost ~]# systemctl status mysqld.service查找root初始密码[root@localhost ~]# grep "password" /var/log/mysqld.log登录MySQLmysql -uroot -p此时需要修改初始密码才能对数据库进行后续操作。又由于数据库默认的密码强度所设置的密码较为复杂,所以需要事先修改密码强度规则。否则在修改密码的过程容易出现以下错误:ERROR 1819 (HY000): Your password does not satisfy the current policy requiremen下面列出常用的关于密码设置方面的MySQL操作命令。查看MySQL密码相关的全局参数:mysql> select @@validate_password_policy;mysql> SHOW VARIABLES LIKE 'validate_password%'; 参数解释validate_password_dictionary_file插件用于验证密码强度的字典文件路径。validate_password_length 密码最小长度,参数默认为8,它有最小值的限制,最小值为:validate_password_number_count + validate_password_special_char_count + (2 * validate_password_mixed_case_count)validate_password_mixed_case_count密码至少要包含的小写字母个数和大写字母个数。 validate_password_number_count密码至少要包含的数字个数。validate_password_policy密码强度检查等级,0/LOW、1/MEDIUM、2/STRONG。validate_password_special_char_count 密码至少要包含的特殊字符数。 修改mysql参数配置1234567891011121314151617mysql> set global validate_password_policy=0; Query OK, 0 rows affected (0.05 sec) mysql> set global validate_password_mixed_case_count=0; Query OK, 0 rows affected (0.00 sec) mysql> set global validate_password_number_count=5; Query OK, 0 rows affected (0.00 sec) mysql> set global validate_password_special_char_count=0; Query OK, 0 rows affected (0.00 sec) mysql> set global validate_password_length=2; Query OK, 0 rows affected (0.00 sec) mysql> SHOW VARIABLES LIKE 'validate_password%'; mysql> FLUSH PRIVILEGES 可能最后两句在执行时,会报错。这是因为还没对初始密码进行修改。在修改完密码以后FLUSH PRIVILEGES,保证密码强度规则的更新。 MySQL密码的修改mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'new password'; MySQL用户的创建及权限设置在MySQL中其实有一个内置且名为mysql的数据库,这个数据库中存储的是MySQL的一些数据,比如用户、权限信息、存储过程等。通过以下命令可查看当前数据库存在哪些用户;SELECT User, Host FROM mysql.user;可看见如下类似信息: 12345678+---------------+-----------+| User | Host |+---------------+-----------+| re_mysql | % || mysql.session | localhost || mysql.sys | localhost || root | localhost |+---------------+-----------+ Host代表用户所能连接的数据库主机 % 代表任何主机localhost 代表只能在本机上使用的用户 创建用户mysql>CREATE USER 'user_name'@'host' IDENTIFIED BY 'password';user_name:需要创建的用户名称。host:表示要这个新创建的用户允许从哪台机登陆,如果只允许从本机登陆,则填‘localhost’ ;如果指定某台主机登录,则填’ip’;如果允许从任意远程登陆,则填 ‘%’;password:新创建用户的数据库登录密码,需符合密码强度规则。 授权用户GRANT ALL PRIVILEGES ON *.* TO 'user_name'@'%' IDENTIFIED BY 'password' WITH GRANT OPTION;PRIVILEGES:表示要授予什么权限。例如可以有 select ,insert ,delete,update等,如果要授予全部权力,则填ALL。 *.*:表示用户的权限能用在哪个库的哪个表中,如果想要用户的权限很作用于所有的数据库所有的表,则填*.*,*是一个通配符,表示全部。user_name:所要授权的用户。'%':表面所有远程都可在此用户登录MySQL服务器,具体使用同节。WITH GRANT OPTION:用以上命令授权的用户不能给其他用户授权,如果想这个用户能够给其他用户授权,就要在后面加上WITH GRANT OPTION。 删除用户DROP USER ‘user_name’@‘localhost/ip/*’ 立即生效flush privileges 修改mysql库里边的user表,限制root用户只能从哪个ip登录update mysql.user set host='localhost' where user='root'; MySQL的远程连接云服务器端部署好了MySQL。那么可以在本机中连接云服务器端的MySQL数据库。这里,我借助的是Navicat for MySQL。创建连接: 连接名:随便写。主机:云服务器的公有IP。端口:默认端口3306。用户名:即在上一节中创建的新用户,并且是能够远程连接的用户。编辑密码:MySQL这一用户的密码。点击测试连接。 成功!以后便可以在本机连接到服务器端的MySQL进行使用。 注意当在虚拟机(Ubuntu16.04)中的MySQL采取同样的操作时,可能连接失败。在百度了一番博文以后,所采用的办法是在虚拟机的终端: cd /etc/mysql 进入到my.cnf文件所在的目录下,sudo cp my.cnf my.cnf.bak,备份文件 打开配置,找到bind-address= 127.0.0.1这一行,注释掉。 重启数据库,使用Navicat进行连接。 附 Markdown常用命令:https://www.runoob.com/markdown/md-tutorial.html vim常用命令:https://www.runoob.com/linux/linux-vim.html 菜鸟:https://www.runoob.com/ 免费的图床-路过图床:https://imgchr.com/ hexo高阶教程:想让你的博客被更多的人在搜索引擎中搜到吗? Hexo 教程:Hexo 博客部署到腾讯云教程 hexo史上最全搭建教程 git 清除缓存 git rm -r –cached .git add .git commit -m ‘update .gitignore’ 参考文献 [1] centos7下安装mysql(完整配置):https://blog.csdn.net/baidu_32872293/article/details/80557668[2] mysql 密码强度规则设置:https://blog.csdn.net/u014236541/article/details/78244601[3] MYSQL的创建用户,授权用户,删除用户,查看用户:https://blog.csdn.net/u014453898/article/details/55064312[4] mysql查看所有用户:https://blog.csdn.net/qq_37996815/article/details/78934536[5] Ubuntu 16.04 安装使用MySQL:https://blog.csdn.net/vXueYing/article/details/52330180[6] 使用navicat 连接虚拟机上的MySQL数据库:https://www.jianshu.com/p/8fa82acb16e9[7] SSH连接服务器 本地记住用户名及密码:https://blog.csdn.net/persist_xyz/article/details/90231433","categories":[{"name":"Linux","slug":"Linux","permalink":"https://chemlez.github.io/categories/Linux/"}],"tags":[{"name":"MySQL","slug":"MySQL","permalink":"https://chemlez.github.io/tags/MySQL/"},{"name":"Linux","slug":"Linux","permalink":"https://chemlez.github.io/tags/Linux/"}]},{"title":"Linux-Ubuntu的安装与相关设置","slug":"Linux-Ubuntu的安装与相关设置","date":"2020-01-17T10:12:49.000Z","updated":"2020-03-15T18:16:42.508Z","comments":true,"path":"2020/01/17/Linux-Ubuntu的安装与相关设置/","link":"","permalink":"https://chemlez.github.io/2020/01/17/Linux-Ubuntu%E7%9A%84%E5%AE%89%E8%A3%85%E4%B8%8E%E7%9B%B8%E5%85%B3%E8%AE%BE%E7%BD%AE/","excerpt":"近日换了电脑,想装一个Linux系统,但也不想在本机上装双系统,就选择了装起虚拟机。此次选择了安装Ubuntu 16.04.6版本,文章最后会附上常用16.04和18.04ubuntu镜像源下载的网址。本文,用来记录此次虚拟机的装载过程、后续配置。 一、VMWare Fusion的下载VMware Fusion是VMware为Macintosh计算机开发的虚拟机管理程序。用来管理虚拟机环境。此次选择了11.5.1的版本-下载地址。序列号查询百度即可。 二、Ubuntu的安装打开VMWare Fusion,点击创建新环境。选择创建自定义虚拟机安装。这里没有选择上方的镜像安装,是因为我在第一次用这种方法安装完以后,尝试了各种方法却不能安装VMware tools。","text":"近日换了电脑,想装一个Linux系统,但也不想在本机上装双系统,就选择了装起虚拟机。此次选择了安装Ubuntu 16.04.6版本,文章最后会附上常用16.04和18.04ubuntu镜像源下载的网址。本文,用来记录此次虚拟机的装载过程、后续配置。 一、VMWare Fusion的下载VMware Fusion是VMware为Macintosh计算机开发的虚拟机管理程序。用来管理虚拟机环境。此次选择了11.5.1的版本-下载地址。序列号查询百度即可。 二、Ubuntu的安装打开VMWare Fusion,点击创建新环境。选择创建自定义虚拟机安装。这里没有选择上方的镜像安装,是因为我在第一次用这种方法安装完以后,尝试了各种方法却不能安装VMware tools。 继续下一步选择所要安装的操作系系统,这里为Ubuntu 64位。选择UEFI安装模式。后续的步骤中就一路下一步。安装过程中,如果VMware没有检测到我们所需要安装的镜像源,那么需要我们将镜像源手动添加到配置中。若卡在这里不动,便需要我们手动添加镜像源。) 后续就是VMware的全自动配置安装了。注意:后续语言环境的安装,请选择默认的English,不要改成简体中文模式。因为,当我们熟练了Linux的终端命令以后,我们就不再进行图形化界面的操作,而是转变为字符界面的操作。如果,开始默认的是中文简体,那么在字符界面中,中文会出现菱形乱码,无法识别。从这里的后续安装可以参考百度百科。在VMware的一系列自动安装配置以后。若出现了:将上述添加的磁盘勾选取消点击虚拟机重启即可!登录,进入图形化界面! 三、Ubuntu的镜像源设置这里介绍两种方式。第一种方式:点击右上方的设置按钮,进入System settings…在系统栏中选择Software&Updates,将Downloads中的镜像源Others选成Chinese,然后点击右方的选取Select Best Server,等待系统测试选取最佳的节点,再依据后续步骤更新即可。第二种方式:参考清华大学开源软件镜像源。按要求和版本号将配置文件改成清华大学的镜像源即可。 四、VMWare Tools的安装此时,Ubuntu已经安装好了。但是,如果要想做到本机和虚拟机能够文件共享,那么还需要下载VMWare Tools。点击:由于我的已经安装过了,所以这里显示为重新安装,后续按照下载。点击安装。 将安装好的压缩包,VMWaretools-XXX(版本号).tar.gz。移动到桌面。 打开Ubuntu下的终端命令窗口。 12345cd Desktop # 进入桌面ls # 查看此压缩包是否在桌面下tar -xzvf VMWaretools-XXX(版本号).tar.gz # 解压文件cd VMware-tools-distrib./vmware-install.pl # 执行vmware-install.pl 进行安装 依次执行以上命令后,按照提示在终端中输入yes和回车即可。 五、共享文件夹的设置上述步骤中中,安装了VMWare tools,后续就需要设置我们可以共享的文件夹。进入Ubuntu的硬件设置中:进入共享文件夹。添加系统文件夹,重启虚拟机。VMWare tools可以使本机和虚拟机之间共享文件;同时可以自动调节虚拟机的分辨率,使得全屏放映使用。 六、图形界面和字符界面的转换首先打开虚拟机中的终端,sudo su进入root模式。 修改grub文件 sudo vi /etc/default/grub 修改grub文件的三处: 将GRUB_CMDLINE_LINUX_DEFAULT=”quiet splash”进行注释,即最前方加#。 GRUB_CMDLINE_LINUX=”text”,添加为text,文本。 GRUB_TERMINAL=console,取消注释。 最终的修改结果如下图:保存退出。 sudo update-grub更新grub文件。 执行sudo systemctl set-default multi-user.target 即将开机默认方式改为字符形界面。 重新启动虚拟机。注意:不要使用reboot总结:以后两种模式的转化只需要两句终端命令即可。 图形转字符界面:sudo systemctl set-default multi-user.target 字符转图形界面:sudo systemctl set-default graphical.target 最后重启虚拟机,即可! 附: Ubuntu安装的镜像网站: 14.04版本:http://mirrors.aliyun.com/ubuntu-releases/14.04/ 16.04版本:http://mirrors.aliyun.com/ubuntu-releases/16.04/ 18.04版本:http://mirrors.aliyun.com/ubuntu-releases/18.04/ 清华大学镜像源:https://mirrors.tuna.tsinghua.edu.cn/ VMWare FUsion安装地址:https://my.vmware.com/cn/web/vmware/info/slug/desktop_end_user_computing/vmware_fusion/11_0 参考文献 [1] mac上用VMWare虚拟机装Ubuntu–及Ubuntu安装Vmware Tools[2] 如何安装ubuntu系统[3] VMware Tools安装[4] Ubuntu16.04 图形界面与字符界面切换","categories":[{"name":"Linux","slug":"Linux","permalink":"https://chemlez.github.io/categories/Linux/"}],"tags":[{"name":"VMware","slug":"VMware","permalink":"https://chemlez.github.io/tags/VMware/"},{"name":"Linux","slug":"Linux","permalink":"https://chemlez.github.io/tags/Linux/"}]},{"title":"抽取JDBCU工具类——JDBCUtils的使用","slug":"抽取JDBC工具类——JDBCUtils的使用","date":"2019-12-20T18:43:33.000Z","updated":"2020-11-18T02:24:23.224Z","comments":true,"path":"2019/12/21/抽取JDBC工具类——JDBCUtils的使用/","link":"","permalink":"https://chemlez.github.io/2019/12/21/%E6%8A%BD%E5%8F%96JDBC%E5%B7%A5%E5%85%B7%E7%B1%BB%E2%80%94%E2%80%94JDBCUtils%E7%9A%84%E4%BD%BF%E7%94%A8/","excerpt":"在上一篇介绍JDBC基础使用的博文中,简单了解到JDBC的使用。但是,也看出了一定的弊端:重复代码量较大。在我们每次新建一个JDBC的类操作数据库时,都要不停的进行驱动的注册,数据库的连接,参数的输入等大量重复性的操作。所以,有没有什么方法简化这一类的操作呢?其实,将这些重复的代码进行抽取,作为一个工具类,每次使用的时候进行调用即可,这样便能够达到代码的可复用性。抽取JDBC工具类的思路: 将注册驱动进行抽取 抽取一个方法获取连接对象 需求:不必传递参数,并且保证工具类的通用性。 解决:配置文件。","text":"在上一篇介绍JDBC基础使用的博文中,简单了解到JDBC的使用。但是,也看出了一定的弊端:重复代码量较大。在我们每次新建一个JDBC的类操作数据库时,都要不停的进行驱动的注册,数据库的连接,参数的输入等大量重复性的操作。所以,有没有什么方法简化这一类的操作呢?其实,将这些重复的代码进行抽取,作为一个工具类,每次使用的时候进行调用即可,这样便能够达到代码的可复用性。抽取JDBC工具类的思路: 将注册驱动进行抽取 抽取一个方法获取连接对象 需求:不必传递参数,并且保证工具类的通用性。 解决:配置文件。 一、获取连接因为我们需要将JDBC抽取为工具类,便于使用。故采取静态方法。 1.注册驱动1234// 用于注册驱动,加载 public static Connection getConnection() throws Exception { return DriverManager.getConnection(url, user, password); } 2.关闭资源 关闭资源 1234567891011121314151617181920212223242526272829303132333435363738394041// 用于关闭资源 public static void close(Statement stmt, Connection conn) { if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }// 方法的重载。当需要对数据库进行查询的操作时,便需要这里的第三个参数,读取完数据后,需要关闭ResultSet占用的资源public static void close(Statement stmt, Connection conn, ResultSet rs) { if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } }} 3.配置文件的读取 配置文件的读取 1234567891011121314151617181920212223242526272829// 因为对配置文件的读取,只需要读取一次即可拿到这些值。故采用静态代码块。 static { //读取资源文件,获取值 try { // 1.创建Properties集合类 Properties pro = new Properties(); // 获取src路径下的文件的方法-->ClassLoader(类加载器,可以将字节码文件,加载进内存,且内获取src下的资源路径) ClassLoader classLoader = JDBCUtils.class.getClassLoader(); // 以当前src为文件绝对路径,获取文件资源的src //URL统一资源标识符 URL resource = classLoader.getResource(\"jdbc.properties\"); //通过getPath获取它的字符串路径 String path = resource.getPath();// System.out.println(path); // 2.加载文件 pro.load(new FileReader(path)); // 3.获取数据、复制 url = pro.getProperty(\"url\"); user = pro.getProperty(\"user\"); password = pro.getProperty(\"password\"); driver = pro.getProperty(\"driver\"); Class.forName(driver); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } 4. 配置文件配置文件放在当前模块src目录下。文件名后缀为.properties。以下为配置文件可写的内容: 1234url=jdbc:mysql://ip:port/database //填写数据库的url,例如本地url=jdbc:mysql://localhost:3306/db3user=root //数据库用户password=123456 // 用户密码driver=com.mysql.jdbc.Driver //注册驱动路径 5.代码总结 代码总结 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495import java.io.FileReader;import java.io.IOException;import java.net.URL;import java.sql.*;import java.util.Properties;public class JDBCUtils { // 为了使Connection方法可以接受到值,故将参数提升到成员变量的位置上 // 只有静态修饰的变量,才能被静态方法所访问,才能被静态代码块所访问 private static String url; private static String user; private static String password; private static String driver;/** * 文件的读取,只需要读取一次,即可拿到这些值。使用静态代码块完成 */static { //读取资源文件,获取值 try { // 1.创建Properties集合类 Properties pro = new Properties(); // 获取src路径下的文件的方法-->ClassLoader(类加载器,可以将字节码文件,加载进内存,且内获取src下的资源路径) ClassLoader classLoader = JDBCUtils.class.getClassLoader(); // 以当前src为文件绝对路径,获取文件资源的src //URL统一资源标识符 URL resource = classLoader.getResource(\"jdbc.properties\"); //通过getPath获取它的字符串路径 String path = resource.getPath();// System.out.println(path); // 2.加载文件 pro.load(new FileReader(path)); // 3.获取数据、复制 url = pro.getProperty(\"url\"); user = pro.getProperty(\"user\"); password = pro.getProperty(\"password\"); driver = pro.getProperty(\"driver\"); Class.forName(driver); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }/** * 获取连接 * 工具类,方便使用,故采用静态方法 * * @return 连接对象 */// 用于注册驱动,加载public static Connection getConnection() throws Exception { return DriverManager.getConnection(url, user, password);}// 用于关闭资源public static void close(Statement stmt, Connection conn) { if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } }}public static void close(Statement stmt, Connection conn, ResultSet rs) { if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } }} 二、 JDBCUtils工具类实例使用 JDBCUtils工具类实例使用 12345678910111213141516171819202122232425262728293031import cn.li.util.JDBCUtils;import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.Statement;public class JDBCDemo10 { public static void main(String[] args) { Connection conn = null; Statement stmt = null; ResultSet rs = null; PreparedStatement pstmt = null; try { conn = JDBCUtils.getConnection(); // 获取数据库连接对象 String sql = \"select * from user where username = ? and password = ?\"; // 采用预编译的方式,提高效率,预防SQL注入 pstmt = conn.prepareStatement(sql); // 获取sql执行对象 pstmt.setString(1, \"Tom\"); pstmt.setString(2, \"1234\"); rs = pstmt.executeQuery(); boolean next = rs.next(); System.out.println(next); // 判断此用户是否存在 } catch (Exception e) { e.printStackTrace(); }finally { JDBCUtils.close(pstmt, conn, rs); //资源的释放 } }} 从以上代码实例中,可以看出我们抽取出的JDBCUtils工具类,大大简化了代码,并且增加了代码的可复用性。当我们需要更改数据库的相关配置时,只需要更改配置文件即可,而我们的JDBCUtils工具类却不用更改。 参考文献 [1] Itcast视频讲义 [2] Java项目读取resources资源文件路径那点事","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/tags/Java/"},{"name":"JDBC","slug":"JDBC","permalink":"https://chemlez.github.io/tags/JDBC/"},{"name":"MySQL","slug":"MySQL","permalink":"https://chemlez.github.io/tags/MySQL/"}]},{"title":"JDBC基础使用","slug":"JDBC基础使用","date":"2019-11-17T17:42:42.000Z","updated":"2020-08-26T08:26:56.429Z","comments":true,"path":"2019/11/18/JDBC基础使用/","link":"","permalink":"https://chemlez.github.io/2019/11/18/JDBC%E5%9F%BA%E7%A1%80%E4%BD%BF%E7%94%A8/","excerpt":"JDBC:Java DataBase Connectivity,即为Java数据库连接。 JDBC是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。即:定义的一套操作所有关系型数据库的规则,是为接口。各个数据库厂商去实现这套接口,提供数据库驱动jar包。我们可以使用这套接(JDBC)编程,真正执行的代码是驱动jar包中的实现类。","text":"JDBC:Java DataBase Connectivity,即为Java数据库连接。 JDBC是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。即:定义的一套操作所有关系型数据库的规则,是为接口。各个数据库厂商去实现这套接口,提供数据库驱动jar包。我们可以使用这套接(JDBC)编程,真正执行的代码是驱动jar包中的实现类。 JDBC简单使用快速入门 步骤: 导入驱动jar包 mysql-connector-java-5.1.37-bin.jar 1.复制mysql-connector-java-5.1.37-bin.jar到项目的libs目录下 2.右键–>Add As Library 注册驱动 获取数据库连接对象 Connection 定义sql 获取执行sql语句的对象 Statement 执行sql,接受返回结果 处理结果 释放资源 12345678910111213141516171819202122232425262728import java.sql.Connection;import java.sql.DriverManager;import java.sql.Statement;public class JDBCDemo01 { /** * 更新一条数据库数据 * @param args * @throws Exception */ public static void main(String[] args) throws Exception { //1.导入驱动jar包(类似于Python中的第三方库 //2.注册驱动 Class.forName(\"com.mysql.jdbc.Driver\");// //3.获取数据库连接对象 jdbc:mysql://localhost:3306/databases 本机 Connection conn = DriverManager.getConnection(\"jdbc:mysql://url:port/Database\", \"username\", \"password\"); //4.定义sql语句 String sql = \"update account set balance = 500 where id = 1\"; //5.获取执行sql的对象Statement Statement stmt = conn.createStatement(); //6.执行sql int count = stmt.executeUpdate(sql); //7.处理结果 System.out.println(count); stmt.close(); conn.close(); }} 因为,在数据库连接、SQL语句的执行等等过程中,可能会发生异常,报错等。但是,数据库的资源要释放,故采用异常处理的方式,关闭数据库连接。 处理异常的方式: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLException;import java.sql.Statement;/** * 1.采用异常的方式通过JDBC连接数据库 * 2.插入一条语句 */public class JDBCDemo02 { public static void main(String[] args) { // 为了使Connection方法可以接受到值,故将参数提升到成员变量的位置上 Statement stmt = null; Connection conn = null; // 异常捕获的方式处理异常 try { //1.注册驱动 Class.forName(\"com.mysql.jdbc.Driver\"); //2.定义SQL语句 String sql = \"insert into account values(null,'Lisa',2000)\"; //3.获取连接对象 conn = DriverManager.getConnection(\"jdbc:mysql://url:port/db3\", \"username\", \"password\"); //4.获取执行sql对象 stmt = conn.createStatement(); //5.执行sql int count = stmt.executeUpdate(sql); if (count > 0) { System.out.println(\"修改成功\"); } else { System.out.println(\"修改失败\"); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } finally { if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }} 详解 DriverManager:驱动管理对象 用于注册驱动,jar包导入。 12345678通过查看源码发现:在com.mysql.jdbc.Driver类中存在静态代码块 static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException(\"Can't register driver!\"); } } 原因:文件的读取,只需要读取一次,即可拿到这些值。故使用静态代码块完成。 static Connection getConnection(String url, String user, String password) Connection:数据库连接对象 功能: 1. 获取执行sql的对象 * `Statement createStatement()` * `PreparedStatement prepareStatement(String sql)` 2. 事务管理: * 开启事务:setAutoCommit(boolean autoCommit) :调用该方法设置参数为false,即开启事务 * 提交事务:commit() * 回滚事务:rollback() 3. `Statement`:执行sql的对象 **注意**:`createStatement`方法会造成SQL注入的问题,后期采用`PreparedStatement`来执行SQL对象,并采用预编译的方式,采用参数`?`作为占位符,且效率更高。 4. `ResultSet`:结果集对象,封装查询结果(next,类似指针移动取值方法) * boolean next(): 游标向下移动一行,判断当前行是否是最后一行末尾(是否有数据),如果是,则返回false,如果不是则返回true. * getXxx(参数):获取数据. - 其中Int代表列的编号,参数**从1开始**。 - String代表列的名称(参数)。 JDBC操作数据库的一般SQL语法 更新操作 String sql = "update account set balance = 500 where id = 1"; 插入操作 String sql = "insert into account values(null,'Lisa',2000)"; 删除操作 String sql = "delete from account where id = 3"; 创建操作 1234String sql = \"create table student (id int primary key not null,name varchar(20))\";stmt = conn.createStatement();int count = stmt.executeUpdate(sql);//处理结果,创建表返回的为0System.out.println(count); 查询操作 123456789String sql = \"select * from account\";stmt = conn.createStatement();resultSet = stmt.executeQuery(sql);while (resultSet.next()) { //resultSet指针下移一行,并判断当前行内容是否为空,内容不为空,进入循环体 int id = resultSet.getInt(1);// 取第一列的元素 String name = resultSet.getString(\"NAME\"); int balance = resultSet.getInt(3); System.out.println(id + \"---\" + name + \"---\" + balance);} 参考文献 [1] Java数据库连接 [2] Itcast视频资料","categories":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/categories/Java/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://chemlez.github.io/tags/Java/"},{"name":"JDBC","slug":"JDBC","permalink":"https://chemlez.github.io/tags/JDBC/"},{"name":"MySQL","slug":"MySQL","permalink":"https://chemlez.github.io/tags/MySQL/"}]},{"title":"序列之深拷贝/浅拷贝","slug":"序列之深拷贝-浅拷贝","date":"2019-10-22T13:57:35.000Z","updated":"2020-03-14T14:11:34.752Z","comments":true,"path":"2019/10/22/序列之深拷贝-浅拷贝/","link":"","permalink":"https://chemlez.github.io/2019/10/22/%E5%BA%8F%E5%88%97%E4%B9%8B%E6%B7%B1%E6%8B%B7%E8%B4%9D-%E6%B5%85%E6%8B%B7%E8%B4%9D/","excerpt":"在上周的Python科学计算课上,老师讲到了Python序列的浅拷贝以及深拷贝方面的知识,个人觉得说得比较言简意赅了,对于我这个刚入Python的新手来说,也基本可以避免今后变量的赋值使用错乱的问题。 这里我们简单的将Python中的标准数据类型分为两类: 不可变数据类型:int、float、string、boolean 可变(组合)数据类型:列表(list)、字典(dict)、集合(set) 先举几个例子: 1234567891011121314a = 1 # a为上述定义的不可变数据类型b = aprint('b={}'.format(b)) # b = 1--------b = 2print('a = {},b = {}'.format(a,b)) # a = 1,b = 2========c = [1,2,3] # b为上述定义的组合数据类型 d = cprint('c = {},d = {}'.format(c,d)) # c = [1, 2, 3],d = [1, 2, 3]d.append(4) print('c = {},d = {}'.format(c,d)) # c = [1, 2, 3, 4],d = [1, 2, 3, 4] 从上述的例子当中看出,在不可变数据类型中,所定义的变量的值在后来改变(这里是b),并不会引起原来赋给它值的那个量的改变(这里是a);而在组合数据类型中就发生了改变,我们只是将d的值进行了改变,并没有直接改变c的值,最后c的值却也发生了变化。","text":"在上周的Python科学计算课上,老师讲到了Python序列的浅拷贝以及深拷贝方面的知识,个人觉得说得比较言简意赅了,对于我这个刚入Python的新手来说,也基本可以避免今后变量的赋值使用错乱的问题。 这里我们简单的将Python中的标准数据类型分为两类: 不可变数据类型:int、float、string、boolean 可变(组合)数据类型:列表(list)、字典(dict)、集合(set) 先举几个例子: 1234567891011121314a = 1 # a为上述定义的不可变数据类型b = aprint('b={}'.format(b)) # b = 1--------b = 2print('a = {},b = {}'.format(a,b)) # a = 1,b = 2========c = [1,2,3] # b为上述定义的组合数据类型 d = cprint('c = {},d = {}'.format(c,d)) # c = [1, 2, 3],d = [1, 2, 3]d.append(4) print('c = {},d = {}'.format(c,d)) # c = [1, 2, 3, 4],d = [1, 2, 3, 4] 从上述的例子当中看出,在不可变数据类型中,所定义的变量的值在后来改变(这里是b),并不会引起原来赋给它值的那个量的改变(这里是a);而在组合数据类型中就发生了改变,我们只是将d的值进行了改变,并没有直接改变c的值,最后c的值却也发生了变化。 这里,基本数据变量的赋值其实就是深拷贝;组合数据类型的赋值就是起了一个别名。 这里先做出组合数据类型中赋值、浅拷贝、深拷贝三种的区别: 直接赋值:其实就是对象的引用(即给对象起一个别名)。 浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象。 深拷贝(deepcopy):copy模块的deepcopy方法,完全拷贝了父对象及其子对象。 关于内部子对象的概念,下方会再解释。 接下来我们再看一组图(上课ppt图片): 这里的a = {1:[1,2,3]}字典类型。b = a : 赋值引用,a 和 b 都指向同一个对象。可以看出,a,b此刻都指向同一个对象,所以改变b的内容,就是在改变a,b同时所指向的对象的内容,可以理解成b就是a的一个别名。 这里 a = {1:[1,2,3]} , b = a.copy(),这里就是一种浅拷贝的方式。可以看出a 和 b 是一个独立的对象,但他们的子对象还是指向统一对象(是引用)。所以在这里L,M就是对象当中的一个子对象([1,2,3])便是这里的子对象。 举个上述的例子: 12345678910import copya = {1:[1,2,3],'北京':'天安门'}b = copy.copy(a) # b = {1:[1,2,3],'北京':'天安门'}b[1].append(4) b['上海'] = '东方明珠'b['北京'] = '鸟巢'print('输出:a = {},b = {}'.format(a,b))-----输出:a = {1: [1, 2, 3, 4], '北京': '天安门'},b = {1: [1, 2, 3, 4], '北京': '鸟巢', '上海': '东方明珠'} b = copy.copy(a) 使得b为单独一个对象,但是它和a的子对象指向统一对象。这里的子对象就是[1,2,3](列表子对象)。故当改变b中1键对中的值[1,2,3]时,a也会改变(统一子对象)。但向b中添加值时,便不会对a造成影响,因为这是b自身的对象所拥有的值(和a没有关系)。 那么如何拷贝一个a,但对这个拷贝的对象任意操作时,不会对a产生任何的影响呢?答:采用深拷贝。 如图: 从图中可以清楚的看出:深度拷贝, a 和 b 完全拷贝了父对象及其子对象,两者是完全独立的。 123456789101112131415161718import copya = [1, 2, 3, 4, ['a', 'b']] #原始对象 b = a #赋值,传对象的引用c = copy.copy(a) #对象拷贝,浅拷贝d = copy.deepcopy(a) #对象拷贝,深拷贝a.append(5) #修改对象aa[4].append('c') #修改对象a中的['a', 'b']数组对象print( 'a = ', a )print( 'b = ', b )print( 'c = ', c )print( 'd = ', d )--------a = [1, 2, 3, 4, ['a', 'b', 'c'], 5]b = [1, 2, 3, 4, ['a', 'b', 'c'], 5] # 给a起了一个别名b,本质相同,故b和a的变化相同c = [1, 2, 3, 4, ['a', 'b', 'c']] # c中子对象发生了变化 -->浅拷贝d = [1, 2, 3, 4, ['a', 'b']] # a的改变和d无关 -->深拷贝 总结对于组合数据类型: 直接赋值:其实就是对象的引用(别名) 浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象 深拷贝(deepcopy):copy模块的deepcopy方法,完全拷贝了父对象及其子对象。","categories":[{"name":"Python","slug":"Python","permalink":"https://chemlez.github.io/categories/Python/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://chemlez.github.io/tags/Python/"}]},{"title":"Hexo yilia 主题添加相册功能","slug":"hexo-yilia-主题添加相册功能","date":"2019-08-28T10:08:42.000Z","updated":"2020-03-14T18:27:47.398Z","comments":true,"path":"2019/08/28/hexo-yilia-主题添加相册功能/","link":"","permalink":"https://chemlez.github.io/2019/08/28/hexo-yilia-%E4%B8%BB%E9%A2%98%E6%B7%BB%E5%8A%A0%E7%9B%B8%E5%86%8C%E5%8A%9F%E8%83%BD/","excerpt":"当我们使用hexo博客框架中的yilia主题时,在我们这一博客页面中,原作者Litten并未帮我们添加相册这一功能。这时,如果想让我们的博客拥有相册的功能,就需要我们自行添加改变、添加主题中的相关参数。从网上百度了很多,看了许多的博客,还是遇到了一些坑爬不过去。最终,还是请教了一个小伙伴才得以解决。 一、博客页面添加相册首先,打开cmd进入blog的source目录下,创建photos文件夹。 1234E:\\>cd blogE:\\blog>cd sourceE:\\blog\\source>hexo new page "photos"INFO Created: E:\\blog\\source\\photos\\index.md 删除文件夹中的index.md文件,否则最终生成的是一个单纯的页面。也可以直接进入source文件下创建photos文件夹。","text":"当我们使用hexo博客框架中的yilia主题时,在我们这一博客页面中,原作者Litten并未帮我们添加相册这一功能。这时,如果想让我们的博客拥有相册的功能,就需要我们自行添加改变、添加主题中的相关参数。从网上百度了很多,看了许多的博客,还是遇到了一些坑爬不过去。最终,还是请教了一个小伙伴才得以解决。 一、博客页面添加相册首先,打开cmd进入blog的source目录下,创建photos文件夹。 1234E:\\>cd blogE:\\blog>cd sourceE:\\blog\\source>hexo new page "photos"INFO Created: E:\\blog\\source\\photos\\index.md 删除文件夹中的index.md文件,否则最终生成的是一个单纯的页面。也可以直接进入source文件下创建photos文件夹。 二、创建图片存储仓库因为,我们的博客是部署到远端,使得每一个人都能够看到,而图片在远端的展示,可借助于图床。所以,我们可以专门在github上创建一个仓库用于存储图片。仓库的创建就不再一一赘述,只需登录自己的github,new repository即可。这里,我的仓库名为blog-Picture.在创建完远端仓库后,将本地与github上远端仓库关联,这样我们以后才能够将图片推送到远端。远端仓库与本地仓库关联的方法:打开博客文件夹,在此根目录下,使用git ,即 git Bush Here,然后输入 $ git clone [email protected]:chemlez/picture-blog.git 其中clone的仓库换成自己的仓库地址。这样便能使本地与远端关联起立。此刻,会产生一个blog-Picture的文件夹,在此文件夹下分别创建min_photos、photos文件夹。其中,在此photos文件夹下存入一张图片,再将整个内容推送至远端。 $ git add .$ git commit -m “照片存放”$ git push -u origin master 这个时候本地的内容就被推送到了远端。关于git推送远端的用法,可参照廖雪峰的教程。这样后面我们可以用来查看图片的存入地址,来修改我们的ins.js参数。 三、创建相册布局样式在一开始的博客主题clone中,主题yilia并没有相册的版块。但作者Litten的博客样式中添加了这一版块。所以,我们可以参照原作者的格式进行相关的修改即可。其中的样式参照这里–样式参考。下载完之后:1.删除其中所有的.json文件。因为,后面的.json文件是我们自己博客在上传图片时生成的.2.修改index.ejs。这一步很重要,我自己查百度和相关博文时,都没有提到这一步。将其中的href修改成自己的博客地址。当初我就没有修改,最终,显示出来的永远都是原作者Litten的相册. 123 <div class="instagram itemscope"> <a href="https://chemlez.github.io/" target="_blank" class="open-ins">图片正在加载中…</a></div> 3.修改ins.js文件里的render()函数,按照上面的注释提醒,进行修改。 1234567891011121314// 修改这里render()函数:修改图片的路径地址.minSrc 小图的路径. src 大图的路径.修改为自己的图片路径(github的路径) // https://raw.githubusercontent.com/ChemLez/blog-Picture/master/photos/ // https://raw.githubusercontent.com/ChemLez/blog-Picture/master/min_photos/ var render = function render(res) { var ulTmpl = ""; for (var j = 0, len2 = res.list.length; j < len2; j++) { var data = res.list[j].arr; var liTmpl = ""; for (var i = 0, len = data.link.length; i < len; i++) { var minSrc = 'https://raw.githubusercontent.com/ChemLez/blog-Picture/master/min_photos/' + data.link[i]; var src = 'https://raw.githubusercontent.com/ChemLez/blog-Picture/master/photos/' + data.link[i]; var type = data.type[i]; var target = src + (type === 'video' ? '.mp4' : '.jpg'); src += ''; 这里的地址,就可以查看我们第二步所做的工作。打开github,进入blog-Picture仓库后。点击在第二步中上传的照片。然后点击Download,此时的浏览框中的地址就是我们所需要的地址。 四、添加脚本这里添加的python脚本主要是用于处理图片。脚本下载-下载地址.因为,当我们点击相册这一页面时,展示在眼前的是一张张缩略图。而当你需要预览具体的某一张图时,其显示的是一张大图。所以,我们的预览图照片大小是经过压缩处理的(使得页面加载快)。当我们具体看某张图片时,再使用原画质的图片。所以,min_photos和photos两个文件夹分别对应着这两种图片。其中,min_photos就是处理过后的压缩图片,而photos就是我们存放的图片。所以,这里的python脚本主要就做着这样的工作。 将其中的.py文件拷贝至本地仓库blog-Picture文件夹中. 根据脚本文件,图片的命名规则为:2019-10-21_xxx.jpg/png. 将图片empty.png下载放入博客目录下的assets/img文件夹中. 打开tool.py文件,修改def handle_photo():1with open("E:/blog/source/photos/data.json", "w") as fp: 将其中的的地址,换成你将要生成data.json的位置,就是在第一步中,我们删除的.json文件夹的目录地址。每次,进行tool.py脚本时,都会产生data.json文件,用于存储我们图片的信息。 五、运行1.首先将用于测试的图片名改成上述的命名规则的名字,推送至github远端,进行修改.2.打开终端命令窗口cmd. 1234567输入:cd blog-Picture //用于进入blog-Picture文件夹python tool.py //python脚本文件的运行第二句的运行这里可能会报错 `no module named PIL`然后输入:pip install pillow可能出现权限不足的情况,按照下方出现的英文,加上权限进行下载。即:一路按照下方的英文,加权限进行下载. 3.hexo s.预览查看。这里我将video功能隐去了,最初的photos旁边还有一个video功能。4.在最初的photos下载中,有个videos.ejs文件,如果想引入一些视频,可将其中的链接即src,视频名进行修改. 12345678910<center> <h1>指弹_女儿情</h1></center><hr/><center> <div class="video-container"> <iframe height="80%" width="80%" src="https://player.youku.com/embed/XMjUzMzY4OTM3Ng==" frameborder=0 allowfullscreen></iframe> </div></center><hr/> 如果不想用这一功能,将以下标签注释. 1<a class="photos-btn" href="/photos/videos.html">Video</a> 六、总结 每次将需要上传的图片,放入到blog-Picture中的photos文件夹.图片的命名一定要遵循上述说的命名规则.注意:如果想让多张图片归类在页面中的某一个年、月份下,必须使得日期一模一样,只能修改xxx。如果命名中,年、月相同,而日期不同便会在相册页面额外生成一个list,其表头相同。 cmd命令窗口进入blog-Picture,再进行python tool.py,运行脚本. 将图片推送到github远端仓库,产生链接. hexo s 进入本地窗口预览,没有问题后:123hexo clean //清除页面缓存hexo g //用于生成改动的文件hexo d //部署到远端网站 最终效果 参考文献[1] hexo Yilia 主题如何添加相册功能:https://www.jianshu.com/p/a9f309aaa0e0[2] hexo yilia 主题如何添加相册:https://blog.csdn.net/qq_40651535/article/details/95061281[3] Hexo+Github实现相册功能:http://lawlite.me/2017/04/13/Hexo-Github%E5%AE%9E%E7%8E%B0%E7%9B%B8%E5%86%8C%E5%8A%9F%E8%83%BD/","categories":[],"tags":[{"name":"hexo","slug":"hexo","permalink":"https://chemlez.github.io/tags/hexo/"}]}]}