├── README.md ├── SSMD.zip ├── SSMD20161009_第3期.zip ├── SSMD20161026_第五期.zip ├── SSMD20161030_第六期.zip ├── SSMD20161109_第七期.zip ├── SSMD20161116_第八期.zip ├── SpringMvcMybatis.zip ├── [java手把手教程][第二季]java后端博客系统文章系统——No2.md ├── [java手把手教程][第二季]java后端博客系统文章系统——No3.md ├── [java手把手教程][第二季]java后端博客系统文章系统——No4.md ├── [java手把手教程][第二季]java后端博客系统文章系统——No5.md ├── [手把手教程][第二季]java后端博客系统文章系统——No1.md ├── [手把手教程][第二季]java后端博客系统起笔.md ├── abc.pac ├── mmblog博客系统第三章.zip ├── mmblog博客系统第二章.zip ├── mmblog博客系统第四章.zip ├── readme20160925.md ├── readme20161009 ├── readme20161020 ├── readme20161026 ├── readme20161030_第六期.md ├── readme20161109第七期.md ├── readme20161116第八期.md └── wp_posts.sql /README.md: -------------------------------------------------------------------------------- 1 | #### [手把手教程][JavaWeb]优雅的SpringMvc+Mybatis整合之路 2 | 手把手教你整合最优雅SSM框架:SpringMVC + Spring + MyBatis 3 | - 前面网友说我为啥很久不更新博客了,我告诉他们我准备潜修.其实是我的博客被人批评是在记流水账(~一脸尴尬~). 4 | - 再次安利一波,博客地址:[acheng1314.cn](http://acheng1314.cn/) 5 | - 本文中的图片用了个人服务器存储,网速较慢,各位老司机耐心等待. 6 | 7 | #### 工具 8 | - IDE为**idea15** 9 | - JDK环境为**1.8** 10 | - maven版本为**maven3** 11 | 12 | #### 目标 13 | - 完成基本的SpringMVC + Spring + MyBatis框架整合 14 | - 数据库使用mysql 15 | - 加入阿里巴巴的druid数据库连接池 16 | - 使用gson作为json解析工具 17 | - 实现日志输出 18 | - maven依赖的版本管理 19 | 20 | #### 优点 21 | ``` 22 | 此处省略若干字,观众们请脑补. 23 | ``` 24 | 25 | ---- 26 | 27 | #### SSM框架整合配置 28 | 29 | 前面说了这么多,现在开始正式的干货. 30 | 31 | ##### 第一步: 使用idea的maven创建一个基本的web工程. 32 | - 打开Idea在欢迎界面选择创建一个新的Project或者是(在菜单界面选择:New→Project),这是会出现一个界面如下图所示: 33 | 34 | ![maven新建WebApp项目第一步](http://acheng1314.cn/wp-content/uploads/2016/09/maven新建WebApp项目第一步.png) 35 | 36 | - 如上图所示,我们需要勾选的地方已经使用红色框标注出来. 37 | - 最左边的是**maven**,是我们需要使用的项目构建工具. 38 | - 勾选右边上面的**Create from archetype**,我们才能在下面选择我们需要构建成什么类型的项目. 39 | - 接着我们选中**maven-archetype-webapp**,这时候我们的项目类型就确定为是web项目. 40 | - 需要注意一点,我上面图中没标注出来的**Project SDK**,这里是选择我们开发的JDK版本. 41 | 42 | - 点击next后,如下图所示: 43 | 44 | ![maven新建WebApp项目第二步](http://acheng1314.cn/wp-content/uploads/2016/09/maven新建WebApp项目第二步.png) 45 | 46 | - 上面图中,我们需要注意地方如下: 47 | - **GroupId**也就是我们常说的组织ID,也可以理解为我们**应用程序的包名** 48 | - **ArtifactId**是我们常说的产品名称(同一个组织下面可以有多个产品),也可以当作是我们的**当前项目名称** 49 | - **Version**顾名思义就是版本号 50 | - 最下面的红色框中,Previous==>返回上一步,Next==>下一步,Cancel==>取消,Help==>帮助 51 | 52 | - 接下来,我们继续点击Next后,如下图所示: 53 | 54 | ![maven新建WebApp项目第三步](http://acheng1314.cn/wp-content/uploads/2016/09/maven新建WebApp项目第三步.png) 55 | 56 | - 上面途中没啥好说的,圈出来部分就是我们的**Maven目录**.继续next后,如下图所示: 57 | 58 | ![maven新建WebApp项目第四步](http://acheng1314.cn/wp-content/uploads/2016/09/maven新建WebApp项目第四步.png) 59 | 60 | - 上面选中部分,**Project name**为**项目名称**,**Project location**是项目的**存储位置**(~右边的省略号意味着可以选择位置~). 61 | - 接下来我们**点击Finish**,我们新建基本的web项目的步骤就完成了. 62 | - 这时候在Idea主窗口的右下角部分,我们可以看到一个滚动条在执行,说明我们的项目正在build中.右上角有一个提示框如下图所示: 63 | 64 | ![maven新建WebApp项目完成后的自动导入提示框](http://acheng1314.cn/wp-content/uploads/2016/09/maven新建WebApp项目完成后的自动导入提示框.png) 65 | 66 | - 这个提示框大概意思是:Maven项目需要被导入.我建议勾选:**Enable Auto-Import**(~自动导入~) 67 | 68 | 此处,使用Idea创建一个**Maven依赖的基本的WebApp项目**已经完成. 69 | 70 | ---- 71 | #### 框架整合前的准备工作. 72 | - 整理项目文件组织结构. 73 | - 通过观察目录结构,我们可以发现,需要的目录不齐全,我们需要手动补齐.初始结构图如下: 74 | 75 | ![maven新建WebApp项目完成后的目录结构](http://acheng1314.cn/wp-content/uploads/2016/09/maven新建WebApp项目完成后的目录结构.png) 76 | - 我们需要的主体结构图应如下: 77 | 78 | ![WebApp项目整合框架前的目录结构](http://acheng1314.cn/wp-content/uploads/2016/09/WebApp项目整合框架前的目录结构.png) 79 | 80 | #### 需要的主体结构目录解释: 81 | ----- 82 | | 目录名称 | 说明 | 83 | | ---- | ----| 84 | | src | 源码、资源等文件的根目录| 85 | | ↓ main | 项目开发主要目录之一,可以放java代码和一些资源文件. | 86 | | ↓↓java | 开发的主要的java代码存放目录 | 87 | | ↓↓↓cn.acheng1314 | 我的应用程序的包名 | 88 | | ↓↓resources | 开发中的主要的资源文件存放目录 | 89 | | ↓↓sql | 开发中主要的sql语句文件存放目录 | 90 | | ↓↓webapp | web页面和其他web配置、资源文件存放目录 | 91 | | ↓ test | 项目开发中的测试模块存放路径,包含java代码和资源文件. | 92 | | ↓↓java | 测试代码存放目录 | 93 | | ↓↓resources | 测试资源文件存放目录 | 94 | - 配置目录: 95 | - 创建main目录下的java目录(用于存放java源代码) 96 | 97 | ![WebApp目录调整第一步创建java源代码目录](http://acheng1314.cn/wp-content/uploads/2016/09/WebApp目录调整第一步创建java源代码目录.png) 98 | 99 | 我们先**右键点击main目录**,接着选中**New**→**Directory**,在弹出的对话框中输入java. 100 | - 接着我们需要把java目录标记为源文目录. 101 | 102 | ![WebApp目录调整第二步标记java目录为资源目录](http://acheng1314.cn/wp-content/uploads/2016/09/WebApp目录调整第二步标记java目录为资源目录.png) 103 | 104 | 我们先**右键点击java**,然后选择**Mark Directory As**→**Sources Root** 105 | 106 | 接着我们在src目录下创建test目录(注意: **test目录和main目录同级**),以及test下面的java和resources目录,分别标记为源文件目录和资源文件目录 107 | 108 | **值得注意的是sql目录为普通文件目录** 109 | 110 | - 根据目标明白我们**需要哪些支援库**,具体结果如下: 111 | 112 | 113 | 115 | 4.0.0 116 | cn.acheng1314 117 | SSM_LOG 118 | war 119 | 1.0-SNAPSHOT 120 | SSM_LOG Maven Webapp 121 | http://maven.apache.org 122 | 123 | 124 | junit 125 | junit 126 | 3.8.1 127 | test 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | org.apache.logging.log4j 139 | log4j-core 140 | ${org.apache.logging.log4j.version} 141 | 142 | 143 | org.apache.logging.log4j 144 | log4j-api 145 | ${org.apache.logging.log4j.version} 146 | 147 | 148 | 149 | 150 | 151 | mysql 152 | mysql-connector-java 153 | ${mysql.version} 154 | runtime 155 | 156 | 157 | 158 | com.alibaba 159 | druid 160 | ${com.alibaba.druid.version} 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | org.mybatis 171 | mybatis 172 | ${com.mybatis.mybatis.version} 173 | 174 | 175 | org.mybatis 176 | mybatis-spring 177 | ${com.mybatis.mybatis_spring.version} 178 | 179 | 180 | 181 | 182 | taglibs 183 | standard 184 | 1.1.2 185 | 186 | 187 | jstl 188 | jstl 189 | 1.2 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | com.google.code.gson 199 | gson 200 | ${com.google.gson.version} 201 | 202 | 203 | 204 | javax.servlet 205 | javax.servlet-api 206 | ${javax.servlet.version} 207 | 208 | 209 | 210 | 211 | 212 | org.springframework 213 | spring-core 214 | ${org.springframework.version} 215 | 216 | 217 | org.springframework 218 | spring-beans 219 | ${org.springframework.version} 220 | 221 | 222 | org.springframework 223 | spring-context 224 | ${org.springframework.version} 225 | 226 | 227 | 228 | org.springframework 229 | spring-jdbc 230 | ${org.springframework.version} 231 | 232 | 233 | org.springframework 234 | spring-tx 235 | ${org.springframework.version} 236 | 237 | 238 | 239 | org.springframework 240 | spring-web 241 | ${org.springframework.version} 242 | 243 | 244 | org.springframework 245 | spring-webmvc 246 | ${org.springframework.version} 247 | 248 | 249 | 250 | org.springframework 251 | spring-test 252 | ${org.springframework.version} 253 | 254 | 255 | 256 | 257 | redis.clients 258 | jedis 259 | ${redis.clients.version} 260 | 261 | 262 | com.dyuproject.protostuff 263 | protostuff-core 264 | ${com.dyuproject.protostuff.version} 265 | 266 | 267 | com.dyuproject.protostuff 268 | protostuff-runtime 269 | ${com.dyuproject.protostuff.version} 270 | 271 | 272 | 273 | 274 | commons-collections 275 | commons-collections 276 | 3.2.2 277 | 278 | 279 | 280 | 281 | commons-fileupload 282 | commons-fileupload 283 | 1.3.2 284 | 285 | 286 | commons-io 287 | commons-io 288 | 2.5 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 2.6.2 298 | 5.1.37 299 | 1.0.25 300 | 3.4.1 301 | 1.3.0 302 | 2.7 303 | 3.1.0 304 | 4.3.2.RELEASE 305 | 2.7.3 306 | 1.0.8 307 | 308 | 309 | 310 | 311 | 312 | SSM_LOG 313 | 314 | 315 | 316 | 317 | 318 | ---- 319 | #### 整合框架 320 | 321 | 在上面,我们已经把基本的目录配置好了,现在我们在已经依赖了项目支援库,接下来我们需要做的是开始**整合Spring+SpringMvc+Mybatis** 322 | 323 | 我们先**打开webapp目录下面的WEB-INF目录中的web.xml文件**,web.xml文件是整合web项目的配置中心.我们在web.xml中加入如下内容: 324 | 325 | ``` 326 | 329 | 333 | 334 | 335 | 336 | index.html 337 | index.htm 338 | index.jsp 339 | default.html 340 | default.htm 341 | default.jsp 342 | 343 | 344 | 345 | 346 | 347 | SSM_LOG 348 | mvc-dispatcher 349 | org.springframework.web.servlet.DispatcherServlet 350 | 354 | 355 | contextConfigLocation 356 | classpath:spring/spring-*.xml 357 | 358 | 359 | 360 | mvc-dispatcher 361 | 362 | /js/* 363 | /css/* 364 | /images/* 365 | /fonts/* 366 | 367 | 368 | 369 | 370 | DruidStatView 371 | com.alibaba.druid.support.http.StatViewServlet 372 | 373 | 374 | DruidStatView 375 | /druid/* 376 | 377 | 378 | druidWebStatFilter 379 | com.alibaba.druid.support.http.WebStatFilter 380 | 381 | exclusions 382 | /public/*,*.js,*.css,/druid*,*.jsp,*.swf 383 | 384 | 385 | principalSessionName 386 | sessionInfo 387 | 388 | 389 | profileEnable 390 | true 391 | 392 | 393 | 394 | druidWebStatFilter 395 | /* 396 | 397 | 398 | 399 | ``` 400 | 401 | #### 快捷生成spring目录 402 | - 在上面的```classpath:spring/spring-*.xml```处,我们选中前面一个spring,按下Alt+Enter自动生成spring目录. 403 | - spring目录位于src→main→resources下. 404 | 405 | #### 在spring目录下创建spring相关的控制文件 406 | - spring-dao.xml 407 | ``` 408 | 409 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | ``` 497 | 498 | 上面的配置中,肯定会出现报错的情况,这时候我们只需要选中报错的地方按下Alt+Enter就能生成相关的资源. 499 | 500 | - spring-service.xml 501 | ``` 502 | 503 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | ``` 529 | 530 | 上面的配置中,肯定会出现报错的情况,这时候我们只需要选中报错的地方按下Alt+Enter就能生成相关的资源. 531 | 532 | **基本的spring系列和druid**已经配置完毕. 接着我们需要解决上面自动生成的一些问题.基本配置截图如下: 533 | 534 | ![基本的spring配置和druid配置后截图](http://acheng1314.cn/wp-content/uploads/2016/09/基本的spring配置和druid配置后截图.png) 535 | 536 | 现在我们会发现我们的jdbc.properties和mybatis-config.xml文件都是空的,我们需要继续写入内容. 537 | 538 | jdbc.properties是数据库连接的配置文件.如下: 539 | 540 | ``` 541 | jdbc.driver=com.mysql.jdbc.Driver 542 | jdbc.url=jdbc:mysql://localhost:3307/wordpress?useUnicode=true&characterEncoding=utf8 543 | jdbc.username=数据库用户名 544 | jdbc.password=数据库用户名对应的密码 545 | ``` 546 | 547 | 上面的jdbc.driver为数据库连接的驱动,jdbc.url为数据库的连接地址. 548 | 549 | mybatis-config.xml 顾名思义是mybatis的配置文件,如下: 550 | 551 | ``` 552 | 553 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | ``` 570 | 571 | 配置完成上面的东西后,大体需要的我们已经完成了.但是,我们会看到我们的日志记录还没有配置,上面我们采用了log4j2,通过查看官网文档,我们发现只需要在资源目录下面添加一个默认的配置文件即可,如下: 572 | 573 | 配置文件文件名: **log4j2.xml** , 存放目录为**src**→**main**→**resources** 574 | ``` 575 | 576 | 577 | 578 | 579 | 580 | /logs/webLog 581 | 582 | {LOG_HOME}/backup 583 | stat 584 | global 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 603 | 605 | 606 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | ``` 640 | 641 | ---- 642 | 至此,我们的基本配置就完成了,结果如下图所示: 643 | 644 | ![ssm框架整合完毕截图](http://acheng1314.cn/wp-content/uploads/2016/09/ssm框架整合完毕截图.png) 645 | 646 | 具体基本配置完毕,下面我们需要进行实际演练方可知道效果,也能根据实际效果检查配置有没有出现问题.至于实际演练如何,且听下回分解. 647 | -------------------------------------------------------------------------------- /SSMD.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pc859107393/SpringMvcMybatis/9a48d6d8a1b279a8d1174a8d52cd5c7c08b5cb27/SSMD.zip -------------------------------------------------------------------------------- /SSMD20161009_第3期.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pc859107393/SpringMvcMybatis/9a48d6d8a1b279a8d1174a8d52cd5c7c08b5cb27/SSMD20161009_第3期.zip -------------------------------------------------------------------------------- /SSMD20161026_第五期.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pc859107393/SpringMvcMybatis/9a48d6d8a1b279a8d1174a8d52cd5c7c08b5cb27/SSMD20161026_第五期.zip -------------------------------------------------------------------------------- /SSMD20161030_第六期.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pc859107393/SpringMvcMybatis/9a48d6d8a1b279a8d1174a8d52cd5c7c08b5cb27/SSMD20161030_第六期.zip -------------------------------------------------------------------------------- /SSMD20161109_第七期.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pc859107393/SpringMvcMybatis/9a48d6d8a1b279a8d1174a8d52cd5c7c08b5cb27/SSMD20161109_第七期.zip -------------------------------------------------------------------------------- /SSMD20161116_第八期.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pc859107393/SpringMvcMybatis/9a48d6d8a1b279a8d1174a8d52cd5c7c08b5cb27/SSMD20161116_第八期.zip -------------------------------------------------------------------------------- /SpringMvcMybatis.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pc859107393/SpringMvcMybatis/9a48d6d8a1b279a8d1174a8d52cd5c7c08b5cb27/SpringMvcMybatis.zip -------------------------------------------------------------------------------- /[java手把手教程][第二季]java后端博客系统文章系统——No2.md: -------------------------------------------------------------------------------- 1 | 这是博客系统的第三章,也是坚持写这个系列文章的第三个月了。在这期间我建立了全栈技术交流群,感谢一路鼓励我的朋友们。也要感谢我的大学导师,是他们在我需要的时候,告诉我做人的品质。 2 | 3 | 今天这一篇,主要是关于上一张的编码实现。为什么我要单路分离出来?因为做事要分先后,明白道理,执行才能确定无误。 4 | 5 | 我们还是从老套路(Dao→Service→Controller)做起来,让我们先看看数据存储相关的东西吧。 6 | 7 | 项目github地址:https://github.com/pc859107393/SpringMvcMybatis 8 | 9 | 我的简书首页是:http://www.jianshu.com/users/86b79c50cfb3/latest_articles 10 | 11 | 上一期是:[优雅的SpringMvc+Mybatis应用(七)](http://www.jianshu.com/p/def0076976aa) 12 | 13 | --- 14 | #### wordpress做的文章存储 15 | 16 | 在上次我们已经看过了wordpress的数据库模型(有朋友问我什么是逆向分析,拿着别人的产品逆向推导这就是逆向分析),我们可以很清楚的看到数据库关于文章存储的两张表,它们分别存储了文章的主体信息和文章的其他信息,具体的我们再看看数据库模型: 17 | 18 | ![第二章文章系统-wordpress数据库模型](http://acheng1314.cn/wp-content/uploads/2016/12/我的博客第二章文章系统-wordpress数据库模型.png) 19 | 20 | 在上面的途中,我们很明显的看到数据库关于文章的存储主要分为两张表: 21 | 22 | - wp_posts 存放文章主体信息 23 | - wp_postmeta 存放文章的附加信息 24 | 25 | 我们先看看wp_posts的主要结构: 26 | 27 | ``` 28 | DROP TABLE IF EXISTS `wp_posts`; 29 | CREATE TABLE `wp_posts` ( 30 | `ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 31 | `post_author` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '作者ID', 32 | `post_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '文章创建时间', 33 | `post_date_gmt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '文章最近修改时间', 34 | `post_content` longtext COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章内容', 35 | `post_title` text COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章标题', 36 | `post_excerpt` text COLLATE utf8mb4_unicode_ci NOT NULL, 37 | `post_status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'publish' COMMENT '文章状态', 38 | `comment_status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open' COMMENT '评论状态', 39 | `ping_status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open' COMMENT 'ping状态', 40 | `post_password` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文章密码', 41 | `post_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文章名字', 42 | `to_ping` text COLLATE utf8mb4_unicode_ci NOT NULL, 43 | `pinged` text COLLATE utf8mb4_unicode_ci NOT NULL, 44 | `post_modified` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', 45 | `post_modified_gmt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', 46 | `post_content_filtered` longtext COLLATE utf8mb4_unicode_ci NOT NULL, 47 | `post_parent` bigint(20) unsigned NOT NULL DEFAULT '0', 48 | `guid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', 49 | `menu_order` int(11) NOT NULL DEFAULT '0', 50 | `post_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'post' COMMENT '文章类型', 51 | `post_mime_type` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文件类型', 52 | `comment_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '评论数', 53 | PRIMARY KEY (`ID`), 54 | KEY `type_status_date` (`post_type`,`post_status`,`post_date`,`ID`), 55 | KEY `post_parent` (`post_parent`), 56 | KEY `post_author` (`post_author`), 57 | KEY `post_name` (`post_name`(191)) 58 | ) ENGINE=InnoDB AUTO_INCREMENT=289 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 59 | ``` 60 | 61 | 上面的数据库表,也是根据表生成的sql语句,当然注释是我加上去的。 62 | 63 | 可能看到这里很多朋友说我们现在只看到了表结构,而且又是你添加的注释,你这想怎么忽悠就怎么忽悠。既然这样,我们不妨一看数据库。 64 | 65 | ![我的博客第三章文章系统-数据库截图片段](http://acheng1314.cn/wp-content/uploads/2016/12/我的博客第三章文章系统-数据库截图片段.png) 66 | 67 | 嗯,上面的图需要放大后才看得清楚== 这个有点尴尬。 68 | 从上面的图中我们可以看到如下关键信息: 69 | 70 | | id | post_author | post_date | post_content | post_title | post_status | post_type | post_mime_type | 71 | | -- | -- | -- | -- | -- | -- | -- | -- | 72 | | 286 | 1 | 2016-11-22 18:51:37 | 这是文章内容 | Android-MVP架构 | publish | post | | 73 | | 277 | 1 | 2016-11-08 00:37:07 | | ssm应用七-访问列表-流程图 | inherit | attachment | image/png | 74 | | 23 | 1 | 2015-09-26 22:55:30 | | YKT主要界面示例--源码 | inherit | attachment | application/rar | 75 | 76 | 关键信息就和上面的类似了,为什么我挑选这三个: 77 | - 博客的系统,主要的就是文章存储和多媒体资源存储 78 | - 上面三个分别代表了文章、图片、rar压缩包 79 | - 上面这个缩略表,刚好提取了目前我们可能会用到区分的不同信息 80 | 81 | 没时间解释了,我们直接分析上面的数据库表中的字段。 82 | - 文章 83 | - post_content一般有内容 84 | - post_status 会有多种状态 85 | - post_type 指向文章 post 86 | - post_mime_type为空 87 | - 多媒体文件 88 | - post_content 为空 89 | - post_status一般是inherit 90 | - post_type 一般是 attachment 91 | - post_mime_type 一般为具体的文件类型,如: image/png、 application/rar 92 | 93 | 所以根据上面的一些信息,我们可以开始实现我们文章系统下面的文章列表接口了,首先按照老规矩实现Dao层: 94 | 95 | ``` 96 | import cn.acheng1314.domain.PostBean; 97 | import org.apache.ibatis.annotations.Param; 98 | import org.springframework.stereotype.Repository; 99 | 100 | import java.io.Serializable; 101 | import java.util.List; 102 | 103 | /** 104 | * Created by 程 on 2016/11/27. 105 | */ 106 | @Repository 107 | public interface PostDao extends Dao { 108 | 109 | @Override 110 | int add(PostBean postBean); 111 | 112 | @Override 113 | int del(PostBean postBean); 114 | 115 | @Override 116 | int update(PostBean postBean); 117 | 118 | @Override 119 | PostBean findOneById(Serializable Id); 120 | 121 | @Override 122 | List findAll(); 123 | 124 | 125 | List findAllNew(); 126 | 127 | /** 128 | * 分页查询 129 | * 130 | * @param offset 起始位置 131 | * @param limit 每页数量 132 | * @return 133 | */ 134 | List findAllPublish(@Param("offset") int offset, @Param("limit") int limit); 135 | 136 | /** 137 | * 获取总的条数 138 | */ 139 | int getAllCount(); 140 | 141 | List getAllPostDateCount(); 142 | } 143 | 144 | ``` 145 | 146 | 其实上面的接口我们可以看到和以前的差不多,毕竟数据库操作就是一些基本的增删改查。没道理的,我们必须接着实现mapper,mapper如下: 147 | 148 | ``` 149 | 150 | 152 | 153 | 167 | 168 | 182 | 183 | 189 | 190 | 198 | 199 | ``` 200 | 201 | 我们上面唯一需要注意的就是我们的文章查询的时候,必须指定`post_type`='post'和`post_status`='publish',这样我们首页展示的文章列表就是公开的文章。 202 | 203 | 每次按照套路来,大家都会知道我这边Dao层完成后,就应该进行Service层的操作,那么我们看下这里我们的Service层是怎么回事。 204 | 205 | ``` 206 | import cn.acheng1314.domain.DateCountBean; 207 | import cn.acheng1314.domain.PostBean; 208 | import cn.acheng1314.service.BaseService; 209 | 210 | import java.util.List; 211 | 212 | /** 213 | * Created by 程 on 2016/11/27. 214 | */ 215 | public interface PostService extends BaseService { 216 | @Override 217 | void add(PostBean postBean) throws Exception; 218 | 219 | @Override 220 | List findAll(int pageNum, int pageSize); 221 | 222 | List findAllPublish(int pageNum, int pageSize); 223 | 224 | /** 225 | * 获取总条数 226 | * @return 获取总条数 227 | */ 228 | int getAllCount(); 229 | 230 | /** 231 | * 获取热点文章 232 | * @return 233 | */ 234 | List findAllNew(); 235 | 236 | /** 237 | * 获取所有文章的日期归档 238 | * @return 返回归档信息 239 | */ 240 | List getAllPostDateCount(); 241 | } 242 | 243 | 244 | import cn.acheng1314.dao.PostDao; 245 | import cn.acheng1314.domain.DateCountBean; 246 | import cn.acheng1314.domain.PostBean; 247 | import org.springframework.beans.factory.annotation.Autowired; 248 | import org.springframework.stereotype.Service; 249 | 250 | import java.text.DateFormat; 251 | import java.text.SimpleDateFormat; 252 | import java.util.ArrayList; 253 | import java.util.List; 254 | import java.util.Locale; 255 | 256 | /** 257 | * Created by 程 on 2016/11/27. 258 | */ 259 | @Service("postService") 260 | public class PostServiceImpl implements PostService { 261 | 262 | @Autowired 263 | private PostDao dao; 264 | 265 | @Override 266 | public void add(PostBean postBean) throws Exception { 267 | 268 | } 269 | 270 | @Override 271 | public List findAll(int pageNum, int pageSize) { 272 | return null; 273 | } 274 | 275 | @Override 276 | public List findAllPublish(int pageNum, int pageSize) { 277 | //因为数据库内容是从第一条出的数据,所以我们查询的 起始位置 = 页码 * 条数 + 1; 278 | pageNum -= 1; 279 | return dao.findAllPublish(pageNum * pageSize + 1, pageSize); 280 | } 281 | 282 | @Override 283 | public int getAllCount() { 284 | return dao.getAllCount(); 285 | } 286 | 287 | @Override 288 | public List findAllNew() { 289 | return dao.findAllNew(); 290 | } 291 | 292 | @Override 293 | public List getAllPostDateCount() { 294 | /* 295 | * 这里存入的json数据为: 296 | * [ {"date": "2015-11-16", "idList": [ "75", "73"] } ] 297 | * 解释一下:日期归档本身应该是个下拉列表。下拉列表中的某个item包含了这个日期有:文章数量,文章ID 298 | */ 299 | List tmpList = new ArrayList<>(); //创建日期归档的数据集合 300 | if (null != dao.getAllPostDateCount() 301 | && !dao.getAllPostDateCount().isEmpty()) { //从dao层获取的文章日期和ID的集合不为空 302 | tmpList.addAll(dao.getAllPostDateCount()); //先将获取的数据存入缓存变量中,避免多次读取数据库 303 | List myDateCount = new ArrayList<>(); //创建一个日期归档的集合格式和上面所诉的json数据格式相同 304 | //也就是外层是一个集合,里面是多个对象 305 | for (PostBean tmpBean : tmpList) { //遍历获取文章信息数据 306 | DateCountBean dateCountBean = new DateCountBean(); //创建文章信息缓存的对象 307 | if (!myDateCount.isEmpty() && 308 | DateFormat.getDateInstance().format(tmpBean.getPostDate().getTime()).equals(myDateCount.get(myDateCount.size() - 1).getDate())) { 309 | //上一个日期和当前日期的相同,则只需存入ID 310 | myDateCount.get(myDateCount.size() - 1).getIdList().add(tmpBean.getId()); 311 | } else { //集合为或者上一条的日期和当前的日期不相同,添加一条数据 312 | //把文章缓存信息添加到集合中 313 | dateCountBean.setDate(DateFormat.getDateInstance().format(tmpBean.getPostDate().getTime())); //日期格式化,把date格式化为String,也就是2015-09-28 00:01:07 ==> 2015-09-28 314 | List idList = new ArrayList<>(); 315 | idList.add(tmpBean.getId()); 316 | dateCountBean.setIdList(idList); 317 | myDateCount.add(dateCountBean); 318 | } 319 | } 320 | return myDateCount; 321 | } else return null; 322 | } 323 | } 324 | 325 | ``` 326 | 327 | 在上面我这里在一个Service层的一个接口写这么多代码?对的,没错,我们强调的是Service层用来做数据驱动,那么我们需要在Service层完成一些基本数据的组织。所以说我们最后首页的数据如下: 328 | 329 | ``` 330 | { 331 | "code": 1, 332 | "msg": "success", 333 | "data": { 334 | "date": [ 335 | { 336 | "date": "2016-5-19", 337 | "idList": [ 338 | "192", 339 | "191" 340 | ] 341 | }, 342 | { 343 | "date": "2016-3-30", 344 | "idList": [ 345 | "187" 346 | ] 347 | } 348 | ], 349 | "posts": [ 350 | { 351 | "id": "282", 352 | "postDate": "Nov 16, 2016 12:51:13 AM", 353 | "postContent": "多角色控制思路整理\r\n关于多角色控制,起始用户角色按照用户职能分工,一般来说思路如下", 354 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)" 355 | }, 356 | { 357 | "id": "278", 358 | "postDate": "Nov 9, 2016 8:46:18 PM", 359 | "postContent": "其实分页列表也没什么,重点在于做出列表局部刷新,减少页面请求。\r\n\r\n我们先要新建一个页面用来显示列表,由于我们的后台网页结构基本已经固定,所以我们在后台主页那边设定一个访问入口,然后链接上我们的网页。这里我把左边的一个菜单改成了列表,具体效果如图:", 360 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(七)" 361 | } 362 | ], 363 | "newPosts": [ 364 | { 365 | "id": "282", 366 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)" 367 | }, 368 | { 369 | "id": "278", 370 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(七)" 371 | }, 372 | { 373 | "id": "192", 374 | "postTitle": "正如雨下" 375 | } 376 | ], 377 | "totalNum": 19, 378 | "pageNum": 1, 379 | "pageSize": 10 380 | } 381 | } 382 | ``` 383 | 384 | **由于后台数据存储的是富文本,所以我们能看到有很多网页标签。** 385 | 386 | 光是这样说明也是没有多少实际意义的,我们仍然需要晒一晒具体的Controller的方法的代码,如下: 387 | 388 | ``` 389 | @RequestMapping("/main") 390 | public ModelAndView frontMain() { 391 | ModelAndView view = new ModelAndView("frontMain"); 392 | view.addObject("homeJson", getHomeJson(null)); //把首页需要的json数据直接扔到view里面,在下面的js代码中可以看到如何使用 393 | return view; 394 | } 395 | 396 | @RequestMapping(value = "/home" 397 | , produces = "application/json; charset=utf-8") 398 | @ResponseBody 399 | public Object getHomeJson(User user) { 400 | if (null != user) { 401 | //埋点,AOP日志记录 402 | } 403 | HomeBean homeBean = new HomeBean(); //首页内容 404 | HomeBean.DataBean dataBean = new HomeBean.DataBean(); //首页下面的Data内容对象 405 | try { 406 | int toalNum; //总页码 407 | 408 | toalNum = postService.getAllCount(); //先把总条数赋值给总页数,作为缓存变量,减少下面算法的查找次数 409 | 410 | toalNum = toalNum % 10 > 0 ? toalNum / 10 + 1 : toalNum / 10; //在每页固定条数下能不能分页完成,有余则加一页码 411 | 412 | List postsData = postService.findAllPublish(1, 10); //首页下面的文章内容 413 | List newData = postService.findAllNew(); //首页下面的文章内容 414 | if (null == postsData || postsData.isEmpty()) { 415 | dataBean.setPosts(null); 416 | } else { 417 | dataBean.setPosts(postsData); //首页文章列表信息设定 418 | } 419 | if (null == newData || newData.isEmpty()) { 420 | dataBean.setNewPosts(null); 421 | } else { 422 | dataBean.setNewPosts(newData); //首页文章列表信息设定 423 | } 424 | List allPostDateCount = postService.getAllPostDateCount(); 425 | if (null != allPostDateCount && !allPostDateCount.isEmpty()) { 426 | dataBean.setDate(allPostDateCount); 427 | } else { 428 | dataBean.setDate(null); 429 | } 430 | dataBean.setPageNum(1); 431 | dataBean.setPageSize(10); 432 | dataBean.setTotalNum(toalNum); 433 | homeBean.setData(dataBean); 434 | homeBean.setCode(ResponseObj.OK); 435 | homeBean.setMsg(ResponseList.OK_STR); 436 | return new GsonUtils().toJson(homeBean); 437 | } catch (Exception e) { 438 | e.printStackTrace(); 439 | //查询失败 440 | homeBean.setCode(ResponseObj.FAILED); 441 | homeBean.setMsg(ResponseList.FAILED_STR); 442 | return new GsonUtils().toJson(homeBean); 443 | } 444 | } 445 | 446 | ``` 447 | 448 | 注意看我上面代码的Try-Catch处理,我这里基本目标是保证程序功能正常,不会因为我这边的异常而产生其他错误信息。 449 | 450 | 那我们都看到了首页上面的一些数据,那么我们现在是不是需要查看前端页面的完成呢?此处不必惊慌,前端页面的完成,我们是不会少的,而且这一期完成后的代码,我也一样会同步到github。 451 | 452 | 现在我们需要的是把前台页面列表加载出来并且实现局部刷新。so,我们需要先获取到前台页面,具体代码省略,我们展示核心代码: 453 | 454 | ``` 455 | 456 | var homeJsonStr = JSON.stringify(${homeJson}); 457 | var homeJsonObj = JSON.parse(homeJsonStr); 458 | var pageNum = homeJsonObj.data.pageNum; //获取当前页码 459 | var pageSize = homeJsonObj.data.pageSize; //获取页面长度 460 | var totalNum = homeJsonObj.data.totalNum; //获取总得页码 461 | if (homeJsonObj.code == 1) { //获取数据成功 462 | pagefn = doT.template($("#pagetmpl").html()); //初始化列表模板 463 | updateList(homeJsonObj.data.posts); //更新数据 464 | } else { 465 | alert("获取数据失败!请联系管理员"); 466 | } 467 | 468 | function updateList(data) { 469 | $("#pagetmpl").empty(); //清空模板数据 470 | $("#blog-table-list").html(pagefn(data)); //加入数据到模板 471 | } 472 | 473 | function goToNextPage() { 474 | pageNum = parseInt(pageNum) + 1; 475 | $.ajax({ 476 | type: "POST", 477 | url: '', 478 | data: {pageNum: pageNum, pageSize: pageSize}, 479 | dataType: 'json', //当这里指定为json的时候,获取到了数据后会自己解析的,只需要 返回值.字段名称 就能使用了 480 | cache: false, 481 | success: function (data) { 482 | if (data.code == 1) { 483 | updateList(data.data); 484 | pageNum = data.pageNum; 485 | $("#log-controller-now").html(pageNum); 486 | } 487 | } 488 | }); 489 | } 490 | 491 | function goToLastPage() { 492 | pageNum = parseInt(pageNum) - 1; 493 | $.ajax({ 494 | type: "POST", 495 | url: '', 496 | data: {pageNum: pageNum, pageSize: pageSize}, 497 | dataType: 'json', //当这里指定为json的时候,获取到了数据后会自己解析的,只需要 返回值.字段名称 就能使用了 498 | cache: false, 499 | success: function (data) { 500 | if (data.code == 1) { 501 | updateList(data.data); 502 | pageNum = data.pageNum; 503 | $("#log-controller-now").html(pageNum); 504 | } 505 | } 506 | }); 507 | } 508 | ``` 509 | 510 | 当然上面的代码,必须有jquery、doT.min.js和json2.js才能运行。 511 | 512 | 最后出来的整体效果,也就和上一章的截图类似,具体图片就不再截图了。稍后上传本章的代码到[github](https://github.com/pc859107393/SpringMvcMybatis) 513 | -------------------------------------------------------------------------------- /[java手把手教程][第二季]java后端博客系统文章系统——No3.md: -------------------------------------------------------------------------------- 1 | 又是新的一期了,这一期我们把前面文章于都相关的基本地方都完成吧。 2 | 3 | 每次新写一章都会有很多话说,到头来觉得这些话好像无关痛痒,毕竟我们做技术的只需要技术足够,除非不只做技术才会想更多。 4 | 5 | 说实话,最近经历了很多事情,只想说:时间才是最长的告白。 6 | 7 | 项目github地址:https://github.com/pc859107393/SpringMvcMybatis 8 | 9 | 我的简书首页是:http://www.jianshu.com/users/86b79c50cfb3/latest_articles 10 | 11 | 上一期是:[[手把手教程][第二季]java 后端博客系统文章系统——No2](https://gold.xitu.io/post/584dacd3a22b9d0058e22a17) 12 | 13 | ![行走的java全栈](http://acheng1314.cn/wp-content/uploads/2016/10/行走的java全栈群二维码.png) 14 | 15 | #### 工具 16 | - IDE为**idea16** 17 | - JDK环境为**1.8** 18 | - gradle构建,版本:2.14.1 19 | - Mysql版本为**5.5.27** 20 | - Tomcat版本为**7.0.52** 21 | - 流程图绘制(xmind) 22 | - 建模分析软件**PowerDesigner16.5** 23 | - 数据库工具MySQLWorkBench,版本:6.3.7build 24 | 25 | #### 本期目标 26 | 27 | - 文章阅读前端页面全部完成 28 | - 根据页面框架进行解耦 29 | - 页面附属信息 30 | - 文章信息 31 | 32 | #### 文章系统前端页面 33 | 34 | 文章系统作为我们博客系统中重要的一环,我们需要的不仅仅是文章系统,更多的是可以理解成一个自媒体平台,我们的核心价值通过这个体现出来了,才能把其他的东西做好。 35 | 36 | 上次我们的文章中可以看到前端页面的一些东西,主要是: 37 | - 文章列表(文章内容) 38 | - 文章分类导航 39 | - 标签聚合导航 40 | - 站点基本信息 41 | - 等··· 42 | 43 | 具体显示信息如图所示: 44 | 45 | ![我的博客第四章文章系统-首页结构](http://acheng1314.cn/wp-content/uploads/2016/12/我的博客第四章文章系统-首页结构.png) 46 | 47 | 从上面的截图中我们可以看到我们的页面大概结构,页面头、页面尾和页面中间的内容,那么出于便利考虑我们需要把头尾单独抽取出来存放,页面其他的内容我们需要根据需要处理。现在我们先不考虑那么多,我们只是基于程序合理建设的角度来说,我们需要把页面上面动态变化的信息都独立做成接口来供外部调用,然后一般不怎么变化的东西我们就直接固化到页面中,即是说: 48 | - 中间的列表我们采用分页加载,全部动态从接口获取 49 | - 上面的一些其他变化的信息我们从请求的时候就附加给它 50 | 51 | 所以,我们需要把前面的首页接口重写一下。 52 | 53 | 首先,我们给首页获取数据的接口打上过时的标记。 54 | ``` 55 | /** 56 | * 获取主页的json数据,按照道理讲这里应该根据页面结构拆分组合的 57 | * 58 | * @param user 用户信息 59 | * @return 返回首页json 60 | */ 61 | @RequestMapping(value = "/home" 62 | , produces = "application/json; charset=utf-8") 63 | @ResponseBody 64 | @Deprecated 65 | public Object getHomeJson(User user) { 66 | //此处代码省略 67 | } 68 | ``` 69 | 70 | 既然我们已经把首页的设置为过时,那么新的接口必须对照着做一个,那么我们需要怎么处理呢?按照前面的思路来讲,我们现在需要根据需求将我们页面信息拆分成多个接口,首先需要把左边我们圈出来的部分整合到一起,那么我们需要先把个人信息分类导航和标签聚合这几个独立出来,所以得我们直接上代码。 71 | 72 | ``` 73 | /** 74 | * 返回主页面 75 | * 76 | * @return 77 | */ 78 | @RequestMapping("/main") 79 | public ModelAndView frontMain(HttpServletRequest request) throws Exception { 80 | ModelAndView view = new ModelAndView("frontMain"); 81 | //把数据存入前端,避免前端再次发起网络请求 82 | view.addObject("framJson", getFramJson()); 83 | view.addObject("postsJson", findPublishPost(request, 1, 10)); 84 | return view; 85 | } 86 | 87 | /** 88 | * 页面框架的变化信息 89 | * 1、个人信息 90 | * 2、最新热点随机文章信息 91 | * 3、标签信息 92 | * 93 | * @return 94 | */ 95 | @RequestMapping(value = "/getFramJson" 96 | , produces = "application/json; charset=utf-8") 97 | @ResponseBody 98 | public Object getFramJson() { 99 | HomeBean homeBean = new HomeBean(); //首页内容 100 | HomeBean.DataBean dataBean = new HomeBean.DataBean(); //首页下面的Data内容对象 101 | try { 102 | int toalNum = postService.getAllCount(); //先把总条数赋值给总页数,作为缓存变量,减少下面算法的查找次数 103 | toalNum = toalNum % 10 > 0 ? toalNum / 10 + 1 : toalNum / 10; //在每页固定条数下能不能分页完成,有余则加一页码 104 | 105 | List newData = postService.findAllNew(); 106 | if (null == newData || newData.isEmpty()) { 107 | //页面上面推荐的文章信息不为空 108 | dataBean.setNewPosts(null); 109 | dataBean.setHotPosts(null); 110 | dataBean.setRandomPosts(null); 111 | } else { 112 | //首页文章列表信息设定 113 | dataBean.setNewPosts(newData); 114 | dataBean.setHotPosts(newData); 115 | dataBean.setRandomPosts(newData); 116 | } 117 | //日期归档 118 | List allPostDateCount = postService.getAllPostDateCount(); 119 | if (null != allPostDateCount && !allPostDateCount.isEmpty()) { 120 | dataBean.setDate(allPostDateCount); 121 | } else { 122 | dataBean.setDate(null); 123 | } 124 | //设置作者信息 125 | List> userMeta = userService.getUserMeta(1); 126 | dataBean.setAuthor(userMeta); 127 | 128 | homeBean.setData(dataBean); 129 | homeBean.setCode(ResponseObj.OK); 130 | homeBean.setMsg(ResponseList.OK_STR); 131 | return new GsonUtils().toJson(homeBean); 132 | } catch (Exception e) { 133 | e.printStackTrace(); 134 | //查询失败 135 | homeBean.setCode(ResponseObj.FAILED); 136 | homeBean.setMsg(ResponseList.FAILED_STR); 137 | return new GsonUtils().toJson(homeBean); 138 | } 139 | } 140 | ``` 141 | 142 | 说实话感觉上面没啥解释的,说白了就是将数据按照一定的结构组合起来,具体展示的json如何,我们可以直接在下面看: 143 | 144 | ``` 145 | { 146 | "msg" : "success", 147 | "data" : { 148 | "author" : [ 149 | { 150 | "meta_key" : "nickname", 151 | "meta_value" : "雨下一整夜" 152 | }, 153 | { 154 | "meta_key" : "description", 155 | "meta_value" : "我想在最好的时候遇见最美的你。那是最美。" 156 | }, 157 | { 158 | "meta_key" : "managenav-menuscolumnshidden", 159 | "meta_value" : "a:2:{i:0;s:11:\"css-classes\";i:1;s:11:\"description\";}" 160 | } 161 | ], 162 | "pageNum" : 0, 163 | "pageSize" : 0, 164 | "newPosts" : [ 165 | { 166 | "id" : "286", 167 | "postTitle" : "Android-MVP架构" 168 | }, 169 | { 170 | "id" : "282", 171 | "postTitle" : "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)" 172 | } 173 | ], 174 | "date" : [ 175 | { 176 | "date" : "2015-11-13", 177 | "idList" : [ 178 | "71", 179 | "69", 180 | "67" 181 | ] 182 | }, 183 | { 184 | "date" : "2015-10-15", 185 | "idList" : [ 186 | "48", 187 | "46" 188 | ] 189 | } 190 | ], 191 | "randomPosts" : [ 192 | { 193 | "id" : "286", 194 | "postTitle" : "Android-MVP架构" 195 | }, 196 | { 197 | "id" : "282", 198 | "postTitle" : "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)" 199 | } 200 | ], 201 | "hotPosts" : [ 202 | { 203 | "id" : "286", 204 | "postTitle" : "Android-MVP架构" 205 | }, 206 | { 207 | "id" : "282", 208 | "postTitle" : "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)" 209 | } 210 | ], 211 | "totalNum" : 0 212 | }, 213 | "code" : 1 214 | } 215 | ``` 216 | 217 | 通过上面我们组织的json,我们可以很清晰明了的看到我们的数据结构是根据页面结构来组合的,所以我们需要数据的时候对应着取值就可以解决问题。 218 | 219 | 说了这么多后端的接口,我们现在需要拿数据去前台展示,所以我们需要在前端获取数据。前台数据展示还是使用doT.min.js来展示,代码如下: 220 | 221 | ``` 222 | 223 | var framJsonStr = JSON.stringify(${framJson}); 224 | var framJsonObj = JSON.parse(framJsonStr); 225 | var postsJsonStr = JSON.stringify(${postsJson}); 226 | var postsJsonObj = JSON.parse(postsJsonStr); 227 | var pageNum = postsJsonObj.pageNum; 228 | var pageSize = postsJsonObj.pageSize; 229 | var totalNum = postsJsonObj.totalNum; 230 | var authorDes = "

