故天将降大任于斯人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身,行拂乱其所为,所以动心忍性,曾益其所不能。
SpringBoot 记录集锦
前言
好记性不如烂笔头,开始学习SpringBoot了,有些东西就是要靠记录的,本篇文章记录了学习过程中遇到的Bug和需要记录的代码
Bug
1 在使用Mybatis做数据库条件查询操作时,不可避免的要增加新的参数,MyBatis在处理多参数的方法时,无法正确地映射这些参数。MyBatis在处理多参数的方法时,会默认将参数映射为param1
,param2
,或者arg0
,arg1
,而不是在注解中直接使用的name
,gender
等参数名。
例如我们接下来要进行条件查询
1 2 3 4 5 6 select * from empwhere name like '%张%' and gender = 1 and entrydate between '2010-01-01' and '2020-01-01' order by update_time desc ;
1 2 3 4 5 6 7 8 9 10 @Test public void list () { List<Emp> empList = empMapper.list("张" , (short ) 1 , LocalDate.of(2010 , 1 , 1 ), LocalDate.of(2020 , 1 , 1 )); System.out.println(empList); } @Select("select * from emp where name like concat('%', #{name}, '%') and gender = #{gender} and " + "entrydate between #{begin} and #{end} order by update_time desc ") public List<Emp> list (String name, Short gender, LocalDate begin , LocalDate end) ;
会出现参数不匹配的错误
Caused by: org.apache.ibatis.binding.BindingException: Parameter ‘name’ not found. Available parameters are [arg3, arg2, arg1, arg0, param3, param4, param1, param2]
此时我们可以使用@Param
注解手动映射即可
1 public List<Emp> list (@Param("name") String name, @Param("gender") Short gender, @Param("begin") LocalDate begin, @Param("end") LocalDate end) ;
Notes
1 Mybatis-Xml文件映射
开发规范:
XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)
XML映射文件的namespace属性为Mapper接口全限定名一致
XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致
1 2 3 4 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
推荐安装插件:MybatisX
2 application.properties 文件常用配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 server.port =8080 mybatis.mapper-locations =classpath:mappers/*xml mybatis.type-aliases-package =wiki.zhr.mybatisdemo.mybatis.entity mybatis.configuration.map-underscore-to-camel-case =true mybatis.configuration.log-impl =org.apache.ibatis.logging.stdout.StdOutImpl spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.url =jdbc:mysql://localhost:3306/mybatis spring.datasource.username =root spring.datasource.password =Zhr202012345 spring.servlet.multipart.max-file-size =10MB spring.servlet.multipart.max-request-size =100MB
3 新建SpringBoot Web项目常见依赖
Spring Web (Web)
Lombok (Developer Tools)
MySQL Driver (SQL)
MyBatis Framework (SQL)
4 前后端交互统一响应结果 pojo/Result.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data @NoArgsConstructor @AllArgsConstructor public class Result { private Integer code; private String msg; private Object data; public static Result success () { return new Result (1 ,"success" ,null ); } public static Result success (Object data) { return new Result (1 ,"success" ,data); } public static Result error (String msg) { return new Result (0 ,msg,null ); } }
5 在controller层使用log进行日志输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Slf4j @RestController public class DeptController { @RequestMapping("/depts") public Result list () { log.info("查询全部部门数据" ); return Result.success(); } }
6 在Controller中向前端请求参数时用到的各个注解
7 后端分页神器,依赖插件:PageHelper
1 2 3 4 5 <dependency > <groupId > com.github.pagehelper</groupId > <artifactId > pagehelper-spring-boot-starter</artifactId > <version > 2.1.0</version > </dependency >
用法:在ServiceImpl
中:
1 2 3 PageHelper.startPage(page, pageSize); List<Emp> empList = empMapper.list(); Page<Emp> p = (Page<Emp>) empList;
eg.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public PageBean list (Integer page, Integer pageSize) { PageHelper.startPage(page, pageSize); List<Emp> empList = empMapper.list(); Page<Emp> p = (Page<Emp>) empList; PageBean pageBean = new PageBean (p.getTotal(), p.getResult()); return pageBean; }
8 服务端处理前端上传的文件 & 本地存储 & 阿里云对象服务云存储
前端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <form action ="/upload" method ="post" enctype ="multipart/form-data" > 姓名: <input type ="text" name ="username" > <br > 年龄: <input type ="text" name ="age" > <br > 头像: <input type ="file" name ="image" > <br > <input type ="submit" value ="提交" > </form >
后端:
本地存储
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Slf4j @RestController @RequestMapping("/upload") public class uploadController { @PostMapping public Result upload (String username, Integer age, MultipartFile image) throws IOException { log.info("文件上传:{}, {}, {}" , username, age, image); String originalFilename = image.getOriginalFilename(); int index = originalFilename.lastIndexOf("." ); String extname = originalFilename.substring(index); String newFileName = UUID.randomUUID().toString() + extname; image.transferTo(new File ("/Users/hrz/Pictures/server-img/" + newFileName)); return Result.success(); } }
云存储 官方文档
安装阿里云oss存储依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.17.4</version> </dependency> <!-- 如果使用的是Java 9及以上的版本,则需要添加JAXB相关依赖 --> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!-- no more than 2.3.3--> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.3</version> </dependency>
注意要在Intellij里配置环境变量 OSS_ACCESS_KEY_ID
和 OSS_ACCESS_KEY_SECRET
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package wiki.zhr.tliaswebmanagement;import com.aliyun.oss.ClientException;import com.aliyun.oss.OSS;import com.aliyun.oss.common.auth.*;import com.aliyun.oss.OSSClientBuilder;import com.aliyun.oss.OSSException;import com.aliyun.oss.model.PutObjectRequest;import com.aliyun.oss.model.PutObjectResult;import java.io.FileInputStream;import java.io.InputStream;public class Demo { public static void main (String[] args) throws Exception { String endpoint = "https://oss-cn-beijing.aliyuncs.com" ; EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); String bucketName = "zouhr-web-tlias" ; String objectName = "user-images/1.jpg" ; String filePath= "/Users/hrz/Pictures/壁纸/jjy.png" ; OSS ossClient = new OSSClientBuilder ().build(endpoint, credentialsProvider); try { InputStream inputStream = new FileInputStream (filePath); PutObjectRequest putObjectRequest = new PutObjectRequest (bucketName, objectName, inputStream); PutObjectResult result = ossClient.putObject(putObjectRequest); } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason." ); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network." ); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null ) { ossClient.shutdown(); } } } }
在日后开发时,可以考虑添加工具 utils/AliOSSUtils.java
,注意注解@Component和@Value的使用,前者可以在使用该工具类时直接注入,后者可以直接从配置文件中获取值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package wiki.zhr.utils;import com.aliyun.oss.OSS;import com.aliyun.oss.OSSClientBuilder;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import org.springframework.web.multipart.MultipartFile;import java.io.*;import java.util.UUID;@Component public class AliOSSUtils { @Value("${aliyun.oss.endpoint}") private String endpoint; @Value("${aliyun.oss.accessKeyId}") private String accessKeyId; @Value("${aliyun.oss.accessKeySecret}") private String accessKeySecret; @Value("${aliyun.oss.bucketName}") private String bucketName; public String upload (MultipartFile file) throws IOException { InputStream inputStream = file.getInputStream(); String originalFilename = file.getOriginalFilename(); String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("." )); OSS ossClient = new OSSClientBuilder ().build(endpoint, accessKeyId, accessKeySecret); ossClient.putObject(bucketName, fileName, inputStream); String url = endpoint.split("//" )[0 ] + "//" + bucketName + "." + endpoint.split("//" )[1 ] + "/" + fileName; ossClient.shutdown(); return url; } }
别忘了在applicaation.properties
中配置OSS
相关信息
1 2 3 4 aliyun.oss.endpoint =https://oss-cn-beijing.aliyuncs.com aliyun.oss.accessKeyId =LTAI5tN3Lq4PRnQvFQLXzXtc aliyun.oss.accessKeySecret =KU1rOO0ErixkMBVrDk7M6jP07sUrhO aliyun.oss.bucketName =zouhr-web-tlias
9 JWT令牌
引入jjwt
依赖:
1 2 3 4 5 <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
JWT工具类 JwtUtils.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class JwtUtils { private static String signKey = "itheima" ; private static Long expire = 43200000L ; public static String generateJwt (Map<String, Object> claims) { String jwt = Jwts.builder() .addClaims(claims) .signWith(SignatureAlgorithm.HS256, signKey) .setExpiration(new Date (System.currentTimeMillis() + expire)) .compact(); return jwt; } public static Claims parseJWT (String jwt) { Claims claims = Jwts.parser() .setSigningKey(signKey) .parseClaimsJws(jwt) .getBody(); return claims; } }
10 Filter过滤器
定义:实现Filter
接口即可
配置:@WebFilter(urlPatterns = “/*”) @ServletComponentScan (加在启动类上,开启对Servlet组件支持)
filter.DemoFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @WebFilter(urlPatterns = "/*") public class DemoFilter implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { System.out.println("init 初始化方法执行了" ); } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("Demo 拦截到了请求...放行前逻辑" ); chain.doFilter(request,response); } @Override public void destroy () { System.out.println("destroy 销毁方法执行了" ); } }
下面给出一个只对login放行的filter过滤器:LoginChectFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @Slf4j @WebFilter(urlPatterns = "/*") public class LoginCheckFilter implements Filter { @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String url = req.getRequestURL().toString(); log.info("请求的url:{}" , url); if (url.contains("login" )) { log.info("登录操作,放行..." ); chain.doFilter(request, response); return ; } String jwt = req.getHeader("token" ); if (!StringUtils.hasLength(jwt)) { log.info("请求头token为空,返回未登录信息" ); Result error = Result.error("NOT_LOGIN" ); String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return ; } try { JwtUtils.parseJWT(jwt); } catch (Exception e) { e.printStackTrace(); log.info("解析令牌失败,返回未登录错误信息" ); Result error = Result.error("NOT_LOGIN" ); String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return ; } log.info("令牌合法,放行!" ); chain.doFilter(request, response); } }
11 Interceptor拦截器 (推荐,因为是Spring体系下的 )
定义:实现HandlerInterceptor
接口,并重写其所有方法
interceptor.LoginCheckInterceptor.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 @Slf4j @Component public class LoginCheckInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String url = request.getRequestURL().toString(); log.info("请求的url:{}" , url); if (url.contains("login" )) { log.info("登录操作,放行..." ); return true ; } String jwt = request.getHeader("token" ); if (!StringUtils.hasLength(jwt)) { log.info("请求头token为空,返回未登录信息" ); Result error = Result.error("NOT_LOGIN" ); String notLogin = JSONObject.toJSONString(error); response.getWriter().write(notLogin); return false ; } try { JwtUtils.parseJWT(jwt); } catch (Exception e) { e.printStackTrace(); log.info("解析令牌失败,返回未登录错误信息" ); Result error = Result.error("NOT_LOGIN" ); String notLogin = JSONObject.toJSONString(error); response.getWriter().write(notLogin); return false ; } log.info("令牌合法,放行!" ); return true ; } @Override public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle ..." ); } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion ..." ); } }
须配置config.Webconfig.java
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class Webconfig implements WebMvcConfigurer { @Autowired private LoginCheckInterceptor loginCheckInterceptor; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**" ).excludePathPatterns("/login" ); } }
12 全局异常处理器
controller,service,mapper都不抛出的异常交给全局异常处理器,例如,新增数据违背了该字段是唯一性,此时可以交给全局异常处理器讲提示消息反馈给前端
exception.GlobalExceptionHandler
1 2 3 4 5 6 7 8 9 10 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public Result ex (Exception e) { e.printStackTrace(); return Result.error("对不起,操作失败,请联系管理员" ); } }
13 Spring事务管理
事务:一组操作的集合,它是一个不可分割的工作单位
使用注解@Transaction
,三种使用位置
有需要的话可以在配置文件中加入其日志
1 logging.level.org.springframework.jdbc.support.JdbcTransactionManager =debug
如果想要事务里的所有异常 (不仅包括运行时异常)都能正常回滚 :
1 @Transactional(rollbackFor=Exception.class)
14 Spring AOP面向切面编程
定义:面向切面编程就是面向特定方法编程,在程序运行期间在不修改源代码的基础上对已有方法进行增强
其实就是定义一个模板方法,里面包含我们需要增强的方法
典型的应用场景:
优势:
代码无侵入
减少重复代码
提高开发效率
维护方便
添加依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
示例:统计各个业务层方法执行耗时 aop.TimeAspect.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Slf4j @Component @Aspect public class TimeAspect { @Around("execution(* wiki.zhr.service.*.*(..))") public Object recordTime (ProceedingJoinPoint pjp) throws Throwable { long begin = System.currentTimeMillis(); Object result = pjp.proceed(); long end = System.currentTimeMillis(); log.info(pjp.getSignature()+"执行耗时: {}毫秒" ,end-begin); return result; } }
核心概念:
连接点(JoinPoint):可以被AOP控制的方法
通知(Advice):指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
切入点(PointCut):匹配连接点的条件,通知仅会在切入点方法执行时被应用
切面(Aspect):描述通知与切入点的对应关系(通知➕切入点)
目标对象(Target):通知所应用的对象