ssm
spring --> applicationContext.xml配置文件
springmvc --> springmvc.xml配置文件
mybatis —> mybatis-config.xml配置文件
—> springboot优化了之前的框架配置,思想是约定大于配置
为了使用SSM框架去开发,准备SSM框架的模板配置。
为了Spring整合第三方框架,单独的去编写xml文件。
后期SSM项目后期xml文件特别多,维护xml文件的成本是很高的
SSM工程部署也是很麻烦,依赖第三方的容器
基于Java的SSM开发方式是很笨重,而现在的python,php,NodeJS的敏捷式开发已经盖过Java一头
SpringBoot是由Pivotal团队研发的,SpringBoot并不是一门新技术,只是将之前常用的Spring,SpringMVC,data-jpa等常用的框架封装到了一起,帮助你隐藏这些框架的整合细节,实现敏捷开发。
约定大于配置
SpringBoot就是一个工具集。
官网:https://spring.io/projects/spring-boot
SpringBoot特点:
- SpringBoot项目不需要模板化的配置。
- SpringBoot中整合第三方框架时,只需要导入相应的starter依赖包,就自动整合了。约定大于配置。
- SpringBoot默认只有一个.properties的配置文件,不推荐使用xml,后期会采用.java的文件去编写配置信息。
- SpringBoot工程在部署时,采用的是jar包的方式,内部自动依赖Tomcat容器,提供了多环境的配置。
- 后期要学习的微服务框架SpringCloud需要建立在SpringBoot的基础上。
选择构建项目的类型 |
---|
注意:根据网络状况,可能会提示无法连接。如果不能连接,使用http://start.springboot.io(或者https://start.aliyun.com)
指定SpringBoot版本和需要的依赖 |
---|
ps: 如果2.7.13版本创建完项目会报错,提示alimaven找不到依赖的情况,就手动将pom文件中版本改成2.7.2
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.2version>
<relativePath/>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
Controller就是之前的Servlet,用于接收请求做出响应
@Controller
public class TestController {
@GetMapping("/test")
public String test(){
return "ok.html";
}
}
在resources/static下创建ok.html页面
主类点击启动项目
效果 |
---|
指定了一个父工程: 指定当前工程为SpringBoot,帮助我们声明了starter依赖的版本。
<parent> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-parentartifactId> <version>2.7.2version> <relativePath/> parent>
项目的元数据:包名,项目名,版本号。
<groupId>com.qfgroupId> <artifactId>test_springboot01artifactId> <version>0.0.1-SNAPSHOTversion> <name>TestSpringboot01name>
指定了properties信息:指定了java的版本为1.8
<properties> <java.version>1.8java.version> properties>
导入依赖:按需导入(web,mysql等等)
插件:spring-boot-maven-plugin (如果有报错,也可以不要)
<build> <plugins> <plugin> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-maven-pluginartifactId> <version>2.7.2version> plugin> plugins> build>
默认帮我们忽略了一些文件和目录,避免提交到Git仓库中
-src
-main
-java
-包名
启动类.java # 需要将controller类,放在启动类的子包中或者同级包下,否则需要使用@ComponentScan 注解,并指定扫描的包即可
-resources # resources下放除了java代码之外其他资源文件
-static # 存放静态资源的,js,css,html
-templates # 存储模板页面的,Thymeleaf,jsp,freemarker
application.properties # SpringBoot提供的配置文件,后缀支持2种:1.properties 2.yml(推荐),用来修改默认配置
-test # 只是为了测试用的
介绍…balabala
spring的核心功能
- IOC,DI
- AOP
以前写的[登录+查询全部功能]
public class LoginServlet extends HttpServlet{
AdminService service = new AdminServiceImpl();
void doGet(){
service.findAdminByLogin(username,password);
}
}
public interface AdminService{
Admin findAdminByLogin(String username,String password);
}
public class AdminServiceImpl implements AdminService {
Admin findAdminByLogin(String username,String password) {
// ...
}
}
以上这样写有缺点:
1 LoginServlet类还是需要和AdminService和AdminServiceImpl耦合
2 扩展性不好,假如有新的实现类AdminServiceImpl2,就需要改动代码
现在需要一种技术,降低耦合且还可以根据运行时状态给属性动态赋值
IOC是Spring框架的核心功能之一,IOC(inversion of control)控制反转
控制: 控制创建对象的能力
反转: 原来创建对象是自己做,反转就是将创建对象的能力交给Spring
IOC(控制反转): 将创建对象的能力反转给Spring,由Spring创建对象
DI(dependency injection) 依赖注入,即 属性赋值
创建对象
的注解
- @Controller 在控制层代码上使用
- @Service 在业务层层代码上使用
- @Repository 在数据层代码上使用
- @Component 在其他代码上使用
属性赋值(依赖注入的注解)
- @Autowired
需求: 项目中控制层servlet需要使用到业务层对象来处理业务,例如AdminController中需要创建AdminService对象使用,使用IOC+DI完成
AdminService和AdminServiceImpl
public interface AdminService {
void login();
}
@Service // 加上该注解,AdminServiceImpl类就会被spring容器创建对象
public class AdminServiceImpl implements AdminService{
@Override
public void login() {
System.out.println("业务层执行..." );
}
}
AdminController
@Controller // 创建对象
public class AdminController {
// 在控制层中需要使用业务层对象
// 不再主动new对象,而是从容器中拿
// @Autowired注解就会从容器中找到该类型的对象赋值给该变量
// 即这就是属性赋值,也就是依赖注入,即DI
@Autowired // 属性赋值
private AdminService adminService;
@GetMapping("/login")
public String login() {
adminService.login();
return "ok.html";
}
}
练习: 在AdminService中使用AdminDao对象
演示@Component注解创建对象
假如有个类User,现在需要该类对象,就可以在该类上加上@Component注解,并在其他地方使用@Autowired注入
Spring中另外一个核心功能,AOP
AOP(Aspect Oriented Programming),即面向切面编程.
OOP(Object Oriented Programming ),即面向对象编程.
AOP面向切面编程,利用 一种称为
"横切"
的技术,剖开封装的对象内部,并将那些影响了多个类的公共行为
抽取出封装到一个可重用模块,并将其命名 为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
面向切面编程的作用:就是将项目中与核心逻辑无关的代码横向抽取成切面类,通过织入作用到目标方法,以使目标方法执行前后达到增强的效果.
原理: AOP底层使用的就是动态代理,给AOP指定哪些类型(目标类)需要增强,就会产生对应的代理对象,代理对象执行方法前后会先执行增 强的方法.
好处:减少系统的重复代码,降低模块之间的耦合度,便于维护,可以只关注核心业务
连接点(Joinpoint):连接点是程序类中客观存在的方法,可被Spring拦截并切入内容。即每个方法在切入之前,都是连接点
切入点
(Pointcut):被Spring切入连接点。即真正会增强的目标方法通知、
增强
(Advice):可以为切入点添加额外功能,分为:前置通知、后置通知、异常通知、环绕通知等。
目标对象
(Target):被代理的目标对象织入(Weaving):把通知应用到具体的类,进而创建新的代理类的过程。
代理
(Proxy):被AOP织入通知后,产生的结代理类。
切面
(Aspect):由切点和通知组成
- 事务管理
- 后续spring管理事务用的AOP原理
- 权限校验
- 后期使用Spring Security注解开发时,其实利用了AOP思想
- 日志记录
- 性能检测
- 等等
需求: 实现业务层代码执行时,能出现一些增强的效果
开发步骤
创建切面类,类上加注解
- @Component ,加上该注解,springboot框架就会创建该类对象
- @Aspect , 加上该注解,springboot框架内部就会知道该类是一个切面类
设置切入点方法,并加注解
- @Pointcut , 用于定义要增强的目标方法路径
设置各种增强(或者叫通知)方法
注解 解释 @Around 环绕通知 @Before 前置通知 @After 后置通知 @AfterReturning 后置返回通知 @AfterThrowing 异常通知
pom.xml添加aop依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
演示给业务层代码增强
package com.qf.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
@Component
@Aspect
public class MyAspect {
/**
* 将目标方法路径,提取成公共的路径,直接调用
* 后续其他增强就不用每次都写
*/
@Pointcut("execution(* com.qf.service.*.*(..))")
public void myPointcut() {}
/**
* @Around 注解说明该方法是环绕通知的方法
* 注解内写的目标方法的路径模板
* execution 固定关键词
* * 返回值任意
* com.qf.day33_springboot.service 包路径
* .* 该包下所有文件
* .* 该类下所有方法
* (..) 该方法所有参数
*-------------------------
* ProceedingJoinPoint 参数代表目标方法对象
*/
@Around("myPointcut()")
public Object myAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 目标方法前:
System.out.println("开启事务/权限校验" );
// 目标方法执行
Object ret = joinPoint.proceed( );
System.out.println("目标方法返回值---> " + ret );
// 目标方法后:
System.out.println("提交事务/日志记录" );
return ret;
}
@Before("myPointcut()")
public void myBefore(JoinPoint joinPoint) {
// 目标对象
Object target = joinPoint.getTarget( );
System.out.println("target = " + target);
// 获得目标方法签名(方法名)
Signature signature = joinPoint.getSignature( );
System.out.println("signature = " + signature);
System.out.println("前置通知--->权限校验--->OK" );
// 假设权限校验没有通过,通过抛出异常让代码停下,不再执行目标方法
// System.out.println("前置通知--->权限校验--->ERROR" );
// throw new RuntimeException("权限校验--->ERROR");
}
@After("myPointcut()")
public void myAfter() {
// 获得ip
// 获得时间
// 获得人名
// 获得日志描述信息
System.out.println("后置通知--->记录日志,释放资源" );
}
/**
* 后置返回增强,一般用于接收目标方法的返回值
* --------------------------------
* 当注解括号内参数只有一个,且参数名是value,那么可以省略value直接写值
* 当多于1个参数,所有参数都需要写k=v
* ---------------------------------
* @AfterReturning 后置返回增强,用于目标方法的返回值
* 参数value用于指定目标方法
* 参数returning用于指定返回值,该返回值需要定义在本方法的参数上
*/
@AfterReturning(value = "myPointcut()",returning = "ret")
public Object myAfterRet(Object ret) {
System.out.println("后置返回通知,接收到目标方法返回值--->" + ret);
return ret;
}
@AfterThrowing(value = "myPointcut()",throwing = "e")
public void myException(Exception e) {
System.out.println("目标方法报的错---> " + e.getMessage());
}
}
启动项目,测试
- 正常发请求访问控制层,控制层调用业务层代码时,就会触发AOP的增强机制
暂时无法完成,还有部分知识没讲,等讲完mvc和mybatis后再来实战,暂时可以大概看一眼
实战需求参考若依系统首页 (ruoyi.vip)
即将用户在系统中做的任何操作都记录到数据库,供后续查看
首先设计日志表,将来存储日志信息
CREATE TABLE `log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`log_time` datetime DEFAULT NULL,
`log` varchar(255) DEFAULT NULL,
`ip` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
# 别忘了,要在idea中设置实体类
日志注解文件
package com.qf.annotation;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
String value();
}
使用注解
@RestController
public class UserController {
@Autowired
private UserService userService;
/**
* 先登录,登录成功后将信息存入Session
* 这样log日志记录时才能取出人名
*/
@GetMapping("/login")
public User login(String name, HttpSession session) {
User user = userService.selectUserByUsername(name);
if (user != null) {
session.setAttribute("user",user);
}
return user;
}
@GetMapping("/list")
@MyLog("查询全部") // 使用日志注解
public List<User> selectAll() {
return userService.selectAll();
}
}
切面类
package com.qf.aspect;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import com.qf.annotation.MyLog;
import com.qf.model.Log;
import com.qf.model.User;
import com.qf.service.MyLogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.WebUtils;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
@Component
@Aspect
public class MyLogAspect {
// 注入业务层对象
@Autowired
private MyLogService logService;
// 切入的目标是注解
@Before("@annotation(com.qf.annotation.MyLog)")
public void after(JoinPoint joinPoint) {
// 调用下方自定义方法,获得注解中的值
String desc = getAnnoDesc(joinPoint);
System.out.println("注解中的值:" + desc);
// 以下代码需要使用Springmvc,既控制层也需要由Spring托管,才会获得ip
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes( )).getRequest( );
// 能取出数据的前提是登录时将用户信息存储到session
User user = (User) WebUtils.getSessionAttribute(request, "user");
String name = "未登录";
if (user != null) { // 防止出现空指针异常
name = user.getName( );
}
//ip
String ip = request.getRemoteAddr( );
System.out.println("请求对象的ip:" + ip);
// 封装数据
Log log = new Log( );
log.setName(name);
log.setIp(ip);
log.setLogTime(new Date());
log.setLog(desc);
// 调用业务层,执行插入
logService.insertLog(log);
}
/*
* 封装的方法,通过反射技术获得目标方法上的注解的值
*/
public static String getAnnoDesc(JoinPoint joinPoint){
String value = "";
// 获得目标方法名
String methodName = joinPoint.getSignature( ).getName( );
System.out.println("---------->" + methodName );// 目标方法名
// 根据目标方法所在的对象得到该类的全部方法
Method[] methods = joinPoint.getTarget().getClass().getDeclaredMethods();
/*
* 1.遍历所有方法
* 2.找到与目标方法一样的方法
* 3.找到该方法上的所有注解
* 4.遍历所有注解,判断注解的类型是否是我们自定义的日志注解
* 5.如果是,就获取该注解对象
* 6.由注解对象得到注解值
*/
// 1.遍历所有方法
for (Method method : methods) {
// 2.找到与目标方法一样的方法
if (methodName.equals(method.getName())) {
// 3.找到该方法上的所有注解
Annotation[] annotations = method.getDeclaredAnnotations();
// 4.遍历所有注解,判断注解的类型是否是我们自定义的日志注解
for (Annotation annotation : annotations) {
// 5.如果是,就获取该注解对象
if (annotation.annotationType( ).equals(com.qf.util.Log.class)) {
// 6.由注解对象得到注解值
value = method.getAnnotation(com.qf.util.Log.class).value( );
return value;
}
}
}
}
return value;
}
}
启动项目,浏览器访问测试
ps: ip是因为使用localhost访问,换成126.0.0.1来访问就会正常
其实是spring框架中关于web,webmvc开发的一个技术
spring核心ioc,aop,web开发
MVC架构: 根据不同的事情由不同的类去处理,内部单一职责
- Model: 模型类,例如封装数据的实体类,业务模型(Service),数据层(Dao)
- View: 视图,展示数据的.HTML,JSP
- Controller: 控制器,控制整个流程走向. 决定是否能接收请求,调用哪个业务,跳转哪个页面,Servlet
MVC框架特点
- 封装了Servlet
- 接收请求方便(一个类中,不同的方法就可以接收不同的请求)
- 接收请求数据方便(自动封装)
- 响应数据方便(自动响应json)
其实我们之前的那些案例中就已经使用了请求和响应
@GetMapping(“/ioc”)、@PostMapping(“/ioc”)等就是绑定映射路径和处理请求的方法的,返回值就是响应(跳转页面)
练习: 类中再定义其他方法和请求路径,(与servlet做对比)
@RequestMapping注解使用
所谓参数绑定,就是前端发请求中的数据,可以直接在Controller的方法参数中接收.即前端请求数据和后端方法参数绑定.
简单类型指,常用的几种类型: 基本类型+String+Date
前端页面
<h2>基本类型数据绑定h2>
<a href="/base?id=1&username=张三&score=10.0&birthday=2020-01-01">请求携带数据-基本类型a>
<hr>
<form action="/base" method="get">
id<input type="text" name="id"><br>
username<input type="text" name="username"><br>
score<input type="text" name="score"><br>
birthday<input type="date" name="birthday"><br>
<input type="submit" value="基本类型">
form>
后端接收
@Controller
public class DataController {
/**
* 基本类型自动封装
* 要求是: 前端请求的参数名,后端方法的参数名要一致
* 【特殊的】 日期,前端发送的日期格式如果是yyyy-MM-dd,springmvc无法默认绑定参数,
* springmvc默认支持yyyy/MM/dd
* 两种方案解决:
* 1. 前端改,发出的日期格式就是yyyy/MM/dd即可
* 2. 后端改,给日期参数加@DateTimeFormat(pattern = "yyyy-MM-dd")
*/
@GetMapping("/base")
public String base(int id, String username, double score, @DateTimeFormat(pattern = "yyyy-MM-dd") Date birthday){
System.out.println("id = " + id);
System.out.println("username = " + username);
System.out.println("score = " + score);
System.out.println("birthday = " + birthday);
return "ok.html";
}
}
场景: 注册/添加/更新
实体类
public class User {
private int id;
private String username;
private String password;
private double score;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
// setget
}
前端
<h2>对象数据绑定h2>
<form action="/obj" method="get">
id<input type="text" name="id"><br>
username<input type="text" name="username"><br>
password<input type="text" name="password"><br>
score<input type="text" name="score"><br>
birthday<input type="date" name="birthday"><br>
<input type="submit" value="对象类型">
form>
后端
/**
* 自动绑定: 要求前端的请求中的参数要和对象的属性名一致
*/
@GetMapping("/obj")
public String obj(User user){
System.out.println("user = " + user);
return "ok.html";
}
场景: 批量删除需要同时接收多个id, (前端是复选框的)
delete from tb_user where id in (1,2,3,4)
前端
<h2>数组绑定h2>
<form action="/array" method="get">
<input type="checkbox" name="ids" value="1">1
<input type="checkbox" name="ids" value="2">2
<input type="checkbox" name="ids" value="3">3
<input type="checkbox" name="ids" value="4">4
<input type="submit" value="数组类型">
form>
后端
/**
* 自动绑定: 要求前端的请求中的参数要和方法参数名(数组名)一致
*/
@GetMapping("/array")
public String array(int[] ids){
System.out.println("ids = " + Arrays.toString(ids));
return "ok.html";
}
List集合使用场景与数组是一样的
前端
<h2>List绑定h2>
<form action="/list" method="get">
<input type="checkbox" name="skill" value="Java">Java
<input type="checkbox" name="skill" value="HTML">HTML
<input type="checkbox" name="skill" value="Linux">Linux
<input type="submit" value="List类型">
form>
SpringMVC默认是不支持直接封装List的,解决方案:
- 加注解@RequestParam
@GetMapping("/list")
public String list(@RequestParam List<String> skill){
System.out.println("skill = " + skill);
return "ok.html";
}
Map是键值对,键和值一一映射.
跟Java对象很类似,属性和属性值一一对应.
所以什么时候需要/可以使用Map类型来接收参数呢?
- 凡是可以用对象接收的都可以使用Map
SpringMVC默认不支持直接将参数封装进Map,需要使用@RequestParam
前端
<h2>Map绑定h2>
<form action="/map" method="get">
id<input type="text" name="id"><br>
username<input type="text" name="username"><br>
score<input type="text" name="score"><br>
birthday<input type="date" name="birthday"><br>
<input type="submit" value="Map类型">
form>
<h2>模糊查询-Map绑定h2>
<form action="/map" method="get">
address<input type="text" name="address"><br>
floor<input type="text" name="floor"><br>
deco<input type="text" name="deco"><br>
<input type="submit" value="模糊-Map类型">
form>
name就是map的key
输入框的值就是map的value
后台
@GetMapping("/map")
public String map(@RequestParam Map<String,Object> map){
System.out.println("map = " + map);
return "ok.html";
}
参考这个路径
https://blog.csdn.net/weixin_39641494/article/details/131625212
这个路径中weixin_39641494是用户编号,131625212是文章id
@GetMapping(“/{userid}/article/details/{aid}”)
前端
<h2>路径参数绑定h2>
<a href="/user/101">路径参数101a>
后端
@GetMapping("/user/{id}")
public String path(@PathVariable int id){
System.out.println("id = " + id); // id=101
return "ok.html";
}
ps: 能接收到请求中的id为101,但是响应回报错.因为使用@PathVariable要求返回的是json数据而不是页面,这个暂时先不管
回顾之前学过的servlet中跳转页面的功能
- 请求转发:forward
- req.getDispatcherServlet().forward(req,resp)
- 请求路径不变
- 是服务器内部请求
- 一次请求
- 请求域的数据可以共享
- 重定向:redirect
- resp.sendRedirect();
- 请求路径改变
- 是浏览器行为
- 两次请求
- 请求域的不能共享
请求转发
注意: 现在我们一直都在使用请求转发,因为默认就是请求转发跳转页面
也可以手动显示的在Controller的方法的返回值中写forward:路径即可完成跳转
例如: forward:/ok.html forward:/test
注意: 跳转后的路径要写完整
/**
* 演示请求转发至其他页面
* @return
*/
@GetMapping("/forward")
public String forward(){
System.out.println("执行请求转发" );
return "forward:/ok.html";
}
/**
* 演示请求转发至其他请求
* @return
*/
@GetMapping("/forward2")
public String forward2(){
System.out.println("执行请求转发" );
return "forward:/test";
}
重定向
在Controller的方法的返回值中写redirect:路径即可完成跳转
例如: redirect:/ok.html redirect:/test
注意: 跳转后的路径要写完整
/**
* 演示重定向至其他页面
* @return
*/
@GetMapping("/redirect")
public String redirect(){
System.out.println("执行重定向" );
return "redirect:/ok.html";
}
/**
* 演示重定向至其他请求
* @return
*/
@GetMapping("/redirect2")
public String redirect2(){
System.out.println("执行重定向" );
return "redirect:/test";
}
其他的请求转发和重定向的特点和之前学习的servlet是一样的,复习.
如果需要在控制层中使用session存储会话数据,比如登录的用户信息,就可以直接在方法的参数列表中定义HttpSession对象即可
@GetMapping("/ts")
public String testSession(HttpSession session) {
session.setAttribute("aa","AA");
System.out.println(session.getAttribute("aa"))
return "ok.html";
}
使用步骤,与Servlet中的拦截器思路基本一致
- 编写自定义拦截器类
- 实现接口
- 重写拦截方法
- 配置拦截器
- 这个不一样,以前是配置在web.xml中或者加上注解@WebFilter
- 现在SpringBoot推荐使用java类的方式配置
自定义拦截器类
@Component
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("my preHandle");
// false 所有都拦截,排除在外的不拦截
return false;
}
}
拦截器配置类
@Configuration // !!!加注解!!!!配置类
public class MyConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addPathPatterns 定义拦截的路径
// excludePathPatterns 定义放行的路径
// 这两个方法支持不定长参数,可以设置多个拦截/放行路径
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").excludePathPatterns("/test");
}
}
后续工作项目,都是前后端分离开发,前后端使用JSON数据交互
- 前端发送json,使用axios技术(类似于ajax),vue中就使用axios发送请求
- 后端接收json,然后响应给前端json
前端发送json等vue时候再演示
现在演示响应JSON数据,非常简单,方法加上@ResponseBody即可,就会将任何解析为json返回
package com.qf.controller;
import com.qf.model.User;
import com.qf.util.R;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.*;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc 演示响应json
* 将后台的数据以json形式返回给前端
*/
@Controller
public class Demo4Controller {
/**
* 控制层代码要想返回/响应给前端json数据,只需要
* 1) 设计方法的返回值为对应类型(String,对象,Map,List)
* 2) 最后在方法上加上@ResponseBody
* ---------------------------------------------
* 真正写项目时,前后端交互的json格式要固定,一般
* {
* code:200,
* msg:"xxx",
* data:{}
* }
* --- 那么就会在java项目中定义该类型的类R
* --- 前端拿到json后,会获取其中数据
* if(json.code == 200) {
* json.data
* } else {
* alert(json.msg)
* }
*/
@GetMapping("/json")
@ResponseBody // 该注解就会把响应的数据当json返回给前端
public String testJson(){
/**
* json格式
* {k:v,k:v}
* {"id":1,"username":"zs"}
*/
String jsonStr = "{\"id\":1,\"username\":\"zs\"}";
return jsonStr;
}
@GetMapping("/json2")
@ResponseBody
public User testJson2(){
// 假设这是从数据库查出的数据,并且封装成对象
User user = new User( );
user.setId(2);
user.setScore(1.1);
user.setPassword("123456");
user.setUsername("老王");
user.setBirthday(new Date( ));
/**
* {
* id:2,
* score:1.1,
* password:"123456",
* username:"老王",
* birthday:
* }
*/
return user;
}
@GetMapping("/json3")
@ResponseBody
public Map<String, Object> testJson3(){
HashMap<String, Object> map = new HashMap<>( );
map.put("id",1);
map.put("username","老李");
map.put("password","123456");
map.put("score",2.2);
map.put("birthday",new Date( ));
return map;
}
@GetMapping("/json4")
@ResponseBody
public List<User> testJson4(){
ArrayList<User> list = new ArrayList<>( );
list.add(new User());
list.add(new User());
list.add(new User());
/**
* [
* {},
* {},
* {}
* ]
*/
return list;
}
}
以上这些响应的json,但是格式不统一,真正开发时,是团队协作开发,前后端交互的json数据的格式要统一! 格式一般如下
{ code:20000, msg:"成功|失败", data:{} }
- code是响应的状态码,自己公司定义
- 2000 成功
- 4000 失败
- 5000 连接超时
- 3000 未登录
- msg是响应的提示信息
- data是后端返回给前端的数据
前后端交互,定义的类,用于统一返回封装数据返回JSON
package com.qf.util;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc R类是用于前后端交互,返回json时固定格式
*
* 命名为R,是随意的,只不过有几个常见命名
* R,ResultObject,ReturnObject
*/
public class R {
/**
* code,指状态码,
* 随意定,20000 是正确,40000 错误
* 50000 请求超时
* 60000 没有权限
* msg,指信息描述
* data,返回的数据
*/
private int code;
private String msg;
private Object data;
public static R ok(){
R r = new R( );
r.setCode(20000);
r.setMsg("成功");
return r;
}
public static R ok(Object data){
R r = new R( );
r.setCode(20000);
r.setMsg("成功");
r.setData(data);
return r;
}
public static R fail(){
R r = new R( );
r.setCode(40000);
r.setMsg("失败");
return r;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
/**
* 演示5: 统一json格式,返回R
*/
@GetMapping("/json5")
@ResponseBody
public R json5(){
// 模拟登录成功,返回一个对象
// R r = new R( );
// r.setCode(2000);
// r.setMsg("登录成功");
// r.setData(new User());
// 模拟查询全部,
// R r = new R( );
// r.setCode(2000);
// r.setMsg("查询全部数据成功");
// ArrayList list = new ArrayList( );
// list.add("北京");
// list.add("上海");
// list.add("广州");
// r.setData(list);
// 现在发现,因为要同一格式返回,所以每次返回都需要设置R对象,以及其属下,很麻烦
// 想办法简化! 提取成工具方法
return R.ok(3000,"登录成功",new User());
}
补充: 如果该类中所有方法都返回json,那就需要在每个方法上都要加@ResponseBody注解,有点麻烦,此时可以直接将@Controller换成@RestController, 以后方法默认返回json,就不需要加@ResponseBody
图片上传
上传tomcat服务器/上传本地磁盘
前端
<h2>文件上传h2>
<form action="/upload" method="post" enctype="multipart/form-data">
文件<input type="file" name="img"><br>
<input type="submit" value="上传"><br>
form>
后端: 上传到tomcat服务器上
<dependency>
<groupId>commons-fileuploadgroupId>
<artifactId>commons-fileuploadartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
<version>2.4version>
dependency>
/**
* 接收文件,上传文件到服务器
* ----------------------
* 方法的参数列表设置MultipartFile类型参数,
* 参数名与前端name一致,即可封装
*/
@PostMapping("/upload")
@ResponseBody
public R upload(MultipartFile img, HttpServletRequest request) throws IOException {
// 1.获得上传的对象 参数img就是文件对象
// 2. 获得最终上传的目的地路径(上传至服务器中当前项目下)
// 通过servlet方法获得路径,即最终上传到Tomcat的/upload
String realPath = request.getServletContext().getRealPath("/upload");
System.out.println(realPath);
// 2.1 将最终目的文件夹创建出来
File file = new File(realPath);
// day35/tomcat/upload/2342895429834.png
// 判断该文件是否存在
if(!file.exists()) {
// 不存在则创建出
file.mkdir();
}
// 2.2 获得文件名
/*
* 文件名重复时不能重复上传文件
*/
String fileName = img.getOriginalFilename();
System.out.println(fileName);
/*
* 根据.拆分字符串,获得文件后缀名
*/
String[] split = fileName.split("\\.");
System.out.println(Arrays.toString(split));
String suffix = split[split.length-1];
// 以当前毫秒值为文件名
long prefix = new Date().getTime();
// 组装文件名
String newFileName = prefix+"."+suffix;
System.out.println("新的文件名 : "+newFileName);
// 2.3 确定上传路径
File newFile = new File(file,newFileName);
// 3. 用工具上传
FileUtils.writeByteArrayToFile(newFile, img.getBytes());
// 4 返回路径,测试使用,放查看是否上传成功
// String path = "http://localhost:8081/upload/"+newFileName;
return R.ok("http://localhost:8080/upload/"+newFileName);
}
因为是上传到服务的,上传后可以通过网络访问到图片
特殊的,上传有文件大小限制,可以通过改变SpringBoot配置文件application.properties或者application.yml文件
spring.servlet.multipart.max-file-size=10Mb spring.servlet.multipart.max-request-size=10Mb
SpringBoot中有一个ControllerAdvice的注解,使用该注解表示开启了全局异常的捕获,我们只需在自定义一个方法使用ExceptionHandler注解然后定义捕获异常的类型即可对这些捕获的异常进行统一的处理。
package com.qf.common;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.io.IOException;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc 对请求响应过程中的异常进行处理
*/
@ControllerAdvice
public class MyGlobalExceptionHandler {
/**
* 处理异常的注解,不指定的话,默认处理所有异常
* 也可以指定
* @param e
* @return
*/
// @ExceptionHandler(value = {ArithmeticException.class, IOException.class} )
@ExceptionHandler(Exception.class)
public String handlerException(Exception e) {
System.out.println("全局异常处理打印中...." );
// 接收到异常信息
e.printStackTrace();
// 真实项目中应该记录异常日志或者将异常记录数据库
return "404.html";
}
}
测试
@RestController
public class TestController {
@GetMapping("/test")
public String test(){
System.out.println(1/0 );
return "Hello SpringBoot!";
}
}
官网: mybatis – MyBatis 3 | Introduction
MyBatis本是apache的一个开源项目
iBatis
,2010年这个项目由apache software foundation迁移到了google code,并且改名为MyBatis
。2013年11月迁移到Github。iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层(Dao)框架。用于操作数据库。
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。
MyBatis 避免了几乎所有的JDBC代码和手动设置参数以及获取结果集
。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。且有缓存机制,可以提高查询效率。
Mybatis是一个
半ORM框架
,可以消除JDBC的代码和步骤,让开发者只关注SQL
本身。
ORM是对象关系映射,是指数据库表和java实体类一一对应.
半ORM框架,还是需要写SQL,由框架帮你完成映射
完全ORM框架,连SQL都不需要写,只需要遵循ORM的要求,就会自动生成SQL完成映射(Hibernate,JPA等)
xml方式在编写复杂SQL时,更适合
mybatis和druid的springboot环境下的依赖
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.47version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.10version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>1.3.2version>
dependency>
安装小辣椒插件
准备数据库
create table tb_user(
id int primary key auto_increment,
username varchar(50),
password varchar(50),
phone varchar(50),
createTime date,
money double
)default charset = utf8;
实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User {
private int id;
private String username;
private String password;
private String phone;
private Date createTime;
private double money;
}
接口就是我们之前的Dao层接口,Mybatis习惯叫做Mapper,所以先创建mapper包,然后再在其中创建接口文件
public interface Userapper {
User findUserById(int id)
}
以前是写接口的实现类,现在mybatis的接口实现功能由xml文件来实现了
在resources/下创建mapper文件夹,在其内创建mapper映射文件
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qf.mapper.UserMapper">
<select id="findUserById" resultType="com.qf.model.User">
select * from tb_user where id = #{id}
select>
mapper>
# mybatis配置
mybatis:
# 扫描映射文件
mapper-locations: classpath:mapper/*.xml
# 配置别名扫描的包
type-aliases-package: com.qf.model
configuration:
# 开启驼峰映射配置
map-underscore-to-camel-case: true
# 连接数据库的信息
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://ip:3306/数据库名?serverTimezone=UTC
username: root
password: 123456
# 数据库连接池
type: com.alibaba.druid.pool.DruidDataSource
# yml文件
logging:
level:
com.qf.mapper: DEBUG
主类扫描mapper
@SpringBootApplication
@MapperScan("com.qf.mapper") // 扫描mapper接口,创建代理对象
public class TestSpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(TestSpringbootApplication.class, args);
}
}
@RestController
public class TestMybatisController {
// todo: 这里省略了service,也可以调用service,由service掉mapper
@Autowired
private UserMapper userMapper;
// http://localhost:8080/find?id=1
@GetMapping("/m1")
public R testMybatis1(int id) {
User user = userMapper.findUserById(id);
return R.ok(user);
}
}
略,详情看入门演示
【特别注意:ORM时字段名要和实体类属性一致,否则封装失败】
<select id="findUserById" resultType="com.qf.model.User">
select id,username,password,phone,createTime,sex,money from tb_user where id = #{id}
select>
设计查询接口方法
public interface UserMapper {
User findUserById(int id)
List<User> findAll();
}
映射文件
<select id="findAll" resultType="com.qf.model.User">
select * from tb_user
select>
需求: 通过用户名和密码查询
接口方法
public interface UserMapper {
User findUserByLogin(String username,String password);
}
映射文件
<select id="findUserByLogin" resultType="com.qf.model.User">
select * from tb_user where
username = #{param1} and password = #{param2}
select>
接口方法(参数加注解)
public interface UserMapper {
User findUserByLogin(@Param("username") String username, @Param("password") String password);
}
映射文件
<select id="findUserByLogin" resultType="com.qf.model.User">
select * from tb_user where
username = #{username} and password = #{password}
select>
需求: 查询时,就要传递分页数据,又要传递模糊查询关键词,此时就可以使用Map来封装参数.
接口方法
public interface UserMapper {
User findUserByLoginMap(HashMap<String,Object> map);
}
映射文件
<select id="findUserByLoginMap" resultType="com.qf.model.User">
select * from tb_user where username = #{usernameKey} and password = #{passwordKey}
select>
页面
<h2>添加h2>
<form action="/add">
用户名<input type="text" name="username"><br>
密码<input type="password" name="password"><br>
手机号<input type="text" name="phone"><br>
余额<input type="text" name="money" > <br>
时间<input type="date" name="createTime" > <br>
<input type="submit" name="添加"><br>
form>
接口方法
public interface UserMapper {
int addUser(User user);
}
映射文件
<insert id="addUser">
insert into tb_user (username,password,phone,money,createTime)
values (#{username},#{password},#{phone},#{money},#{createTime})
insert>
前端页面
<h2>更新h2>
<h2>更新h2>
<form action="/update">
<input type="hidden" name="id" value="3"><br>
用户名<input type="text" name="username"><br>
密码<input type="password" name="password"><br>
手机号<input type="text" name="phone"><br>
余额<input type="text" name="money" > <br>
时间<input type="date" name="createTime" > <br>
<input type="submit" name="添加"><br>
form>
接口方法
public interface UserMapper {
int updateUser(User user); // 修改方法的参数是对象
}
映射文件
<update id="updateUser">
update tb_user set username=#{username},password=#{password},
phone= #{phone},createTime=#{createTime},money=#{money}
where id = #{id}
update>
页面
<h2>删除h2>
<a href="/delete?id=8">删除id=8a>
接口方法
int deleteById(int id);
映射文件
<delete id="deleteById">
delete from tb_user where id = #{id}
delete>
MyBatis只能自动维护库表”列名“与”属性名“相同时的一一对应关系,二者不同时,无法自动ORM。
自动ORM失效 |
---|
在SQL中使用 as 为查询字段添加列别名,以匹配属性名。
通过取别名,让列的别名和实体类属性名一致即可!
<mapper namespace="com.qf.mapper.UserMapper">
<select id="findUserById" resultType="User">
select id as idd,username,password,phone,create_time,sex,money from tb_user where id = #{id}
select>
mapper>
通过< resultMap id=“” type=“” >映射,匹配列名与属性名。
<mapper namespace="com.qf.mapper.UserMapper">
<resultMap id="findUserByIdResultMap" type="user">
<id property="idd" column="id" />
resultMap>
<select id="findUserById" resultMap="findUserByIdResultMap">
select id,username,password,phone,create_time,sex,money from tb_user where id = #{id}
select>
mapper>
总结
- 当数据库的列和实体类属性不一致时,可以通过手动映射来完成
- 手动关联映射,就不再使用resultType,而是使用resultMap,其中写resultMap标签的id
表关系: 一对一,一对多,多对多
- 1vs1 丈夫表 --> 妻子表
- 1 vs n 用户 --> 车辆/房产
- n vs n 老师/商品 --> 学生/订单
多表联查的SQL
- 内连接
- select * from 表1 inner join 表2 on 表1.字段 = 表2.字段
- select * from 表1, 表2 where 表1.字段 = 表2.字段
- 外连接
- select * from 表1 left join 表2 on 表1.字段 = 表2.字段
- 子查询
需求: 实现一对一查询,查询订单以及对应的用户信息
数据: tb_user表, tb_order表
关系:
用户 —> 订单 (1 VS N) 一个用户有多个订单
订单 —> 用户 (1 VS 1) 一个订单只会属于一个人
tb_user表
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`username` varchar(10) DEFAULT NULL COMMENT '用户名',
`password` varchar(10) DEFAULT NULL COMMENT '密码',
`phone` varchar(11) DEFAULT NULL COMMENT '手机号',
`money` double(10,2) DEFAULT NULL COMMENT '账户余额',
`createTime` date DEFAULT NULL COMMENT '注册时间'
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8;
tb_order表
CREATE TABLE `tb_order` (
`oid` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单编号',
`order_time` datetime DEFAULT NULL COMMENT '订单时间',
`order_desc` varchar(255) DEFAULT NULL COMMENT '订单详情',
`uid` int(11) DEFAULT NULL COMMENT '关联用户id',
PRIMARY KEY (`oid`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `tb_order` VALUES (1, '2022-11-17 15:06:29', '笔记本电脑', 1);
INSERT INTO `tb_order` VALUES (2, '2022-12-16 11:00:41', 'Cherry键盘', 1);
INSERT INTO `tb_order` VALUES (3, '2022-12-16 11:01:23', 'Logi鼠标', 2);
实体类
public class Order {
private int oid;
private Date orderTime;
private String orderDesc;
private int uid;
// set get...
}
思考: 查询订单以及关联的用户,sql怎么写?
select * from tb_order o,tb_user u where o.uid = u.id
思考2: 这个sql结果如何映射封装到对象?
**[重点]**但是上面的实体类,只有订单信息,我们要查询的是订单和用户! 上面的类就无法展现以及封装全部数据,所以需要扩展类(即包含Order又包含User)
public class OrderVO extends Order {
private User user;
// set get
}
OrderMapper.java接口文件
public interface OrderMapper {
OrderVO findOrderWithUserByOid(int oid);
}
OrderMapper.xml映射文件
<resultMap id="orderWithUserResultMap" type="OrderVO">
<id column="oid" property="oid"/>
<result column="order_time" property="orderTime"/>
<result column="order_desc" property="orderDesc"/>
<result column="uid" property="uid"/>
<association property="user" javaType="com.qf.model.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="phone" property="phone"/>
<result column="money" property="money"/>
<result column="createTime" property="createTime"/>
association>
resultMap>
<select id="findOrderWithUserById" resultMap="orderWithUserResultMap">
SELECT
o.*,
u.*
FROM
tb_order o,
tb_user u
WHERE
o.uid = u.id
AND o.oid = #{oid}
select>
测试
OrderController 调用 OrderService
OrderService 调用 OrderMapper
需求: 一对多,查询用户关联查询出所有的订单
SELECT
*
FROM
tb_user u
LEFT JOIN tb_order o ON u.id = o.uid
WHERE
u.id = 1
目的查询用户,以及关联多个订单,User类不够展现全部数据,那么就创建扩展类UserVO,UserVO类继承User就可以存储用户信息,还需要再UserVO类中添加Order类来存储信息,但是!!不是一个Order类,因为是一对多,一个用户关联多个订单,所有要设置List
User扩展实体类
public class UserVO extends User{
private List<Order> orderList;
// set get
}
UserMapper.java接口
public interface UserMapper {
UserVO findUserWithOrdersById(int id);
}
UserMapper.xml映射文件
<resultMap id="userWithOrdersResultMap" type="UserVO">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="phone" property="phone"/>
<result column="create_time" property="createTime"/>
<result column="money" property="money"/>
<collection property="orderList" ofType="com.qf.model.Order">
<id column="oid" property="oid"/>
<result column="order_time" property="orderTime"/>
<result column="order_desc" property="orderDesc"/>
<result column="uid" property="uid"/>
collection>
resultMap>
<select id="findUserWithOrdersById" resultMap="userWithOrdersResultMap">
SELECT
*
FROM
tb_user u
LEFT JOIN tb_order o ON u.id = o.uid
WHERE
u.id = #{id}
select>
实体类要设置扩展类以用于封装多表的数据
正常封装使用resultMap
- 一对一封装使用association
- 一对多封装使用collection
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
自己话理解: 帮助我们拼接SQL
常见的动态SQL语法
- SQL片段(官方不是在动态SQL章节)
- where , if
- set
- trim
- foreach
这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。
自己的话: 减少代码重复,主要用于抽取字段,表名等
<sql id="userField">
id,
username,
password,
phone,
create_time,
money,
sex
sql>
<select id="findAll" resultType="User">
select
<include refid="userField"/>
from
tb_user
select>
if就是用来判断,主要用于判断要不要拼接对应的条件语句
-- 需求:查询用户,条件是money=1000,如果密码不为空,也根据密码查 select * from tb_user where money = 1000 select * from tb_user where money = 1000 and password= '123456'
UserMapper.java接口方法
public interface UserMapper {
/**
* 演示if动态sql
*/
List<User> findByMap(HashMap<String,Object> map);
}
UserMapper.xml
<select id="findByMap" resultMap="userResultMap">
select
<include refid="userFields"/>
from
tb_user
where
1 = 1
<if test="username != null and username != ''">
and username = #{username}
if>
<if test="password != null and password != ''">
and password = #{password}
if>
select>
测试
@GetMapping("/map")
@ResponseBody
public R map(){
HashMap<String, Object> map = new HashMap<>( );
// map.put("password","123456");
// map.put("username","Uzi");
User user = userService.findByMap(map);
if (user != null) {
return R.ok(user);
}
return R.fail();
}
注意: 通过application.yml添加日志,显示sql. 通过打印的sql来判断是否拼接成功
如果说只有if,可能会出现这么一种情况
SELECT * FROM tb_user WHERE 1=1
多出一个where关键词!!
所以我们需要一个智能的,有条件时帮我们拼接where关键词,没有条件查询时,不拼接where,并且去掉多余的and关键词,但是不会主动帮助拼接and
<select id="findUserByWhere" resultType="User">
select
<include refid="userField"/>
from
tb_user
<where>
<if test="username != null and username != ''">
and username = #{username}
if>
<if test="password != null and password != ''">
and password = #{password}
if>
where>
select>
所以一般会where和if一起用
用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,
忽略其它不更新的列
。
UserMapper.java接口方法
public interface UserMapper {
int updateUser(User user);
}
UserMapper.xml
<update id="updateUser">
update tb_user
<set>
<if test="username != null and username != ''">username = #{username},if>
<if test="password != null and password != ''">password = #{password},if>
<if test="phone != null">phone = #{phone},if>
<if test="createTime != null">create_time = #{createTime},if>
<if test="money != 0.0">money = #{money},if>
set>
where id = #{id}
update>
测试
场景: 批量删除
delete from tb_user where id in (1,2,3,...);
String sql = "delete from tb_user where id in ("; int iMax = idsArr.length - 1;// 最大下标 for (int i = 0; i < idsArr.length; i++) { int id = idsArr[i]; sql += id; if (i != iMax) { sql += ","; } else { sql += ")"; } }
UserMapper.java
public interface UserMapper {
// 为了演示动态sql foreach
int deleteBatch(List<Integer> ids);
}
UserMapper.xml
<delete id="deleteBatch">
delete from tb_user
where id in
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
foreach>
delete>
测试
// 批量删除
@GetMapping("/delete/batch")
// http://localhost:8888/delete?id=1&id=2&id=3
public R deleteBatch(@RequestParam List<Integer> list){
int i = userService.deleteBatch(list);
if (i > 0) {
return R.ok(i);
}
return R.fail();
}
关于分页有些数据
- 默认访问首页,即默认当前页是 pageNum= 1
- 数据有总条数, total = select count(*)
- 页面大小/每页展示多少条数据, pageSize = 10
- 总页数 , pageCount = total / pageSize (需要注意除不尽情况)
-- total共7条 select count(*) from tb_user -- 每页多少条数据: pageSize 3条 -- 总页数pageCount pageCount = total % pageSize == 0? total/pageSize :(total/pageSize)+1 -- 当前页pageNum=1 -- 查询第1页 select * from tb_user limit 0,3 -- 查询第2页 select * from tb_user limit 3,3 -- 查询第pageNum页 select * from tb_user limit (pageNo-1)*pageSize,pageSize -- 查询第3页面 select * from tb_user limit 6,3
现在使用的是分页助手-pagehelper
- 原理拦截sql,帮助我们拼接limit
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-starterartifactId>
<version>1.4.2version>
dependency>
SpringBoot会自动完成配置,我们直接写代码
测试是在Controller中直接设置开启分页即可
特别注意!!! 无需改动sql,即不需要自己写limit,分页工具自己会拼接
/**
* 使用分页查全部
* pageNum 当前页码
* pageSize 页面大小
* 这两个参数需要前端发请求带过来
*/
@GetMapping("/m2")
public R testMybatis2(int pageNum,int pageSize) {
// 使用步骤
// 1 先设置分页信息
PageHelper.startPage(pageNum,pageSize);
// 2 正常执行查询
List<User> list = mapper.findAll( );
// 3 通过查询返回的list创建出分页信息,PageInfo内包含所有分页数据,可以点入源码查看
PageInfo<User> info = new PageInfo<>(list);
System.out.println("当前面"+info.getPageNum());
System.out.println("页面大小"+info.getPageSize() );
System.out.println("总条数"+info.getTotal() );
System.out.println("总页数"+info.getPages() );
System.out.println("数据"+info.getList());
return R.ok(info);
}
开启事务
执行sql
如果成功,提交事务
如果失败,回滚事务
方法加@Transactional 注解即可,一般加在业务层
@Override
@Transactional // 加上注解,该方法执行时就有事务控制
public int deleteById(int id) {
int i = mapper.deleteById(id);
System.out.println(1/0 );
// 无事务管理时,即使会报错抛异常,但是上面删除还会成功执行
// 但是有事务管理时,如果有报错异常抛出,上面的删除会回滚回去
return i;
}
该注解加在业务层方法上,那么该方法被事务管理
如果加业务层类上,那么该类的所有方法被事务管理
缓存主要目的是为了
提高查询效率
.缓存其实就是一个内存空间,存储在程序的某个地方,存储数据.mybatis支持缓存的,且有两级缓存
- 一级缓存
- 二级缓存
无缓存:用户在访问相同数据时,需要发起多次对数据库的直接访问,导致产生大量IO、读写硬盘的操作,效率低下 |
---|
有缓存:首次访问时,查询数据库,将数据存储到缓存中;再次访问时,直接访问缓存,减少IO、硬盘读写次数、提高效率 |
---|
MyBatis的
一级缓存是默认的
.无需配置,自动实现.默认的
一级缓存是SqlSession级别
,是指同一个SqlSession发起的多次查询同一条数据,会使用缓存.
ps: Mybatis内部存储缓存使用的是一个HashMap对象,key为 hashCode + sqlId + sql 语句。而value值就是从查询出来映射生成的java对象。
@GetMapping("/user/{id}")
@Transactional // 【重点】需要开启事务才会生效一级缓存,因为mysql默认每句话都是独立的事务,即每句话都是独立的SqlSession,那么就不符合一级缓存的要求
public R findUserById(@PathVariable int id) {
User user = userMapper.findUserById(id);// 第一次查
System.out.println(user );
System.out.println("-------------" );
User user2 = userMapper.findUserById(id);// 第二次查
System.out.println(user2 );
return R.ok(user);
}
在更新(更新,删除,插入)数据后,会清空缓存,下次重新查最新的数据.避免脏读
@GetMapping("/user/{id}")
@Transactional
public R findUserById(@PathVariable int id) {
User user = userMapper.findUserById(id);// 查一次
System.out.println(user );
System.out.println("-------------------" );
userMapper.deleteById(3);// 中间执行删除,会清空缓存
System.out.println("-------------------" );
User user2 = userMapper.findUserById(id);// 再查一次
System.out.println(user2 );
return R.ok(user);
}
二级缓存是Mapper级别
,比SqlSession级别范围更大.
需要在mapper中设置caceh即可
<cache/>
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
// 两次请求,查询的是同一个mapper下的同一个方法的同一个参数的同一条数据
@GetMapping("/one")
public R findUserById(int id){
User user = userService.findUserById(id);
if (user != null) {
return R.ok(user);
}
return R.fail();
}
@GetMapping("/one2")
public R findUserById2(int id){
User user = userService.findUserById(id);
System.out.println(user );
if (user != null) {
return R.ok(user);
}
return R.fail();
}
@GetMapping("/user/{id}")
public R findUserById(@PathVariable int id) {
User user = userMapper.findUserById(id);
System.out.println(user );
System.out.println("-------------------" );
userMapper.deleteById(2);// 删除会清空缓存
System.out.println("-------------------" );
User user2 = userMapper.findUserById(id);
System.out.println(user2 );
return R.ok(user);
}
什么是缓存?有什么好处?
mybatis有没有缓存?
一级 二级什么区别?
一级二级是如何设置缓存?
注解方式在编写配置简单,简单SQL推荐使用
create table `emp` (
`id` int auto_increment,
`name` varchar(255),
`age` int ,
`birthday` date ,
primary key (`id`)
) ;
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class Emp {
private int id;
private String name;
private int age;
private Date birthday;
}
public interface EmpMapper {
@Select("select * from emp")
List<Emp> findAll();
}
针对增删改查:@Insert,@Delete,@Update,@Select
还是需要在启动类中添加@MapperScan注解
public interface EmpMapper {
@Select("select * from emp where id = #{id}")
public Emp getEmpById(int id); // 查一个
@Insert("insert into emp (name,age,birthday) values (#{name},#{age},#{birthday})")
public int insertEmp(Emp emp);// 增
@Delete("delete from emp where id = #{id}")
public int deleteEmpById(int id);//删
@Update("update emp set name=#{name},age=#{age},birthday=#{birthday} where id=#{id}")
public int updateEmpById(Emp emp);//改
}
# yml文件
logging:
level:
com.taotie.testspringboot.mapper: DEBUG
@SpringBootApplication
@MapperScan("com.taotie.testspringboot.mapper")
public class TestSpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(TestSpringbootApplication.class, args);
}
}
// ============================================
@RestController
public class TestMybatisController {
@Autowired
private EmpMapper empMapper;
@GetMapping("/m2")
public R testMybatis2() {
List<Emp> list = empMapper.findAll( );
return R.ok(list);
}
}
重点
】SpringBoot的配置文件,
文件名必须是application
,格式支持properties
和yml
。更推荐使用yml文件格式:
yml文件,会根据换行和缩进帮助咱们管理配置文件所在位置
yml文件,相比properties更轻量级一些
K: V 表示一对键值对(冒号: 后一定有一个空格)
以空格的缩进来控制层级关系;只要是左对齐的都是属于一个层级的数据
属性和值大小写敏感.
yml文件的劣势:
严格遵循换行和缩进
在填写value时,一定要在: 后面跟上空格
配置文件的作用
修改SpringBoot的配置的默认值:
比如默认启动的Tomcat的端口是8080,可以修改为8081
properties | yml |
---|---|
配置文件的位置:
- 一般默认都是放在resources/下
- 也有其他位置的,暂且不讨论
实际开发中,有三种环境:
1.开发环境dev-程序员日常开发所需
2.测试环境test-项目的集成测试
3.生产环境prod-最终项目部署的环境,真实环境
SpringBoot支持多环境的配置。只需要根据环境需要,编写多个配置文件,通过配置属性选择使用哪个环境
使用步骤:
1.多环境的配置文件命名:application-环境名.yml
2.在总文件application.yml中通过属性:spring profiles active 环境名
ps:也可在部署工程时,通过 java -jar jar文件 --spring.profiles.active=环境
场景: 1) 加密盐值 2) 秘钥
解释: 将yml配置的值,赋值给对应的类
方案:
- 方案一: @ConfigurationProperties
- 方案二: @Value
方案一: @ConfigurationProperties [了解]
# 示例
aliyun:
accessKey: ATYSBD23B1N44
accessSecret: 123456
@Data
@Component
@ConfigurationProperties(prefix = "aliyun") // yml中的前缀
public class AliyunProperties {
// yml中的key
private String accessKey;
private String accessSecret;
}
// java代码中使用AliyunProperties对象即可获得数据
@RestController
public class TestController {
@Autowired
private AliyunProperties aliyun;
@GetMapping("/yml")
public AliyunProperties testGetYmlValue(){
return aliyun;
}
}
方案二: @Value [推荐]
# 示例
aliyun:
accessKey: ATYSBD23B1N44
accessSecret: 123456
// 哪里需要获得yml中的数据,哪里只需要加@Value注解取值即可
@RestController
public class UserController {
@Value("${aliyun.accessKey}")
private String accessKey;
@GetMapping("/user")
public R findUserById(){
System.out.println(accessKey );
return R.ok();
}
热加载/热部署 : 实现不停机更新代码
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<optional>trueoptional>
dependency>
修改settings中的配置 |
---|
编辑界面同时 ctrl+shift+alt+/ 选择registry,勾选自动部署
勾选自动部署 |
---|
注意,要以**debug方式启动
**,代码写完,光标离开idea 过1-3秒钟就会自动更新…
新版idea(2022版以后)在设置里找File->Settings->Advanced Settings中找到Compiler并勾选Allow auto-make to start even if developed application is currently running
前言: springboot项目一般都是前后端分离开发,即我们idea中只有后台代码,那么现在写完成controller接收请求,调用业务层处理业务,调用dao层操作完数据库之后如何测试?
目前是使用浏览器,自己手动在地址栏输入请求路径和拼接参数;或者写一个html页面,其中写a标签或者form表单来设置数据和发送请求
但是这样自己编写路径或者编写页面测试很麻烦! 那么接口测试工具(api测试工具)就是来做这个事情的,即帮助测试接口
在这里如何理解接口? 此处的接口是前后端对接的
"接口"
,是一套完整接收请求处理请求返回结果的代码,即那就是controller-service-dao层有哪些接口测试工具
- postman
- apipost
- apifox
- 以及各种插件
安装fast-request插件
选择到本地那个zip文件
点击右下角apply , 后再点ok
此时idea中就可以使用
作用:
- 方便调试
- 记录运行信息
- 记录异常信息
现在如何实现记录日志的呢
- sout 这个输出语句
弊端
- 无论什么情况,只要到此处输出语句一定执行,不能有选择的可控输出
- 只能输出到控制台
- 信息不完整
- 输出语句要删掉
- slf4j
slf4j 只是一个日志标准,并不是日志系统的具体实现。它用于提供日志操作的接口,提供获取日志对象的方法
- log4j
apache 实现的一个开源日志组件
- logback
相对于logback,有更好的特性,
springboot默认使用logback
- log4j2
是 log4j的升级版本,拥有更好的性能,支持异步日志
注意:slf4j属于日志接口,log4j、logback、log4j2属于日志实现
springboot默认使用logcak处理日志,
本例中,使用log4j2处理日志
日志级别按照从低到高为:
ALL < TRACE <
DEBUG < INFO < WARN < ERROR
< FATAL < OFF程序会打印高于或等于所设置级别的日志,设置的**
日志等级越高,打印出来的日志就越少
**All:最低等级,会输出所有日志记录
Trace:追踪,就是程序推进一下
Debug:调试日志
Info:消息日志,可用于输出应用程序的运行过程
Warn:输出警告级别的日志
Error:输出错误信息日志
Fatal:输出每个严重的错误日志.
OFF:最高等级的,用于关闭所有日志记录
Spring Boot默认使用LogBack,但是我们没有看到显示依赖的jar包,其实是因为所在的jar包spring-boot-starter-logging都是作为spring-boot-starter-web或者spring-boot-starter依赖的一部分。
如果这里要使用Log4j2,需要从spring-boot-starter-web中排除spring-boot-starter-logging依赖,同时显示声明使用Log4j2的依赖jar包,具体如下:
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starterartifactId> <exclusions> <exclusion> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-loggingartifactId> exclusion> exclusions> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-webartifactId> <exclusions> <exclusion> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-loggingartifactId> exclusion> exclusions> dependency>
再单独引入log4j2的依赖
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-log4j2artifactId> dependency>
在resources下面新建log4j2.xml,输入以下内容:
<Configuration status="fatal" monitorInterval="30">
<Properties>
<Property name="baseDir" value="./logs"/>
<Property name="pattern">%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p %c{1}:%L -%m%nProperty>
Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout>
<pattern>${pattern}pattern>
PatternLayout>
Console>
<RollingFile name="debug_appender" fileName="${baseDir}/debug.log"
filePattern="${baseDir}/debug_%i.log.%d{yyyy-MM-dd}">
<Filters>
<ThresholdFilter level="debug"/>
<ThresholdFilter level="info" onMatch="DENY" onMismatch="NEUTRAL"/>
Filters>
<PatternLayout>
<pattern>${pattern}pattern>
PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<SizeBasedTriggeringPolicy size="100 MB"/>
Policies>
RollingFile>
Appenders>
<Loggers>
<Root level="ERROR">
<AppenderRef ref="Console"/>
<AppenderRef ref="debug_appender"/>
Root>
Loggers>
Configuration>
log4j2 的依赖包随着搭建springboot,自动导入,无需再导入
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
@RestController
public class TestLogController {
// 导入包import org.apache.logging.log4j.LogManager;
// 导入包import org.apache.logging.log4j.Logger;
private Logger logger = LogManager.getLogger(TestLogController.class);
@GetMapping("/log")
public R log() {
logger.debug("这是bug级别");
logger.warn("我是警告");
logger.info("我是消息");
logger.error("我是错误!");
return R.ok( );
}
}
注意:实际开发中,不允许使用输出语句定位问题,需要采用debug要么就是日志
TODO: 特别注意!! 发现BUG 使用aliyun创建的SpringBoot按上面配置,无法完成这样的日志记录!!暂未解决
日志占位符
知识点重点
编码上熟悉架构
- 熟悉springboot整合ssm需要的jar包/依赖
- 熟系三层架构编码风格
其他东西都是锦上添花
spring-aop
springmvc的会话,拦截器,文件上传,全局异常
mybatis多表联查,缓存,事务,注解开发,分页
SpringBoot 多环境切换,热加载,接口工具
整合日志框架
aop+自定义注解实现日志记录
m:ss,SSS} %-5p %c{1}:%L -%m%n
${pattern}
log4j2 的依赖包随着搭建springboot,自动导入,无需再导入
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
@RestController
public class TestLogController {
// 导入包import org.apache.logging.log4j.LogManager;
// 导入包import org.apache.logging.log4j.Logger;
private Logger logger = LogManager.getLogger(TestLogController.class);
@GetMapping("/log")
public R log() {
logger.debug("这是bug级别");
logger.warn("我是警告");
logger.info("我是消息");
logger.error("我是错误!");
return R.ok( );
}
}
[外链图片转存中…(img-B5CnQEmp-1702287323807)]
[外链图片转存中…(img-ML6pXhnM-1702287323807)]
注意:实际开发中,不允许使用输出语句定位问题,需要采用debug要么就是日志
TODO: 特别注意!! 发现BUG 使用aliyun创建的SpringBoot按上面配置,无法完成这样的日志记录!!暂未解决
日志占位符
[外链图片转存中…(img-NoFJoYmy-1702287323807)]
知识点重点
编码上熟悉架构
- 熟悉springboot整合ssm需要的jar包/依赖
- 熟系三层架构编码风格
其他东西都是锦上添花