" + framJsonObj.data.author[0].meta_value + "
" + framJsonObj.data.author[1].meta_value + "

"; 231 | document.getElementById("authorDescription").innerHTML = authorDes; 232 | 233 | if (framJsonObj.code == 1) { 234 | pagefn = doT.template($("#listHot").html()); //初始化列表模板 235 | updateHotList(framJsonObj.data.hotPosts); //更新数据 236 | updateNewList(framJsonObj.data.newPosts); //更新数据 237 | updateRandList(framJsonObj.data.randomPosts); //更新数据 238 | } 239 | 240 | function updateHotList(data) { 241 | $("#listHot").empty(); //清空模板数据 242 | $("#hotList").html(pagefn(data)); //加入数据到模板 243 | } 244 | 245 | function updateNewList(data) { 246 | $("#listNew").empty(); //清空模板数据 247 | $("#newList").html(pagefn(data)); //加入数据到模板 248 | } 249 | 250 | function updateRandList(data) { 251 | $("#listRand").empty(); //清空模板数据 252 | $("#randList").html(pagefn(data)); //加入数据到模板 253 | } 254 | 255 | //开始加载列表数据 256 | if (postsJsonObj.code == 1) { 257 | pagefn = doT.template($("#pagetmpl").html()); //初始化列表模板 258 | updateList(postsJsonObj.data); //更新数据 259 | } else { 260 | alert("获取数据失败!请联系管理员"); 261 | } 262 | 263 | function updateList(data) { 264 | $("#pagetmpl").empty(); //清空模板数据 265 | $("#blog-table-list").html(pagefn(data)); //加入数据到模板 266 | } 267 | function goToNextPage() { 268 | pageNum = parseInt(pageNum) + 1; 269 | $.ajax({ 270 | type: "POST", 271 | url: '', 272 | data: {pageNum: pageNum, pageSize: pageSize}, 273 | dataType: 'json', //当这里指定为json的时候,获取到了数据后会自己解析的,只需要 返回值.字段名称 就能使用了 274 | cache: false, 275 | success: function (data) { 276 | if (data.code == 1) { 277 | updateList(data.data); 278 | pageNum = data.pageNum; 279 | $("#log-controller-now").html(pageNum); 280 | } 281 | } 282 | }); 283 | } 284 | 285 | function goToLastPage() { 286 | pageNum = parseInt(pageNum) - 1; 287 | $.ajax({ 288 | type: "POST", 289 | url: '', 290 | data: {pageNum: pageNum, pageSize: pageSize}, 291 | dataType: 'json', //当这里指定为json的时候,获取到了数据后会自己解析的,只需要 返回值.字段名称 就能使用了 292 | cache: false, 293 | success: function (data) { 294 | if (data.code == 1) { 295 | updateList(data.data); 296 | pageNum = data.pageNum; 297 | $("#log-controller-now").html(pageNum); 298 | } 299 | } 300 | }); 301 | } 302 | 303 | ``` 304 | 305 | 上面我们可以看到pagefn用了几次,这个是doT的关键词,意思是设置模板。 306 | 307 | ``` 308 | 309 |
310 | 330 |
331 | 332 | 333 |
334 |
335 |
    336 | 346 |
347 |
348 |
349 |
    350 | 360 |
361 |
362 |
363 |
    364 | 374 |
