title: ssm框架学习笔记
date: 2018-03-21 16:49:52
updated: 2020-03-12 14:34:23
categories: spring
tags:
- spring
此文档记录研二学习ssm框架的知识点笔记
最初:jsp + servlet + jdbc
现在:springmvc + spring + mybatis
官网地址:
logback配置:https://logback.qos.ch/manual/configuration.html
spring-api-doc:https://docs.spring.io/spring/docs/
mybatis中文官网:http://www.mybatis.org/mybatis-3/zh/index.html
Spring通过DI(依赖注入)实现IOC(控制反转),常用的注入方式主要有三种:setter设值注入,构造注入,基于注解的注入。
基于注解的注入:注解方式注册bean,注入依赖
@Component:可以用于注册所有bean
@Repository:主要用于注册dao层的bean
@Controller:主要用于注册控制层的bean
@Service:主要用于注册服务层的bean
@Resource:java的注解,默认以byName的方式去匹配与属性名相同的bean的id,如果没有找到就会以byType的方式查找,如果byType查找到多个的话,使用@Qualifier注解(spring注解)指定某个具体名称的bean。
```
@Resource
@Qualifier(“userDaoMyBatis”)
private IUserDao userDao;
public UserService(){
}
@Autowired:spring注解,默认也是以byName的方式去匹配与属性名相同的bean的id,如果没有找到,就通过byType的方式去查找,如果查找到多个,用@Qualifier注解限定具体使用哪个。
@Autowired @Qualifier(“userDaoJdbc”)
private IUserDao userDao;
```
入门请参考:Spring AOP入门Demo
、Java Spring AOP用法
1.预编译
2.运行期动态代理(JDK动态代理、CGLib动态代理)
Spring 所有的切面和通知器都必须放在一个
由Spring创建了一个car对象。Spring在创建该对象时,发现它的一个方法被配置成了切点(pointcut),所以,在实例化该对象时,会创建一个代理对象,当切点方法go()执行时,会被Spring创建的代理对象所拦截,运行go方法之前,会调用所对应的切面类CarLogger的前置方法beforeRun(),然后调用Car.go()方法,再然后就调用切面类CarLogger的后置方法afterRun()。
略
IOC即DI,不必调用者自己去new被调用对象,而是通过spring IOC容器把配置好的bean对象注入,可以通过设置注入即setter方法和构造器注入。bean装载可以通过xml配置设定,也可以同过设定扫描路径,然后通过注解来让容器识别到要装载的bean。aop面向切面编程,切面与业务是垂直的,不同业务往往都要做一些公共的类似的额外操作,在业务之前做,或在业务之后做,或在业务出了异常时做,或者在业务前后都要做,甚至这些要做的额外操作要用到业务本身的输入参数和业务完成的输出结果。比如业务一般都得记录日志,比如涉及数据更新的业务完成后都得伴随数据库操作,账户各种操作前都要验证用户权限,这些业务伴随的操作往往大致相似,如果每个业务都要写这些操作,特别繁琐,把这些操作提出来就成了切面,与业务分离。xml 和API方式都可以实现aop配置,pointcut是业务,aspect是切面,它俩怎么交互执行,怎么传参和调用结果,都可以通过xml和API方式实现。另外还有配置代理这一块比较蒙逼。最牛逼的是,之前看得傻了眼那么繁琐和复杂的xml,api方式用简单直观的aspectj方式竟然能等效实现,用的纯Java标签,在xml 里设一下自动代理。不过仅仅@Aspect容器不识别,要加上@Component 才识别。我觉得标签简直就是福音,差点我就被吓的放弃了。我以为一辈子就只能写xml了。orz。。。
1、若代理类有接口,使用JDK代理。也可以通过设置proxyTargetClass为true,强制使用CGLIB代理
1、若代理类无接口,使用CGLIB代理
3、如果proxyInterfaces属性被设置为一个或者多个全限定接口名,则使用JDK代理;如果该属性没有被设置,但是目标类实现了接口,也使用JDK代理。
事务:指的是逻辑上的一组操作,这组操作要么全部成功,要么全部失败
Spring事务管理主要包含3个接口
PlatformTransactionManager–事务管理器
TransactionDefinition–事务定义信息(隔离、传播、超时、只读)
TransactionStatus–事务具体运行状态
以上三个接口详细看Spring api即可,下面简要介绍几个重点知识。
事务的传播行为:web层->业务层(Service)->持久层(DAO)
略
spring支持两种方式事务管理:
编程式事务管理
在实际应用中很少使用,通过TransactionTemplate手动管理事务
使用XML配置声明式事务
开发中推荐使用(代码侵入性最小),spring的声明式事务是通过AOP实现的
具体可看 转账案例
详情参考声明式事务管理的三种方式
声明式事务管理的三种方式:基于TransactionProxyFactoryBean的方式(很少使用),基于AspectJ的配置方式(经常),以及基于注解的方式(经常)。
基于TransactionProxyFactoryBean的方式不常用,因为要对需要事务的每个类都设置一个代理类,繁琐
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource">property>
bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="transfer" propagation="REQUIRED"/>
tx:attributes>
tx:advice>
<aop:config>
<aop:pointcut id="pointcut" expression="execution(* transactiondemo02.service.AccountService+.*(..))">aop:pointcut>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut">aop:advisor>
aop:config>
很明显这个更简单啊!
xml:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource">property>
bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
ServiceImpl:
//注解式的事务管理
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT))
public class AccountServiceImpl implements AccountService {
@Transactional的属性:
```
propagation :传播事务行为
isolation :事务隔离级别
readOnly :只读
rollbackFor :发生哪些异常回滚
noRollbackFor :发生哪些异常不回滚
## 四、Spring MVC
mvc: Model-Viewer-Controller
mvc的核心思想是业务数据**抽取**同业务数据**呈现**相**分离**。
前端控制器Front Controller(MVC)
![1583754122](https://img.braincao.cn/blogimg/1583754122.jpg)
Spring MVC为我们提供了一个基于组件和松耦合的MVC实现框架。在使用Java中其它MVC框架多年之后,面对Spring MVC有一种相见恨晚的感觉。Spring MVC是如此的优雅,轻盈与简洁, 让人从其它框架的桎梏解脱出来。
SpringMVC是一个基于DispatcherServlet的MVC框架,每一个请求最先访问的都是DispatcherServlet,DispatcherServlet负责转发每一个Request请求给相应的Handler,Handler处理以后再返回相应的视图(View)和模型(Model),返回的视图和模型都可以不指定,即可以只返回Model或只返回View或都不返回。SpringMVC是基于DispatcherServlet的,DispatcherServlet是继承自HttpServlet的,HttpServlet是在web.xml文件中声明的。(相关配置请左转至: maven_pom等相关配置)
![1583754147](https://img.braincao.cn/blogimg/1583754147.png)
如图,其中我们最主要写的就是Controller,其他的基本都是框架的东西。
### Spring MVC具体的项目实践
详情参考下面,重点学习了四大块:
- 基本的controller编写
- 数据绑定
- 文件上传
- json协同
视频:[Spring MVC实操](https://www.imooc.com/video/7681)
视频太慢并且已经看完,复习直接看这个人的同步笔记即可[Spring MVC起步](https://www.cnblogs.com/zjfjava/p/6746704.html)
这个视频我自己学习实践的同步工程也已上传到github了,直接看自己的项目工程即可**[springmvc_demoproject](https://github.com/braincao/springmvc_demoproject)**
注意:项目工程的WEB-INF下所有文件是私有的,必须经过后端调用才能访问到的,如果要公共访问的东西需要放在webapps目录下
### controller三种方式
controller的多种操作:基本的controller编写、数据绑定、文件上传、json协同
```java
package com.braincao.controller;
import com.braincao.model.Course;
import com.braincao.service.CourseService;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.Map;
@Controller
@RequestMapping("/courses")
public class CourseController {
private CourseService courseService;
@Autowired
public void setCourseService(CourseService courseService) {
this.courseService = courseService;
}
//法一:本方法将处理http://localhost:8080/courses/view?courseId=123
//此方法url中必须指定?courseId=xxx的参数名courseId,参数名不可自适应改变,不好
// @RequestParam 和@PathVariable绑定了请求中的参数。@RequestParam("courseId")显式指明参数
@RequestMapping(value = "/view", method = RequestMethod.GET)
public String viewCourse(@RequestParam("courseId") Integer courseId, Model model){//Model就是spring mvc的部分
Course course = courseService.getCoursebyId(courseId);
model.addAttribute(course);//将查询到的course放到model中
return "course_overview";//返回jsps目录下的course_overview.jsp
}
//法二:这个方法就不用指定参数名courseId了,自动识别/view2/后面的东西并放到mvc中
@RequestMapping(value = "/view2/{courseId}", method = RequestMethod.GET)
public String viewCourse2(@PathVariable("courseId") Integer courseId, Map model){//Model就是spring mvc的部分
Course course = courseService.getCoursebyId(courseId);
model.put("course", course);//将查询到的course放到model中
return "course_overview";//返回jsps目录下的course_overview.jsp
}
//法三:上面两种方法都是spring mvc方法,下面这个是传统的HttpServletRequest方法
//处理/courses/view3?courseId=123形式的url
@RequestMapping(value = "/view3", method = RequestMethod.GET)
public String viewCourse3(HttpServletRequest request){
Integer courseId = Integer.valueOf(request.getParameter("courseId"));
Course course = courseService.getCoursebyId(courseId);
request.setAttribute("course", course);//将查询到的course放到model中
return "course_overview";//返回jsps目录下的course_overview.jsp
}
//绑定binding:将请求中的字段(前端)按照名字匹配原则填入模型对象(后端)
//处理/courses/create?add形式的url来创建表单,表示一个参数params = "add"
@RequestMapping(value = "/create", method=RequestMethod.GET, params = "add")
public String createCourse(){
return "admin_create/edit";//二级目录也能行哦
}
//绑定binding法一:不用@ModelAttribute
@RequestMapping(value = "/save", method = RequestMethod.POST)
public String doSave(Course course){
//这里进行业务操作,比如数据库持久化
course.setCourseId(456);
return "redirect:view2/" + course.getCourseId();//请求重定向:因为这里提交表单后courseId更新了,需要请求重定向,这样就完成更新
}
//绑定binding法二:用@ModelAttribute也可以
@RequestMapping(value = "/save2", method = RequestMethod.POST)
public String doSave2(@ModelAttribute Course course){
//这里进行业务操作,比如数据库持久化
course.setCourseId(456);
return "redirect:view2/" + course.getCourseId();//请求重定向:因为这里提交表单后courseId更新了,需要请求重定向,这样就完成更新
}
//文件上传的controller
@RequestMapping(value = "/upload", method = RequestMethod.GET)
public String showUploadPage(){
return "admin_upload/edit";
}
@RequestMapping(value = "/doUpload", method = RequestMethod.POST)
public String showUploadPage(@RequestParam("file") MultipartFile file) throws IOException {//file从页面上来
if(!file.isEmpty()){//无损的(InputStream)将上传的文件保存到指定目录下,并返回success界面
FileUtils.copyInputStreamToFile(file.getInputStream(), new File("/Users/braincao", System.currentTimeMillis()+file.getOriginalFilename()));
}
return "admin_upload/success";
}
//JSON格式的前后端交互之一:给定courseId,返回对应课程的json数据,给前端展示
@RequestMapping(value="/{courseId}", method = RequestMethod.GET)
public @ResponseBody Course getCourseInJson(@PathVariable Integer courseId){
return courseService.getCoursebyId(courseId);
}
//上面方法的第二种实现(泛型实现)。这两种都可以
@RequestMapping(value="/jsontype/{courseId}", method = RequestMethod.GET)
public ResponseEntity getCourseInJson2(@PathVariable Integer courseId){
Course course = courseService.getCoursebyId(courseId);
return new ResponseEntity(course, HttpStatus.OK);
}
//JSON格式的前后端交互之二:异步方式获取数据,前端通过js代码(ajax)来整合页面,
// 即前端写个框架本来没数据,运行时异步加载获取数据填充到界面中,
// 且注意前端的这个加载页面一定要放在webapps下,WEB-INF是私有的不能访问
// 现在用上面两个已实现的json交互方法进行:前端异步加载后端json数据的demo。给定json数据,返回对应课程的信息,前端展示信息界面
//启动tomcat后直接浏览器访问http://localhost:8080/courses_json.jsp?courseId=123即可
}
启动tomcat后直接浏览器访问http://localhost:8080/courses_json.jsp?courseId=123即可
webapps目录下的courses_json.jsp:
<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>前端异步加载后端json数据的demotitle>
<%--这个css样式我没有,展示就简朴一点吧<link rel="stylesheet" href="<%=request.getContextPath()%>/resources/css/main.css" type="text/html">--%>
<script type="text/javascript" src="<%=request.getContextPath()%>/resources/js/jquery-1.11.3.min.js">script>
head>
<script>
jQuery(function($){
var urlStr = "<%=request.getContextPath()%>/courses/<%=request.getParameter("courseId")%>";
//alert("Before Call: " + urlStr);
$.ajax({//通过ajax异步从服务端拿到json数据并动态加载页面
method: "GET",
url: urlStr,
success: function(data, status, jqXHR){
//alert("Success: " + data);
var course = data;
var path = "<%=request.getContextPath()%>/";
$(".course_title").text(course.courseTitle);
$(".course_info").text(course.courseDesc);
$(".course_imgPath").attr("src", path+course.imgPath);
$("#course_duration").text(course.courseDuration);
}
});//end ajax
});
script>
<body>
<p>This is MvcHome, your world!p>
<h3 class="course_title">h3>
<h3 class="course_info">h3>
<h3 id="course_duration">h3>
<div><img class="course_imgPath"/>div>
body>
html>
拦截器是指通过统一拦截从浏览器发往服务器的请求来完成功能的增强,解决请求的共性问题(乱码、权限验证等问题)
springmvc过滤器也能起到一定的拦截器作用
拦截器的实现:
过滤器与拦截器的区别:
过滤器Filter依赖于Servlet容器,基于回调函数,过滤范围大
拦截器Interceptor依赖于框架容器,基于反射机制,只拦截请求
通过自动回复机器人学Mybatis
1.sql语句与代码分离。优点:便于管理和维护;缺点:不便于调试,需要借助日志工具获得信息
2.用标签控制动态sql语句的拼接。优点:用标签代替编写逻辑代码;缺点:拼接复杂sql语句时,没有代码灵活,比较复杂
3.结果集与java对象的自动映射。优点:保证名称相同可自动映射;缺点:对开发人员所写的sql依赖性很强
4.编写原生sql。优点:接近JDBC,很灵活;缺点:对sql语句依赖很高(不同数据库sql语句可能不相同),半自动,数据库移植不方便
基本功能
接受发送指令
根据指令自动回复对应的内容
模块划分
回复内容列表
回复内容维护–后台新建、更改、删除指令及对应回复的维护界面
对话功能
回复内容删除
项目开发流程(当然,每个人的开发顺序可能都不同)
在没有 Mybatis的情况下,完整的Jsp + Servlet + Jdbc实现案例中的回复内容列表模块,流程如下。
1.数据库建表
2.写一个jsp前端界面
3.servlet:
web.xml中注册一个servlet并建立映射请求
写一个servlet类继承HttpServlet,重写doGet、doPost方法,用:访问数据库并把数据传给jsp。
4.代码重构下:bean装数据实体、dao层jdbc数据库增删改查、service业务操作、servlet是前后端页面控制
下面是最简单完整的Jsp + Servlet + Jdbc实现代码,工程名MicroMessage_jdbc。
bean.Message:
```java
package com.braincao.bean;
/**
一个表对应一个类、表中的一个列属性对应一个成员
*/
public class Message {
//主键
private String id;
//指令名称
private String command;
//描述
private String description;
//内容
private String content;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
dao.MessageDao:
```java
package com.braincao.dao;
import com.braincao.bean.Message;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* @FileName: MessageDao
* @Author: braincao
* @Date: 2018/11/19 15:42
* @Description: 使用jdbc访问数据库,和message表相关的数据库操作
*/
public class MessageDao {
/**
* 根据查询条件查询消息列表
* @param : 查询条件:command指令名称、description描述
* @return : 根据查询条件查询到的结果
*/
public List queryListMessage(String command, String description){
List messageList = new ArrayList<>();//把查询出来的结果放在这里
try {
//连接jdbc数据库
Class.forName("com.mysql.jdbc.Driver");
//下面一定要再jdbc的数据库连接地址后面加?useUnicode=true&characterEncoding=UTF-8,否则查询不到,编码问题
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/micro_message?useUnicode=true&characterEncoding=UTF-8", "root", "123456");
//拼接查询语句
StringBuilder sql = new StringBuilder("SELECT ID, COMMAND, DESCRIPTION, CONTENT FROM message WHERE 1=1");
List paramList = new ArrayList<>();
if(command!=null && !"".equals(command.trim())){//查询
sql.append(" AND COMMAND= ?");
paramList.add(command);
}
if(description!=null && !"".equals(description.trim())){//查询
sql.append(" AND DESCRIPTION LIKE '%' ? '%'");//%是sql的通配符,类似于*
paramList.add(description);
}
PreparedStatement statement = conn.prepareStatement(sql.toString());//防止sql注入
for(int i=0; i
service.ListService:
package com.braincao.service;
import com.braincao.bean.Message;
import com.braincao.dao.MessageDao;
import java.util.List;
/**
* @FileName: ListService
* @Author: braincao
* @Date: 2018/11/19 15:50
* @Description: 和message表相关的业务操作
*/
public class ListService {
public List<Message> queryListMessage(String command, String description){
MessageDao messageDao = new MessageDao();
return messageDao.queryListMessage(command, description);
}
}
servlet.ListServlet:
package com.braincao.servlet;
import com.braincao.service.ListService;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @FileName: ListServlet
* @Author: braincao
* @Date: 2018/11/18 21:15
* @Description: 列表页面初始化控制的servlet,使用jdbc访问数据库
*/
@SuppressWarnings("serial")
public class ListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//设置编码格式
req.setCharacterEncoding("UTF-8");
//接受页面的值
String command = req.getParameter("command");
String description = req.getParameter("description");
//向页面传值
req.setAttribute("command", command);
req.setAttribute("description", description);
//查询消息列表并传给前端页面
ListService listService = new ListService();
req.setAttribute("messageList", listService.queryListMessage(command, description));
req.getRequestDispatcher("/WEB-INF/jsps/back/list.jsp").forward(req, resp);//把数据传给前端jsp
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
前端list.jsp:
<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>留言评论功能title>
<link rel="stylesheet" href="<%=request.getContextPath()%>/resources/css/base.css"/>
<link rel="stylesheet" href="<%=request.getContextPath()%>/resources/css/jquery.datetimepicker.css"/>
<link rel="stylesheet" href="<%=request.getContextPath()%>/resources/css/plan.css"/>
<script type="text/javascript" src="<%=request.getContextPath()%>/resources/js/jquery-3.1.1.js">script>
<script type="text/javascript" src="<%=request.getContextPath()%>/resources/js/jquery.datetimepicker.full.js">script>
<script type="text/javascript" src="<%=request.getContextPath()%>/resources/js/store.min.js">script>
<script type="text/javascript" src="<%=request.getContextPath()%>/resources/js/plan2.js">script>
head>
<body style="background: #e1e9eb;">
<form id="form" action="<%=request.getContextPath()%>/list.action" method="post">
<div class="right">
<div class="current">当前位置: <a href="javascript:void(0)" style="color:#6e6e6e;">哈哈a>div>
<div class="rightCont">
<p class="g_title fix">内容列表: <a class="btn03" href="#">新增a>p>
<table class="tab1">
<tbody>
<tr>
<td width="90" align="right">指令名称: td>
<td>
<input name="command" type="text" class="allInput" value="${command}"/>
td>
<td name="description" width="90" align="right">描述: td>
<td>
<input name="description" type="text" class="allInput" value="${description}"/>
td>
<td width="85" align="right"><input type="submit" class="tabSub" value="查询">td>
tr>
tbody>
table>
<div class="zuxun fix">
<table class="tab2" width="100%">
<tbody align="center">
<tr style="background-color: #33aaff">
<th><input type="checkbox" id="all" onclick="#"/>th>
<th>序号th>
<th>指令名称th>
<th>描述th>
<th>操作th>
tr>
<c:forEach items="${messageList}" var="message" varStatus="status">
<tr if test="${status.index%2!=0}">style="background-color: #33aaff"c:if> >
<td><input type="checkbox"/>td>
<td>${status.index + 1}td>
<td>${message.command}td>
<td>${message.description}td>
<td>
<a href="#">修改a>
<a href="#">删除a>
td>
tr>
c:forEach>
tbody>
table>
div>
div>
div>
form>
body>
web.xml代码:
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>Mybatis Demo Studydisplay-name>
<servlet>
<servlet-name>ListServletservlet-name>
<servlet-class>com.braincao.servlet.ListServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>ListServletservlet-name>
<url-pattern>/list.actionurl-pattern>
servlet-mapping>
web-app>
上面完成了在没有 Mybatis的情况下,完整的Jsp + Servlet + Jdbc实现案例中的回复内容列表模块。现在将JDBC部分替换为 Mybatis,实践如下。
mybatis作用
mybatis特点:只需要给出参数+sql语句即可,剩下的数据表与实体的映射自动实现
SqlSession的作用
1.向sql语句传入参数
2.执行sql语句
3.获取执行sql语句的结果
4.事务的控制
如何得到SqlSession:
1.通过配置文件获取数据库连接相关信息
2.通过配置信息构建SqlSessionFactory
3.通过SqlSessionFactory打开数据库会话
Mybatis的配置文件conf.xml:
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
<property name="username" value="root" />
<property name="password" value="XDP" />
dataSource>
environment>
environments>
configuration>
在MessageMapper.xml中的sql语句中,要进行if、foreach等判断时,不能直接在<>中写,这时用强大的OGNL表达式(和jstl表达式类似),它可以直接在标签中引入java语句。
注意的是原java语句中的&&,空字符串"" 等在标签中需要转义。如:" --> "
这是常用对照表
MessageMapper.xml中select按照给定条件查询的语句如下:
<mapper namespace="com.braincao.configs.sql.MessageMapper">
<resultMap type="com.braincao.bean.Message" id="MessageResult">
<id column="ID" jdbcType="INTEGER" property="id"/>
<result column="COMMAND" jdbcType="VARCHAR" property="command"/>
<result column="DESCRIPTION" jdbcType="VARCHAR" property="description"/>
<result column="CONTENT" jdbcType="VARCHAR" property="content"/>
resultMap>
<select id="queryListMessage" parameterType="com.braincao.bean.Message" resultMap="MessageResult">
SELECT ID, COMMAND, DESCRIPTION, CONTENT FROM message WHERE 1=1
<if test="command!=null and !"".equals(command.trim())">AND COMMAND = #{command}if>
<if test="description!=null and !"".equals(description.trim())">AND DESCRIPTION LIKE '%' #{description} '%'if>
select>
mapper>
xml中sql语句如果报错需要调试,没法像java代码一样打断点调试,这时就需要log4j了。Log4J日志整合及配置详解
#默认输出路径,下面指定为输出到控制台
log4j.rootLogger=debug,logfile #debug是输出级别,logfile是输出的文件名(自定义)
log4j.appender.logfile=org.apache.log4j.ConsoleAppender #定义上面的logfile是输出到控制台ConsoleAppender
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d [%t] %-5p [%c] - %m %n
log4j.logger.org.apache=info #这是个性化,在指定包下的日志输出级别,会覆盖rootLogger的级别
ConversionPattern=%d [%t] %-5p [%c] - %m %n。
%d时间,%t所处的线程名称,%-5p级别(-5表示至少占5个字,不足5个空格补齐,-5补齐的空格在右边,5-表示补齐的空格在左边),%c所在类的全名,%m附加信息,%n换行
输出级别:
#输出级别依次从低到高,如为debug,则四个全部输出
log.debug("asf");
log.info(message);
log.warn(message);
log.error(message);
setvlet:接收页面的值、像页面传值、如果有业务逻辑需要处理调用service进行处理
service:接收servlet传过来的处理要求,进行业务处理,如果有需要则调用相应的dao层
dao:完成与数据库的操作,这个过程用mybatis的sqlsession完成,具体的sql语句在mapper.xml中写
几个注意:
1.service处理的每个业务都要对应一个servlet,每个servlet都要在web.xml中注册
2.数据库中查询与增加、删除、修改操作不同,后三个修改数据库后sqlSession需要commit才会提交一个事务
js中ajax的url要用路径,需要basePath,那么jsp调用js方法时不用传递参数,可以在jsp文件中埋一个hidden标签,之后在js中直接通过id就能找到basePath的参数,如下:
<input type="hidden" value="<%= basePath %>" id="basePath"/>
1.jsp文件:
<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>微信公众号title>
<link rel="stylesheet" type="text/css" href="<%= basePath %>resources/css/jscrollpane1.css" />
<script src="<%= basePath %>resources/js/common/jquery.min.js" type="text/javascript">script>
<script type="text/javascript" src="<%= basePath %>resources/js/common/jquery.mousewheel.js">script>
<script type="text/javascript" src="<%= basePath %>resources/js/common/jquery.jscrollpane.min.js">script>
<script type="text/javascript" src="<%= basePath %>resources/js/common/scroll-startstop.events.jquery.js">script>
<script type="text/javascript" src="<%= basePath %>resources/js/front/talk.js">script>
head>
<body>
<input type="hidden" value="<%= basePath %>" id="basePath"/>
<br/>
<div class="talk">
<div class="talk_title"><span>正在与公众号对话span>div>
<div class="talk_record">
<div id="jp-container" class="jp-container">
<div class="talk_recordbox">
<div class="user"><img src="<%= basePath %>images/thumbs/talk_recordbox.jpg"/>公众号div>
<div class="talk_recordtextbg"> div>
<div class="talk_recordtext">
<h3>客官,来啦,坐吧!回复[查看]收取更多精彩内容。h3>
<span class="talk_time">2018年11月20日21:30:07span>
div>
div>
<div class="talk_recordboxme">
<div class="user"><img src="<%= basePath %>images/thumbs/talk_recordboxme.jpg"/>公众号div>
<div class="talk_recordtextbg"> div>
<div class="talk_recordtext">
<h3>查看h3>
<span class="talk_time">2018年11月20日21:30:07span>
div>
div>
div>
div>
<div class="talk_word">
<input class="add_face" id="facial" type="button" title="添加表情" value="" />
<input id="content" class="messages emotion" />
<input class="talk_send" onclick="send();" type="button" title="发送" value="发送" />
div>
div>
<div style="text-align:center;margin:50px 0; font:normal 14px/24px 'MicroSoft YaHei';">div>
body>
html>
2.前端jsp页面中需要的.js文件:
.js文件包含jsp中点击等操作触发的ajax动态查询后台加载页面的方法:
/**
* 发送消息。前端界面输入文字,用ajax将文字传给后端,返回的回复动态加载到前端页面
* @param basePath
*/
function send() {
var content = $("#content").val();//获取id为content文本框的文字
if(!content) {
alert("请输入内容!");
return;
}
$.ajax({
url : $("#basePath").val() + "AutoReplyServlet.action",
type : "POST",
dataType : "text",//dataType期望服务端给我的响应类型
timeout : 10000,//超时时间
success : function (data) {//成功回调函数
appendDialog("talk_recordboxme","My账号",content); //在对话框中显示我发送的文字
appendDialog("talk_recordbox","公众号",data);//在对话框中显示后台的回复
$("#content").val("");//清空发送框中的文字
render();//这个东西是输入后在对话框中显示后计算是否需要出现滚动条的方法,知道干啥的就行了忽略这里的细节
},
data : {"content":content}//向服务端发送ajax请求的参数
});
不用看下面的代码了,直接bootstrap-modal,遇到这个需求的最大收获就是学到了bootstrap框架,简直神器。
<html>
<head>
<meta charset="utf-8">
<title>My Test Documenttitle>
<style>
.box{
width:50%; margin-top:10%; margin:auto; padding:28px;
height:350px; border:1px #111 solid;
display:none; /* 默认对话框隐藏 */
}
.box.show{display:block;}
.box .x{ font-size:18px; text-align:right; display:block;}
.box input{width:80%; font-size:18px; margin-top:18px;}
style>
head>
<body>
<h2>测试h2>
<input type="button" onClick="msgbox(1)" value="点击弹出输入框">
<script>
function msgbox(n){
document.getElementById('inputbox').style.display=n?'block':'none'; /* 点击按钮打开/关闭 对话框 */
}
script>
<div id='inputbox' class="box">
关闭a>
<input type="text">
<input type="text">
<input type="button" value="确定">
div>
body>
<input type="text" value="模糊型号查询" onfocus="if(value=='模糊型号查询') {value=''}" onblur="if (value=='') {value='模糊型号查询'}" name="keyword" size="30" style="color:#e5e1e1;"/>
SELECT A.DESCRIPTION,B.CONTENT FROM COMMAND A LEFT JOIN COMMAND_CONTENT B ON A.COMMAND_ID=B.COMMAND_ID WHERE A.COMMAND_ID='查看';
这里需要注意一个问题,请看这个LEFT JOIN 使用时的注意事项、left join on and 与 left join on where的区别
具体的实践如下,一个CommandMapper.xml文件:
注意
中的A_ID不能是ID
代码:
<mapper namespace="CommandMapper">
<resultMap type="com.braincao.bean.Command" id="CommandResult">
<id column="A_ID" jdbcType="INTEGER" property="id"/>
<result column="COMMAND" jdbcType="VARCHAR" property="command"/>
<result column="DESCRIPTION" jdbcType="VARCHAR" property="description"/>
<collection resultMap="CommandContentMapper.CommandContentResult" property="commandContentList"/>
resultMap>
<select id="queryListCommand" parameterType="com.braincao.bean.Command" resultMap="CommandResult">
SELECT A.ID A_ID,A.COMMAND,A.DESCRIPTION,B.CONTENT FROM COMMAND A LEFT JOIN COMMAND_CONTENT B
ON A.ID=B.COMMAND_ID
<where>
<if test="command!=null and !"".equals(command.trim())">AND A.COMMAND = #{command}if>
<if test="description!=null and !"".equals(description.trim())">AND A.DESCRIPTION LIKE '%' #{description} '%'if>
where>
select>
mapper>
来源:通过自动回复机器人学Mybatis—加强版
这个知识的学习至关重要,对后面的mybatis接口编程、spring-aop思想理解都有帮助,来源:模式的秘密—代理模式
代理模式定义:为其他对象提供一种代理以控制对这个对象的访问。
代理两种实现方式:静态代理、动态代理
静态代理又有两种实现方式:继承方式、聚合方式,其中聚合方式更适合静态代理。
一个接口,实现了一个类,现在想对这个类进行代理,方便增加额外的服务。那么就创建一个代理类,实现相同的接口,用构造方法将原始接口传参进去,直接增加额外服务即可。如下:
package com.braincao.proxy;
/**
* @FileName: Car2
* @Author: braincao
* @Date: 2018/11/23 16:06
* @Description: 相同的接口Moveable
* CarLogProxy实现相同接口Moveable---使用聚合方式(一个类中调用另一个类)的静态代理
*/
public class CarLogProxy implements Moveable {
private Moveable moveable;
public CarLogProxy(Moveable moveable){
this.moveable = moveable;
}
@Override
public void move() {//为Car原始类增加额外功能
System.out.println("日志开始....");
moveable.move();
System.out.println("日志结束...");
}
public static void main(String[] args){//测试
Moveable car = new CarLogProxy(new Car());
car.move();
}
}
静态代理,每一个额外服务(日志记录、时间记录)都需要新建一个代理类,对汽车的日志记录和对火车的日志记录都需要新建,解决这个问题的方式就是动态代理。
动态代理:动态产生代理,实现对不同类、不同方法的代理。动态代理也有两种:jdk动态代理、cglib动态代理
1.jdk动态代理
package com.braincao.jdkproxy;
import com.braincao.proxy.Car;
import com.braincao.proxy.Car3;
import com.braincao.proxy.Moveable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @FileName: TimeHandler
* @Author: braincao
* @Date: 2018/11/23 17:18
* @Description: jdk动态代理
*/
public class TimeHandler implements InvocationHandler{
private Object target;
public TimeHandler(Object target) {
this.target = target;
}
/*
* 参数:
* proxy--被代理对象
* method--被代理对象的方法
* args--被代理对象的方法的参数
* 返回:
* Object 被代理对象的方法的返回值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//开车起始时间
long startTime = System.currentTimeMillis();
System.out.println("汽车开始行驶....");
method.invoke(target);
//开车终止时间
long endTime = System.currentTimeMillis();
System.out.println("汽车停止行驶....行驶时间:" + (endTime-startTime) + "ms");
return null;
}
//动态代理测试
public static void main(String[] args){
Car car = new Car();
InvocationHandler h = new TimeHandler(car);
/**
* 生成代理类。
* 参数:
* loader 类的加载器
* interfaces 类的实现接口
* h(InvocationHandler)
*/
Moveable m = (Moveable)Proxy.newProxyInstance(car.getClass().getClassLoader(),
car.getClass().getInterfaces(),h);
m.move();
}
}
2.cglib动态代理
下面的代码需引入cglib-nodep-2.2.jar包
package com.braincao.cglibproxy;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* @FileName: CglibProxy
* @Author: braincao
* @Date: 2018/11/23 17:45
* @Description:
*/
public class CglibProxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz){
//设置创建子类的类
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create();
}
/*
*拦截所有目标类方法的调用
* 参数:
* o 目标类的实例
* method 目标方法
* objects 方法的参数
* methodProxy 代理类的实例
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("日志开始.....");
//代理类调用父类的方法
methodProxy.invokeSuper(o, objects);
System.out.println("日志结束.....");
return null;
}
public static void main(String[] args){
CglibProxy proxy = new CglibProxy();
Train train =(Train)proxy.getProxy(Train.class);
train.move();
}
}
总结代理模式:日常项目中代理模式有很重要的作用,比如日志管理、事务处理、权限管理等,在不改变原有类的基础利用代理类增加额外功能,这个就是aop,面向切面编程
动态代理的代理类是实现了一个InvocationHandler的接口,我们通过reflect.Proxy的类的newProxyInstance方法就可以得到这个接口的实例,然后再来作为参数传递进去,这里每一个在代理类上处理的东西也会被重定向到调用处理器上。
至于动态代理和静态代理的区别,即动态代理是动态的创建代理和动态的处理方法的,这也是反射的一个重要体现之处。
FileUtils.writeString(new File(filename), strData);
来源:
反射专题视频
Java动态代理与反射详解_博客
任何一个类都是Class的实例对象,这个实例对象有三种表示方式。
1.Class c1 = Foo.class;
2.Class c2 = foo1.getClass();
Class c3 = null;
try{
try {
c3 = Class.forName("com.braincao.reflect.Foo");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
我们可以通过类的类类型创建该类的对象实例:
Foo foo = (Foo) c1.newInstance();
编译时刻加载的类是静态加载类,运行时刻加载的类是动态加载类.
new创建对象是静态加载的类,在编译时刻就需要加载所有的可能使用到的类,即java代码中Word word=new Word();如果没有相关的Word类则编译不通过,找不到Word类,这就是静态加载的类
动态加载类:
package com.braincao.reflect;
/**
* @FileName: OfficeReflect
* @Author: braincao
* @Date: 2018/11/23 18:53
* @Description:
*/
public class OfficeReflect {
public static void main(String[] args){
Class c1 = null;
try {
//动态加载类,在运行时刻加载
c1 = Class.forName(args[0]);//命令行输入想加载的类名(Word/Excel,他们都实现了接口Office)
Office office = (Office)c1.newInstance();//动态创建指定的对象
office.start();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
反射的操作是绕过编译,在运行时刻来操作的。
package com.braincao.reflect;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @FileName: Reflect
* @Author: braincao
* @Date: 2018/11/23 20:36
* @Description: java的反射操作
*/
public class Reflect {
public static void main(String[] args){
//A a = new A();
//a.add(10, 20);
//java方法的反射操作
try {
Class c = A.class;
Method m = c.getMethod("add", new Class[]{int.class, int.class}); //获得方法对象
//Method m = c.getMethod("add", int.class, int.class); //这样写也行
m.invoke(c.newInstance(), 10, 20);//用方法对象进行反射操作
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
class A{
public void add(int a, int b){
System.out.println(a + b);
}
}
/**
* @FileName: MessageDao
* @Author: braincao
* @Date: 2018/11/19 15:42
* @Description: 和command表相关的数据库操作
* 不用原始的jdbc了,用mybatis的sqlsession更方便的访问数据库,另外用了接口式编程,用反射来进行mapper.xml进行sql方法的调用
*/
public class CommandDao {
/**
* 根据查询条件查询指令列表。
* @param : 查询条件:command指令名称||description描述
* @return : 根据查询条件查询到的结果
*/
public List<Command> queryCommandList(String commandData, String descriptionData) {
DBAccess dbAccess = new DBAccess();
List<Command> commandList = new ArrayList<>();//把查询出来的结果放在这里
SqlSession sqlSession = null;
try {
sqlSession = dbAccess.getSqlSession();//得到SqlSession
Command command = new Command();
command.setCommand(commandData);
command.setDescription(descriptionData);//把要传递的参数放在对象里,再把这个对象当成参数给SqlSession即可
//通过sqlSession执行sql语句,这里改成了接口式mybatis编程
//commandList = sqlSession.selectList("CommandMapper.queryListCommand", command);//xxx.Message是命名空间,queryListMessage是具体的sql语句
//用反射来操作:ICommandMapper是一个接口,里面有sql相关的方法,现在不创建ICommandMapper就调用其中的方法,用反射操作
ICommandMapper iCommandMapper = (ICommandMapper)sqlSession.getMapper(ICommandMapper.class);
commandList = iCommandMapper.queryListCommand(command);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
return commandList;
}
总结接口式编程:
将:
commandList = sqlSession.selectList("CommandMapper.queryListCommand", command);
换成了:
ICommandMapper iCommandMapper = sqlSession.getMapper(ICommandMapper.class);//反射操作,动态代理。getMapper方法返回时代理示例已经对目标示例进行强转,不需再(ICommandMapper)强转
commandList = iCommandMapper.queryListCommand(command);
直接在加强版的项目上实现了,具体看代码实现
为什么需要拦截器实现分页:上面虽然已经实现,但是实际项目中每一个页面都要重写一个分页功能,这样真成码农了,为了实现分页的共同处理,我们在不改变原来查询列表不分页显示的sql语句及相关逻辑基础上,用拦截器对需要分页的需求对象进行拦截,处理(在原来sql基础上拼接分页需要的limitsql语句,再返回查询结果即可),实现共同处理。
视频来源: 拦截器实现分页
mybatis拦截器是一个类似于log4j的能直接拿到配置文件中的sql参数的东西,有动态代理的思想。拦截器就是在不改变mybatis源码的情况下利用代理改变mybatis原本的一些逻辑行为便于我们更好的使用
拦截器要做的事:
现将分页功能的实现1的实现去掉,恢复原来没有分页的功能的界面,现在要通过拦截器实现分页功能(即带byPage 结 束 符 的 s q l I d 进 行 拦 截 分 页 显 示 , 不 带 b y P a g e 结束符的sqlId进行拦截分页显示,不带byPage 结束符的sqlId进行拦截分页显示,不带byPage结束符的sqlId的查询正常显示)
拦截器代码:(注意,写完拦截器需要在配置文件中注册拦截器)
package com.braincao.interceptor;
import com.braincao.entity.Page;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.Properties;
/**
* @FileName: PageInterceptor
* @Author: braincao
* @Date: 2018/11/25 17:20
* @Description: 分页拦截器
* 实现mybatis的拦截器接口
*/
//下面的注解准确的描述了我们需要拦截的对象中的方法,本例中就是想获取mybatis中执行sql前能获取到sql语句的方法prepare
@Intercepts({@Signature(type=StatementHandler.class, method="prepare", args={Connection.class})})
public class PageInterceptor implements Interceptor {
//invocation参数中就有被拦截下来的对象invocation.getTarget(),对拦截下来的对象处理
@Override
public Object intercept(Invocation invocation) throws Throwable {
//获取拦截下来的对象
StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
//查看拦截下来的对象(查询需求)是否是想要实现分页的--即mapper.xml的查询id是否以"byPage$"结尾
//这里用mybatis已经封装好的MetaObject类来获取查询id
MetaObject metaObject = MetaObject.forObject(statementHandler,
SystemMetaObject.DEFAULT_OBJECT_FACTORY,
SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
new DefaultReflectorFactory());
MappedStatement mappedStatement =(MappedStatement) metaObject.getValue("delegate.mappedStatement");
String id = mappedStatement.getId();//拿到了mapper.xml该查询语句的id
if(id.matches(".+ByPage$")){//String.matches方法用来匹配
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();//原来的sql查询语句
//原来的查询参数Map
Map<?, ?> parameter = (Map<?, ?>)boundSql.getParameterObject();
Page page = (Page) parameter.get("page");
//获取查询结果的总条数
String countSql = "select count(*) from (" + sql + ")a"; //a是别名
Connection connection = (Connection)invocation.getArgs()[0];
PreparedStatement countStatement = connection.prepareStatement(countSql);
ParameterHandler parameterHandler = (ParameterHandler)metaObject.getValue("delegate.parameterHandler");
parameterHandler.setParameters(countStatement);
ResultSet rs = countStatement.executeQuery();
if(rs.next()){
page.setTotalNumber(rs.getInt(1));//获取查询结果的总条数
}
//改造后的sql语句,拼接了分页需要的limit
String newSql = sql + " limit " + page.getDbIndex() + "," + page.getDbNumber();
//将改造后的sql写入,偷天换日
metaObject.setValue("delegate.boundSql.sql", newSql);
}
return invocation.proceed();//拦截下来对象,处理完成后,需要把拦截下来的对象主权返回,必须执行这步
}
//target就是被拦截下来的对象,返回的就是拦截下来动态代理的代理类对象
//this会根据上面的注解找到需要拦截对象和方法的class,用动态代理操作创建拦截对象的代理类target来进行处理
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//这个是注册用的,如果已经在配置文件xml中注册了就不用在这里注册了
@Override
public void setProperties(Properties properties) {
}
}
总结拦截器的思路:
1.创建拦截器类实现Interceptor接口,通过注解@Intercepts({@Signature(type=StatementHandler.class, method="prepare", args={Connection.class})})
定位需要拦截的对象及方法
2.注册拦截器到配置文件中
3.逐步重写第一步中Interceptor接口的的方法,进行拦截、处理、返回
来源:Java高并发秒杀API之业务分析与DAO层
本案例整合了上述的spring mvc + spring + mybatis,所以这个应该重点总结,重点回顾
mysql:
mybatis:
spring:
spring MVC:
前端:
高并发:
1.秒杀业务流程分析–>秒杀业务的核心:对库存的处理
2.用户针对库存业务分析–>减库存+记录购买明细
减库存+记录购买明细这两件事必须组成一个事务,否则将出现超卖/少卖情况。
关于数据落地的数据库选择:mysql vs nosql
mysql:关系型数据库。事务机制很好,本秒杀系统的案例采用mysql数据库
nosql:非关系型数据库。Redis、HBASE等,nosql创新很多,应用现在很挺多,追求性能,追求分布式,但是对事务的支持不是很好
3.用户的购买行为:
事务:
上面第二步中的行级锁:当一个用户在update时其他用户等待
秒杀接口暴露
执行秒杀
相关查询
DAO设计编码
Service设计编码
Web设计编码
schema.sql:
-- 创建数据库
CREATE DATABASE seckill;
-- 使用数据库
USE seckill;
-- 创建秒杀库存表
CREATE TABLE seckill(
`seckill_id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品库存id',
`name` varchar(120) NOT NULL COMMENT '商品名称',
`number` int NOT NULL COMMENT '库存数量',
`start_time`timestamp NOT NULL COMMENT '秒杀开启时间',
`end_time` timestamp NOT NULL COMMENT '秒杀结束时间',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY(seckill_id),
key idx_start_time(start_time),
key idx_end_time(end_time),
key idx_create_time(create_time)
)ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT '秒杀库存表';
-- 初始化数据
INSERT INTO seckill
(name,number,start_time,end_time) VALUES
('1000元秒杀iphoneX', 100, '2018-12-01 00:00:00', '2018-12-02 00:00:00'),
('500元秒杀ipadPro', 200, '2018-12-01 00:00:00', '2018-12-02 00:00:00'),
('300元秒杀小米Max', 300, '2018-12-01 00:00:00', '2018-12-02 00:00:00'),
('200元秒杀华为mete20', 400, '2018-12-01 00:00:00', '2018-12-02 00:00:00');
-- 秒杀成功明细表:包含用户登录认证相关的信息
CREATE TABLE success_killed(
`seckill_id` bigint NOT NULL AUTO_INCREMENT COMMENT '秒杀商品id',
`user_phone` bigint NOT NULL COMMENT '用户手机号',
`state` tinyint NOT NULL DEFAULT -1 COMMENT '状态标示:-1无效 0成功 1已付款 2已发货',
`create_time` timestamp NOT NULL COMMENT '创建时间',
PRIMARY KEY(seckill_id,user_phone), /*联合主键*/
key idx_create_time(create_time)
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '秒杀成功明细表';
DAO层开发就是三件事:创建表、接口设计、sql编写。具体流程如下。
1.Seckill.java开发。首先开发entity包中的实体类:数据表table对应实体entity类,表中的列名对应实体类中的属性
2.SeckillDao.java开发(这是interface)。编写完实体类后,编码DAO包下的增删改查接口
3.Mapper.xml开发。对应实体类的dao层接口写完后,下面就是基于mybatis实现dao接口的开发。(mybatis只需要知道参数+sql语句就可以Mapper自动完成数据表与实体的映射)
4.spring-dao.xml编写。放在resources目录下,配置整合spring-mybatis。这个整合又省去了很多原生mybatis中mapper.xml的配置编码,比如类型Seckill不用再写包名而直接写类名、不用写参数类型parameterType和返回类型resultMap等,很多都不用再写了,另外在mybatis-config.xml也不用一个一个配置mapper.xml文件,而是自动扫描配置文件。
整合后做两件事:XML提供sql + DAO接口Mapper -->就可以了
5.至此DAO部分基本写完了,现在进行DAO层单元测试和问题排查。直接在SeckillDao.java类名上右键go to test即可进行junit4测试,同时也自动在src/test/java包下创建对应目录的test类。
注:我们需要在test类中配置spring和junit整合(@RunWith等),这样junit启动时会加载springIOC容器,拿到相应的bean。请左转参考SeckillDaoTest.java示例
至此,DAO层开发完毕
基于Spring托管Service实现类,并使用了Spring声明式事务,具体流程如下。
1.service层的业务接口开发。service层的业务接口应站在"使用者"角度设计接口,三个方面考虑:方法定义粒度、参数、返回类型(return 类型/异常),如下:
package com.braincao.service;
import com.braincao.dto.Exposer;
import com.braincao.dto.SeckillExecution;
import com.braincao.entity.Seckill;
import com.braincao.exception.RepeatKillException;
import com.braincao.exception.SeckillCloseException;
import com.braincao.exception.SeckillException;
import java.util.List;
/**
* 业务接口:Service层接口应站在使用者角度设计接口
*
*/
public interface SeckillService {
/**
* 查询所有秒杀商品
* @param void
* @return List
*/
List<Seckill> getSeckillList();
/**
* 根据id查询一个秒杀商品
* @param seckillId
* @return Seckill
*/
Seckill getById(long seckillId);
/**
* 用户根据id查询该秒杀商品的秒杀接口地址(点击即可秒杀),如果秒杀未开启时输出系统时间和秒杀开启时间
* @param seckillId
* @return 秒杀接口地址。用dto传输层的实体来封装秒杀接口地址,dto方便web层拿到秒杀暴露接口相关数据
*/
Exposer exportSeckillUrl(long seckillId);
/**
* 执行秒杀操作
* @param seckillId,userPhone,md5
* md5用来验证是否是同一用户操作,如果md5变了,说明用户被篡改
* @return dto传输层的实体来封装秒杀操作的返回实体,包含成功、失败
* 当秒杀失败时输出自定义运行期异常RepeatKillException
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, SeckillCloseException, RepeatKillException;
}
2.service层接口的实现开发。
略,出门左转参考示例。
3.基于spring管理service依赖。service层接口和实现都开发完后,就要进行spring-IOC注入。创建spring-service.xml + serviceImpl.java(service层的注解+类内部的spring注入依赖),左转看参考示例
4.spring声明式事务
什么时候回滚事务:抛出运行期异常(RunTimeException)时回滚事务
4.1配置使用spring声明式事务,采用注解方式。见spring-service.xml,在service层实现类的应用就是在需要声明事务的方法上@Transactional即可
使用注解控制事务方法的优点:
5.还需要resources下配置logback.xml配置下日志管理。至此Service层基本写完了,现在进行service层单元测试和问题排查。
至此,Service层开发完毕
秒杀业务Web层的设计和实现,使⽤SpringMVC整合spring,实现秒杀restful接⼝。
主要有:
1.前端交互设计
1.1前端页面流程:
1.2 详情页的流程逻辑
2.Restful接口的设计
什么是Restful?
兴起于Rails,一种优雅的URL表述方式,规范url的设计,理念就是:资源的状态和状态转移。
Restful规范:
Restful接口示例:/模块/资源/{id标示}/名词1/名词2
3.SpringMVC整合spring。
springMVC运行流程:
需要配置两个东西,具体请左转:
4.controller开发.
实现Restful接口的Controller层:接受请求与参数,跳转页面的控制,并且返回参数都是已经dto传输层封装好的类型
一个例子SeckillController.java请出门左转
springMVC开发技巧:DTO封装传递数据,便于controller与前端交互传递数据
5.采用bootstrap进行jsp主页面开发。
6.前端页面写完后,用jquery开发交互设计。
5、6两步请左转:依次看jsp主页面、.js文件
至此,dao、service、web层所有开发完毕,整个项目跑通,测试完毕,开发完毕。后续要做的就是高并发秒杀优化
上述实现的秒杀系统实际上是扛不住多少并发量的,下面将进行高并发优化分析与实现。主要包括:前端CDN、后端redis缓存、mysql并发优化(减少行级锁持有时间+存储过程)
1.redis后端缓存优化编码。
秒杀接口地址可以放到redis服务器缓存中,而不用每次都访问数据库,redis可以定期同步更新一次数据库即可。
对应的redisDao,java:
package com.braincao.dao.cache;
import com.braincao.entity.Seckill;
import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
/**
* @FileName: RedisDao
* @Author: braincao
* @Date: 2018/11/30 10:27
* @Description: 高并发优化:读取/存取redis操作。这个dao就不做接口了,直接实现即可
*/
public class RedisDao {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private final JedisPool jedisPool;
//java存取redis采用RuntimeSchema序列化工具类高效转化
private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);
public RedisDao(String ip, int port){
jedisPool = new JedisPool(ip, port);
}
//读取redis:根据id读取Seckill
public Seckill getSeckill(long seckillId){
//redis操作逻辑
try{
Jedis jedis = jedisPool.getResource();
try {
String key = "seckill:" + seckillId;
//redis并没有实现内部序列化操作
//get->byte[] ->反序列化 ->Object[Seckill]
//采用自定义序列化工具protostuff进行java存取redis
byte[] bytes = jedis.get(key.getBytes());
if (bytes != null) {
Seckill seckill = schema.newMessage();
ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
//反序列化seckill
return seckill;
}
}finally {
jedis.close();
}
}catch (Exception e){
logger.error(e.getMessage(),e);
}
return null;
}
//存取redis:将Seckill存储到redis中
public String putSeckill(Seckill seckill){
//set Onject[Seckill] -> 序列化 -> byte[]
//redis操作逻辑
try{
Jedis jedis = jedisPool.getResource();
try{
String key = "seckill:" + seckill.getSeckillId();
//redis并没有实现内部序列化操作
//get->byte[] ->反序列化 ->Object[Seckill]
//采用自定义序列化工具protostuff进行java存取redis
byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,
LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
//超时缓存1h
int timeout = 60*60;
//redis存储,result是存储成功或失败的结果
String result = jedis.setex(key.getBytes(),timeout,bytes);
return result;
}finally {
jedis.close();
}
}catch (Exception e){
logger.error(e.getMessage(),e);
}
return null;
}
}
在service层的优化如下:
原来:Seckill seckill = seckillDao.queryById(id);
优化:
//将这句话进行优化:1.先访问redis
Seckill seckill = redisDao.getSeckill(seckillId);
if(seckill == null){
//2.访问数据库
seckill = seckillDao.queryById(id);
if(seckill==null) {
return new Exposer(false, seckillId);
}else{
//3.放入redis
redisDao.putSeckill(seckill);
}
}
2.执行秒杀操作(减库存+记录购买明细)高并发优化。调整sql顺序:记录购买行为在前
,减库存在后。因为记录购买行为只是插入,而减库存需要行级锁,这样能减少行级锁持有时间
//执行秒杀逻辑:减库存 + 记录购买行为,对里面抛出的异常进行try/catch并记录到日志,汇总后向外只抛一个总异常就好
Date date = new Date();
try{
//优化高并发:记录购买行为在前
int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
if(insertCount<=0){//重复秒杀
throw new RepeatKillException("Repeat seckill");
}else{
//优化高并发:减库存在后,热点商品竞争。因为减库存需要行级锁,这样能减少行级锁持有时间
int updateCount = seckillDao.reduceNumber(seckillId, date);
if(updateCount<=0){//减库存失败,秒杀结束
throw new SeckillCloseException("seckill is closed");
}else{
//秒杀成功
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, successKilled);
}
}
}catch(SeckillCloseException e1){
throw e1;
}catch (RepeatKillException e2){
throw e2;
}catch (Exception e){
logger.error(e.getMessage(), e);
//所有编译期异常转化为运行期异常,这样spring声明式事务会帮我们做roll back
throw new SeckillException("seckill inner error: " + e.getMessage());
}
3.事务sql放在Mysql端执行(定义存储过程)。上面第2步的update减库存事务操作需要用户在客户端执行秒杀操作,现在将这个事务放在Mysql端执行(定义存储过程),这样可以完全避免网络延迟和gc(垃圾回收)时间。
MySQL存储过程
SQL语句需要先编译然后执行,而存储过程(Stored Procedure)是一组为了完成特定功能的SQL语句集,经编译后存储在数据库中,用户通过指定存储过程的名字并给定参数(如果该存储过程带有参数)来调用执行它。
利用存储过程将执行秒杀的一条事务逻辑放到mysql服务端去执行,减少了客户端和服务端之间的延迟和gc时间,客户端只需要传入参数执行存储过程并根据得到的返回结果做相应的逻辑处理。存储过程比较适合于简单的逻辑。
存储过程的优点:增强SQL语言的功能和灵活性、标准组件式编程、较快的执行速度、减少网络流量、作为一种安全机制来充分利用。
但存储过程 (只在银行被大量的时候,互联网公司用的很少,但是在秒杀中用)
下面就是sql包下的seckill.sql:(定义存储过程:insert+update)
-- 执行秒杀的存储过程(插入购买明细+减库存)
DELIMITER $$ -- 结束符 ; ->
$$
-- 定义存储过程
-- 参数:in输入参数;out输出参数
-- row_count():返回上一条修改类型sql(delete\insert\update)的影响行数
-- row_count: 0未修改数据; >0影响修改的行数; <0sql错误/未执行修改sql
CREATE PROCEDURE `seckill`.`execute_seckill`
(IN v_seckill_id BIGINT, IN v_phone BIGINT,
IN v_kill_time TIMESTAMP, OUT r_result INT)
BEGIN
DECLARE insert_count INT DEFAULT 0;
START TRANSACTION;
INSERT IGNORE INTO success_killed
(seckill_id, user_phone, create_time)
VALUES (v_seckill_id, v_phone, v_kill_time);
SELECT row_count()
INTO insert_count;
IF (insert_count = 0)
THEN
ROLLBACK;
SET r_result = -1;
ELSEIF (insert_count < 0)
THEN
ROLLBACK;
SET r_result = -2;
ELSE
UPDATE seckill
SET number = number - 1
WHERE seckill_id = v_seckill_id
AND end_time > v_kill_time
AND start_time < v_kill_time
AND number > 0;
SELECT row_count()
INTO insert_count;
IF (insert_count = 0)
THEN
ROLLBACK;
SET r_result = 0;
ELSEIF (insert_count < 0)
THEN
ROLLBACK;
SET r_result = -2;
ELSE
COMMIT;
SET r_result = 1;
END IF;
END IF;
END;
$$
-- 定义存储过程结束
DELIMITER ;
set @r_result = -3;
-- 执行存储过程
CALL execute_seckill(1003,15652965942,now(),@r_result);
-- 获取结果
至此,基于SSM框架的dao、service、web、并发优化四章的秒杀系统完结,收获颇多,再次热烈祝贺自己。