375 |
376 |
Ï 377 | ``` 378 | 379 | 其實在上面的代碼中我們可以看到doT模板和其他的都差不多,無非就是按照固定的格式組裝数据,反正就是网页怎么写的,然后把格式套上,然后按照格式输出就行了。 380 | 381 | 做到这里后,我们就能看到做出的结果是什么样子的了。效果图暂时不上,大家后面自行下载项目运行就知道了。 382 | 383 | 既然这里做了,那么我们势必要做一下项目的文章详情。文章详情我们应该怎么办呢? 我们需要**通过关键数据去查找到具体的文章信息**。 384 | 385 | 我们可以看到上面的json数据中包含一个id的字段,然后我们对照数据库会看到id和数据库的id也是对应的。所以我们需要用接口实现通过ID查找数据库对应的文章信息。思路有了,那么代码实现就是很容易的,直接代码如下: 386 | ``` 387 | /** 388 | * RESTful风格的文章页面 389 | * @RequestMapping(path = "/post/{postId}", method = RequestMethod.GET) 390 | * 通过上面的语句配置访问路径/post/后面指定是文档ID,在下面的方法参数中配置注解@PathVariable可以自动赋值,然后获取数据。 391 | * @param postId 文章ID 392 | * @return 返回文章页面 393 | */ 394 | @RequestMapping(path = "/post/{postId}", method = RequestMethod.GET) 395 | public ModelAndView getPostView(@PathVariable int postId) { 396 | ModelAndView resultView = new ModelAndView("frontPost"); 397 | resultView.addObject("framJson", getFramJson()); 398 | resultView.addObject("postJson", getPostById(postId)); 399 | return resultView; 400 | } 401 | 402 | 403 | /** 404 | * 根据文章ID获取文章内容 405 | * 406 | * @param postId 文章ID 407 | * @return 返回文章ID对应的文章内容 408 | */ 409 | @RequestMapping(value = "/getPost" 410 | , produces = "application/json; charset=utf-8") 411 | @ResponseBody 412 | public Object getPostById(int postId) { 413 | ResponseObj responseObj = new ResponseObj<>(); 414 | try { 415 | PostBean postBean = postService.findPostById(postId); 416 | if (null == postBean) { 417 | responseObj.setCode(ResponseObj.EMPUTY); 418 | responseObj.setMsg(ResponseObj.EMPUTY_STR); 419 | } else { 420 | responseObj.setCode(ResponseObj.OK); 421 | responseObj.setMsg(ResponseObj.OK_STR); 422 | responseObj.setData(postBean); 423 | } 424 | return new GsonUtils().toJson(responseObj); 425 | } catch (Exception e) { 426 | e.printStackTrace(); 427 | responseObj.setCode(ResponseObj.FAILED); 428 | responseObj.setMsg(ResponseObj.FAILED_STR); 429 | return new GsonUtils().toJson(responseObj); 430 | } 431 | } 432 | 433 | 434 | ``` 435 | 436 | 上面的代码一个是RESTful风格请求地址的文章页面,一个是api接口访问地址。下面分别是Service和Dao层的代码。 437 | 438 | ``` 439 | /** 440 | *这是Service 441 | */ 442 | @Override 443 | public PostBean findPostById(Serializable postId) { 444 | return dao.findOneById(postId); 445 | } 446 | 447 | /** 448 | *这是Dao 449 | */ 450 | @Override 451 | PostBean findOneById(Serializable postId); 452 | 453 | 454 | 465 | ``` 466 | 467 | 最后页面的样子如下,页面展示代码也就是获取到内容直接输出,具体代码就不贴了,影响文章篇幅。大家可以直接到我的GitHub上面下载。 468 | 469 | ![我的博客第四章文章系统-文章页面](http://acheng1314.cn/wp-content/uploads/2016/12/我的博客第四章文章系统-文章页面.png) 470 | 471 | 到目前为止,我们前端的页面基本完成后面是需要把页面头和页面尾独立出来,然后我们这种部分方便定制。 472 | 473 | 那么目前前端页面相关的东西基本完成,这一期也结束了,后面我们就是配置系统后端了。加油,有兴趣额的一起来吧。 -------------------------------------------------------------------------------- /[java手把手教程][第二季]java后端博客系统文章系统——No4.md: -------------------------------------------------------------------------------- 1 | 转眼间第二季来到了第五章,也是我们博客系统的第四章。前段时间因为个人私事较多,项目停更了两期,但是这都不是问题,我们继续接着走下去。毕竟承诺的事情就得完成。 2 | 3 | 这一期我们的目标是完成后端博客系统的博客发布功能。 4 | 5 | 按照我们前面的设定,我们的后端博客系统需要完成最简单的博文发布,我们也得有后台管理界面,同时需要将用户权限这些都附带上,但是由于时间关系,我们后端默认账户就是管理员吧,毕竟这一期的重点是实现博客的发布。 6 | 7 | --- 8 | 9 | > 博文发布系统 10 | 11 | 我们需要发布博文,那么后端必不可少的是登录和发布系统,至于其他的我们可以先缓一缓,毕竟我也没想好后端页面怎么设计,嘿嘿。 12 | 13 | 前面我看了一下,确实是完美兼容WordPress还是有很多难度,毕竟很多技术细节我们并不知道,不过,至少说目前我们已经兼容了博客文章,剩下的只需要一点点的适配就能大概完成任务。 14 | 15 | 不多说了,我们先完成后端登录功能。 16 | 17 | #### 后端登录 18 | 19 | 后端登录,我们不可能说一味的兼容WordPress,还有一些技术上面的设计理念可能也不是那么那啥,所以我们需要拿出一些自己的玩意。首先还是老规矩,从Dao→Service→Controller。 20 | 21 | - Dao按照老规矩就是对数据库的操作,所以我们只需要写上接口和mapper就行了。 22 | - Service层还是一样进行单元数据操作。 23 | - Controller是web应用的入口地点。 24 | 有了上面的这些我们只需要进行一个登录验证,也就是前面说过的密码规则验证,不过具体代码如下: 25 | 26 | ```java 27 | @RequestMapping(value = "/login" 28 | , produces = {APPLICATION_JSON_UTF8_VALUE} 29 | , method = RequestMethod.POST) 30 | @ApiOperation( 31 | value = "用户登录" 32 | , notes = "用户登录的接口,输入用户名和密码进行登录" 33 | , response = User.class) 34 | @ResponseBody 35 | public Object userLogin(HttpServletRequest request, 36 | @ApiParam(value = "用户名不能为空,否则不允许登录" 37 | , required = true) @RequestParam("userLogin") String userLogin, 38 | @ApiParam(value = "用户密码不能为空且必须为16位小写MD5,否则不允许登录" 39 | , required = true) @RequestParam("userPass") String userPass) { 40 | ResponseObj responseObj = new ResponseObj<>(); 41 | User user; 42 | if (PublicUtil.isJsonRequest(request)) { //确认是否是post的json提交 43 | try { 44 | user = new GsonUtils().jsonRequest2Bean(request.getInputStream(), User.class); //转换为指定类型的对象 45 | userLogin = user.getUserLogin(); 46 | userPass = user.getUserPass(); 47 | } catch (IOException e) { 48 | e.printStackTrace(); 49 | } 50 | } 51 | 52 | if (StringUtils.isEmpty(userLogin) || StringUtils.isEmpty(userPass)) { 53 | responseObj.setCode(ResponseObj.FAILED); 54 | responseObj.setMsg("用户名和密码不能为空!请检查!"); 55 | return new GsonUtils().toJson(responseObj); 56 | } 57 | 58 | user = userService.findOneById(userLogin); 59 | 60 | if (null == user) { 61 | responseObj.setCode(ResponseObj.EMPUTY); 62 | responseObj.setMsg("用户不存在!请检查!"); 63 | return new GsonUtils().toJson(responseObj); 64 | } 65 | 66 | userPass = userPass.toLowerCase(); //将大写md5转换为小写md5 67 | 68 | if (userPass.length() > 16 && userPass.length() == 32) { //32位小写转换为16位小写 69 | userPass = userPass.substring(8, 24).toLowerCase(); 70 | } else if (userPass.length() > 32) { 71 | responseObj.setCode(ResponseObj.FAILED); 72 | responseObj.setMsg("密码不规范!请检查!"); 73 | return new GsonUtils().toJson(responseObj); 74 | } 75 | 76 | String encryptPassword = EncryptUtils.encryptPassword(userPass, user.getUserActivationKey()); 77 | 78 | if (!encryptPassword.equals(user.getUserPass())) { 79 | responseObj.setCode(ResponseObj.FAILED); 80 | responseObj.setMsg("用户名和密码不匹配!请检查!"); 81 | return new GsonUtils().toJson(responseObj); 82 | } 83 | 84 | user.setUserPass(request.getSession().getId()); //将sessionId放入用户信息中 85 | user.setUserActivationKey(""); //将用户注册的salt清空 86 | user.setUserUrl("/user/endSupport/index"); 87 | 88 | responseObj.setData(user); 89 | responseObj.setCode(ResponseObj.OK); 90 | responseObj.setMsg("登录成功"); 91 | 92 | return new GsonUtils().toJson(responseObj); 93 | } 94 | ``` 95 | 虽然说很多东西我们在前端或者是客户端已经做了限制,但是为了防止别人搞事情我们还是需要这样做才行。 96 | 97 | #### Spring-Fox,Api测试页面 98 | 99 | 什么是Spring-Fox呢?Springfox的前身是swagger-springmvc,是一个开源的API doc框架,可以将我们的Controller的方法以文档的形式展现。 100 | 101 | 为什么我们要大费周章的做这些呢? 102 | - 它可以帮助我们归类web访问入口 103 | - 它可以整理接口 104 | - 它可以··· 105 | 106 | 确实语言描述是我的弱点,不过呢,我这个理工直男癌就需要直截了当的说出来,没时间解释,直接上图。 107 | 108 | ![我的博客第五章-API_doc框架](http://acheng1314.cn/wp-content/uploads/2017/01/我的博客第五章-API_doc框架.png) 109 | 110 | 正如上面的截图所示,我们首先应该找到对应的spring-fox的说明文档,然后仔细一看网上分为两个版本,一个是开源中国的引入说明,一个是[Spring-Fox官方的使用说明](http://springfox.github.io/springfox/docs/current/),那么肯定选择[官方](http://springfox.github.io/springfox/docs/current/)的。 111 | 112 | 按照官方文档,我们简单总结一下: 113 | - 版本选择(Release或者Snapshot,推荐使用Release) 114 | - 依赖引入(maven或者gradle) 115 | - swagger设置 116 | - 重要细节(Spring-Fox官方文档中没有指明!!!) 117 | 118 | 按照官方文档说明的是,他们的demo是在SpringBoot下面实现的,现在我们需要单一的拆分出来,可以看成我们的项目就是Spring-Mvc,所以一些细节需要改变,当然当中一个很重要的细节官方文档也是没有指明,所以看官们且看我细细道来。 119 | 120 | > 引入依赖资源 121 | 122 | 首先我们引入引来资源,通读全文最基本的依赖是:springfox-swagger、springfox-swagger-ui,所以我们直接老规矩,在gradle的配置文件中引入依赖: 123 | 124 | ``` 125 | compile "io.springfox:springfox-swagger2:2.6.1" 126 | compile 'io.springfox:springfox-swagger-ui:2.6.1' 127 | ``` 128 | 129 | 在引入上面的基本依赖后,我们查看他们关联的依赖可以发现这些依赖里面还有引入jackson,这个时候我们可以选择提升我们的Jackson或者不管他们也行,不过我还是吧Jackson的版本提升了: 130 | 131 | ``` 132 | compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.8.4' 133 | compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.8.4' 134 | compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.8.4' 135 | ``` 136 | > swagger设置 137 | 138 | 根据官方文档我们可以看到有一个swagger设置需要先引入后,才能让我们设定的东西生效,所以我们先引入设置: 139 | 140 | ```java 141 | import org.springframework.context.annotation.Bean; 142 | import org.springframework.context.annotation.ComponentScan; 143 | import org.springframework.context.annotation.Configuration; 144 | import org.springframework.context.annotation.Import; 145 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 146 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 147 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 148 | import springfox.documentation.builders.ApiInfoBuilder; 149 | import springfox.documentation.builders.PathSelectors; 150 | import springfox.documentation.builders.RequestHandlerSelectors; 151 | import springfox.documentation.service.ApiInfo; 152 | import springfox.documentation.spi.DocumentationType; 153 | import springfox.documentation.spring.web.plugins.Docket; 154 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 155 | 156 | /** 157 | * Created by mac on 2017/1/8. 158 | */ 159 | @Configuration //说明这个是spring的设置 160 | @EnableWebMvc //不是SpringBoot需要引入这个 161 | @EnableSwagger2 //开启Swagger2 162 | @ComponentScan("cn.acheng1314.controller") //指定被扫描Controller的位置 163 | public class Swagger2SpringMvc extends WebMvcConfigurerAdapter { 164 | 165 | @Bean 166 | public Docket createRestApi() { 167 | return new Docket(DocumentationType.SWAGGER_2) //Docket,Springfox的私有API设置初始化为Swagger2 168 | .select() 169 | // .apis(RequestHandlerSelectors.basePackage("cn.acheng1314.controller")) //此处设置扫描包和上面设置的扫描包一样效果 170 | .paths(PathSelectors.any()) 171 | .build() 172 | .apiInfo(new ApiInfoBuilder() //设置API文档的主体说明 173 | .title("博客Apis") 174 | .description("雨下一整夜的博客APIs") 175 | .version("1.01") 176 | .termsOfServiceUrl("http://acheng1314.cn/") 177 | .build()) 178 | // .pathMapping("/") 179 | // .genericModelSubstitutes(ResponseEntity.class) 180 | // .alternateTypeRules( 181 | // newRule(typeResolver.resolve(DeferredResult.class, 182 | // typeResolver.resolve(ResponseEntity.class, WildcardType.class)), 183 | // typeResolver.resolve(WildcardType.class))) 184 | ; 185 | } 186 | 187 | } 188 | 189 | ``` 190 | 191 | 设置完成上面的东西后,我们需要干什么呢? 上面我们很明显的看到我们 **@configuration是一个spring框架的注解** ,顾名思义肯定就应该是一个spring的设置。同时我们可以在idea的编辑器中看到类名有一层淡黄色的标记,然后选中类名按下代码提示(Alt+Enter)会有提示告诉我们这个设置没有使用,然后自动完成后会给我们自动添加到Spring的ApplicationContext中作为CodeContext使用。 192 | 193 | 当然,上面的是懒人做事这样做的后果会是导致apiInfo的设置不能生效。 194 | 195 | 那么正常一点的应该是怎么做呢?按照Spring的思想来说,我们需要在Spring的设置文件中直接引入bean。所以我们在spring-web.xml中插入对应的bean,如下: 196 | 197 | ```xml 198 | 199 | 200 | ``` 201 | 通过这样的在spring的配置文件中设置后,我们感觉应该是能用的,所以我们可以先跑起来看看。 202 | 203 | > 重要细节,我有特殊的装X技巧 204 | 205 | 按照官方文档我们完全设定了后,我们可以看到就算我们在代码中引入了配置后,一样的在里面不能看到网页的接口列表(只看得到上面的标题栏,下面的是空白),然后我们仔细的查看网页的请求会发现 206 | ``` 207 | http://localhost:8080/swagger-resources/configuration/ui 208 | ``` 209 | 这个请求是404。说实话这个错误困扰了我很久,同时这个问题前面我处理的时候还是有一系列的综合问题,后面整个工程师重建后完成的。 210 | 211 | 但是现在这个问题简单直接找到问题所在了,那就是在我们spring的设置中,关于web的设置我们都是在spring-web.xml中完成的,同时里面的东西我们需要改动一下才能适应现在的需要,如下: 212 | 213 | ``` 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | ``` 227 | 也就是说,我们除了要在Spring的配置文件中引入bean来初始化swagger相关的东西以外,我们还需要在web扫描那里添加springfox的扫描。所以我们spring-fox的设置相关的完成了。 228 | 229 | > Spring-Fox的使用 230 | 231 | 从前面的学习中我们可以明白我们所有的网络请求都是在controller中来实现的,所以我们这里需要通过对controller做适当的修改才能实现SpringFox的使用。具体的直接仍代码上来,大家详细的看看就行了,不需要什么深入钻研。 232 | 233 | ```java 234 | 235 | import cn.acheng1314.domain.*; 236 | import cn.acheng1314.domain.Response.HomeBean; 237 | import cn.acheng1314.service.postService.PostService; 238 | import cn.acheng1314.service.userService.UserService; 239 | import cn.acheng1314.utils.GsonUtils; 240 | import cn.acheng1314.utils.PublicUtil; 241 | import io.swagger.annotations.Api; 242 | import io.swagger.annotations.ApiOperation; 243 | import io.swagger.annotations.ApiParam; 244 | import org.springframework.beans.factory.annotation.Autowired; 245 | import org.springframework.stereotype.Controller; 246 | import org.springframework.web.bind.annotation.*; 247 | import org.springframework.web.servlet.ModelAndView; 248 | 249 | import javax.servlet.http.HttpServletRequest; 250 | import java.util.HashMap; 251 | import java.util.List; 252 | 253 | import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE; 254 | 255 | /** 256 | * 前端页面的控制器,博客系统前端页面相对较少,所以都扔在这里了 257 | * Created by 程 on 2016/11/25. 258 | */ 259 | @Controller 260 | @RequestMapping("/front") 261 | @Api(value = "/front", description = "前台页面") 262 | public class FrontWebController { 263 | 264 | @Autowired 265 | private PostService postService; 266 | @Autowired 267 | private UserService userService; 268 | 269 | /** 270 | * 返回主页面 271 | * 272 | * @return 273 | */ 274 | @RequestMapping(value = "/main", method = RequestMethod.GET) 275 | @ApiOperation( 276 | value = "打开首页界面" 277 | , notes = "首页web界面,js模板加载网页数据") 278 | public ModelAndView frontMain(HttpServletRequest request) throws Exception { 279 | ModelAndView view = new ModelAndView("frontMain"); 280 | view.addObject("framJson", getFramJson()); 281 | view.addObject("postsJson", findPublishPost(request, 1, 10)); 282 | return view; 283 | } 284 | 285 | /** 286 | * 获取文章分页列表 287 | * 288 | * @param request 用户请求 289 | * @param pageNum 当前页码 290 | * @param pageSize 每一页的长度 291 | * @return 292 | * @throws Exception 293 | */ 294 | @RequestMapping(value = "/findPublishPost" 295 | , produces = {APPLICATION_JSON_UTF8_VALUE} 296 | , method = {RequestMethod.GET, RequestMethod.POST}) 297 | @ResponseBody 298 | @ApiOperation( //接口描述 299 | value = "获取文章分页列表" //功能简介 300 | , notes = "返回文章列表,分页加载" //接口功能说明 301 | , response = PostBean.class, //返回数据的值说明 302 | responseContainer = "List") //返回数据类型说明 303 | public Object findPublishPost(HttpServletRequest request, 304 | @ApiParam(value = "当前页码,默认不能为空,否则为1", 305 | required = true, //参数是否必传 306 | defaultValue = "1" //参数默认值为1 307 | // ,allowableValues = "available,pending,sold" //暂时未知,待查阅文章 308 | // ,allowMultiple = true //是否允许allowMultiple类型的参数 309 | ) @RequestParam("pageNum") 310 | int pageNum, 311 | @ApiParam(value = "每一页的长度,默认不能为空,否则列表条目数量为10", 312 | required = true, //参数是否必传 313 | defaultValue = "10" //参数默认值为1 314 | // ,allowableValues = "available,pending,sold" //暂时未知,待查阅文章 315 | // ,allowMultiple = true //是否允许allowMultiple类型的参数 316 | ) @RequestParam("pageSize") 317 | int pageSize) throws Exception { 318 | PageSplit page; 319 | ResponseList list = new ResponseList<>(); 320 | 321 | if (PublicUtil.isJsonRequest(request)) { //确认是否是post的json提交 322 | page = new GsonUtils().jsonRequest2Bean(request.getInputStream(), PageSplit.class); //转换为指定类型的对象 323 | pageNum = page.getPageNum(); 324 | pageSize = page.getPageSize(); 325 | } 326 | if (pageNum <= 0) { 327 | pageNum = 1; 328 | } 329 | if (pageSize == 0) { 330 | pageSize = 10; 331 | } 332 | 333 | try { 334 | int toalNum; //总页码 335 | toalNum = postService.getAllCount(); //先把总条数赋值给总页数,作为缓存变量,减少下面算法的查找次数 336 | 337 | toalNum = toalNum % pageSize > 0 ? toalNum / pageSize + 1 : toalNum / pageSize; //在每页固定条数下能不能分页完成,有余则加一页码 338 | 339 | List tmp = postService.findAllPublish(pageNum, pageSize); 340 | if (null == tmp || tmp.size() == 0) { 341 | list.setCode(ResponseList.EMPUTY); 342 | list.setMsg(ResponseList.EMPUTY_STR); 343 | return new GsonUtils().toJson(list); 344 | } 345 | list.setCode(ResponseList.OK); 346 | list.setMsg(ResponseList.OK_STR); 347 | list.setPageNum(pageNum); 348 | list.setTotalNum(toalNum); 349 | list.setPageSize(pageSize); 350 | list.setData(tmp); 351 | return new GsonUtils().toJson(list); 352 | } catch (Exception e) { 353 | e.printStackTrace(); 354 | //查询失败 355 | list.setCode(ResponseObj.FAILED); 356 | list.setMsg(ResponseList.FAILED_STR); 357 | return new GsonUtils().toJson(list); 358 | } 359 | 360 | } 361 | 362 | /** 363 | * 查找最近的文章 364 | * 365 | * @return 366 | */ 367 | @RequestMapping(value = "/findNewPost" 368 | , produces = {APPLICATION_JSON_UTF8_VALUE} 369 | , method = {RequestMethod.GET, RequestMethod.POST}) 370 | @ApiOperation( 371 | value = "获取最近文章" 372 | , notes = "获取最近文章的json,具体字段请参照输出的json数据" 373 | , response = PostBean.class) 374 | @ResponseBody 375 | public Object findNewPost() { 376 | ResponseObj responseObj = new ResponseObj<>(); 377 | try { 378 | List allNew = postService.findAllNew(); 379 | if (null == allNew || allNew.isEmpty()) { 380 | responseObj.setCode(ResponseObj.EMPUTY); 381 | responseObj.setMsg(ResponseObj.EMPUTY_STR); 382 | } else { 383 | responseObj.setCode(ResponseObj.OK); 384 | responseObj.setMsg(ResponseObj.OK_STR); 385 | responseObj.setData(allNew); 386 | } 387 | return new GsonUtils().toJson(responseObj); 388 | } catch (Exception e) { 389 | e.printStackTrace(); 390 | responseObj.setCode(ResponseObj.FAILED); 391 | responseObj.setMsg(ResponseObj.FAILED_STR); 392 | return new GsonUtils().toJson(responseObj); 393 | } 394 | } 395 | 396 | /** 397 | * 获取热点文章信息 398 | * 399 | * @return 400 | */ 401 | @RequestMapping(value = "/findHotPost" 402 | , produces = {APPLICATION_JSON_UTF8_VALUE} 403 | , method = {RequestMethod.GET, RequestMethod.POST}) 404 | @ApiOperation( 405 | value = "获取最热文章" 406 | , notes = "获取最热文章的json,具体字段请参照输出的json数据" 407 | , response = PostBean.class) 408 | @ResponseBody 409 | public Object findHotPost() { 410 | return findNewPost(); 411 | } 412 | 413 | /** 414 | * 获取随机文章信息 415 | * 416 | * @return 返回json 417 | */ 418 | @RequestMapping(value = "/findRandomPost" 419 | , produces = {APPLICATION_JSON_UTF8_VALUE} 420 | , method = {RequestMethod.GET, RequestMethod.POST}) 421 | @ApiOperation( 422 | value = "随机获取文章" 423 | , notes = "随机获取文章的json,具体字段请参照输出的json数据" 424 | , response = PostBean.class) 425 | @ResponseBody 426 | public Object findRandomPost() { 427 | return findNewPost(); 428 | } 429 | 430 | /** 431 | * 获取主页的json数据,按照道理讲这里应该根据页面结构拆分组合的 432 | * 433 | * @param user 用户信息 434 | * @return 返回首页json 435 | */ 436 | @RequestMapping(value = "/home" 437 | , produces = {APPLICATION_JSON_UTF8_VALUE} 438 | , method = {RequestMethod.GET, RequestMethod.POST}) 439 | @ResponseBody 440 | @Deprecated 441 | public Object getHomeJson(User user) { 442 | if (null != user) { 443 | //埋点,AOP日志记录 444 | } 445 | HomeBean homeBean = new HomeBean(); //首页内容 446 | HomeBean.DataBean dataBean = new HomeBean.DataBean(); //首页下面的Data内容对象 447 | try { 448 | int toalNum; //总页码 449 | 450 | toalNum = postService.getAllCount(); //先把总条数赋值给总页数,作为缓存变量,减少下面算法的查找次数 451 | 452 | toalNum = toalNum % 10 > 0 ? toalNum / 10 + 1 : toalNum / 10; //在每页固定条数下能不能分页完成,有余则加一页码 453 | 454 | List postsData = postService.findAllPublish(1, 10); //首页下面的文章内容 455 | List newData = postService.findAllNew(); //首页下面的文章内容 456 | if (null == postsData || postsData.isEmpty()) { 457 | dataBean.setPosts(null); 458 | } else { 459 | dataBean.setPosts(postsData); //首页文章列表信息设定 460 | } 461 | if (null == newData || newData.isEmpty()) { 462 | dataBean.setNewPosts(null); 463 | dataBean.setHotPosts(null); 464 | dataBean.setRandomPosts(null); 465 | } else { 466 | dataBean.setNewPosts(newData); //首页文章列表信息设定 467 | dataBean.setHotPosts(newData); 468 | dataBean.setRandomPosts(newData); 469 | } 470 | List allPostDateCount = postService.getAllPostDateCount(); 471 | if (null != allPostDateCount && !allPostDateCount.isEmpty()) { 472 | dataBean.setDate(allPostDateCount); 473 | } else { 474 | dataBean.setDate(null); 475 | } 476 | //设置作者信息 477 | List> userMeta = userService.getUserMeta(1); 478 | dataBean.setAuthor(userMeta); 479 | 480 | dataBean.setPageNum(1); 481 | dataBean.setPageSize(10); 482 | dataBean.setTotalNum(toalNum); 483 | homeBean.setData(dataBean); 484 | homeBean.setCode(ResponseObj.OK); 485 | homeBean.setMsg(ResponseList.OK_STR); 486 | return new GsonUtils().toJson(homeBean); 487 | } catch (Exception e) { 488 | e.printStackTrace(); 489 | //查询失败 490 | homeBean.setCode(ResponseObj.FAILED); 491 | homeBean.setMsg(ResponseList.FAILED_STR); 492 | return new GsonUtils().toJson(homeBean); 493 | } 494 | } 495 | 496 | /** 497 | * 页面框架的变化信息 498 | * 1、个人信息 499 | * 2、最新热点随机文章信息 500 | * 3、标签信息 501 | * 502 | * @return 503 | */ 504 | @RequestMapping(value = "/getFramJson" 505 | , produces = {APPLICATION_JSON_UTF8_VALUE} 506 | , method = {RequestMethod.GET, RequestMethod.POST}) 507 | @ApiOperation( 508 | value = "获取主题框架的json" 509 | , notes = "整个页面的主体框架的json数据") 510 | @ResponseBody 511 | public Object getFramJson() { 512 | HomeBean homeBean = new HomeBean(); //首页内容 513 | HomeBean.DataBean dataBean = new HomeBean.DataBean(); //首页下面的Data内容对象 514 | try { 515 | List newData = postService.findAllNew(); 516 | if (null == newData || newData.isEmpty()) { 517 | //页面上面推荐的文章信息不为空 518 | dataBean.setNewPosts(null); 519 | dataBean.setHotPosts(null); 520 | dataBean.setRandomPosts(null); 521 | } else { 522 | //首页文章列表信息设定 523 | dataBean.setNewPosts(newData); 524 | dataBean.setHotPosts(newData); 525 | dataBean.setRandomPosts(newData); 526 | } 527 | 528 | //日期归档 529 | List allPostDateCount = postService.getAllPostDateCount(); 530 | if (null != allPostDateCount && !allPostDateCount.isEmpty()) { 531 | dataBean.setDate(allPostDateCount); 532 | } else { 533 | dataBean.setDate(null); 534 | } 535 | //设置作者信息 536 | List> userMeta = userService.getUserMeta(1); 537 | dataBean.setAuthor(userMeta); 538 | 539 | homeBean.setData(dataBean); 540 | homeBean.setCode(ResponseObj.OK); 541 | homeBean.setMsg(ResponseList.OK_STR); 542 | return new GsonUtils().toJson(homeBean); 543 | } catch (Exception e) { 544 | e.printStackTrace(); 545 | //查询失败 546 | homeBean.setCode(ResponseObj.FAILED); 547 | homeBean.setMsg(ResponseList.FAILED_STR); 548 | return new GsonUtils().toJson(homeBean); 549 | } 550 | } 551 | 552 | /** 553 | * 根据作者的ID获取作者的信息 554 | * 555 | * @param userId 作者ID 556 | * @return 返回作者的json信息 557 | */ 558 | @RequestMapping(value = "/getAuthorInfo" 559 | , produces = {APPLICATION_JSON_UTF8_VALUE} 560 | , method = {RequestMethod.GET, RequestMethod.POST}) 561 | @ApiOperation( 562 | value = "获取作者信息" 563 | , notes = "获取作者基本信息的json,具体字段请参照输出的json数据" 564 | , response = PostBean.class) 565 | @ResponseBody 566 | public Object getAuthorJson(int userId) { 567 | ResponseObj responseObj = new ResponseObj<>(); 568 | try { 569 | List> userMeta = userService.getUserMeta(userId); 570 | if (null == userMeta || userMeta.isEmpty()) { 571 | responseObj.setCode(ResponseObj.EMPUTY); 572 | responseObj.setMsg(ResponseObj.EMPUTY_STR); 573 | } else { 574 | responseObj.setCode(ResponseObj.OK); 575 | responseObj.setMsg(ResponseObj.OK_STR); 576 | responseObj.setData(userMeta); 577 | } 578 | return new GsonUtils().toJson(responseObj); 579 | } catch (Exception e) { 580 | e.printStackTrace(); 581 | responseObj.setCode(ResponseObj.FAILED); 582 | responseObj.setMsg(ResponseObj.FAILED_STR); 583 | return new GsonUtils().toJson(responseObj); 584 | } 585 | } 586 | 587 | /** 588 | * RESTful风格的文章页面 589 | * 590 | * @param postId 文章ID 591 | * @return 返回文章页面 592 | */ 593 | @RequestMapping(path = "/post/{postId}", method = RequestMethod.GET) 594 | @ApiOperation( 595 | value = "打开文章详情web界面" 596 | , notes = "文章详情web界面,js模板加载网页数据") 597 | public ModelAndView getPostView(@PathVariable int postId) { 598 | ModelAndView resultView = new ModelAndView("frontPost"); 599 | resultView.addObject("framJson", getFramJson()); 600 | resultView.addObject("postJson", getPostById(postId)); 601 | return resultView; 602 | } 603 | 604 | /** 605 | * 根据文章ID获取文章内容 606 | * 607 | * @param postId 文章ID 608 | * @return 返回文章ID对应的文章内容 609 | */ 610 | @RequestMapping(value = "/getPost" 611 | , produces = {APPLICATION_JSON_UTF8_VALUE} 612 | , method = {RequestMethod.GET, RequestMethod.POST}) 613 | @ApiOperation( 614 | value = "根据id获取文章json" 615 | , notes = "根据文章的ID获取文章的详情json" 616 | , response = PostBean.class) 617 | @ResponseBody 618 | public Object getPostById( 619 | @ApiParam(value = "文章ID", required = true) 620 | @RequestParam("postId") 621 | int postId) { 622 | ResponseObj responseObj = new ResponseObj<>(); 623 | try { 624 | PostBean postBean = postService.findPostById(postId); 625 | if (null == postBean) { 626 | responseObj.setCode(ResponseObj.EMPUTY); 627 | responseObj.setMsg(ResponseObj.EMPUTY_STR); 628 | } else { 629 | responseObj.setCode(ResponseObj.OK); 630 | responseObj.setMsg(ResponseObj.OK_STR); 631 | responseObj.setData(postBean); 632 | } 633 | return new GsonUtils().toJson(responseObj); 634 | } catch (Exception e) { 635 | e.printStackTrace(); 636 | responseObj.setCode(ResponseObj.FAILED); 637 | responseObj.setMsg(ResponseObj.FAILED_STR); 638 | return new GsonUtils().toJson(responseObj); 639 | } 640 | } 641 | 642 | } 643 | 644 | ``` 645 | 646 | 至此我们的Spring-Fox简单实用已经完成,后续的操作我们在需要的地方再查找资料就行了。 647 | 648 | #### 总结 649 | 本期项目都是简单的介绍了一些东西,主要有: 650 | - 登录密码校验规则(MD5→SHA256) 651 | - Spring-Fox的引入 652 | - Spring-Fox在非springBoot中的使用 653 | - Spring-Fox的使用 654 | 655 | 这一期本来是上一周就应该完成的,但是回家懒癌发作又拖了一周,期间女朋友还各种生病,每天也没心思写代码,很对不起大家了,后面我会更加努力,也感谢一些哥们在我博客上面的留言鼓励,谢谢大家。 -------------------------------------------------------------------------------- /[java手把手教程][第二季]java后端博客系统文章系统——No5.md: -------------------------------------------------------------------------------- 1 | #### [java手把手教程][第二季]java后端博客系统文章系统——No5 2 | 3 | 停更了一个月后,我们再次开始更新。具体原因只能说是过年事情太多,幼时不知努力,长大了却又事事缠身。 4 | 5 | 这一期主要是根据WordPress执行的效果来观察数据库,从而分析WordPress的程序设计。 6 | 7 | 项目github地址:https://github.com/pc859107393/SpringMvcMybatis 8 | 9 | 我的简书首页是:http://www.jianshu.com/users/86b79c50cfb3/latest_articles 10 | 11 | 上一期是:[[手把手教程][第二季]java 后端博客系统文章系统——No4](http://acheng1314.cn/?p=328) 12 | 13 | ![行走的java全栈](http://acheng1314.cn/wp-content/uploads/2016/10/行走的java全栈群二维码.png) 14 | 15 | #### 工具 16 | - IDE为**idea16** 17 | - JDK环境为**1.8** 18 | - gradle构建,版本:2.14.1 19 | - Mysql版本为**5.5.27** 20 | - Tomcat版本为**7.0.52** 21 | - 流程图绘制(xmind) 22 | - 建模分析软件**PowerDesigner16.5** 23 | - 数据库工具MySQLWorkBench,版本:6.3.7build 24 | 25 | #### 本期目标 26 | 27 | - 根据WordPress的工作进行程序设计分析 28 | - 完成文章保存和草稿保存相关程序流程分析 29 | 30 | > 根据WordPress文章保存和草稿保存分析程序设计 31 | 32 | 首先我们打开WordPress登录到控制台后随便保存草稿和文章,然后导出数据库中posts表增加内容如下: 33 | 34 | ```json 35 | { 36 | "RECORDS":[ 37 | { 38 | "ID":"329", 39 | "post_author":"1", 40 | "post_date":"02-16-2017 09:57:29", 41 | "post_date_gmt":"02-16-2017 01:57:29", 42 | "post_title":"[java 手把手教程][第二季]java 后端博客系统文章系统——No4", 43 | "post_excerpt":"", 44 | "post_status":"inherit", 45 | "comment_status":"closed", 46 | "comment_count":"0", 47 | "ping_status":"closed", 48 | "post_password":"", 49 | "post_name":"328-revision-v1", 50 | "to_ping":"", 51 | "pinged":"", 52 | "post_modified":"02-16-2017 09:57:29", 53 | "post_modified_gmt":"02-16-2017 01:57:29", 54 | "post_content_filtered":"", 55 | "post_parent":"328", 56 | "guid":"http://acheng1314.cn/?p=329", 57 | "menu_order":"0", 58 | "post_type":"revision", 59 | "post_mime_type":"", 60 | "comment_count(2)":"0" 61 | }, 62 | { 63 | "ID":"328", 64 | "post_author":"1", 65 | "post_date":"02-16-2017 09:58:19", 66 | "post_date_gmt":"02-16-2017 01:58:19", 67 | "post_title":"[java 手把手教程][第二季]java 后端博客系统文章系统——No4", 68 | "post_excerpt":"", 69 | "post_status":"publish", 70 | "comment_status":"open", 71 | "comment_count":"0", 72 | "ping_status":"open", 73 | "post_password":"", 74 | "post_name":"java-%e6%89%8b%e6%8a%8a%e6%89%8b%e6%95%99%e7%a8%8b%e7%ac%ac%e4%ba%8c%e5%ad%a3java-%e5%90%8e%e7%ab%af%e5%8d%9a%e5%ae%a2%e7%b3%bb%e7%bb%9f%e6%96%87%e7%ab%a0%e7%b3%bb%e7%bb%9f-no4", 75 | "to_ping":"", 76 | "pinged":"", 77 | "post_modified":"02-16-2017 09:58:19", 78 | "post_modified_gmt":"02-16-2017 01:58:19", 79 | "post_content_filtered":"", 80 | "post_parent":"0", 81 | "guid":"http://acheng1314.cn/?p=328", 82 | "menu_order":"0", 83 | "post_type":"post", 84 | "post_mime_type":"", 85 | "comment_count(2)":"0" 86 | }, 87 | { 88 | "ID":"327", 89 | "post_author":"1", 90 | "post_date":"02-14-2017 23:20:15", 91 | "post_date_gmt":"02-14-2017 15:20:15", 92 | "post_title":"我的草稿", 93 | "post_excerpt":"", 94 | "post_status":"inherit", 95 | "comment_status":"closed", 96 | "comment_count":"0", 97 | "ping_status":"closed", 98 | "post_password":"", 99 | "post_name":"323-revision-v1", 100 | "to_ping":"", 101 | "pinged":"", 102 | "post_modified":"02-14-2017 23:20:15", 103 | "post_modified_gmt":"02-14-2017 15:20:15", 104 | "post_content_filtered":"", 105 | "post_parent":"323", 106 | "guid":"http://acheng1314.cn/?p=327", 107 | "menu_order":"0", 108 | "post_type":"revision", 109 | "post_mime_type":"", 110 | "comment_count(2)":"0" 111 | }, 112 | { 113 | "ID":"326", 114 | "post_author":"1", 115 | "post_date":"02-14-2017 23:20:01", 116 | "post_date_gmt":"02-14-2017 15:20:01", 117 | "post_title":"", 118 | "post_excerpt":"", 119 | "post_status":"inherit", 120 | "comment_status":"closed", 121 | "comment_count":"0", 122 | "ping_status":"closed", 123 | "post_password":"", 124 | "post_name":"323-revision-v1", 125 | "to_ping":"", 126 | "pinged":"", 127 | "post_modified":"02-14-2017 23:20:01", 128 | "post_modified_gmt":"02-14-2017 15:20:01", 129 | "post_content_filtered":"", 130 | "post_parent":"323", 131 | "guid":"http://acheng1314.cn/?p=326", 132 | "menu_order":"0", 133 | "post_type":"revision", 134 | "post_mime_type":"", 135 | "comment_count(2)":"0" 136 | }, 137 | { 138 | "ID":"325", 139 | "post_author":"1", 140 | "post_date":"02-10-2017 22:34:41", 141 | "post_date_gmt":"00-00-00 00:00:00", 142 | "post_title":"自动草稿", 143 | "post_excerpt":"", 144 | "post_status":"auto-draft", 145 | "comment_status":"open", 146 | "comment_count":"0", 147 | "ping_status":"open", 148 | "post_password":"", 149 | "post_name":"", 150 | "to_ping":"", 151 | "pinged":"", 152 | "post_modified":"02-10-2017 22:34:41", 153 | "post_modified_gmt":"00-00-00 00:00:00", 154 | "post_content_filtered":"", 155 | "post_parent":"0", 156 | "guid":"http://acheng1314.cn/?p=325", 157 | "menu_order":"0", 158 | "post_type":"post", 159 | "post_mime_type":"", 160 | "comment_count(2)":"0" 161 | }, 162 | { 163 | "ID":"323", 164 | "post_author":"1", 165 | "post_date":"02-14-2017 23:20:15", 166 | "post_date_gmt":"00-00-00 00:00:00", 167 | "post_title":"我的草稿", 168 | "post_excerpt":"", 169 | "post_status":"draft", 170 | "comment_status":"open", 171 | "comment_count":"0", 172 | "ping_status":"open", 173 | "post_password":"", 174 | "post_name":"", 175 | "to_ping":"", 176 | "pinged":"", 177 | "post_modified":"02-14-2017 23:20:15", 178 | "post_modified_gmt":"02-14-2017 15:20:15", 179 | "post_content_filtered":"", 180 | "post_parent":"0", 181 | "guid":"http://acheng1314.cn/?p=323", 182 | "menu_order":"0", 183 | "post_type":"post", 184 | "post_mime_type":"", 185 | "comment_count(2)":"0" 186 | } 187 | ] 188 | } 189 | ``` 190 | 191 | 在上面的数据中我们已经删除了文章内容的数据(数据量太大,不方便查阅)。然后我们仔细分析上面的json数据,我们可以得出结论如下: 192 | 193 | 1. 文章: 194 | - ID为329和328的表示文章,且为同一篇文章(编辑完成立即发布)。 195 | - 不同字段为: 196 | * ID 197 | * post_date 198 | * post_date_gmt 199 | * post_status 200 | * comment_status 201 | * ping_status 202 | * post_name 203 | * post_modified 204 | * post_modified_gmt 205 | * post_parent 206 | * post_type 207 | - 208 | 209 | 通过上面的对比我们大致可以得出这样一个结论: 210 | 211 | - 文章编辑完成发布后,会留下一个初始版本的记录和一个正式发布版本的记录。 212 | - 正式发布的文章和文章历史记录的主要区别如下: 213 | 214 | ```json 215 | ---->正式发布 216 | "post_status":"publish", 217 | "comment_status":"open", 218 | "ping_status":"open", 219 | "post_name":"java-%e6%89%8b%e6%8a%8a%e6%89%8b%e6%95%99%e7%a8%8b%e7%ac%ac%e4%ba%8c%e5%ad%a3java-%e5%90%8e%e7%ab%af%e5%8d%9a%e5%ae%a2%e7%b3%bb%e7%bb%9f%e6%96%87%e7%ab%a0%e7%b3%bb%e7%bb%9f-no4", 220 | "post_type":"post", 221 | "post_parent":"0", 222 | ---->历史记录 223 | "post_status":"inherit", 224 | "comment_status":"closed", 225 | "ping_status":"closed", 226 | "post_name":"328-revision-v1", 227 | "post_type":"revision", 228 | "post_parent":"328", 229 | ``` 230 | 231 | 2. 草稿: 232 | - ID为323、325、326、327的均为草稿,且为同一篇草稿。 233 | - 具体的不同区别也和上面的类似,所以说我们可以自行整理下即可。 234 | 235 | 3. 小结: 236 | 237 | - 文章和草稿都是有完整的版本记录。 238 | - 文章和草稿的格式类似。 239 | - 草稿分为自动草稿和手动草稿。 240 | - 版本记录也是完整的记录,只是一些关键的字段改变了下。 241 | 242 | > 文章分组相关分析 243 | 244 | ```sql 245 | SELECT 246 | `ID`, 247 | `post_title`, 248 | `post_date`, 249 | `post_content` 250 | FROM 251 | `wp_posts` 252 | WHERE 253 | `post_type` = 'post' 254 | AND 255 | `post_status` = 'publish' 256 | ORDER BY 257 | `ID` 258 | ``` 259 | 260 | 上面的语句能够查找出来公开的文章,文章ID一目了然。 261 | 262 | 同时我们观察数据库可以得出跟文章的归类相关的数据库有: 263 | 264 | - wp_terms 265 | - wp_term_taxonomy 266 | - wp_term_relationships 267 | 268 | 但是这么多表都是文章分类相关的东西,那么文章分类又分为什么些呢?按照WordPress的简单构架支撑大量的数据来看,那么我们可以肯定文章标签和目录分类肯定是在一起的。所以我们先看最根本的wp_terms。 269 | 270 | | term_id | name | slug | term_group | 271 | |--|--|--|--| 272 | |1|java web|java-web|0| 273 | |2|C语言学习|how2use_c|0| 274 | |3|Android开发|makeandroid|0| 275 | |4|综合总结|all_log|0| 276 | |5|个人生活|myself_life|0| 277 | |6|post-format-aside|post-format-aside|0| 278 | |7|转载|from_others|0| 279 | |8|Android Coder|android-coder|0| 280 | |9|友情链接|%e5%8f%8b%e6%83%85%e9%93%be%e6%8e%a5|0| 281 | |10|JavaWeb|javaweb|0| 282 | |11|java web|java-web|0| 283 | |12|Spring|spring|0| 284 | |13|Mybatis|mybatis|0| 285 | |14|java后端|java%e5%90%8e%e7%ab%af|0| 286 | |15|JavaWeb|javaweb|0| 287 | |16|MySQL数据库|mysql%e6%95%b0%e6%8d%ae%e5%ba%93|0| 288 | |17|全栈教程|%e5%85%a8%e6%a0%88%e6%95%99%e7%a8%8b|0| 289 | |18|java|java|0| 290 | 291 | 上面这张表是我线上服务器上面的wp_term表,可能我们暂时不明白什么意思,不过问题不大。我们接着看wp_term_taxonomy。 292 | 293 | | term_taxonomy_id | term_id | taxonomy | description | parent | count | 294 | |--|--|--|--|--|--| 295 | |1|1|category||0|17| 296 | |2|2|category||0|0| 297 | |3|3|category||0|20| 298 | |4|4|category||0|6| 299 | |5|5|category||0|1| 300 | |6|6|post_format||0|44| 301 | |7|7|category||0|8| 302 | |8|8|link_category||0|2| 303 | |9|9|nav_menu||0|0| 304 | |10|10|link_category||0|0| 305 | |11|11|post_tag||0|2| 306 | |12|12|post_tag||0|4| 307 | |13|13|post_tag||0|4| 308 | |14|14|post_tag||0|3| 309 | |15|15|post_tag||0|2| 310 | |16|16|post_tag||0|3| 311 | |17|17|post_tag||0|3| 312 | |18|18|post_tag||0|1| 313 | 314 | 通过上面这种表我们就可以明白了term_id所对应的name分别是什么用的,他们分别有文章分组、文章标签、链接标记等。 315 | 316 | 但是说这么多都没把上面文章的文章分类在哪找到,所以我们接着看wp_term_relationships表里面的东西。 317 | 318 | | object_id | term_taxonomy_id | term_order | 319 | |--|--|--| 320 | |1|8|0| 321 | |2|8|0| 322 | |9|4|0| 323 | |9|6|0| 324 | |11|4|0| 325 | |11|6|0| 326 | |16|3|0| 327 | |16|6|0| 328 | |···|···|···| 329 | 330 | 表里面数据还有很多此处暂时省略。 331 | 332 | 上面表中的object_id顾名思义就是说对象的ID,说明它不单是文章也还有其他分类的信息。 333 | 334 | 我们再看看我们线上的wp_posts(文章)表,里面的简略内容如下: 335 | 336 | | ID | post_title | post_date | post_content | 337 | |--|--|--|--| 338 | |9|IT产品文档列表|2015-09-22 16:44:48|内容省略···| 339 | |11|建站伊始,一切从头再来,本站用wordpress搭建,基于PHP。|2015-09-22 16:45:32|···| 340 | 341 | 其实数据不需要那么多,我们只需要一丢丢数据简单对比就能知道结果了。 342 | - 文章ID为9和11的文章的term_taxonomy_id分别为:4、6、4、6 343 | - term_taxonomy_id为4和6的term_id和taxonomy分别为: 344 | 345 | | term_id | taxonomy | 346 | |--|--| 347 | |category|post_format| 348 | 349 | - 最后我们在wp_terms这个表中可以看到term_id分别为4和6的分别是 350 | 351 | | term_id | name | slug | term_group | 352 | |--|--|--|--| 353 | |4|综合总结|all_log|0| 354 | |6|post-format-aside|post-format-aside|0| 355 | 356 | 所以最后我们通过这样就可以明白分类信息的大概查找结构,文章分类的大概查找如下: 357 | 358 | 文章id ➡ wp_term_relationships中的object_id对应的term_taxonomy_id ➡ wp_term_taxonomy的ID可以看到分别是什么分类同时可以查找到term_id ➡ 最后在wp_term表中根据term_id可以查找到具体的名称。 359 | 360 | 至此分类信息基本查找完成。 361 | 362 | 363 | > 总结 364 | 365 | 1. 文章和草稿只是一些关键信息的不同 366 | 2. 文章和草稿都有完整的历史记录 367 | 3. 文章分类在文章关系表中 368 | 4. 文章关系表包含了文章目录、文章标签等 369 | 5. 文章其他属性都可以通过先在WordPress上面执行后逆向观察数据库窥到一二 -------------------------------------------------------------------------------- /[手把手教程][第二季]java后端博客系统文章系统——No1.md: -------------------------------------------------------------------------------- 1 | 转眼到了博客系统第二章了。这一张我们主要介绍文章系统。毕竟博客系统的核心就是文章的发布和阅读。闲话不多说,老规矩走起来。 2 | 3 | [[手把手教程][JavaWeb]第一季点击这里查看所有文章](http://www.jianshu.com/notebooks/4409922/latest)。当然,也可以直接访问[我的博客](http://acheng1314.cn)。 4 | 5 | #### 工具 6 | - IDE为**idea16** 7 | - JDK环境为**1.8** 8 | - gradle构建,版本:2.14.1 9 | - Mysql版本为**5.5.27** 10 | - Tomcat版本为**7.0.52** 11 | - 流程图绘制(xmind) 12 | - 建模分析软件**PowerDesigner16.5** 13 | 14 | --- 15 | > 思维导图 16 | 17 | 按照前面我们**第二季第一章**阐述的,我们需要先了解我们这个文章系统的整个功能模块组合,也就是我们的思维导图,只有这样才能实现整体功能的架设。 18 | 19 | ![第二章文章系统思维导图](http://acheng1314.cn/wp-content/uploads/2016/11/我的博客第二章文章系统思维导图.png) 20 | 21 | 其实在上面的系统中,我已经把前端用户的文章查阅功能排除掉的。为什么我这里会单独排掉前端的查阅呢?前端的文章查阅功能基本在后端的所有文章中已经有体现相应功能。大概功能如下: 22 | - 前端文章查阅 23 | - 文章列表 24 | - 文章归档 25 | - 文章分类 26 | - 文章详情 27 | 28 | > 流程图 29 | 30 | ![第二章文章阅读-发布流程图](http://acheng1314.cn/wp-content/uploads/2016/11/我的博客第二章文章阅读-发布流程图.png) 31 | 32 | 在上面的流程图中,我们可以看到我们清楚的把业务流程描述出来了。可能很多哥们会说我们有其他不一样的方式,或者类似的方式但是实现比现在的强势,这个无可否认。但是我认为这个是别人项目中存在且我使用的很符合个人习惯的东西。好的东西要学习,不友好的东西我们需要自己改进。 33 | 34 | 首先我们访问站点的方式只有访问主页,然后才会有web应用的展示,也就是说我们网站的首页是我们web应用的总入口。 35 | 36 | 而我们主页的功能也是需要围绕我们的中心——博客来制作,这样才能达到我们建设这个后端的目的。所以首页元素需要有以下方面: 37 | 38 | - 文章列表 39 | - 文章归类 40 | - 作者介绍 41 | - 热门文章 42 | - 最高评论 43 | - 最近动态 44 | - 联系信息(二维码) 45 | - 标签导航 46 | - 等··· 47 | 48 | 49 | > 数据流图 50 | 51 | ![第二章文章系统-数据流图](http://acheng1314.cn/wp-content/uploads/2016/12/我的博客第二章文章系统-数据流图.png) 52 | 53 | 为什么我们需要数据流图,我们不是为了软件工程二故意做这个数据流图。而是数据流图能清晰的表明我们这些流程中需要哪些关键的东西,能在一定程度上反应业务逻辑。所以我们做这个还是有意义。在上面我们可以看到在我们程序流转的过程中,我们需要知道具体的文章ID才能进行详情查看操作,所以我们在拿到列表的时候就需要把文章ID拿到,同时文章归档的依据信息,也需要拿到,大概需要哪些简单的东西,具体跟下面首页的json数据相关。具体的首页预想效果如下图: 54 | 55 | ![第二章文章系统-博客样图](http://acheng1314.cn/wp-content/uploads/2016/12/我的博客第二章文章系统-博客样图.png) 56 | 57 | 当然具体的[原作者的博客请看这里](https://www.vtrois.com/)。原作者的导航栏在右边,个人喜好,所以改到左边。根据这一张图,我们也能看到大概的功能如下: 58 | 59 | - 博客文章列表展示 60 | - 作者信息展示 61 | - 最新、热点、随机文章 62 | - *日期归档导航 63 | - 标签导航 64 | 65 | > 数据来源 66 | 67 | 按照第二季开发标准来说,前端页面展示的数据都是尽量从服务器接口获得,将前后端解耦。所以按照通用接口标准来说,我们首页数据需要JSON的标准数据。分析可得,我们的json格式大概如下: 68 | 69 | ``` json 70 | {"code":1, 71 | "msg":"success", 72 | "data":{ 73 | "posts":[ 74 | { 75 | "id": "282", 76 | "postDate": "Nov 16, 2016 12:51:13 AM", 77 | "postContent": "文章内容", 78 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"}, 79 | { 80 | "id": "282", 81 | "postDate": "Nov 16, 2016 12:51:13 AM", 82 | "postContent": "文章内容", 83 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"} 84 | ], 85 | "totalNum":20, 86 | "author":{}, 87 | "newPosts":[ 88 | { 89 | "id": "282", 90 | "postDate": "Nov 16, 2016 12:51:13 AM", 91 | "postContent": "文章内容", 92 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"}, 93 | { 94 | "id": "282", 95 | "postDate": "Nov 16, 2016 12:51:13 AM", 96 | "postContent": "文章内容", 97 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"} 98 | ], 99 | "hotPosts":[ 100 | { 101 | "id": "282", 102 | "postDate": "Nov 16, 2016 12:51:13 AM", 103 | "postContent": "文章内容", 104 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"}, 105 | { 106 | "id": "282", 107 | "postDate": "Nov 16, 2016 12:51:13 AM", 108 | "postContent": "文章内容", 109 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"} 110 | ], 111 | "randomPosts":[ 112 | { 113 | "id": "282", 114 | "postDate": "Nov 16, 2016 12:51:13 AM", 115 | "postContent": "文章内容", 116 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"}, 117 | { 118 | "id": "282", 119 | "postDate": "Nov 16, 2016 12:51:13 AM", 120 | "postContent": "文章内容", 121 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"} 122 | ], 123 | "tag":{}, 124 | "date":{} 125 | } 126 | } 127 | ``` 128 | 129 | 可能一些朋友看到这里就会迷糊了,你的json数据的实体类型怎么来的呢?其实我们一开始就提过我们的数据库是wordpress的数据库,也就是数据内容是来自我的个人博客系统上面的数据库。所以我们需要看看wrodpress的博客系统上面文章表的结构和内容才能推测是表中字段及其分布各有什么意义。具体的数据库表结构如下: 130 | 131 | ``` 132 | DROP TABLE IF EXISTS `wp_posts`; 133 | CREATE TABLE `wp_posts` ( 134 | `ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 135 | `post_author` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '作者ID', 136 | `post_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '文章创建时间', 137 | `post_date_gmt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '文章最近修改时间', 138 | `post_content` longtext COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章内容', 139 | `post_title` text COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章标题', 140 | `post_excerpt` text COLLATE utf8mb4_unicode_ci NOT NULL, 141 | `post_status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'publish' COMMENT '文章状态', 142 | `comment_status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open' COMMENT '评论状态', 143 | `ping_status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open' COMMENT 'ping状态', 144 | `post_password` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文章密码', 145 | `post_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文章名字', 146 | `to_ping` text COLLATE utf8mb4_unicode_ci NOT NULL, 147 | `pinged` text COLLATE utf8mb4_unicode_ci NOT NULL, 148 | `post_modified` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', 149 | `post_modified_gmt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', 150 | `post_content_filtered` longtext COLLATE utf8mb4_unicode_ci NOT NULL, 151 | `post_parent` bigint(20) unsigned NOT NULL DEFAULT '0', 152 | `guid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', 153 | `menu_order` int(11) NOT NULL DEFAULT '0', 154 | `post_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'post' COMMENT '文章类型', 155 | `post_mime_type` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文件类型', 156 | `comment_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '评论数', 157 | PRIMARY KEY (`ID`), 158 | KEY `type_status_date` (`post_type`,`post_status`,`post_date`,`ID`), 159 | KEY `post_parent` (`post_parent`), 160 | KEY `post_author` (`post_author`), 161 | KEY `post_name` (`post_name`(191)) 162 | ) ENGINE=InnoDB AUTO_INCREMENT=289 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 163 | ``` 164 | 从上面的文章信息表中我们可以看到这一张表只是用来存储所有的文章的基本信息,但是文章的一些其他信息都是没有的,比如说: 165 | 166 | - 评论 167 | - 特色图片 168 | - 文章归档 169 | - 等··· 170 | 171 | 一般来说,我们的常规思路是需要将这些信息关联在一起的,而且这个思路也是没错的。但是可能有的实现我们并没有较好的设计思想,所以我们可以简单的把数据库逆向到模型。所以闲话不多说,直接在有wrodpress环境的电脑上面链接数据库,打开wordpress数据库,选择逆向到模型。那么,数据库逆向模型如下所示: 172 | 173 | ![第二章文章系统-wordpress数据库模型](http://acheng1314.cn/wp-content/uploads/2016/12/我的博客第二章文章系统-wordpress数据库模型.png) 174 | 175 | 从上面的数据库模型中我们可以看出维持wordpress中心的有几张表,如下: 176 | 177 | - wp_posts 文章基础信息表 178 | - wp_postmeta 文章扩展数据表 179 | - wp_comments 评论基本表 180 | - wp_commentmeta 评论扩展表 181 | - wp_links 链接表 182 | - wp_options 设置信息表 183 | - wp_users 用户信息表 184 | - wp_usermeta 用户信息扩展表 185 | 186 | 为什么我说上面这几张表是核心表呢?首先我们可以看到这几张表都是存储了博客系统的一些基本的东西。接着我们可以看到这些各个表中一些关联的表都是有彼此的键对应其他表的主键,所以看到这里大家可能也就心里有数。 187 | 188 | 所以上面我们的json信息中的实体类型该怎么设定也就是很明显的,必须对应数据库字段嘛。既然都这样了,那我们是不是也可以进一步猜想出其他的json内容呢? 189 | 190 | > 日期归档 191 | 192 | 文章按照日期归档相信很多人都看到过,大概样子就是一个下拉列表中显示年月日后面加上数量,大概样子如下(节约流量,不上图): 193 | - 请选择日期 ↓ 194 | - 所有 195 | - 2016年11月12日(2) 196 | - 2016年11月15日(1) 197 | - 2016年10月28日(3) 198 | 199 | 我们要把这样的效果做出来,其实可以直接把文章信息传递给前台让前端完成。但是数据量过多的时候,网络传输也就相对吃力,所以我们还是直接后端处理,将网络传输的数据最精简。 200 | 201 | 那么我们简单的首页集合的数据应该如下所示了: 202 | 203 | ``` 204 | {"code":1, 205 | "msg":"success", 206 | "data":{ 207 | "posts":[ 208 | { 209 | "id": "282", 210 | "postDate": "Nov 16, 2016 12:51:13 AM", 211 | "postContent": "文章内容", 212 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"}, 213 | { 214 | "id": "282", 215 | "postDate": "Nov 16, 2016 12:51:13 AM", 216 | "postContent": "文章内容", 217 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"} 218 | ], 219 | "totalNum":20, 220 | "author":{}, 221 | "newPosts":[ 222 | { 223 | "id": "282", 224 | "postDate": "Nov 16, 2016 12:51:13 AM", 225 | "postContent": "文章内容", 226 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"}, 227 | { 228 | "id": "282", 229 | "postDate": "Nov 16, 2016 12:51:13 AM", 230 | "postContent": "文章内容", 231 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"} 232 | ], 233 | "hotPosts":[ 234 | { 235 | "id": "282", 236 | "postDate": "Nov 16, 2016 12:51:13 AM", 237 | "postContent": "文章内容", 238 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"}, 239 | { 240 | "id": "282", 241 | "postDate": "Nov 16, 2016 12:51:13 AM", 242 | "postContent": "文章内容", 243 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"} 244 | ], 245 | "randomPosts":[ 246 | { 247 | "id": "282", 248 | "postDate": "Nov 16, 2016 12:51:13 AM", 249 | "postContent": "文章内容", 250 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"}, 251 | { 252 | "id": "282", 253 | "postDate": "Nov 16, 2016 12:51:13 AM", 254 | "postContent": "文章内容", 255 | "postTitle": "[手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八)"} 256 | ], 257 | "tag":{}, 258 | "date": [ 259 | { 260 | "date": "2016-11-22", 261 | "idList": [ 262 | "286" 263 | ] 264 | }, 265 | { 266 | "date": "2016-5-19", 267 | "idList": [ 268 | "192", 269 | "191" 270 | ] 271 | } 272 | ] 273 | } 274 | } 275 | ``` 276 | 这里应该有朋友可能会问,为啥你的date(根据日期归档)的json数据这么奇怪呢? 277 | 278 | 其实我们最直接的可以看到,在上面的日期归档的json中,日期可以很直观的看出来,同时idList中把文章ID也是展示出来的,所以我们根据ID和日期都还是可以互相参考的,同时ID的数量可以让我们明白每个日期有多少篇文章。 279 | 280 | 既然我们在上面把基本的首页框架数据归类,写出的json接口,同时通过逆向开发的思路等把项目我们需要使用的一些模型图完成了,这样接下来就是具体编码的事情。 具体的编码问题,且听下回分解。 281 | 282 | --- 283 | > 福利:用户密码算法 284 | 285 | 核心算法:SHA-256 286 | 287 | 步骤: 288 | 289 | - 注册用户 290 | - 客户端进行16位MD5小写加密 291 | - 生成随机的salt 292 | - 将密码和salt进行SHA-256加密 293 | - 数据库存入用户信息和对应的salt 294 | 295 | 296 | --- 297 | 这一期,我们把文章系统一些做了基础的分析,下一期我们需要完成wordpress数据库内容分析和文章系统模块开发,和文章的撰写相关的东西。其实经过上一季的一些东西我们能明白,项目开发中的一些基本思想,但是可能我们最终目的是倚赖wordpress的博客。所以在实际开发中,我们可以参考别人的完成并加以列用。 298 | -------------------------------------------------------------------------------- /[手把手教程][第二季]java后端博客系统起笔.md: -------------------------------------------------------------------------------- 1 | 转眼间时间就从9月份到现在的十一月份了。这段时间说实话做的有意义的事情太少。现在还是单身··· 2 | 3 | 闲话直接跳过了,嗯,手把手教程第二季已经来了,第一季就不用再写什么第一季汇总资源之类的记录了,直接扔出第一季的总集合地址。 4 | [[手把手教程][JavaWeb]第一季点击这里查看所有文章](http://www.jianshu.com/notebooks/4409922/latest)。当然,也可以直接访问[我的博客](http://acheng1314.cn)。 5 | 6 | 最近一直在想怎么搞的更好,怎样描述能更加简单直观的解决问题。第一季我们采用了以下的描述方法: 7 | - 列表 8 | - 画流程图 9 | - 贴效果图 10 | - 语言描述 11 | - 直接贴代码 12 | 13 | 第二季我考虑适当的引入一些软件工程的概念,以及常用的思维模式的一些实现,大概想做一些下面的东西: 14 | - [思维导图](http://baike.baidu.com/link?url=bWRI4YoX8CE7krCZgFBNA7V_YJ2xe6jqg6sqyijipOJUSk_vNb7SUXBxz0KNHb7PKW9fSWThFyHaCfLe4n4Oy2C0C3pNTwYxPIFb4kZOX-Bq4M74XINBEGLmEYcXP4OT) 15 | - [流程图](http://baike.baidu.com/link?url=Sa6_MwnUpZ8kmoX7Gj9IJnG7zgSAjxSYV4ZV-NSVilCIW7UN3xWYX4rKGLssfBTt3eL0ujH5rHFSKc8iP2lE8rIbu3FwBehdG68Oucpp6k41GW5Tr8k9lxEl5YqWXd5a) 16 | - [数据流图](http://baike.baidu.com/link?url=w0ek92zthobLfoZ1a7Cl5_ivun6bGSIpy7r98y66LzIcLLhQQB7p4OHWGTLKJPSe9oGiqRBenvgHzN_DK2VnUe24VW1lxz43MyvsKhh3li3PsupYkAeYnSqei79B4Bxx) 17 | - [E-R图](http://baike.baidu.com/link?url=uK05RHdstOYc9iEI0ep_IKr0FspwXsV2XBg9dal1IKesLm4PuzffTT-YKVleDb0nmZ7-Asoxddh-zvQhOVEc1zpvVmjU6oEgLxyluRBjVSa) 18 | - [UML建模](http://baike.baidu.com/link?url=9FrEX5BNxDkzyDZHxvfItP3mdPPgAzkBBckm9ZWkwaX1c0mSn6KJdQ55Y5ZxuanxsaeqZvKPydq0-QEgcOuy99wMA7SWx5oxkYEVWVjHuTe9B5F-Emsl72JgjnpQ3RFe) 19 | 20 | 说实话上面的这些东西,在实际开发中我们可能不是每次开发都准备这些东西,但是我们在平时可以考虑把这些东西都准备一下,到了一些时候我们的脑袋里自然会有这些相关的概念浮现。而且这样分析程序组织结构和执行流程对我们每个人的成长也已有利的,所以希望同学们能一起互勉。 21 | 22 | ---- 23 | 软件工程讲究的是以工程学的角度来控制软件的研发。核心目的是:提高效率降低成本。我们在实际开发中如何体现这些东西呢? 24 | 25 | > 思维导图 26 | 27 | 为什么要把思维导图放在最前面?思维导图又叫心智图,是表达发散性思维的有效的图形思维工具,是一种将放射性思考具体化的方法,是一种图像式思维的工具以及一种利用图像式思考辅助工具。简单思维导图如下: 28 | 29 | ![我的博客第一章第一图](http://acheng1314.cn/wp-content/uploads/2016/11/我的博客第一章第一图.png) 30 | 31 | 上面这个图是我画的一个关于文章系统设计的图(中间有小瑕疵,将就的看=,=),这个就是我们常用的思维导图的作用之一,能帮助我们理清思路和功能结构。具体的思维导图我们就不再多做介绍了,在上面的链接中都可以查看,思维导图推荐的工具是xmind。 32 | 33 | > 流程图 34 | 35 | 流程图相对来说是我们现在相对更加熟悉的东西,在前面的第一季的文章中我们能看到很多关于流程图的绘画。流程图是流经一个系统的信息流、观点流或部件流的图形代表,它以特定的图形符号加上说明来表示事物执行流程。 36 | 37 | > 数据流图 38 | 39 | 数据流图:简称DFD(Data Flow Diagram),它从数据传递和加工角度,以图形方式来表达系统的逻辑功能、数据在系统内部的逻辑流向和逻辑变换过程,是结构化系统分析方法的主要表达工具及用于表示软件模型的一种图示方法。 40 | 41 | - 指明数据存在的数据符号,这些数据符号也可指明该数据所使用的媒体; 42 | - 指明对数据执行的处理的处理符号,这些符号也可指明该处理所用到的机器功能; 43 | - 指明几个处理和(或)数据媒体之间的数据流的流线符号; 44 | - 便于读、写数据流程图的特殊符号。 45 | 46 | ![简单的数据流图实例](http://p.blog.csdn.net/images/p_blog_csdn_net/turkeyzhou/EntryImages/20100106/4.jpg) 47 | 48 | 数据流图虽然说在名字上面听起来有点类似流程图,但是实际上两者差异还是较大,同时我们可以很明显的看到数据流图把程序执行的数据流转示意表现的很清楚,所以我们也需要他来帮我们完成一些事情。 49 | 50 | > E-R图 51 | 52 | E-R图:实体-联系图(Entity Relationship Diagram),提供了表示实体类型、属性和联系的方法,用来描述现实世界的概念模型。 53 | 54 | > UML建模 55 | 56 | UML建模技术就是用模型元素来组建整个系统的模型,模型元素包括系统中的类、类和类之间的关联、类的实例相互配合实现系统的动态行为等。 57 | 58 | UML是面向对象开发中一种通用的图形化建模语言。面向对象的分析主要在加强对问题空间和系统任务的理解、改进各方交流、与需求保持一致和支持软件重用等4个方面比较突出,因此也成为现在主流的建模方法(在IDEA中我们能看到项目对应的Uml模型)。 59 | 60 | 相对于其他的图示,我更加喜欢UML建模,他能很生动形象的表现出各个类、接口之间的关系,如下图: 61 | 62 | ![泛型接口的实现和接口继承](http://acheng1314.cn/wp-content/uploads/2016/11/我的博客第一章第二图泛型接口的实现和接口继承.png) 63 | 64 | ![javaBean实现Serializable接口](http://acheng1314.cn/wp-content/uploads/2016/11/我的博客第一章第三图javaBean实现Serializable接口.png) 65 | 66 | 上面的第一张图中我们可以看到是我的UserDao继承了BaseDao并且将泛型T具体化为User。 67 | ``` java 68 | public interface UserDao extends Dao { 69 | int add(User user); 70 | 71 | int del(User user); 72 | 73 | int update(User user); 74 | 75 | User findOneById(Serializable Id); 76 | 77 | List findAll(); 78 | 79 | void updateLoginSession(@Param("sessionId") String sessionId, @Param("loginId") String loginId); 80 | 81 | void addSessionId(String id); 82 | } 83 | ``` 84 | 同理可得,我们的PostDao也是继承BaseDao并且将泛型T具体化为PostBean。 85 | 86 | 第二张图中,实际就是我们的User和PostBean这两个javaBean,他们同时实现了接口Serializable。 87 | 88 | 上面两张图中我们可以看到: 89 | - 类或者接口的继承用实线箭头表示 90 | - 类实现接口用虚线箭头表示 91 | - 泛型具体化也是用实线箭头表示 92 | - 类使用淡蓝色方框表示 93 | - 接口使用淡紫色方框表示 94 | 95 | 具体的一些东西我们后面再详细介绍,现在大概明白即可(当然老司机肯定是直接跳过)。 96 | 97 | ---- 98 | 99 | > 倚赖wordpress数据库的博客系统 100 | 101 | 这一季我们的正式目标是做一个博客系统,然后倚赖的是以前的wordpress博客的数据库。这几天大概整理了功能如下: 102 | 103 | ![博客系统整体结构图](http://acheng1314.cn/wp-content/uploads/2016/11/我的博客第一章第四图博客系统整体结构图.png) 104 | 105 | 为什么说打算做这一个东西,主要是因为首先我个人的博客被人家刷评论了,第二点是博客一直被人攻击,想用自己的系统来和别人斗智斗勇看看。 106 | 107 | 做重要的是想自己作一些属于自己的东西,留下一些记录的痕迹。 108 | 109 | 这个第一期只能说不算开篇的开篇吧,在后面的文章中可能我们很多时候更多是怎么样去引导思维这样子做事,而不是怎么样去编码。 110 | 111 | 希望在这新的一季里面我们能有更多的收获,一起加油吧。 -------------------------------------------------------------------------------- /mmblog博客系统第三章.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pc859107393/SpringMvcMybatis/9a48d6d8a1b279a8d1174a8d52cd5c7c08b5cb27/mmblog博客系统第三章.zip -------------------------------------------------------------------------------- /mmblog博客系统第二章.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pc859107393/SpringMvcMybatis/9a48d6d8a1b279a8d1174a8d52cd5c7c08b5cb27/mmblog博客系统第二章.zip -------------------------------------------------------------------------------- /mmblog博客系统第四章.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pc859107393/SpringMvcMybatis/9a48d6d8a1b279a8d1174a8d52cd5c7c08b5cb27/mmblog博客系统第四章.zip -------------------------------------------------------------------------------- /readme20161009: -------------------------------------------------------------------------------- 1 | #### [手把手教程][JavaWeb]优雅的SSM应用(三) 2 | 3 | 文章正式改名为:[手把手教程][JavaWeb]优雅的SSM应用 4 | 5 | 这几天一直在踩坑,为什么这么说呢,主要是一直自己没找到优雅的方式来实现一些东西。 6 | 7 | 虽然说前面也做了一些功能模块,但是总感觉不对劲,毕竟也是刚转做后端。 8 | 9 | 后面朋友给了我一些他们公司同事写的demo,虽然说不上是牛逼的作品,但是确实符合我现在的需要。毕竟人家的实现方式也是经过实战项目演练出来的。 10 | 11 | - 再次安利一波,博客地址:[acheng1314.cn](http://acheng1314.cn/) 12 | 13 | #### 工具 14 | - IDE为**idea15** 15 | - JDK环境为**1.8** 16 | - maven版本为**maven3** 17 | - Mysql版本为**5.5.27** 18 | - Tomcat版本为**7.0.52** 19 | - 流程图绘制(xmind) 20 | 21 | #### 本期目标 22 | - 仓库管理系统的登录注册模块实现 23 | - 其他一些开发细节的体现 24 | - 功能模块分层设计的具体实现 25 | 26 | 27 | #### 其他 28 | - 我这姑且算是文章吧,文章都是先用有道云笔记写的,然后在简书上面查看有没有冲突,最后再放到稀土掘金上面 29 | - 但是稀土掘金上面文章出问题了,反馈上去也没能解决,本来想抓包看看他们的数据的,后面还是没做 30 | - **···**其他想说的就太多了,但都是不是今天的主题。 31 | 32 | #### 注册 33 | 34 | 首先,我们webapp要实现用户登录,必须得能新建用户。所以,我们先把注册用户放在前面。 35 | 36 | - 预期功能: 37 | - 打开注册页面 38 | - 填写注册信息 39 | - 点击注册 40 | - 显示注册后的提示信息 41 | 42 | 有了功能后,我们就能大概明白我们是想要一个什么样子的注册模块了。 43 | - 一个web注册页面 44 | - web页面能进行基本的数据效验 45 | - 服务器能存储用户的注册信息 46 | - 注册动作完成后,返回提示页面。 47 | 48 | 一般的,我们在开发中,有了大概样子的功能模块,我们需要整理一下业务流程和程序执行流程(在企业开发中,有项目经理的话,一般这些都是他们整理出来的,我们只需要开发实现就行。)经过一番挠头,大概的流程图如下所示: 49 | 50 | ![ssm应用三-注册流程](http://acheng1314.cn/wp-content/uploads/2016/10/ssm应用三-注册流程.png) 51 | 52 | 上图说明: 53 | - 我们在web页面完成注册信息的填写后,我们需要在web页面做一些基本的数据效验。当然后面我们会演示。 54 | - 注册信息通过基本的验证后,我们直接提交到服务器,**tomact → servelt → spring** 。我们的后端程序,一切都被spring接管了,所以,我们需要在spring中完成这些功能。 55 | - spring和外界通信,我们都是在Controller中完成。所以我们在Controller中处理数据。 56 | - 当数据通过了Controller中的校验后,我们需要在Controller中来查找数据库是否存在同样的用户名,**通用的数据操作流程如:Controller → Service → Dao**。 57 | - 前面我们提到过,**Service是为我们的程序提供服务的,我们尽量每个Service对应一个Dao,这样我们只需要提供单一的数据驱动,在外层进行业务组装**,这样就能达到我们的目的,同样的,我们这样也能将程序[**解耦**](http://blog.csdn.net/hb0746/article/details/7410524),以后的维护也就相对简单了。 58 | 59 | 好的,我们上面已经把思路想明白了。现在我们接着就开始实战。 60 | 61 | - 生成注册页面的连接。 62 | 63 | 我们要生成一个连接,经过查找资料,我们知道我们需要创建一个Controller的类。代码如下: 64 | 65 | ``` 66 | @Controller 67 | @RequestMapping("/mvc") 68 | public class MainController { 69 | 70 | /** 71 | * 登陆页面 72 | * @return 73 | */ 74 | @RequestMapping(value = "/login",method = RequestMethod.GET) 75 | public String login(){ 76 | return "login"; 77 | } 78 | 79 | } 80 | 81 | ``` 82 | 83 | 在上面我们使用了@Controller和@RequestMapping("/mvc")注解。[详细资料点这里。](https://my.oschina.net/zhdkn/blog/316530) 84 | 85 | 通俗的来说,我们需要在我们前面配置的Controller路径中,建立**使用@Controller的注解的类**告诉Spring这是一个控制器。 86 | 87 | ``` 88 | 在类上面的 @RequestMapping("/mvc") ,是说明这个类的访问地址是 /mvc 。 89 | 90 | 在方法上面的 @RequestMapping(value = "/login",method = RequestMethod.GET) ,是说明我这个方法的访问地址是 /mvc/login ,请求方式是http请求的get方式。 91 | 92 | 这里我的方法是String方法,则是直接返回一个web页面的名字。 93 | 94 | 当然,我们并不需要说直接去设定某个jsp文件。我们需要的是在这里指定好名称,然后使用对应的自动完成就能创建出那个jsp文件。 95 | 96 | 然后我们直接在jsp文件中填写对应的代码就行了。 97 | ``` 98 | 99 | 好的,基本的东西我们都说了,那么我们先去百度找一个登录界面(一定要能看,不能那啥太直接的粗糙的东西,毕竟我们都是有品位的人)。如下图: 100 | 101 | ![ssm应用三-登录注册页面](http://acheng1314.cn/wp-content/uploads/2016/10/ssm应用三-登录注册.png) 102 | 103 | 上面的图中样子还不错的样子,同时他们还是同一个页面,这下就很nice了,又可以少写一个界面了。 104 | 105 | 按照前面两期我们的东西综合起来,我们需要先**把CSS、JS、图片等东西,扔到我们的静态目录中**。如下图所示: 106 | 107 | ![ssm应用三-登录注册静态资源](http://acheng1314.cn/wp-content/uploads/2016/10/ssm应用三-登录注册静态资源.png) 108 | 109 | 接着我们把登录的html的页面的东西,全部放到login.jsp中。如下: 110 | 111 | ``` 112 | <%@ page language="java" import="java.util.*" pageEncoding="UTF-8" %> 113 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 114 | 115 | <%-- 上面这两行是java代码的引用 --%> 116 | 117 | 118 | 119 | 120 | 121 | 仓库管理系统→登录 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |

仓库管理系统登陆注册2016

131 | 132 | 283 |
*推荐使用ie8或以上版本ie浏览器或Chrome内核浏览器访问本站
284 | 285 | 286 | 287 | 288 | ``` 289 | 290 | 上面的网页代码中的东西,我们也可以不求甚解,只要会调用就行。调用地址是在我们的form表单的action那里填写我们的服务器地址。这里我们甚至可以做前后端分离,用纯粹的html+js来调用Api接口实现前后端分离。 291 | 292 | ``` 293 | action="<%=request.getContextPath()%>/userAction/reg" method="post" 294 | 295 | <%=request.getContextPath()%> 这是指向我们应用的根路径 296 | 297 | mothod是说明我们请求的方式,我们这里才用了post,至于其他的方法就不一一介绍了,详细信息请百度查找“ http请求 ” 298 | 299 | form表单中,每个input的name我们需要和后端的接口那边的字段对应。 300 | 301 | 当我们的字段对应后,spring可以自动把请求的内容转换为适应的对象。 302 | 303 | 小提示:我们可以直接debug我们的程序,只要取消断点程序就可以顺序执行,加入断点只要程序流转到那里,他就会自动调试。 304 | ``` 305 | 306 | 当然,我们的jsp写完后,我们需要给我们的表单请求那里指定请求路径。由于上面我已经指定了路径,所以我们需要在对应的路径创建请求的接口(实际开发中都是先写好请求接口,再让程序调用。由于我这里是提前写好的,所以这里我们得照着路径写代码)。 307 | 308 | 我们在Controller目录下创建一个UserController类,代码内容如下: 309 | 310 | ``` 311 | package cn.acheng1314.mvc.controller; 312 | 313 | import cn.acheng1314.domain.ResponseObj; 314 | import cn.acheng1314.domain.User; 315 | import cn.acheng1314.exception.*; 316 | import cn.acheng1314.service.serviceImpl.UserServiceImpl; 317 | import cn.acheng1314.utils.GsonUtils; 318 | import cn.acheng1314.utils.StringUtils; 319 | import org.springframework.beans.factory.annotation.Autowired; 320 | import org.springframework.stereotype.Controller; 321 | import org.springframework.ui.ModelMap; 322 | import org.springframework.web.bind.annotation.RequestMapping; 323 | import org.springframework.web.bind.annotation.RequestMethod; 324 | import org.springframework.web.bind.annotation.ResponseBody; 325 | import org.springframework.web.servlet.ModelAndView; 326 | 327 | import javax.servlet.http.HttpServletRequest; 328 | 329 | /** 330 | * 用户请求相关控制器 331 | *
Created by acheng on 2016/9/26. 332 | */ 333 | @Controller 334 | @RequestMapping("/userAction") 335 | public class UserController { 336 | 337 | @Autowired 338 | private UserServiceImpl userService; //自动载入Service对象 339 | private ResponseObj responseObj; //bean对象 340 | 341 | /** 342 | * 为什么返回值是一个ModelAndView,ModelAndView代表一个web页面
343 | * setViewName是设置一个jsp页面的名称 344 | * @param req http请求 345 | * @param user 发起请求后,spring接收到请求,然后封装的bean数据 346 | * @return 返回一个web页面 347 | * @throws Exception 348 | */ 349 | @RequestMapping(value = "/reg", method = RequestMethod.POST) 350 | public ModelAndView reg(HttpServletRequest req, User user) throws Exception { 351 | ModelAndView mav = new ModelAndView(); //创建一个jsp页面对象 352 | mav.setViewName("home"); //设置JSP文件名 353 | if (null == user) { 354 | mav.addObject("message", "用户信息不能为空!"); //加入提示信息,在jsp中我们直接使用${对象名称}就能获取对应的内容 355 | return mav; //返回页面 356 | } 357 | if (StringUtils.isEmpty(user.getName()) || StringUtils.isEmpty(user.getPwd())) { 358 | mav.addObject("message", "用户名或密码不能为空!"); 359 | return mav; 360 | } 361 | if (null != userService.findUser(user)) { 362 | mav.addObject("message", "用户已经存在!"); 363 | return mav; 364 | } 365 | try { 366 | userService.add(user); 367 | } catch (Exception e) { 368 | e.printStackTrace(); 369 | mav.addObject("message", "错误:用户其他信息错误"); 370 | return mav; 371 | } 372 | mav.addObject("code", 110); 373 | mav.addObject("message", "恭喜。注册成功"); 374 | req.getSession().setAttribute("user", user); 375 | return mav; 376 | } 377 | 378 | /** 379 | * 登录接口 380 | * @param req 381 | * @param user 382 | * @return 383 | */ 384 | @RequestMapping(value = "/login", method = RequestMethod.POST, produces = { 385 | "application/json; charset=utf-8"}) 386 | @ResponseBody 387 | public ModelAndView login(HttpServletRequest req, User user) { 388 | ModelAndView mav = new ModelAndView("home"); 389 | String result; 390 | if (null == user) { 391 | responseObj = new ResponseObj(); 392 | responseObj.setCode(ResponseObj.EMPUTY); 393 | responseObj.setMsg("登录信息不能为空"); 394 | result = GsonUtils.gson.toJson(responseObj); //转换的json数据 395 | mav.addObject("result", result); 396 | return mav; //返回页面 397 | } 398 | if (StringUtils.isEmpty(user.getLoginId()) || StringUtils.isEmpty(user.getPwd())) { 399 | responseObj = new ResponseObj(); 400 | responseObj.setCode(ResponseObj.FAILED); 401 | responseObj.setMsg("用户名或密码不能为空"); 402 | result = GsonUtils.gson.toJson(responseObj); 403 | mav.addObject("result", result); 404 | return mav; 405 | } 406 | //查找用户 407 | User user1 = userService.findUser(user); 408 | if (null == user1) { 409 | responseObj = new ResponseObj(); 410 | responseObj.setCode(ResponseObj.EMPUTY); 411 | responseObj.setMsg("未找到该用户"); 412 | result = GsonUtils.gson.toJson(responseObj); 413 | } else { 414 | if (user.getPwd().equals(user1.getPwd())) { 415 | responseObj = new ResponseObj(); 416 | responseObj.setCode(ResponseObj.OK); 417 | responseObj.setMsg(ResponseObj.OK_STR); 418 | result = GsonUtils.gson.toJson(responseObj); 419 | } else { 420 | responseObj = new ResponseObj(); 421 | responseObj.setCode(ResponseObj.FAILED); 422 | responseObj.setMsg("用户密码错误"); 423 | result = GsonUtils.gson.toJson(responseObj); 424 | } 425 | } 426 | mav.addObject("result", result); 427 | return mav; 428 | } 429 | 430 | } 431 | 432 | ``` 433 | 434 | 当然很多数据效验我们不能只在后端做,我们需要将数据检查的粒度细化。 435 | 436 | 不但要在后端做,而且我们的前端页面也要做的,比如说手机号、邮箱帐号、用户名规则等等,用的最多的也就是web页面上面拿到数据用js来判断,使用[正则表达式](http://baike.baidu.com/link?url=tw3uQ7hzZQaXrKAz5EU700k9mZuE-2H3TvkLFYsZ2yjANLB0sVlnj-Ig7L0E8SwTf9gI-f_N65ud7tL3RW_SzD_bI6A7ozb7PNspH8aniRACUiYm16gl_NJ2RecEyzgmEdzLG6h3FftX1Ax0vXGLxK)来判断是否符合标准。 437 | 438 | 具体的js我也就不写了,因为我也不是很了解JS,只能对着别人写的自己来做修改== 439 | 440 | ![蘑菇头-好伤心](http://imgsrc.baidu.com/forum/w=580/sign=8e7ed72518178a82ce3c7fa8c602737f/e61fb7003af33a87e5082df3c05c10385243b593.jpg) 441 | 442 | 好的,我们现在已经把东西都弄完了,debug开启程序,然后加入断点调试。运行结果如下: 443 | 444 | ![ssm应用三-注册页面和调试](http://acheng1314.cn/wp-content/uploads/2016/10/ssm应用三-注册页面和调试.png) 445 | 446 | 这样我们现在能拿到对应的数据,并且在Controller中加入了数据校验。同时,我们的web页面中也加入了js验证。 447 | 448 | 现在我们的注册页面也可以了,功能也有了。既然如此,我们应该接着把登录页面做成功,但是我们已经有了这个的思路,那么剩下的只需要依样画瓢就能完成。 449 | 450 | 具体的东西,都已经在后面的代码中贴出来了。详情请看github: 451 | 452 | 项目地址:[点击访问github](https://github.com/pc859107393/SpringMvcMybatis/tree/master) 453 | 454 | 总结: 455 | - URL生成 456 | - 注册登录完成 457 | - 简单的前端验证(在代码包中可以看到) 458 | - form表单提交 459 | - http请求 460 | - 功能模块分析 461 | - 流程图(使用xmind制作) 462 | 463 | 464 | 下期预告:完整的后台主页,前端使用json数据,列表数据分页。 465 | -------------------------------------------------------------------------------- /readme20161020: -------------------------------------------------------------------------------- 1 | #### 优雅的SpringMvc+Mybatis应用(四) 2 | 3 | 转眼间文章已经到了第四期了。坚持做一件事,确实是很难的。特别是要不断的转换思维,一个习惯前端开发的人,做什么还是前端的考虑的多一点,后端的架构设计之类的,现在还谈不上,一切稳稳的前进就行了。 4 | 5 | 关于上一期,本来是投了首页的,后来不知道什么原因没上,检查了一下,也就是推荐了下自己的博客和github,有点惆怅。 6 | 7 | #### 工具 8 | - IDE为**idea15** 9 | - JDK环境为**1.8** 10 | - maven版本为**maven3** 11 | - Mysql版本为**5.5.27** 12 | - Tomcat版本为**7.0.52** 13 | - 流程图绘制(xmind) 14 | 15 | #### 本期目标 16 | - 登录注册的简单体验优化 17 | - 完整的后台主页 18 | - 前端使用json数据 19 | - 列表数据分页 20 | #### 注册登录的简单体验优化 21 | 22 | 上一期我们**注册登录都成功**了,但是后台主页显示很丑陋,所以这里我换了个主页,但是前面没有注意到的细节又看到了。如下图: 23 | 24 | ![ssm应用四-注册成功-地址栏错误](http://acheng1314.cn/wp-content/uploads/2016/10/ssm应用四-注册成功-地址栏错误.png) 25 | 26 | 在上面的图中,地址栏显示的地址是前面注册接口的地址,并不是我们常规看到的**xxx/home**这种主页地址。所以我们需要进行优化处理。 27 | 28 | 同时,我们可以看到我们的Form表单提交的提示信息是在新产生的**ModelAndView**界面里面addObject("字段名",数据),这样我们的数据都显示到新的界面去了。也就是说前面的设计不合符现在主流的开发思路,用户体验也相对糟糕。我们需要做到在登录界面前端效验数据,同时登录注册的提示信息也是在对应的界面完成的。如下: 29 | 30 | ![ssm应用四-前端form错误-提示示例](http://acheng1314.cn/wp-content/uploads/2016/10/ssm应用四-前端form错误-提示示例.png) 31 | 32 | 值得注意的是:为了程序执行效率、数据完整性和程序健壮性,我们的前端必须做对应的基础数据效验,后端的控制器必须做所有需要的数据的效验。 33 | - 前端数据效验我们使用js完成 34 | - 前端界面样式是由CSS完成 35 | - 网络请求采用异步请求,具体的实现是使用的ajax完成 36 | - js获取web页面数据统一使用标签的ID,格式为:**$("#标签ID")** 37 | - web页面标签最好一个标签一行,这样代码看起来更加舒服 38 | 39 | 我们先重构登录页面: 40 | 首先,我不擅长写web页面,我能做的也就是少量的修改,CSS和js本身不是我的强项,需要大量的时间来磨合。 41 | 42 | 所以,我选择了在网上找web界面,然后自己做少量的修改,同时一些简单的小控件我也从网络获取资源来解决需要,**合理的查找资源是最快的学习方法**。 43 | 44 | 登录页面重构目标: 45 | - web前端完成基本的数据效验 46 | - 数据效验完成后,有基本的对应提示。如上面登录界面的小标签。 47 | - 异步登录 48 | - 后端接口返回数据为json 49 | - 前端页面解析json控制程序流转 50 | 51 | 首先,按照上面的提示,我们可以知道的是前端页面上面的基本数据效验是要使用js完成的,同时js中获取web页面标签的数据是需要使用标签的ID完成,简单的示例如下: 52 | 53 | ``` 54 | 100 | ``` 101 | 102 | 上面的注释已经能很明显的看出我们的 **前端效验、网络请求和js解析json**,下面我们在前端页面中调用这个js,如下: 103 | ``` 104 |
110 | 111 | 112 | 113 |
114 | 115 |
116 | 117 |
118 |
119 | 120 |
121 | 122 |
123 | 124 |
125 |
126 | 127 |
128 | 134 |
135 |
136 | ``` 137 | 138 | 上面就是web中调用js的简单实现,注意的是,**FORM表单必须删除action的值,在点击后需要触发对应事件的地方调用js**。 139 | 140 | 当然,我们的前端页面完成后,我们必须在后端接口处,做出对应的修改,让他符合我们前端的调用规则。后端修改如下: 141 | ``` 142 | /** 143 | * 用户请求相关控制器 144 | *
Created by acheng on 2016/9/26. 145 | */ 146 | @Controller //标明本类是控制器 147 | @RequestMapping("/userAction") //外层地址 148 | public class UserController { 149 | 150 | @Autowired 151 | private UserServiceImpl userService; //自动载入Service对象 152 | private ResponseObj responseObj; //返回json数据的实体 153 | 154 | /** 155 | * 登录接口,因为json数据外层一般都是Object类型,所以返回值必须是Object
156 | * 这里的地址是: 域名/userAction/login 157 | * 158 | * @param req 159 | * @param user 160 | * @return 161 | */ 162 | @RequestMapping(value = "/login" //内层地址 163 | , method = RequestMethod.POST //限定请求方式 164 | , produces = "application/json; charset=utf-8") //设置返回值是json数据类型 165 | @ResponseBody 166 | public Object login(HttpServletRequest req, User user) { 167 | Object result; 168 | if (null == user) { 169 | responseObj = new ResponseObj(); 170 | responseObj.setCode(ResponseObj.EMPUTY); 171 | responseObj.setMsg("登录信息不能为空"); 172 | result = new GsonUtils().toJson(responseObj); //通过gson把java bean转换为json 173 | return result; //返回json 174 | } 175 | if (StringUtils.isEmpty(user.getLoginId()) || StringUtils.isEmpty(user.getPwd())) { 176 | responseObj = new ResponseObj(); 177 | responseObj.setCode(ResponseObj.FAILED); 178 | responseObj.setMsg("用户名或密码不能为空"); 179 | result = new GsonUtils().toJson(responseObj); 180 | return result; 181 | } 182 | //查找用户 183 | User user1 = userService.findUser(user); 184 | if (null == user1) { 185 | responseObj = new ResponseObj(); 186 | responseObj.setCode(ResponseObj.EMPUTY); 187 | responseObj.setMsg("未找到该用户"); 188 | result = new GsonUtils().toJson(responseObj); 189 | } else { 190 | if (user.getPwd().equals(user1.getPwd())) { 191 | responseObj = new ResponseObj(); 192 | responseObj.setCode(ResponseObj.OK); //登录成功,状态为1 193 | responseObj.setMsg(ResponseObj.OK_STR); 194 | responseObj.setData(user1); //登陆成功后返回用户信息 195 | result = new GsonUtils().toJson(responseObj); 196 | } else { 197 | responseObj = new ResponseObj(); 198 | responseObj.setCode(ResponseObj.FAILED); 199 | responseObj.setMsg("用户密码错误"); 200 | result = new GsonUtils().toJson(responseObj); 201 | } 202 | } 203 | return result; 204 | } 205 | } 206 | ``` 207 | 注意:如果为了返回数据为json,那么我们需要设定某个方法对应的注解为:**@ResponseBody** 。 否则会**产生404错误**! 208 | 209 | 我们通过上面的重构可以明白以下几点: 210 | - 前端 211 | - js实现基本的数据效验 212 | - js发起网络请求 213 | - ajax发起网络请求,返回类型设置json能自动解析 214 | - js获取页面控件 215 | - 页面控件调用js 216 | - js获取解析后的json数据的值,进行程序流转控制 217 | - 后端: 218 | - 后端控制器必须申明 219 | - 后端的地址必须配置 220 | - 每个地址返回的数据类型要匹配 221 | - 返回json数据,方法上面必须配置:**@ResponseBody** 222 | - 可以使用工具类来方便开发 223 | ---- 224 | #### 后台主页→个人信息修改 225 | 226 | 上期我们可以看到,我们的登录和注册都是已经OK了。现在我们登录和注册成功后,我们都让他跳转到主页去。同时完善登录和注册的错误提示页面。 227 | 228 | 一般来说,大家更喜欢看到登录成功后的主页界面,毕竟大多数人都是有喜新厌旧之嫌。我也是一样的。哈哈。 229 | 230 | 为了程序的执行逻辑,考虑后端需求都不是那么单一,我们先做一些公共的建设。比如说用户信息修改现实之类的。如下图: 231 | 232 | ![ssm应用四-后台主页-修改个人信息](http://acheng1314.cn/wp-content/uploads/2016/10/ssm应用四-后台主页-修改个人信息.png) 233 | 234 | 如上图所示,我们需要一个可以弹出的对话框,我去百度了一下,那个[“妹子UI”](http://note.youdao.com/)还是很受人欢迎,所以就集成进来了。 235 | 236 | 我们选取一个比较喜欢的后端主页,然后把对应的资源放入到对应的文件目录(js、css、images等),需要新加入的资源如果在以前的目录中没有的话,那么我们需要在里面进行配置。比如说这里我加入了字体文件,那么我现在需要先把字体文件指定目录为: 237 | 238 | static/font/ 239 | 240 | 目录指定后我需要在Spring的配置文件,spring-web.xml中配置静态资源的目录如下: 241 | 242 | 243 | 244 | 剩下的就是写好jsp页面(Copy+Pause+修改资源文件路径)。然后我们在Controller中配置好路径 245 | 246 | /** 247 | * 后台主页 248 | * 249 | * @return 250 | */ 251 | @RequestMapping(value = "/home", method = RequestMethod.GET) 252 | public String home() { 253 | return "home"; 254 | } 255 | 256 | 这样子配置好了后,我们就可以直接用“域名/mvc/home”来访问我们的主页了。同时按照上面的设置,我们登录成功后,直接解析json确认用户登录成功,然后前端使用js来进行页面跳转,如: 257 | 258 | ``` 259 | window.location.href = "<%=request.getContextPath()%>/mvc/home"; //跳转到主页 260 | ``` 261 | 262 | 这样,我们就能修复上面说道的页面和地址显示不匹配的问题。 263 | 264 | 同时,通过上面的可以看出,我们在jsp页面中,纯粹没加入任何jsva代码,全是使用的前端+接口实现的功能。我们这样做,以后维护和重构中也能降低一部分压力。 265 | 266 | 言归正传,我们这里主要是想做一个**个人信息修改**的功能。首先我们进行功能和业务流程分析。 267 | 268 | 功能和业务流程分析: 269 | - 1.web点击头像显示修改信息对话框。 270 | - 2.根据后端定义的用户信息表,得出用户信息修改需要填写的资料。 271 | - 3.用户上传个人资料,上传之前前端必须先进行基础信息验证。 272 | - 4.用户个人信息验证通过后,上传到服务器。(重点) 273 | - 5.服务器接收上传的信息,进行存储,并返回修改结果。(重点) 274 | 275 | 从上面我们可以看到我画出两个重点,而且这两个重点都是java web避免不了事情。为什么这样说呢? 276 | 277 | - 1.任何一个动态的web服务器都免不了数据资料的更新,数据资料更新一般分为两种。 278 | - 有文件的信息上传 279 | - 无文件的信息上传 280 | - 2.可能其他童鞋看到http请求的方法有很多种,但是一般来说get和post我们能做出任何的操作。 281 | - 3.在大量数据的服务器中,考虑到很多因素(历史记录查询、数据库增量等),一般不会进行真正的物理数据删除,一般都是通过控制输出来实现的。(实战经验,血泪教训,切记) 282 | 283 | 现在我们开始实现对话框: 284 | 285 | 打开[“妹子UI”的js插件页面](http://amazeui.org/javascript),我们找到[模态窗口相关的文档](http://amazeui.org/javascript/modal),在“模拟 Prompt”这里,我们可以看到具体的对话框的实现和调用如下: 286 | 287 | ``` 288 | 289 | 295 | 296 |
297 |
298 |
Amaze UI
299 |
300 | 来来来,吐槽点啥吧 301 | 302 |
303 | 307 |
308 |
309 | 310 | 311 | $(function() { 312 | $('#doc-prompt-toggle').on('click', function() { //在这里设定上面的按钮的点击函数 313 | $('#my-prompt').modal({ //显示ID为my-prompt的窗口 314 | relatedTarget: this, 315 | onConfirm: function(e) { //窗口的确定按钮的响应事件 316 | alert('你输入的是:' + e.data || '') 317 | }, 318 | onCancel: function(e) { //取消按钮的响应事件 319 | alert('不想说!'); 320 | } 321 | }); 322 | }); 323 | }); 324 | ``` 325 | 326 | 关于上面的相关代码,我们需要引入妹子UI后才能使用!!!接下来我们需要改造成符合我们实际需求的界面,如下: 327 | ``` 328 | 329 |
330 |
331 |
用户信息修改
332 |
333 |
334 | 姓名: 335 | 336 | 337 | 性别: 338 | 339 | 340 | 手机号: 341 | 342 | 343 | 年龄: 344 | 345 | 346 | 头像: 347 |
348 | 349 | 351 | 352 |
353 |
354 |
355 | 359 |
360 |
361 | 362 | 363 | var fileName; 364 | 365 | function uploadFile() { 366 | //这里应该加入Loading 窗口开启 367 | fileName = document.getElementById('changeHeadPic').value; 368 | $.ajaxFileUpload({ 369 | url: "<%=request.getContextPath()%>/userAction/uploadHeadPic", 370 | secureuri: false, //是否需要安全协议,一般设置为false 371 | fileElementId: 'changeHeadPic', //文件上传域的ID 372 | dataType: 'json', //返回值类型 一般设置为json 373 | contentType: "application/x-www-form-urlencoded; charset=utf-8", 374 | success: function (data) { 375 | alert(data.msg); 376 | //先根据返回的code确定文件是否上传成功 377 | //文件上传失败,直接弹出错误提示,根据错误进行相应的事物处理(关闭Loading窗口,弹出提示对话框) 378 | //文件上传成功后,继续现实loading窗口,接着执行上传表单信息等事物 379 | } 380 | 381 | }); 382 | } 383 | 384 | function changeUserInfo() { //显示个人信息修改窗口 385 | $('#my-prompt').modal({ 386 | relatedTarget: this, 387 | onConfirm: function () { 388 | uploadFile(); //调用上面的文件上传函数 389 | }, 390 | onCancel: function (e) { 391 | } 392 | }); 393 | } 394 | 395 | 396 | ``` 397 | 398 | 上面的代码,我们完成了控制窗口显示的函数,完成了修改个人信息界面的构建。现在我们需要的是找到执行程序入口。按照我的习惯,肯定是找到头像控件,然后设置点击事件为上面的changeUserInfo()。实现如下: 399 | 400 | ``` 401 | 402 |
403 | user-img 405 |
406 | 407 |
408 |
409 | ``` 410 | 411 | 好的,上面我们可以看到我的前端界面代码基本上完成了。接下来,我们需要在我们后端上面写上对应的程序接口,实现功能即可。 412 | 413 | 本来计划文件上传单独使用commons-fileupload和commons-io完成的,毕竟这是在Servelt上面的老套路,但是我发现Spring里面已经考虑到这一点,有新的东西来完成,所以就使用了Spring的实现方式。具体代码如下: 414 | ``` 415 | //我们在UserController这个控制器里添加这个方法 416 | @RequestMapping(value = "/uploadHeadPic" 417 | , method = RequestMethod.POST 418 | , produces = "application/json; charset=utf-8") 419 | @ResponseBody 420 | public Object uploadHeadPic(@RequestParam(required = false) MultipartFile file, HttpServletRequest request) { 421 | //在这里面文件存储的方案一般是:收到文件→获取文件名→在本地存储目录建立防重名文件→写入文件→返回成功信息 422 | //如果上面的步骤中在结束前任意一步失败,那就直接失败了。 423 | if (null == file || file.isEmpty()) { 424 | responseObj = new ResponseObj(); 425 | responseObj.setCode(ResponseObj.FAILED); 426 | responseObj.setMsg("文件不能为空"); 427 | return new GsonUtils().toJson(responseObj); 428 | } 429 | responseObj = new ResponseObj(); 430 | responseObj.setCode(ResponseObj.OK); 431 | responseObj.setMsg("文件长度为:" + file.getSize()); 432 | return new GsonUtils().toJson(responseObj); 433 | } 434 | ``` 435 | 完成了上面的方法后,我们觉得应该是没问题了,毕竟这样一个接口来接受请求是没问题的嘛,是的,我也是这么认为的。 436 | 437 | 但是现实的打脸是很严重的,因为按照这么写后,**我无论如何都收不到文件(文件一直为null)**,Why?我的思路是正确的啊。经过我的仔细查找,发现我的Spring的配置文件中,没有添加文件的支持设置,所以我们又得补充配置文件,spring-web.xml新增配置如下: 438 | ``` 439 | 440 | 441 | 442 | 443 | ``` 444 | 445 | 经过上面的一番折腾,我们发现框架这个东西,也不是一劳永逸的,毕竟很多东西需要不断的增加。 446 | 447 | 总结: 448 | - 任何东西都需要根据需求不断的变化,可增可减,张弛有度。 449 | - Spring接收文件上传时,Controller的具体方法的参数前面插入注解,同时数据类型是MultipartFile。 450 | - 引入第三方资源的时候,必须查看文档,根据说明文档好办事。 451 | - js进行前端流程控制,后端接口隔离,前后端解耦。 452 | ---- 453 | 前两天刮台风,停电导致数据丢失,是个很尴尬的事情,拖慢了进度。同时,朋友遇到点问题,我在开导他。文章写到现在也是凌晨4点过了,本期计划的列表分页也没做,很对不起大家对我的期待。真诚的说一声:对不起。对不起你们的期待。 454 | 455 | 给朋友开导的时候,我也总结了下做人做事:**随心、追梦、勇敢、独行**。希望有心做事的,都用这几句话勉励自己吧。 456 | 457 | 前行的路上不只是孤独,还有满山的鲜花,更有远方和诗。 458 | 459 | ---- 460 | #### 下期预告: 461 | - 列表分页 462 | - 简易用户角色控制 463 | - 拦截器的使用 464 | -------------------------------------------------------------------------------- /readme20161026: -------------------------------------------------------------------------------- 1 | #### 优雅的SpringMvc+Mybatis应用(五) 2 | 3 | 转眼间文章已经到了第五期了。这段时间一直在考虑考研的事情,周一请老师吃饭还喝醉了,到家就直接睡着了。十分抱歉。 4 | 5 | 本来这次是攒着劲头要写很多东西,但是十分尴尬的事情是最近身边的事情太多了,十分抱歉,该做的事情,我一定不会推诿的,大家理解一下。谢谢你们的支持。 6 | 7 | 感谢热心亲们的支持,你们的鼓励我才走到今天,很多时候感觉自己写代码还是经常踩坑,所以思前想后,问了些朋友。大家都让我建群,一起交流才是最快的进步。所以我们的QQ群开通了,扫描二维码: 8 | 9 | ![行走的java全栈](http://acheng1314.cn/wp-content/uploads/2016/10/行走的java全栈群二维码.png) 10 | 11 | #### 工具 12 | - IDE为**idea16** 13 | - JDK环境为**1.8** 14 | - ~~maven版本为**maven3**~~ 15 | - gradle构建,版本:2.14.1 16 | - Mysql版本为**5.5.27** 17 | - Tomcat版本为**7.0.52** 18 | - 流程图绘制(xmind) 19 | 20 | #### 本期目标 21 | - 简单更改项目的构建工具 22 | - 列表分页 23 | - 简易用户角色控制 24 | - 拦截器的使用 25 | 26 | #### 简单更改项目的构建工具 27 | 最近发现电脑突然变得很卡,所以专门把系统清理,顺带把Idea升级为16。接着事情就来了,发现在idea16里面能把项目升级为gradle构建,一番升级后,项目突然不能运行==,那么问题来了。整了半天也没发现问题所在,百度、谷歌也没找到答案,然后放出大招: 28 | 29 | - 新建同样包名和项目名称的gradle构建的web工程 30 | - 把上一个项目中gradle自动生成的build.gradle中的支援库代码拷贝下来,如下: 31 | ``` 32 | compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.1' 33 | compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.6' 34 | compile group: 'com.alibaba', name: 'druid', version: '1.0.25' 35 | compile group: 'org.mybatis', name: 'mybatis', version: '3.4.1' 36 | compile group: 'org.mybatis', name: 'mybatis-spring', version: '1.3.0' 37 | compile group: 'taglibs', name: 'standard', version: '1.1.2' 38 | compile group: 'jstl', name: 'jstl', version: '1.2' 39 | compile group: 'com.google.code.gson', name: 'gson', version: '2.7' 40 | compile group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0' 41 | compile group: 'org.springframework', name: 'spring-core', version: '4.3.2.RELEASE' 42 | compile group: 'org.springframework', name: 'spring-beans', version: '4.3.2.RELEASE' 43 | compile group: 'org.springframework', name: 'spring-context', version: '4.3.2.RELEASE' 44 | compile group: 'org.springframework', name: 'spring-jdbc', version: '4.3.2.RELEASE' 45 | compile group: 'org.springframework', name: 'spring-tx', version: '4.3.2.RELEASE' 46 | compile group: 'org.springframework', name: 'spring-web', version: '4.3.2.RELEASE' 47 | compile group: 'org.springframework', name: 'spring-webmvc', version: '4.3.2.RELEASE' 48 | compile group: 'org.springframework', name: 'spring-test', version: '4.3.2.RELEASE' 49 | compile group: 'redis.clients', name: 'jedis', version: '2.7.3' 50 | compile group: 'com.dyuproject.protostuff', name: 'protostuff-core', version: '1.0.8' 51 | compile group: 'com.dyuproject.protostuff', name: 'protostuff-runtime', version: '1.0.8' 52 | compile group: 'commons-collections', name: 'commons-collections', version: '3.2.2' 53 | compile group: 'commons-fileupload', name: 'commons-fileupload', version: '1.3.2' 54 | compile group: 'commons-io', name: 'commons-io', version: '2.5' 55 | runtime group: 'mysql', name: 'mysql-connector-java', version: '5.1.37' 56 | //关于具体的gradle构建,请百度一下。 一般来说在maven仓库里面有gradle引用的代码的,直接拷贝就行 57 | //可能我们的gradle构建包很难下载,建议直接用迅雷去下载对应的gradle版本,然后把gradle的压缩包放到我们程序的下载目录就可以了。 58 | ``` 59 | 60 | 等项目构建完成后,我们把以前的项目直接把源码和资源文件什么的都直接拷贝过来,对应着目录就行了,然后接着build项目,最后就可以了。 61 | 62 | #### 角色控制 63 | 64 | 前面和读者交流了一下,他们建议在项目中加入角色控制。好吧,我们先看看简单的角色控制。 65 | 66 | 角色控制,顾名思义就是不同的用户角色依赖于他的身份属性而做出具体的行为控制。具体的行为控制,往往取决于用户界面的操作。而还有一些更加复杂的东西,我们在以后的项目实战中再来阐述。 67 | 68 | 在这个项目中,我们用户的角色分为以下几类: 69 | - 超级管理员 70 | - 出入库人员 71 | - 仓库管理人员 72 | - 台账管理(账单汇总) 73 | 74 | 按照我们以前开发的一些经验来说,我们大概解决方法有下面的几种: 75 | 76 | | 用户访问路径 | 举例说明 | 77 | | ---- | ----| 78 | | 前端页面设定路径 | 网页顶部的导航栏的操作跳转 | 79 | | 后端接口控制路径 | 不同职位的用户登录后显示的界面 | 80 | 81 | 前端界面设置路径,也就是一个a标签写入地址为:http://acheng1314.cn 这样就能实现跳转。 82 | 83 | 后端接口控制路径,我们还是拿登录接口来说,我们在用户类中加入一个nextUrl字段,说明是下一步的地址,然后我们在控制器中完成写入,如下: 84 | 85 | ``` 86 | /** 87 | * 用户请求相关控制器 88 | *
Created by acheng on 2016/9/26. 89 | */ 90 | @Controller 91 | @RequestMapping("/userAction") 92 | public class UserController { 93 | 94 | @Autowired 95 | private UserServiceImpl userService; //自动载入Service对象 96 | private ResponseObj responseObj; 97 | 98 | /** 99 | * 为什么返回值是一个ModelAndView,ModelAndView代表一个web页面
100 | * setViewName是设置一个jsp页面的名称 101 | * 102 | * @param response http响应 103 | * @param user 发起请求后,spring接收到请求,然后封装的bean数据 104 | * @return 返回一个web页面 105 | * @throws Exception 106 | */ 107 | @RequestMapping(value = "/reg" 108 | , method = RequestMethod.POST 109 | , produces = "application/json; charset=utf-8") 110 | @ResponseBody 111 | public Object reg(HttpServletRequest request, HttpServletResponse response, User user, HttpSession session) throws Exception { 112 | Object result; 113 | responseObj = new ResponseObj(); 114 | if (null == user) { 115 | responseObj.setCode(ResponseObj.FAILED); 116 | responseObj.setMsg("用户信息不能为空!"); 117 | result = new GsonUtils().toJson(responseObj); 118 | return result; 119 | } 120 | if (StringUtils.isEmpty(user.getLoginId()) || StringUtils.isEmpty(user.getPwd())) { 121 | responseObj.setCode(ResponseObj.FAILED); 122 | responseObj.setMsg("用户名或密码不能为空!"); 123 | result = new GsonUtils().toJson(responseObj); 124 | return result; 125 | } 126 | if (null != userService.findUser(user)) { 127 | responseObj.setCode(ResponseObj.FAILED); 128 | responseObj.setMsg("用户已经存在!"); 129 | result = new GsonUtils().toJson(responseObj); 130 | return result; 131 | } 132 | try { 133 | userService.add(user); 134 | } catch (Exception e) { 135 | e.printStackTrace(); 136 | responseObj.setCode(ResponseObj.FAILED); 137 | responseObj.setMsg("其他错误!"); 138 | result = new GsonUtils().toJson(responseObj); 139 | return result; 140 | } 141 | responseObj.setCode(ResponseObj.OK); 142 | responseObj.setMsg("注册成功"); 143 | user.setPwd(session.getId()); //单独设置密码为sessionId 误导黑客,前端访问服务器的时候必须有这个信息才能操作 144 | user.setNextUrl(request.getContextPath() + "/mvc/home"); //单独控制地址 145 | responseObj.setData(user); 146 | session.setAttribute("userInfo", user); //将一些基本信息写入到session中 147 | result = new GsonUtils().toJson(responseObj); 148 | return result; 149 | } 150 | 151 | /** 152 | * 登录接口 153 | * 154 | * @param request 155 | * @param user 156 | * @return 157 | */ 158 | @RequestMapping(value = "/login" 159 | , method = RequestMethod.POST 160 | , produces = "application/json; charset=utf-8") 161 | @ResponseBody 162 | public Object login(HttpServletRequest request, HttpServletResponse response, User user, HttpSession session) throws Exception { 163 | Object result; 164 | if (null == user) { 165 | responseObj = new ResponseObj(); 166 | responseObj.setCode(ResponseObj.EMPUTY); 167 | responseObj.setMsg("登录信息不能为空"); 168 | result = new GsonUtils().toJson(responseObj); 169 | return result; //返回json 170 | } 171 | if (StringUtils.isEmpty(user.getLoginId()) || StringUtils.isEmpty(user.getPwd())) { 172 | responseObj = new ResponseObj(); 173 | responseObj.setCode(ResponseObj.FAILED); 174 | responseObj.setMsg("用户名或密码不能为空"); 175 | result = new GsonUtils().toJson(responseObj); 176 | return result; 177 | } 178 | //查找用户 179 | User user1 = userService.findUser(user); 180 | if (null == user1) { 181 | responseObj = new ResponseObj(); 182 | responseObj.setCode(ResponseObj.EMPUTY); 183 | responseObj.setMsg("未找到该用户"); 184 | result = new GsonUtils().toJson(responseObj); 185 | } else { 186 | if (user.getPwd().equals(user1.getPwd())) { 187 | user1.setPwd(session.getId()); 188 | user1.setNextUrl(request.getContextPath() + "/mvc/home"); 189 | responseObj = new ResponseObj(); 190 | responseObj.setCode(ResponseObj.OK); 191 | responseObj.setMsg(ResponseObj.OK_STR); 192 | responseObj.setData(user1); 193 | session.setAttribute("userInfo", user1); 194 | result = new GsonUtils().toJson(responseObj); 195 | } else { 196 | responseObj = new ResponseObj(); 197 | responseObj.setCode(ResponseObj.FAILED); 198 | responseObj.setMsg("用户密码错误"); 199 | result = new GsonUtils().toJson(responseObj); 200 | } 201 | } 202 | return result; 203 | } 204 | 205 | } 206 | 207 | ``` 208 | 209 | 我们前端接收到具体的json数据后,我们解析json数据,并实现相关功能,如下: 210 | ``` 211 | function webLogin() { 212 | if (checkLoginInfo()) { 213 | var loginname = $("#u").val(); 214 | var password = $("#p").val(); 215 | $.ajax({ 216 | type: "POST", 217 | url: '<%=request.getContextPath()%>/userAction/login', 218 | data: {loginId: loginname, pwd: password}, 219 | dataType: 'json', //当这里指定为json的时候,获取到了数据后会自己解析的,只需要 返回值.字段名称 就能使用了 220 | cache: false, 221 | success: function (data) { 222 | if (data.code == 1) { 223 | window.location.href = data.data.nextUrl; //这里拿到服务器返回的地址,然后进行跳转操作 224 | } else { 225 | alert(data.msg); 226 | $("#u").focus(); 227 | } 228 | } 229 | }); 230 | } 231 | } 232 | 233 | //其他前端的相关代码省略,具体详情,请查看我们的放出的源码包。 234 | 235 | ``` 236 | 237 | #### 拦截器的使用 238 | 拦截器的详细介绍,[请参阅这里](http://blog.csdn.net/tonytfjing/article/details/39207551),为了节省篇幅,本文不再介绍,只是放出如何使用。 239 | 240 | 首先,在我们的spring-web.xml中配置拦截器,整体代码如下: 241 | ``` 242 | 243 | 253 | 254 | 255 | 259 | 260 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | ``` 298 | 具体的拦截器代码如下,因为我们已经在代码中加入了注释,所以不需要再次说明什么东西之类的。 299 | ``` 300 | package cn.acheng1314.intercepter; 301 | 302 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 303 | 304 | import javax.servlet.http.HttpServletRequest; 305 | import javax.servlet.http.HttpServletResponse; 306 | import java.io.IOException; 307 | 308 | public class LoginHandlerInterceptor extends HandlerInterceptorAdapter { 309 | String NO_INTERCEPTOR_PATH = ".*/((login)|(reg)|(logout)|(code)|(app)|(weixin)|(static)|(main)|(websocket)).*"; //不对匹配该值的访问路径拦截(正则) 310 | 311 | @Override 312 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 313 | // TODO Auto-generated method stub 314 | String path = request.getServletPath(); 315 | if (path.matches(NO_INTERCEPTOR_PATH)) { //匹配正则表达式的不拦截 316 | return true; 317 | } else { //不匹配的进行处理 318 | try { 319 | if (request.getSession().getAttribute("userInfo") == null) { //session中是否存在用户信息,不存在则是未登录状态 320 | response.sendRedirect(request.getContextPath() + "/mvc/login"); 321 | return false; 322 | } 323 | } catch (IOException e) { 324 | response.sendRedirect(request.getContextPath() + "/mvc/login"); 325 | e.printStackTrace(); 326 | return false; 327 | } 328 | } 329 | return true; //默认是不拦截···当然具体的还看一些需求设计啊 之类的 330 | } 331 | 332 | } 333 | 334 | ``` 335 | 336 | 看了下时间,已经是四点过了,今天只能先这样,这两天补一篇列表分页。这段时间很忙,快要毕业了,老师也找做东西,以前的团队项目做不过来也找我,然后还在准备考研的事情咨询,很多很多,望大家理解下,该做的事情,一定会完成,另外以后我们尽量缩短篇幅,但是,更多的是多次发文章,这样的话,相对来说感觉可能稍微舒服点。 337 | 338 | ---- 339 | #### 总结: 340 | - springMvc拦截器的基本使用 341 | - 简单的过滤器可以拦截简单的非法请求,防止越界操作 342 | - gradle比maven更加方便配置 343 | - 角色控制的简单实现方式 344 | -------------------------------------------------------------------------------- /readme20161030_第六期.md: -------------------------------------------------------------------------------- 1 | #### 优雅的SpringMvc+Mybatis应用(六) 2 | 3 | 第六期文章也到了,其实这应该算是第五期续的。毕竟上次的列表分页还没做。 4 | 5 | 后面可能还有几期,我们的仓库管理系统会结束的。按照老规矩,结束的时候肯定有项目总结解析。 6 | 7 | 项目github地址:https://github.com/pc859107393/SpringMvcMybatis 8 | 9 | 我的简书首页是:http://www.jianshu.com/users/86b79c50cfb3/latest_articles 10 | 11 | 上一期是:[优雅的SpringMvc+Mybatis应用(五)](http://www.jianshu.com/p/3c5888f30996) 12 | 13 | 扫描下面二维码加入交流QQ群: 14 | 15 | ![行走的java全栈](http://acheng1314.cn/wp-content/uploads/2016/10/行走的java全栈群二维码.png) 16 | 17 | #### 工具 18 | - IDE为**idea16** 19 | - JDK环境为**1.8** 20 | - gradle构建,版本:2.14.1 21 | - Mysql版本为**5.5.27** 22 | - Tomcat版本为**7.0.52** 23 | - 流程图绘制(xmind) 24 | 25 | #### 本期目标 26 | - 列表分页 27 | 28 | #### 列表分页 29 | 前面很早就说了列表分页,一直没怎么做,这一次就做一个登录的主机信息的分页。 30 | 31 | 首先我们分析一下我们的功能设定: 32 | - 记录基本主机信息 33 | - 操作系统 34 | - IP地址 35 | - 访问的浏览器内核 36 | - Session的Id 37 | - 发生时间 38 | - 其他信息 39 | 40 | 关于上面的东西,我们可以预设很多字段,唯一的注意的地方是我这边把记录行为日志的表做了个自增的ID。我这边是所有的请求目前都是加入了信息记录,实际项目中可不能这么搞(根据需求搞事情,搞事情,搞事情)。 41 | 42 | 既然我们都说过我们是记录用户登录信息的列表,那么我们需要先获取用户的请求内部相关的信息。也就是说我们的这个信息获取是基于用户请求设定的,那么解决思路就是从拦截器来实现。 43 | 44 | 既然上面我们分析了需要记录的信息,那么接着的思路应该是什么呢? 45 | - 根据数据需求建表 46 | - 完成存储数据业务 47 | - Dao → Service → HandlerInterceptor(拦截器) 48 | - 单元测试 49 | - 因为我前面提过,我们尽量把service作为简单的数据驱动,也就是说不涉及到复杂事务,我们尽量写简单的service。 50 | - 简单的service情况下, 我们一个service对应一个dao,则一定程度上可以少写一点单元测试==、 51 | 52 | 日志获取大概流程图如下所示: 53 | 54 | ![ssm应用六-后台主页-日志采集流程](http://acheng1314.cn/wp-content/uploads/2016/10/ssm应用六-后台主页-日志采集流程.png) 55 | 56 | 日志列表输出大概流程图如下所示: 57 | 58 | ![ssm应用六-后台主页-分页列表流程](http://acheng1314.cn/wp-content/uploads/2016/10/ssm应用六-后台主页-分页列表流程.png) 59 | 60 | 具体的一些细节的东西没必要追究,先把数据库表建立起来,数据库:warehouse,建表代码如下: 61 | ``` 62 | CREATE TABLE `user_action_log` ( 63 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID', 64 | `login_id` varchar(20) DEFAULT NULL COMMENT '登录ID', 65 | `session_id` varchar(45) NOT NULL COMMENT '访问session的ID\r\n', 66 | `time` datetime DEFAULT NULL COMMENT '操作时间', 67 | `ip_addr_v4` varchar(15) DEFAULT NULL COMMENT 'ipV4地址', 68 | `ip_addr_v6` varchar(128) DEFAULT NULL COMMENT 'ipv6地址\r\n', 69 | `os_name` varchar(20) DEFAULT NULL COMMENT '操作系统名称', 70 | `os_version` varchar(20) DEFAULT NULL, 71 | `bro_name` varchar(20) DEFAULT NULL COMMENT '浏览器名称', 72 | `bro_version` varchar(20) DEFAULT NULL COMMENT '浏览器版本', 73 | `request_body` varchar(60) DEFAULT NULL COMMENT '请求体信息', 74 | `description` varchar(100) DEFAULT NULL COMMENT '操作描述', 75 | `other` varchar(150) DEFAULT NULL COMMENT '其他描述', 76 | `method` varchar(10) DEFAULT NULL COMMENT 'HTTP请求方法', 77 | PRIMARY KEY (`id`) 78 | ) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8 COMMENT='行为日志表'; 79 | ``` 80 | 81 | 我们数据库建表完成后,只需要写对应的Dao层,按照我们前面的思路,那就是对应着写Bean → Dao → Service。然后Service提供给其他地方调用。 82 | 83 | 既然如此,我们先实现我们的Bean和Dao层,因为我们在项目中使用了Mybatis这个持久化框架,所以我们需要去实现Mybatis的Mapper文件对应Dao层的接口就行,具体的代码如下: 84 | 85 | ``` 86 | 87 | public class UserActionLog implements Serializable { 88 | 89 | private long id; 90 | private String loginId, sessionId, ipAddrV4, ipAddrV6, osName, osVersion, broName, broVersion, requestBody, description, other, method; 91 | private Date time; 92 | //省略get和set以及toString方法 93 | } 94 | 95 | 96 | public interface ActionLogDao extends Dao { 97 | 98 | int add(UserActionLog userActionLog); 99 | 100 | UserActionLog findOneById(Serializable Id); 101 | 102 | /** 103 | * 分页查询 104 | * @param offset 起始位置 105 | * @param limit 每页数量 106 | * @return 107 | */ 108 | List findAll(@Param("offset") int offset, @Param("limit") int limit); 109 | } 110 | 111 | 112 | 113 | 115 | 116 | 117 | 118 | INSERT INTO 119 | `user_action_log` 120 | (`login_id`,`session_id`,`time`,`ip_addr_v4`,`ip_addr_v6`,`os_name`,`os_version`,`bro_name`,`bro_version`,`request_body`,`description`,`other`,`method`) 121 | VALUES 122 | (#{loginId},#{sessionId},#{time},#{ipAddrV4},#{ipAddrV6},#{osName},#{osVersion},#{broName},#{broVersion},#{requestBody},#{description},#{other},#{method}) 123 | 124 | 125 | 126 | 136 | 137 | ``` 138 | 139 | 上面我们可以看到Dao层基本上是简单的插入和分页查询,因为使用了Mysql数据库,所以我们使用 Select···Limit 起始位置,每页数量 这种方式进行列表查询。因为我们是需要查看最近信息,所以查询列表的数据是倒序排列的。 140 | 141 | 现在我们的Dao层完成了,我们需要接着完成Service层实现外层调用的接口。Service层代码如下: 142 | ``` 143 | @Service("actionLogService") 144 | public class ActionLogServiceImpl implements ActionLogService { 145 | 146 | @Autowired 147 | private ActionLogDao actionLogDao; 148 | 149 | private UserActionLog userActionLog; 150 | 151 | public void add(HttpServletRequest request) { 152 | //获取请求参数集合 153 | Map params = request.getParameterMap(); 154 | String queryString = ""; 155 | for (String key : params.keySet()) { 156 | String[] values = params.get(key); 157 | for (int i = 0; i < values.length; i++) { 158 | String value = values[i]; 159 | queryString += key + "=" + value + "&"; 160 | } 161 | } 162 | 163 | 164 | userActionLog = new UserActionLog(); 165 | userActionLog.setMethod(request.getMethod()); //获取请求方式 166 | if (request.getHeader("x-forwarded-for") == null) { //获取请求IP 167 | userActionLog.setIpAddrV4(request.getRemoteAddr()); 168 | } else { 169 | userActionLog.setIpAddrV4(request.getHeader("x-forwarded-for")); 170 | } 171 | userActionLog.setOther(request.getHeader("User-Agent")); //获取user-agent 172 | userActionLog.setSessionId(request.getSession().getId()); //获取用户操作的sessionID,必须 173 | userActionLog.setDescription(request.getRequestURI()); //获取访问的地址 174 | if (!StringUtils.isEmpty(queryString)) userActionLog.setRequestBody(queryString); //参数集合内容不为空存入数据库 175 | 176 | try { 177 | UserAgent agent = new UserAgent(request.getHeader("User-Agent")); //载入user-agent 178 | userActionLog.setOsName(agent.getOperatingSystem().getName()); //设定os名称 179 | userActionLog.setBroName(StringUtils.isEmpty(agent.getBrowser().getName()) ? "" : agent.getBrowser().getName()); //设定浏览器名称 180 | userActionLog.setBroVersion(StringUtils.isEmpty(agent.getBrowserVersion().getVersion()) ? "" : agent.getBrowserVersion().getVersion()); //设定浏览器版本 181 | } catch (Exception e) { 182 | e.printStackTrace(); 183 | } finally { 184 | actionLogDao.add(userActionLog); //UserAgent信息能否获取到,我们都需要存入数据库。 185 | } 186 | 187 | } 188 | 189 | @Deprecated 190 | public void add(UserActionLog userActionLog) throws Exception { 191 | //其实在这里我们应该直接调用这个方法来实现功能。毕竟我们的原则是Service层是数据驱动服务。但是我们在这里写,也能实现功能 192 | } 193 | 194 | public List findAll(int pageNum, int pageSize) { 195 | //因为数据库内容是从第一条出的数据,所以我们查询的 起始位置 = (页码-1) * 条数 + 1; 196 | pageNum -= 1; 197 | return actionLogDao.findAll(pageNum * pageSize + 1, pageSize); 198 | } 199 | } 200 | 201 | ``` 202 | 好了,到现在我们的ActionLogServiceImpl也完成了。按照我们的需求来说,我们要在所有的请求上面加上一层访问日志监控,同时我们需要对外提供接口方便我们的查看数据。具体代码如下: 203 | ``` 204 | 205 | public class LoginHandlerInterceptor extends HandlerInterceptorAdapter { 206 | String NO_INTERCEPTOR_PATH = ".*/((login)|(reg)|(logout)|(code)|(app)|(weixin)|(static)|(main)|(websocket)).*"; //不对匹配该值的访问路径拦截(正则) 207 | @Autowired 208 | ActionLogServiceImpl service; 209 | 210 | @Override 211 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 212 | // TODO Auto-generated method stub 213 | String path = request.getServletPath(); 214 | if (!path.matches(".*/((static)|(login)|(reg)).*")) service.add(request); //不包含静态资源和登陆注册的请求 215 | if (path.matches(NO_INTERCEPTOR_PATH)) { //匹配正则表达式的不拦截 216 | return true; 217 | } else { //不匹配的进行处理 218 | try { 219 | if (request.getSession().getAttribute("userInfo") == null) { //session中是否存在用户信息,不存在则是未登录状态 220 | response.sendRedirect(request.getContextPath() + "/mvc/login"); 221 | return false; 222 | } 223 | } catch (IOException e) { 224 | response.sendRedirect(request.getContextPath() + "/mvc/login"); 225 | e.printStackTrace(); 226 | return false; 227 | } 228 | } 229 | return true; //默认是不拦截···当然具体的还看一些需求设计之类的 230 | } 231 | } 232 | 233 | 234 | @Controller 235 | @RequestMapping("/actionLog") 236 | public class ActionLogController { 237 | @Autowired 238 | ActionLogService actionLogService; 239 | 240 | @RequestMapping(value = "/findLogList" 241 | , produces = "application/json; charset=utf-8") 242 | @ResponseBody 243 | public Object findLog(@Param("pageNum") int pageNum, @Param("pageSize") int pageSize) { 244 | if (pageNum <= 0) { //错误页码默认跳转到第一页 245 | pageNum = 1; 246 | } 247 | if (pageSize <= 0) { //错误数据长度默认设置为10条 248 | pageSize = 10; 249 | } 250 | 251 | List result = actionLogService.findAll(pageNum, pageSize); 252 | ResponseObj responseObj = new ResponseObj(); 253 | if (result == null || result.size() == 0) { 254 | responseObj.setCode(ResponseObj.EMPUTY); 255 | responseObj.setMsg("查询结果为空"); 256 | return new GsonUtils().toJson(responseObj); 257 | } 258 | responseObj.setCode(ResponseObj.OK); 259 | responseObj.setMsg("查询成功"); 260 | responseObj.setData(result); 261 | return new GsonUtils().toJson(responseObj); 262 | } 263 | } 264 | 265 | ``` 266 | 267 | 上面设置完成后,我们运行项目,先输入错误的网址,大家会看到它先跳转到登录界面(因为现在的用户信息位空,所以默认需要用户登录),登录成功后,我们在浏览器中输入: 268 | ``` 269 | http://localhost:8080/actionLog/findLogList?pageNum=1&pageSize=10 270 | //因为我在Controller中没有配置具体的请求方法,那么我们这里gte和post都可以获取数据 271 | ``` 272 | 可以得到返回的json数据: 273 | ``` 274 | { 275 | "code": 1, 276 | "msg": "查询成功", 277 | "data": [ 278 | { 279 | "id": 212, 280 | "sessionId": "A65E46FA47CBA4385FEA67594632FE2A", 281 | "ipAddrV4": "127.0.0.1", 282 | "osName": "Windows 10", 283 | "broName": "Microsoft Edge 14", 284 | "broVersion": "14.14393", 285 | "description": "/mvc/home", 286 | "other": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393", 287 | "method": "GET" 288 | } 289 | ] 290 | } 291 | ``` 292 | 293 | 然后我们可以设置其他的页码和条数测试,均可以通过,所以我们的分页的接口已经完成。但是,我们这样直接显示json肯定是不友好的,我们还需要找个地方显示,我把它显示在首页的默认位置,下一期再单独拿出一个页面来实现,具体如图: 294 | ![ssm应用六-后台主页-最近访问列表截图](http://acheng1314.cn/wp-content/uploads/2016/10/ssm应用六-后台主页-最近访问列表截图.png) 295 | 296 | 首先我们需要先把以前列表找到,在home.jsp中如下: 297 | ``` 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | ``` 327 | 这就是以前的代码片段,我们不需要说什么精通前端,但是最基本的能看懂,能百度到解决方案也是不错的。 328 | 329 | **注意:由于妹子UI封装了列表,然后我在js中要追加列表内容的时候,始终找不到Body一直报错null。解决办法是给tbody加上ID,然后直接给它追加内容。** 330 | 331 | 我们先删除原来tbody的内容,然后对应着格式js添加,如下: 332 | ``` 333 | $("#log-table-body").append( //log-table-body是我们列表的body的ID 334 | ""); 340 | //注意:格式(标签结构)一定要和以前的一样,否则列表会走样的== 341 | ``` 342 | 我们知道应该怎么追加列表条目了,现在我们需要的是实现追加。按照代码结构观察我们可以发现,我们要想实现数据自动装载到页面上面,我们需要让程序顺序执行就对了。但是前面我们的JS是写在头部的,如果说自动执行肯定会找不到控件,所以我们需要让自动加载在页面完成后加载。如下: 343 | ``` 344 | 345 | 346 | 366 | ``` 367 | ---- 368 | 总结: 369 | - 日志记录 370 | - 列表输出 371 | - 数据库查询分页 372 | - web页面追加数据 373 | - js位置对web页面的影响 374 | 375 | ---- 376 | 仓库管理系统,应该快要完结了,后面做完整的博客后端+web前端(模版)+Android客户端。现在的仓库管理系统可以明显看到是为了实现而实现,至于所谓的程序设计,还有很多地方没使用。后面开发的时候,尽量使用程序设计的模式来实现。后面会专门针对这个仓库管理系统进行总结,好的不好的,都要一一找出来,在博客系统的实现上面尽量简单优越。 377 | 378 | 只有不断的审视自己,才能找到自己的不足,并且长期前进。 379 | 380 | 2016-10-30 381 | -------------------------------------------------------------------------------- /readme20161109第七期.md: -------------------------------------------------------------------------------- 1 | [手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(七) 2 | 3 | 4 | 项目github地址:https://github.com/pc859107393/SpringMvcMybatis 5 | 6 | 我的简书首页是:http://www.jianshu.com/users/86b79c50cfb3/latest_articles 7 | 8 | 上一期是:[优雅的SpringMvc+Mybatis应用(六)](http://www.jianshu.com/p/e94541db9901) 9 | 10 | 扫描下面二维码加入交流QQ群: 11 | 12 | ![行走的java全栈](http://acheng1314.cn/wp-content/uploads/2016/10/行走的java全栈群二维码.png) 13 | 14 | #### 工具 15 | - IDE为**idea16** 16 | - JDK环境为**1.8** 17 | - gradle构建,版本:2.14.1 18 | - Mysql版本为**5.5.27** 19 | - Tomcat版本为**7.0.52** 20 | - 流程图绘制(xmind) 21 | 22 | #### 本期目标 23 | - 完整列表分页 24 | 25 | #### 完整分页列表界面 26 | 其实分页列表也没什么,重点在于做出**列表局部刷新,减少页面请求**。 27 | 28 | 我们先要新建一个页面用来显示列表,由于我们的后台网页结构基本已经固定,所以我们在后台主页那边设定一个访问入口,然后链接上我们的网页。这里我把左边的一个菜单改成了列表,具体效果如图: 29 | ![ssm应用七-访问列表-分页列表](http://acheng1314.cn/wp-content/uploads/2016/11/ssm应用七-访问列表-分页列表.png) 30 | 31 | 在上一期结束后,我已经在列表的返回数据中加入了总页码和当前页码。这是返回的json数据: 32 | ``` 33 | { 34 | "code": 1, 35 | "msg": "查询成功", 36 | "data": [ 37 | { 38 | "id": 713, 39 | "sessionId": "35B4776D32F8E12679FBC8F45A11F8F1", 40 | "ipAddrV4": "127.0.0.1", 41 | "osName": "Windows 10", 42 | "broName": "Microsoft Edge 14", 43 | "broVersion": "14.14393", 44 | "description": "/mvc/listActionLog", 45 | "other": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393", 46 | "method": "GET" 47 | } 48 | ], 49 | "pageNum": 1, 50 | "pageSize": 15, 51 | "totalNum": 44 52 | } 53 | ``` 54 | 55 | 具体的流程图如下: 56 | 57 | ![ssm应用七-访问列表-流程图](http://acheng1314.cn/wp-content/uploads/2016/11/ssm应用七-访问列表-流程图.png) 58 | 59 | 按照上面我的截图中,我们可以看到我们只需要把上一页和下一页的ajax调用写好就能完成我们这个简单分页的网络请求。 至于第一页和最后一页的按钮事件也就是把页码设定为1和最后一页。那我们先看看下一页的js调用: 60 | ``` 61 | var pageNum; //页码变量 62 | function goToNextPage() { 63 | pageNum = parseInt(pageNum) + 1; //这里必须用parseInt(pageNum)这样才能拿到int型值,否则这里拿出来的是字符串 64 | $.ajax({ 65 | type: "GET", //后端分页接口这里是没有指定请求方式 66 | url: '/actionLog/findLogList?pageNum=' + pageNum + '&pageSize=15', 67 | dataType: 'json', //当这里指定为json的时候,获取到了数据后会自己解析的,只需要 返回值.字段名称 就能使用了 68 | cache: false, //不适用缓存 69 | success: function (data) { 70 | if (data.code == 1) { 71 | updateList(data); //更新列表界面 72 | pageNum = data.pageNum; 73 | $("#log-controller-now").html(pageNum); //把当前页面输出到网页对应ID的标签上面 74 | } 75 | } 76 | }); 77 | } 78 | ``` 79 | 80 | 从上面我们可以看到,我们的分页列表的请求变化的是页码,然后每一页长度是固定(也可以按照你的喜好来)的,然后我们拿到返回的数据进行加载就行了。 81 | 82 | 既然我们上面已经看到了下一页的界面数据加载了,同理我们可以得出上一页的代码如下: 83 | ``` 84 | function goToLastPage() { 85 | pageNum = parseInt(pageNum) - 1; 86 | $.ajax({ 87 | type: "GET", 88 | url: '/actionLog/findLogList?pageNum=' + pageNum + '&pageSize=15', 89 | dataType: 'json', //当这里指定为json的时候,获取到了数据后会自己解析的,只需要 返回值.字段名称 就能使用了 90 | cache: false, 91 | success: function (data) { 92 | if (data.code == 1) { 93 | updateList(data); 94 | pageNum = data.pageNum; 95 | $("#log-controller-now").html(pageNum); 96 | } 97 | } 98 | }); 99 | } 100 | ``` 101 | 但是我们虽然说js写出来,但是怎么把网页数据加载进去呢???就这一点我踩了4个钟头的坑,然后问了下老司机,然后老司机告诉我**使用模版**进行网页数据的加载。**在这里我们需要引入模版的js,js名称是:doT.min.js**。[模版官方文档](http://dotjs.cn/)。 102 | 103 | 从上面的上一页和下一页访问的js方法中,我们可以看到都使用了updateList(data)这个方法,这个方法就是来加载界面的,具体如下: 104 | ``` 105 | //这一点js我们写在网页的body后面,因为我们网页的列表数据是异步加载的。所以让他直接执行就好了 106 | 127 | ``` 128 | 129 | 引入js后,我们需要开始写网页代码了,先建立一个table,然后写表头(head,因为我使用的是妹子UI,所以我的表头是thead),接着写列表部分(body,妹子UI里列表位tbody),最后再把上一页、下一页和当前页的标签写上,如下: 130 | ``` 131 |
132 |
#项目名称开始时间结束时间状态责任人
1Adminto Admin v101/01/201626/04/2016已发布Coderthemes
2Adminto Frontend v101/01/201626/04/2016已发布Adminto admin
" + data.data[i].id + "" 335 | + data.data[i].ipAddrV4 + "01/01/2016" 336 | + data.data[i].osName + "" 337 | + data.data[i].description + "" 338 | + data.data[i].sessionId + "" 339 | + data.data[i].broName + "
133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 157 | 158 |
idIP地址系统名称访问地址SessionId浏览器名
159 |
160 |
161 | 166 |
167 |
168 | 169 | ``` 170 | 171 | 可能上面的html中混合js代码会麻烦一点,看起来也不是那么清晰,但是我们只要细心体会也是没有问题的。网页中的js操作我都喜欢用标签的onclick属性,个人习惯纯属爱好,勿喷。 172 | 173 | 同时插入一个[web页面优化](http://www.csdn.net/article/2013-09-23/2817020-web-performance-optimization)的文章,有兴趣的可以看看。 174 | 175 | 里面讲解的知识点有: 176 | - Google的Web优化最佳实践 177 | - 雅虎的Web优化最佳实践 178 | - 一些工具 179 | 180 | 虽然说文章很古老了,但是很多原理现在一样通用。 181 | 182 | ---- 183 | 这一期比较短,我这边很多事情花费的时间太多了,今天就先这样。等两天出多角色管理和权限控制。 184 | -------------------------------------------------------------------------------- /readme20161116第八期.md: -------------------------------------------------------------------------------- 1 | [手把手教程][JavaWeb]优雅的SpringMvc+Mybatis应用(八) 2 | 3 | 4 | 项目github地址:https://github.com/pc859107393/SpringMvcMybatis 5 | 6 | 我的简书首页是:http://www.jianshu.com/users/86b79c50cfb3/latest_articles 7 | 8 | 上一期是:[优雅的SpringMvc+Mybatis应用(七)](http://www.jianshu.com/p/def0076976aa) 9 | 10 | 扫描下面二维码加入交流QQ群: 11 | 12 | ![行走的java全栈](http://acheng1314.cn/wp-content/uploads/2016/10/行走的java全栈群二维码.png) 13 | 14 | #### 工具 15 | - IDE为**idea16** 16 | - JDK环境为**1.8** 17 | - gradle构建,版本:2.14.1 18 | - Mysql版本为**5.5.27** 19 | - Tomcat版本为**7.0.52** 20 | - 流程图绘制(xmind) 21 | 22 | #### 本期目标 23 | - 多角色控制思路整理 24 | - 第一季项目总结 25 | 26 | ---- 27 | 28 | ### 多角色控制思路整理 29 | 关于多角色控制,起始用户角色按照用户职能分工,一般来说思路如下: 30 | - 登陆成功根据用户角色,跳转不同的界面模块 31 | - 每个界面模块都有用户权限校验,防止用户逾越雷池一步 32 | - 后端接口需要做用户角色校验,用户异常调用接口,就中断用户访问。 33 | - 后端的web页面根据不同用户分组存放,然后各个用户之间不做关联 34 | ---- 35 | 36 | ### 第一季项目总结 37 | 38 | 第一季的仓库管理系统到目前为止基本上算是结束了。虽然说不是完整的系统,但是我们在里面已经大概把基本的web开发的东西都梳理过了。我们现在来从头到尾的梳理一下吧。从我们项目从头到尾,我们分为几个阶段: 39 | - 项目基本框架选择 40 | - 项目框架整合、验证 41 | - 项目需求分析 42 | - 功能模块开发思路整理 43 | - Spring经典三层应用 44 | - 开发细节思考 45 | - web页面简单优化 46 | - js网络请求实现 47 | - **···** 等等 48 | 49 | ---- 50 | 51 | 项目基本框架选择: 52 | 53 | 说实话这是一个技术活,我们程序员的角度来说无外乎就是实现容易、扩展便捷、运行稳定等等。按照软件工程的话来说就是节约成本、提高质量。 54 | 55 | 说点人话就是:从产品设计到编码,这一切基本都是人的活动,所以我们选择的框架首先要降低学习技术成本、开发技术成本、设计转换编码的成本、不同应用(模块)整合成本等等。 56 | 57 | 再总结一下:我们程序的编码语言应该是我们最熟悉的,应用的各个模块之间交互应该是我们擅长的。 58 | 59 | 所以我选择了java作为主要的编程语言,后端框架就是Spring+SpringMVC+Mybatis,后端数据库为Mysql。 60 | 61 | 项目框架整合、验证: 62 | 63 | 首先我们需要使用构建工具引入Spring+SpringMVC+Mybatis这些框架的jar包,方式有大概两种:①自行下载jar包②使用自动化构建工具完成下载。方式一中需要我们自行选择jar包手动下载然后引入到lib文件夹。方式二我们只需要使用类似代码的方式控制程序自动下载。先进的工具能提高生产力,所以我们选择自动化构建完成jar包引入。 64 | 65 | 第一步,引入jar包,我们在gradle中引入,代码如下: 66 | 67 | ``` 68 | testCompile group: 'junit', name: 'junit', version: '4.11' 69 | compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.1' 70 | compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.6' 71 | compile group: 'com.alibaba', name: 'druid', version: '1.0.25' 72 | compile group: 'org.mybatis', name: 'mybatis', version: '3.4.1' 73 | compile group: 'org.mybatis', name: 'mybatis-spring', version: '1.3.0' 74 | compile group: 'taglibs', name: 'standard', version: '1.1.2' 75 | compile group: 'jstl', name: 'jstl', version: '1.2' 76 | compile group: 'com.google.code.gson', name: 'gson', version: '2.7' 77 | compile group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0' 78 | //Spring 框架基本的核心工具类 79 | compile group: 'org.springframework', name: 'spring-core', version: '4.3.2.RELEASE' 80 | //访问配置文件、创建和管理bean 以及进行Inversion of Control / Dependency Injection(IoC/DI)操作相关的所有类。 81 | //如果应用只需基本的IoC/DI 支持,引入spring-core.jar 及spring-beans.jar 文件就可以了。 82 | compile group: 'org.springframework', name: 'spring-beans', version: '4.3.2.RELEASE' 83 | //Spring 核心提供了大量扩展。可以找到使用Spring ApplicationContext特性时所需的全部类,JDNI 所需的全部类,instrumentation组件以及校验Validation 方面的相关类。 84 | compile group: 'org.springframework', name: 'spring-context', version: '4.3.2.RELEASE' 85 | //Spring 对JDBC 数据访问进行封装的所有类。 86 | compile group: 'org.springframework', name: 'spring-jdbc', version: '4.3.2.RELEASE' 87 | //spring-tx 事务管理 88 | compile group: 'org.springframework', name: 'spring-tx', version: '4.3.2.RELEASE' 89 | //Web 应用开发时,用到Spring 框架时所需的核心类,包括自动载入Web Application Context 特性的类、Struts 与JSF 集成类、文件上传的支持类、Filter 类和大量工具辅助类。 90 | //外部依赖spring-context, Servlet API, (JSP API, JSTL, Commons FileUpload, COS)。 91 | compile group: 'org.springframework', name: 'spring-web', version: '4.3.2.RELEASE' 92 | //Spring MVC 框架相关的所有类。包括框架的Servlets,Web MVC框架,控制器和视图支持。 93 | compile group: 'org.springframework', name: 'spring-webmvc', version: '4.3.2.RELEASE' 94 | compile group: 'org.springframework', name: 'spring-test', version: '4.3.2.RELEASE' 95 | compile group: 'redis.clients', name: 'jedis', version: '2.7.3' 96 | //序列化和反序列化工具 97 | compile group: 'com.dyuproject.protostuff', name: 'protostuff-core', version: '1.0.8' 98 | compile group: 'com.dyuproject.protostuff', name: 'protostuff-runtime', version: '1.0.8' 99 | //文件上传工具类,不过可以使用Spring自带的文件工具 100 | compile group: 'commons-collections', name: 'commons-collections', version: '3.2.2' 101 | compile group: 'commons-fileupload', name: 'commons-fileupload', version: '1.3.2' 102 | compile group: 'commons-io', name: 'commons-io', version: '2.5' 103 | //请求的UserAgent拆装箱工具 104 | compile group: 'eu.bitwalker', name: 'UserAgentUtils', version: '1.20' 105 | runtime group: 'mysql', name: 'mysql-connector-java', version: '5.1.37' 106 | ``` 107 | 第二步,设置各个框架,并保存到配置文件中,我们的WebApp最重要的最基本的配置是web.xml,这里配置了我们基本程序的设定,同样的我们需要在这里导入一些设置,如下: 108 | ``` 109 | 112 | 116 | 117 | index.html 118 | index.htm 119 | index.jsp 120 | default.html 121 | default.htm 122 | default.jsp 123 | 124 | 125 | 126 | 127 | SSM 128 | mvc-dispatcher 129 | org.springframework.web.servlet.DispatcherServlet 130 | 134 | 135 | contextConfigLocation 136 | classpath:spring/spring-*.xml 137 | 138 | 139 | 140 | mvc-dispatcher 141 | 142 | / 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | encodingFilter 152 | org.springframework.web.filter.CharacterEncodingFilter 153 | 154 | encoding 155 | UTF-8 156 | 157 | 158 | 159 | forceEncoding 160 | true 161 | 162 | 163 | 164 | encodingFilter 165 | /* 166 | 167 | 168 | 169 | 170 | DruidStatView 171 | com.alibaba.druid.support.http.StatViewServlet 172 | 173 | 174 | loginUsername 175 | pc859107393 176 | 177 | 178 | 179 | loginPassword 180 | laopo5201314 181 | 182 | 183 | 184 | DruidStatView 185 | /druid/* 186 | 187 | 188 | druidWebStatFilter 189 | com.alibaba.druid.support.http.WebStatFilter 190 | 191 | exclusions 192 | /public/*,*.js,*.css,/druid*,*.jsp,*.swf 193 | 194 | 195 | principalSessionName 196 | sessionInfo 197 | 198 | 199 | profileEnable 200 | true 201 | 202 | 203 | 204 | druidWebStatFilter 205 | /* 206 | 207 | 208 | 209 | 404 210 | /static/view/404.html 211 | 212 | 213 | ``` 214 | 按照上面所说的,我们生成spring文件的存放路径后,我们接着应该做的是控制spring,那么spring的配置文件如下(很多时候我们可以看到他们有的只有一个配置文件,而我将它们拆分成为三个配置文件),具体实现如下: 215 | 216 | - spring-dao.xml配置文件 217 | ``` 218 | 219 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | ``` 303 | 304 | - spring-service.xml配置文件 305 | ``` 306 | 307 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | ``` 335 | 336 | - spring-web.xml配置文件 337 | ``` 338 | 339 | 349 | 350 | 351 | 355 | 356 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | ``` 394 | 395 | 上面的Spring的配置文件我们完成后,我们需要把对应的其他文件配置好,如jdbc、mapper、mybatis等的配置文件,以及开发、测试的代码和资源文件的存放目录等等。这些在我们的第一期就能看到了,这里不再赘述。 396 | 397 | 关于项目框架验证,我们需要在搭建完成后,打开日志调试来看信息,有这几点原则: 398 | - 数据库链接正常 399 | - 数据库驱动、数据库服务 400 | - 数据库配置文件 401 | - 数据库测试 402 | - 网页资源访问正常 403 | - 静态html、js、css、font、image、MP3等 404 | - 动态的接口 405 | - 动态页面如:jsp 406 | - 提示信息正常 407 | - 异常输出 408 | - log输出 409 | - 等等··· 410 | 411 | 具体的检测我们在第二期里面提到过,这里我们也就跳过吧,毕竟主角还在后面。 412 | 413 | 项目需求分析,本身来说也不是我们作为程序员应该考虑的,毕竟涉及到的东西很多,这里我么略过,我们在以后的开发中再提。 414 | 415 | 起始这里我最想说的额就是前面开发的细节,也就是我们当中用到的知识点。按照我们开发的思路来说,我们先从Dao层来实现,来一起看看前面用到的知识点。 416 | 417 | 首先我们需要一个基类的Dao接口,同时我们需要用泛型来解耦,告诉程序我们这里需要的什么样的对象来存入数据库,同时某些对象特有的方法那么就在该对象的自身的接口中实现。我们的基类Dao层如下: 418 | 419 | ``` 420 | /** 421 | * 通过接口编程 422 | * 423 | * @param 泛型用于解耦,同时避免写重复代码 424 | */ 425 | interface Dao { 426 | /** 427 | * 添加某个对象 428 | * 429 | * @param t 待添加的对象 430 | * @return 返回受影响的行数 431 | */ 432 | int add(T t); 433 | 434 | /** 435 | * 删除某个对象,在企业开发中,我们一般不做物理删除,只是添加某个字段对其数据进行可用控制 436 | * 437 | * @param t 待删除对象 438 | * @return 返回受影响的条数 439 | */ 440 | int del(T t); 441 | 442 | /** 443 | * 更新某个对象 444 | * 445 | * @param t 待更新对象 446 | * @return 返回受影响的条数 447 | */ 448 | int update(T t); 449 | 450 | /** 451 | * 通过ID查找一个对象 452 | * 453 | * @param Id 待查询的对象的ID 454 | * @return 返回该ID对应的对象 455 | */ 456 | T findOneById(Serializable Id); 457 | 458 | /** 459 | * 查找对象集合 460 | * 461 | * @return 返回对象集合 462 | */ 463 | List findAll(); 464 | } 465 | ``` 466 | 467 | 这里我们需要重点说一下多参数的Dao方法和返回List的Dao方法,话不多说,直接上代码: 468 | 469 | ``` 470 | //Dao层中,多参数的方法如何让Mybatis响应? 471 | /** 472 | * 分页查询 473 | * @param offset 起始位置 474 | * @param limit 每页数量 475 | * @return 476 | */ 477 | List findAll(@Param("offset") int offset, @Param("limit") int limit); 478 | //从上面我们可以看到,我们方法参数的前面都加上了注解@Param(),同时在注解中填写了对应的名字,这是为何?请看下面的Mybatis的xml中的内容: 479 | 480 | 490 | //在上面的Mybatis的xml中的内容看来,我们是需要拿到上面参数对应的注解名字。 491 | //同时,我在xml文件中的select语句的id为findAll也和Dao中的方法相对应,resultType返回数据类型设定为UserActionLog。 492 | //通过这样简单的设定就可以实现列表查找了。 493 | 494 | //我们接着看看下面的Dao层的代码: 495 | int update(User user); 496 | 497 | //上面的是在UserDao里复制出来的,它对应的mapper为UserDao.xml,对应的方法为: 498 | 499 | 500 | UPDATE 501 | `user` 502 | SET 503 | `name`=#{name}, `age`=#{age}, `sex`=#{sex}, `duty`=#{duty}, `cell_number`=#{cellNumber}, `photo_url`=#{photoUrl} 504 | WHERE 505 | `login_id`=#{loginId}; 506 | 507 | //注意我前面提到过mapper中的id必须和方法名一样的,“#{字段名}”这种格式表示: 508 | //①如果接口的方法中传递的是对象,则表示该字段为对象的某一个属性 509 | //②如果接口的方法中传递的是一个或者多个参数,则该字段对应为接口中参数的注解,如上面的findAll 510 | ``` 511 | Service层基本没啥好复习的,毕竟现在是直接调用Dao层。 Service层作为web应用的数据驱动层,我们需要在当中加入事务管理、考虑在Dao层中使用存储过程等设计来使我们程序执行更加高效。一般来说在java web中,我们后端开发长提的是面向接口编程,同理我们需要通过泛型解耦然后继承和实现BaseService接口。我们要使框架自动加载我们的Service我们需要做到以下几点: 512 | - 在Service的实现上面使用@Service("xxxService")注解 513 | - 在Dao层调用的地方打上注解@Autowired 514 | - 在controller里面调用Service这里同样需要在定义的地方注解@Autowired 515 | 516 | 起始我们应该重点强调下Controller层,毕竟我们web服务的动态资源都是从Controller层这里出来的,好的闲话不说,直接从代码走起: 517 | ``` 518 | @Controller //表明这个是Controller,只要这个类放在Spring配置文件指定的Controller路径中就能自动装载 519 | @RequestMapping("/actionLog") //域名后面跟的最外层地址 520 | public class ActionLogController { 521 | @Autowired 522 | ActionLogService actionLogService; //自动注入ActionLogService 523 | 524 | /** 525 | * 分页查找行为日志,其实druid里面已经包含了行为日志 526 | * 527 | * @param pageNum 页码 528 | * @param pageSize 每一页的条数 529 | * @return 530 | */ 531 | @RequestMapping(value = "/findLogList" 532 | , produces = "application/json; charset=utf-8") //这里访问地址的形式是:http://xxx.cn/actionLog/findLogList,响应请求头的ContentType表明响应是json数据,字符编码为utf8 533 | @ResponseBody //表明这个方法直接返回的是响应体的内容 534 | public Object findLog(int pageNum, int pageSize) { 535 | //···代码省略 536 | return json数据; 537 | } 538 | //关于请求的参数说明: 539 | //①当请求的解析方法中有基本数据类型的参数(无论个数)时候,mvc框架会自动把请求数据存储为名称相同的变量的值 540 | //比如说上面我们的访问为:http://域名/actionLog/findLogList?pageNum=10&pageSize=10 541 | //②当请求的解析方法中有封装数据类型的参数(无论个数)时候,mvc框架会自动根据请求数据的名字查找封装数据的对应字段并且自动存值,且无论该数据使用了几次。 542 | // 比如说我网页登陆的时候有两个用户体系,但是他们是通过用户名关联在一起的,那么如下: 543 | 544 | //请求为:http:acheng1314.cn/user/login?userId=acheng&pwd=123456 545 | 546 | //请求的解析方法是: 547 | @RequestMapping(value = "/login" 548 | , produces = "application/json; charset=utf-8") 549 | @ResponseBody //表明这个方法直接返回的是响应体的内容 550 | public Object findLog(User user, Person person) { 551 | //···代码省略 552 | System.out.printf("log:\t"+user.toString()); 553 | System.out.printf("log:\t"+person.toString()); 554 | return json数据; 555 | } 556 | //我们可以看到输出的日志为: 557 | User{"userId=acheng, pwd=123456, xxx=xxx···"} 558 | Person{"userId=acheng, pwd=123456, xxx=xxx···"} 559 | //所以当你后端接收这里无论你又多少实体,但是只要包含对应的字段,那么就会自动赋值。 560 | } 561 | ``` 562 | 在Controller里面我们要注意的是: 563 | - @RequestMapping 注解在类上 564 | - @RequestMapping 注解在方法上 565 | - 方法里的参数前面的@RequestParam注解 566 | - URI模板完成RESTful风格的站点和API → 下一季会详细介绍 567 | ---- 568 | 关于web只有大概下面几点: 569 | - 减少每个页面的请求数。 570 | - js方法整合到页面,工具js封装成工具。 571 | - js方法一般写在页面头部 572 | - web页面除非整体刷新,其他情况考虑异步请求。 573 | - 页面图片资源整合到一起,然后根据位置取图片 574 | - web页面良好体验可以考虑组件引入 575 | - 页面需要大量重用的地方可以考虑模板完成 576 | ---- 577 | 其实很多东西这个总结都没详细的说出来,毕竟开发细节的东西都不是三言两语说的明白的,但是完全不慌,即将开始第二季《完整的博客后端+Android客户端+分模块开发+七牛云存储+微信支付宝整合》正在蕴量,小伙伴们请鼓励我一下。 578 | 579 | 希望喜欢我的这个系列的读者,可以点击喜欢和收藏。谢谢。 580 | 581 | 本期项目包里面有福利,后端请求兼容Form表单提交、json数据post接收,请大家自行查找。拜拜,下期见。 --------------------------------------------------------------------------------