1.快速入门
简单例子
需求:使用Spring boot开发一个web应用,浏览器发起请求 /hello 后,给浏览器返回字符串 “hello world” 。
浏览器中输入:http://localhost:8080/hello
步骤:
- 创建springboot工程,并勾选web开发相关依赖
- 定义HelloController类,添加方法hello,并添加注解
- 运行测试
创建一个请求处理类:HelloController
package com.lonely.springboottest.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// 请求处理类
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
System.out.println("hello world");
return "hello";
}
}
浏览器中访问
2.传参:
简单参数
http://localhost:8080/test?name=Tom&age=10
参数名与形参变量名相同,定义形参即可接收参数。
接收过程中会自动进行类型转换
@RequestMapping("/simpleParams")
public String simpleParams(String name,int age){ //
System.out.println("前端传来的参数为:"+name+":"+age);
return "已收到前端传来的参数";
}
post请求也一样,在表单中输入数据即可
如果方法形参名称与请求参数名称不匹配,可以使用@RequestParam完成映射,@RequestParam中的required属性默认为true,代表该请求参数必须传递,如果不传递将报错,如果该参数是可选的,可以将required属性设置为false
@RequestMapping("/simpleParams2")
public String simpleParams2(@RequestParam(name="name",required=true)String username,Integer age){
// 会将地址栏中的name参数映射到username形参上
System.out.println("接收到来自前端的数据:"+username+":"+age);
return "前端的数据:"+username+":"+age;
}
实体参数
简单实体对象:请求参数名与形参对象属性名相同,定义POJO接收即可
@RequestMapping("/simplePojo")
public String simplePojo(User user){
System.out.println(user);
return "OK";
}
public class User{
private String name;
private Integer age;
}
复杂实体对象:请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套POJO属性参数。
@RequestMapping("/complexPojo")
public String complexPojo(User user){
System.out.println(user);
return "已接受到参数:"+user;
}
public class User{
private String name;
private Integer age;
private Address address;
}
public class Address {
private String province;
private String city;
}
浏览器访问:http://localhost:8080/complexPojo?name=Tom&age=22&address.province=河南省&address.city=郑州
数组集合参数
数组参数:
请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数
@RequestMapping("/arrayParam")
public String arrayParam(String hobby[]){
for (String s : hobby) {
System.out.print(s+" ");
}
return "ok";
}
浏览器访问:http://localhost:8080/arrayParam?hobby=game&hobby=java
集合参数:
请求参数名与形参中集合变量名相同,通过@RequestParam绑定参数关系
@RequestMapping("/listParam")
public String listParam(@RequestParam List<String> hobbies){
for (String s : hobbies) {
System.out.print(s+" ");
}
return "ok";
}
浏览器访问:http://localhost:8080/listParam?hobbies=game&hobbies=java
日期参数
使用 @DateTimeFormat 注解完成日期参数格式转换
浏览器中输入:
http://localhost:8080/dateParam?updateTime=2022-12-12 10:05:34
@RequestMapping("/dateParam")
public String dateParam(@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")LocalDateTime updateTime){
System.out.println(updateTime);
return "后端已收到数据"+updateTime;
}
JSON参数
JSON参数:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数,需要使用 @RequestBody 标识
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user){
System.out.println(user);
return user.toString();
}
路径参数
路径参数:通过请求URL直接传递参数,使用{…}来标识该路径参数,需要使用@PathVariable获取路径参数
http://localhost:8080/path/1
@RequestMapping("/path/{id}")
public String pathParam(@PathVariable Integer id){
System.out.println(id);
return "后端已接受到:"+id.toString();
}
传递多个参数:
http://localhost:8080/pathParam/1/tom
@RequestMapping("/pathParam/{id}/{name}")
public String pathParam(@PathVariable Integer id,@PathVariable String name){
System.out.println(id+name);
return "后端已接受到:id="+id.toString()+",name="+name;
}
3.响应数据
@ResponseBody
类型:方法注解、类注解
位置:Controller方法上/类上
作用:将方法返回值直接响应,如果返回值类型是实体对象/集合,将会转换为JSON格式响应
说明:@RestController = @Controller+@ResponseBody
@RequestMapping("/responseTest1")
public List<User> responseTest1(){
User u1 = new User("张三",22,new Address("河南省","郑州市"));
User u2 = new User("李四",23,new Address("广东省","广州市"));
User u3 = new User("王五",21,new Address("甘肃省","兰州市"));
List<User> list = new ArrayList<>();
list.add(u1);
list.add(u2);
list.add(u3);
return list;
}
统一响应结果
需要有一个统一的类作为响应结果
public class Result {
private Integer code;
private String msg;
private Object data;
public Result() {}
public Result(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer 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;
}
public static Result success(Object data){
return new Result(1,"success",data);
}
public static Result success(){
return new Result(1,"success",null);
}
public static Result error(String msg){
return new Result(0,msg,null);
}
@Override
public String toString() {
return "Result{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}
@RequestMapping("/responseTest1")
public Result responseTest1(){
User u1 = new User("张三",22,new Address("河南省","郑州市"));
User u2 = new User("李四",23,new Address("广东省","广州市"));
User u3 = new User("王五",21,new Address("甘肃省","兰州市"));
List<User> list = new ArrayList<>();
list.add(u1);
list.add(u2);
list.add(u3);
return Result.success(list);
}
浏览器输入:localhost:8080/responseTest1
4.分层解耦
三层架构
- Controller:控制层,接收前端发送的请求,对请求进行处理,并响应数据
- Service:业务逻辑层,处理具体的业务逻辑
- Dao:数据访问层(持久层),负责数据访问操作,包括数据的增、删、改、查
分层解耦
内聚:软件中各个功能模块内部的功能联系
耦合:衡量软件中各个层/模块之间的依赖、关联的程度
软件设计原则:高内聚低耦合
控制反转:Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转
依赖注入:Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
Bean对象:IOC容器中创建、管理的对象,称之为bean
IOC
IOC&DI入门
- Service层及Dao层的实现类,交给IOC容器管理
- 为Controller及Service注入运行时,依赖的对象
- 运行测试
@RestController
public class UserController { // 用户控制层
@AutoWired //运行时,IOC容器会提供该类型的bean对象,并赋值给改变了--依赖注入
private UserService userService;
@RequestMapping("/responseTest1")
public Result responseTest1(){
List<User> list = userService.listUser();
return Result.success(list);
}
}
@Component // 将当前类交给IOC容器管理,成为IOC容器中的Bean
public class UserServiceA implements UserService { // 用户服务层
@AutoWired //运行时,IOC容器会提供该类型的bean对象,并赋值给改变了--依赖注入
private UserDao userDao;
@Override
public List<User> listUser() {
List<User> list = userDao.users();
// 数据处理
return list;
}
}
@Component // 将当前类交给IOC容器管理,成为IOC容器中的Bean
public class UserDaoA implements UserDao { // 用户Dao层
@Override
public List<User> users() {
User u1 = new User("张三",22,new Address("河南省","郑州市"));
User u2 = new User("李四",23,new Address("广东省","广州市"));
User u3 = new User("王五",21,new Address("甘肃省","兰州市"));
List<User> list = new ArrayList<>();
list.add(u1);
list.add(u2);
list.add(u3);
return list;
}
}
Bean的声明:
要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:
注解 | 说明 | 位置 |
---|---|---|
@Component | 声明bean的基础注解 | 不属于以下三类时,用此注解 |
@Controller | @Component的衍生注解 | 标注在控制器类上 |
@Service | @Component的衍生注解 | 标注在业务类上 |
@Repository | @Component的衍生注解 | 标注在数据访问类上(由于与mybatis整合,用的少) |
注意:
- 声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写
- 使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller
@SpringBootApplication具有包扫描作用,默认扫描当前包及其子包
Bean注入
@Autowired注解,默认是按照类型进行,如果存在多个相同类型的bean,将会报错
通过以下几种解决方案来解决:
@Primary
@Autowired+@Qualifier(“bean的名称”)
@Resource(name=“bean的名称”)
@Primary
@Service
public class UserServiceA implements UserService{
}
@RestController
public class EmpController{
@Resource(name="userServiceA")
private UserService userServce;
}
@RestController
public class EmpController{
@Autowired
@Qualifier("userServiceA")
private UserService userService;
}
@resource和@Autowired区别:
- @Autowired是spring框架提供的注解,而@Resource是JDK提供的注解
- @Autowired默认是按照类型注入,而@Resource默认是按照名称注入
5.发送请求
GET请求
通过@RequestMapping或者@GetMapping实现
@RestController
public class DeptController {
// 第一种 @RequestMapping(value = "/depts",method = RequestMethod.GET)
// 第二种
@GetMapping("/depts")
public Result list(){
// 代码块
return Result.success();
}
}
6.springboot常用配置选项
可以通过@Value注解将外部的属性注入,具体用法为:@Value(“${配置文件中的key}”)
springboot中提供了多种属性配置方式
1.application.properties
server.port=8080
server.address=127.0.0.1
2.application.yml或application.yaml
server:
port:8080
address:127.0.0.1
企业开发当中使用的大多为第2种方式
yml基本语法:
- 大小写敏感
- 数值前边必须有空格,作为分隔符
- 使用缩进表示层级关系,缩进时不允许使用tab键,只能用空格
- 缩进的空格不重要,只要相同层级的元素左侧对其即可
- #表示注释,从这个字符一直到行尾,都会被解析器忽略
yml数据格式:
1.对象/Map集合:
user:
name:zhangsan
age:18
password:123456
2.数组/List/Set集合:
languages:
-java
-golang
-python
文件上传相关配置:
spring:
servlet:
multipart:
max-file-size: 10MB #配置单个上传文件的最大文件大小
max-request-size: 100MB #配置一次请求可以上传文件的总大小
将配置项直接注入到类中的属性当中:
使用@ConfigurationProperrties(prefix=“”)注解
一个例子
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
accessKeyId: ${env.OSS_ACCESS_KEY_ID}
accessSecret: ${env.OSS_ACCESS_KEY_SECRET}
bucketName: lonely-bucket
@Data
@Component
@ConfigurationProperties(prefix="aliyun.oss")
public class AliOssUtils{
private String endPoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
@Value与@ConfigurationProperties区别:
相同点:
都是用来注入外部配置的属性的
不同点:
@Value只能一个个的进行外部注入
@ConfigurationProperties可以批量的将外部的属性配置注入到bean对象的属性中
7.会话跟踪技术
1.常用的会话跟踪技术
1.cookie的使用
示例用法:
@GetMapping("/setcookie")
public Result setCookie(HttpServletResponse response){
response.addCookie(new Cookie("login_username","sssssskkkkkk"));
return Result.success();
}
@GetMapping("/getcookie")
public Result getCookie(HttpServletRequest request){
Cookie [] cookies = request.getCookies();
String login_username = "";
String login_username_value = "";
for (Cookie cookie : cookies){
if(cookie.getName().equals("login_username")){
login_username = cookie.getName();
login_username_value = cookie.getValue();
}
}
return Result.success(new Cookie(login_username,login_username_value));
}
cookie的优缺点:
优点:HTTP协议中支持的技术
缺点:
- 移动端app无法使用cookie
- 不安全,用户可以自己禁用cookie
- Cookie不能跨域
2.session的使用
示例:
@GetMapping("/setsession")
public Result setsession(HttpSession session){
session.setAttribute("loginuser","jack");
return Result.success("成功");
}
@GetMapping("/getsession")
public Result getsession(HttpServletRequest request){
HttpSession session = request.getSession();
Object loginuser = session.getAttribute("loginuser");
return Result.success(loginuser);
}
session的优缺点:
优点:存储在服务端,安全
缺点:
服务器集群环境下无法直接使用session
移动端app无法使用session
Session不能跨域
3.令牌技术(主流方案)
优点:
- 支持PC端,移动端
- 解决集群环境下的认证问题
- 减轻服务器端存储压力
缺点:需要自己实现
1.JWT令牌
定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
组成:
- 第一部分:Header(头),记录令牌类型、签名算法等。例如:{“alg”:“HS256”,“type”:“JWT”}
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{“id”:“1”,“username”:“tom”}
- 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定密钥,通过指定签名算法计算而来。
JWT-生成:
引入依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成:
@Test
public void testGenJwt(){
Map<String,Object> claims = new HashMap<>();
claims.put("name","zhangsanfeng");
claims.put("id",1);
String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256,"lonely") //签名算法
.setClaims(claims) // 自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis()+1000*3600)) // 设置1小时后过期
.compact();
System.out.println(jwt);
}
校验JWT令牌:
@Test
public void testParseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("lonely") // 指定签名密钥
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW5mZW5nIiwiaWQiOjEsImV4cCI6MTcxODYyNTY4M30.CibHAVdkRW7Wm_jGnHducGp2RZXmQLUdPYVVXARCwJc") // 解析令牌
.getBody(); // 可以获取载荷
System.out.println(claims);
注意事项:
- JWT校验时使用的签名密钥,必须和生成JWT令牌时使用的密钥是配套的
- 如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效了,令牌非法
服务端需要对请求进行统一拦截,看其中是否包含JWT,如果包含看是否已经过期,如果没有过期,则执行相应的请求。
2.请求拦截器
1.过滤器
过滤器可以把对资源的请求拦截下来从而实现一些特殊的功能。
过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等
快速入门:
1.定义Filter:定义一个类,实现Filter接口,并重写其所有方法
2.配置Filter:Filter类上加@WebFilter注解,配置拦截资源的路径。引导启动类上加@ServletComponentScan开启Servlet组件支持
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
@Override // 初始化方法,只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override // 拦截到请求之后调用,调用多次
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("拦截到了请求");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override // 销毁方法,只调用一次
public void destroy() {
Filter.super.destroy();
}
}
2.Filter详解
filter拦截路径:
拦截路径 | urlPattern值 | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问/login路径时,才会被拦截 |
目录拦截 | /emps/* | 访问/emp下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
检查登录的Filter示例:
import com.alibaba.fastjson.JSONObject;
import com.lonely.mybatisdemo.pojo.Result;
import com.lonely.mybatisdemo.utils.JwtUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.io.IOException;
@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;
// 1.获取请求url
String requestURL = req.getRequestURL().toString();
// 2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if (requestURL.contains("login")){
log.info("登陆操作,放行");
chain.doFilter(req,resp);
return ;
}
// 3.获取请求头中的令牌(token)
String jwt = req.getHeader("token");
// 4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回错误信息");
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error); // 手动转换为json
resp.getWriter().write(notLogin); // 响应给浏览器未登录的信息
return ;
}
// 5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtil.parseJWT(jwt);
}catch(Exception e){ // jwt解析失败
e.printStackTrace();
log.info("解析令牌失败,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error); // 手动转换为json
resp.getWriter().write(notLogin); // 响应给浏览器未登录的信息
return ;
}
// 6.放行
log.info("令牌合法,放行");
chain.doFilter(request,response);
}
}
3.拦截器Interceptor
概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。
作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码
1.快速入门
1.定义拦截器,实现HandlerInterceptor接口,并重写其所有方法。
2.注册拦截器
定义拦截器:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override // 目标资源方法运行前运行,返回值为true代表放行,返回值为false代表不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle...");
return true;
}
@Override // 目标资源方法运行之后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...");
// HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override // 视图渲染完毕后运行,最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
// HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
注册拦截器:
import com.lonely.mybatisdemo.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**"); // 设置拦截器的路径为/*
WebMvcConfigurer.super.addInterceptors(registry);
}
}
2.拦截路径
拦截器可以根据需求,配置不同的拦截路径:
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); // 拦截所有的路径并排除/login路径
}
拦截路径 | 含义 | 举例 |
---|---|---|
/* | 一级路径 | 能匹配/depts,/emps,/login,不能匹配 /depts/1 |
/** | 任意级路径 | 能匹配/depts,/depts/1,/depts/1/2 |
/depts/* | /depts下的一级路径 | 能匹配/depts/1,不能匹配/depts/1/2,/depts |
/depts/** | /depts下的任意级路径 | 能匹配/depts,/depts/1,/depts/1/2 |
8.异常处理
出现异常,该如何处理
方案一:在Controller的方法中进行try…catch处理(不推荐)
方案二:全局异常处理器(推荐)
定义一个全局异常处理器:
import com.lonely.mybatisdemo.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
//全局异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result ex(Exception ex){
ex.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
}
9.事务管理
事务是一组操作的集合,它是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败。
案例:解散部门:删除部门,同时删除该部门下的员工
完善删除部门功能:
DeptServiceImpl类中:
@Service
public class DeptServiceImpl implements DeptService{
@AutoWired
private DeptMapper deptMapper;
@AutoWired
private EmpMapper empMapper;
@Override
public void delete(Integer id){
deptMapper.delete(id);// 1.删除部门
empMapper.deleteByDeptId(id); // 2.根据部门id,删除部门下的员工信息
}
}
DeptMapper中:
@Delete("delete from emp where dept_id = #{deptId}")
void deleteByDeptId(Integer Id);
但这样存在问题,删除部门和删除部门下的员工并不是同时发生的,可能会导致数据不一致的问题。
所以要使用到事务
1.Spring事务管理:
注解:@Transactional
位置:业务(service)层的方法上、类上、接口上
作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
需要在DeptServiceImpl类中的delete方法前加上注解@Transactional:
@Service
public class DeptServiceImpl implements DeptService{
@AutoWired
private DeptMapper deptMapper;
@AutoWired
private EmpMapper empMapper;
@Transactional
@Override
public void delete(Integer id){
deptMapper.delete(id);// 1.删除部门
empMapper.deleteByDeptId(id); // 2.根据部门id,删除部门下的员工信息
}
}
如果要开启事务管理日志,需要在application.yml中添加配置:
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
2.事务属性
1.事务属性-回滚
rollbackFor:默认情况下,只有出现RuntimeException(运行时异常)才回滚异常。rollbackFor属性用于控制出现何种异常类型,回滚事务。
@Transactional(rollbackFor=Exception.class)代表出现所有异常都会回滚事务
2.事务属性-传播行为
propagation
事务传播行为:指的是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
@Transactional
public void a(){
// ...
userService.b();
// ...
}
@Transactional(propagation = Propagation.REQUIRED)
public void b(){
// ...
}
属性值 | 含义 |
---|---|
REQUIRED | (默认值)需要事务,有则加入,无则创建新事物 |
REQUIRES_NEW | 需要新事物,无论有无,总是创建新事物 |
SUPPORTS | 支持事务,有则加入,无则在无事务状态中运行 |
NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
MANDATORY | 必须有事务,否则抛异常 |
NEVER | 必须没事务,否则抛异常 |
10.AOP(面向切面编程)
1.AOP概述
面向切面编程
场景:某些部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的耗时。
动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
2.快速入门
统计各个业务层方法执行耗时
- 导入依赖:在pom.xml中导入AOP的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 编写AOP程序:针对于特定方法根据业务需要进行编程
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect // 当前类是一个AOP类
@Slf4j
public class TimeAspect {
@Around("execution(* com.lonely.mybatisdemo.service.*.*(..))") // 切入点表达式
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
// 1.记录开始时间
long begin = System.currentTimeMillis();
// 2.调用原始方法
Object result = joinPoint.proceed();
// 3.记录结束时间,计算方法执行耗时
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature()+"方法执行总耗时:{}",end-begin);
return result;
}
}
AOP场景:记录操作日志,权限控制,事务管理
连接点:JoinPoint,可以被AOP控制的方法。
通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用。
切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
目标对象:Target,通知所应用的对象
3.通知类型
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行
注意事项:
- @Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
- @Around环绕通知方法的返回值,必须指定该为Object,来接收原始方法的返回值
@Component
@Aspect
public class MyAspect{
// 可以用@Pointcut将切入点抽取出来
@Pointcut("exection(* com.lonely.test.impl.DeptServiceImpl.*(..))")
private void pt(){}
@Before("pt()")
public void before(){
System.out.println("before ...")
}
}
通知的顺序:当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行
通过在切面类上加@Order(数字)注解来控制顺序:
目标方法前的通知方法:数字小的先执行
目标方法后的通知方法:数字小的后执行
切入点表达式:execution
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?的表示可以省略的部分
访问修饰符:可省略(比如public、private)
包名.类名:可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
pl:
@Before("execution(public void com.lonely.test.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
public void before(JoinPoint joinPoint){
// 代码块
}
可以使用通配符描述切入点:
* :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
execution(* com.*.service.*.update*(*))
.. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
execution(* com.lonely.test..DeptService.*(..))
切入点表达式:@annotation
@annotation切入点表达式,用于匹配标识有特定注解的方法
@annotation(com.lonely.anno.Log)
pl:
@Before("@annotation(com.lonely.anno.MyLog)")
public void before(){
log.info("before ...");
}
首先自定义注解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // 运行时有效
@Target(ElementType.METHOD) // 作用在方法上
public @interface MyLog {
}
4.连接点
在spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
- 对于@Around通知,获取连接点信息只能使用 ProceedingJoinPoint
- 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是Proceeding Join Point的父类型。
@Around("execution(* com.lonely.test.service.DeptService.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getName(); // 获取目标类型
Signature signature = joinPoint.getSignature(); // 获取目标方法签名
String methodName = joinPoint.getSignature().getName(); // 获取目标方法名
Object[] args = joinPoint.getArgs(); // 获取目标方法运行参数
Object res = joinPoint.proceed(); // 执行原始方法,获取返回值
return res;
}
原理篇
1.配置优先级
SpringBoot中支持三种格式的配置文件:
server.port=8081 // application.properties
server:
port: 8082 // application.yml
server:
port: 8083 // application.yaml
优先级:properties–> yml ->yaml
推荐使用yml
SpringBoot除了支持配置文件属性配置,还支持Java系统属性和命令行参数的方式进行属性配置
-Dserver.port=9000 // Java系统属性
--server.port=10010 // 命令行参数
执行maven打包指令package
test-web-system-0.0.1-SNAPSHOT.jar
执行java指令,运行jar包
java -Dserver.port=9000 -jar test-web-system-0.0.1-SNAPSHOT.jar --server.port=10010
优先级:
命令行参数>java系统属性>application.properties>application.yml>application.yaml
2.bean的管理
@Autowired
private ApplicationContext applicationContext;
@Test
public void testGetBean(){
// 1.根据bean的名称获取
DeptController bean1 = (DeptController) applicationContext.getBean("deptController");
System.out.println(bean1);
// 2.根据bean的类型获取
DeptController bean2 = applicationContext.getBean(DeptController.class);
System.out.println(bean2);
// 3.根据bean的名称及类型获取
DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);
System.out.println(bean3);
}
bean作用域:
作用域 | 说明 |
---|---|
singleton | 容器内同名称的bean只有一个实例(单例)(默认) |
prototype | 每次 使用该bean时会创建新的实例(非单例) |
request | 每个请求范围内会创建新的实例(web环境,了解) |
session | 每个会话范围内会创建新的实例(web环境,了解) |
application | 每个应用范围内会创建新的实例(web环境,了解) |
通过@Scope
注解来进行配置作用域
@Lazy // 延迟初始化,延迟到第一次使用该bean对象
@Scope("prototype") // 设置作用域为prototype
@RestController
@RequestMapping("/depts")
public class DeptController{
}
注意事项:
- 默认singleton的bean,在容器启动时被创建,可以使用@Lazy注解来延迟初始化(延迟到第一次使用时)
- prototype的bean,每一次使用该bean的时候都会创建一个新的实例
- 实际开发当中,绝大部分的Bean是单例的,也就是说绝大部分Bean不需要配置scope属性
第三方bean
如果要管理的bean对象来自于第三方,是无法使用@Component及衍生注解声明bean的,就需要用到@Bean注解。
@SpringBootApplication
public class SpringbootWebConfigApplication{
@Bean // 将方法返回值交给IOC容器管理,成为IOC容器的bean对象
public SAXReader saxReader(){
return new SAXReader();
} // 在启动类中不建议
}
推荐在配置类中管理
@Configuration
public class CommonConfig{
@Bean // 将当前方法的返回值对象交给IOC容器管理,成为IOC容器bean
// 通过@Bean注解的name/value属性指定bean名称,如果未指定,默认是方法名
public SAXReader saxReader(DeptService deptService){
return new SAXReader();
}
}
注意事项:
- 通过@Bean注解的name或value属性可以声明bean的名称,如果不知道,默认bean的名称就是方法名。
- 如果第三方bean需要依赖其他bean对象,直接在bean定义方法中设置形参即可,容器会根据类型自动装配
3.SpringBoot原理
1.起步依赖原理
maven的依赖传递
2.自动配置原理
SpringBoot的自动配置就是当spring容器启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作。
由于springboot只能扫描本包及子包下的类
方案一:可以使用@ComponentScan({包名})扫描第三方包及其子包
@ComponentScan({"com.example","com.lonely"})
@SpringBootApplication
public class SpringbootWebConfigApplication{
}
方案二:@Import导入。使用@Import导入的类会被Spring加载到IOC容器中,导入形式主要有以下几种:
- 导入普通类
- 导入配置类
- 导入 ImportSelector 接口实现类
- @EnableXxxx注解,封装@Import注解
@Import({TokenParser.class,HeaderConfig.class})
@SpringBootApplication
public class SpringbootwebConfigApplication{
}
条件装配:@Conditional
作用:按照一定的条件进行判断,在满足给定条件后才会注册对应的bean对象到Spring IOC容器中。
位置:方法、类
@Conditional本身是一个父注解,派生出大量的子注解:
- @ConditionalOnClass:判断环境中是否有对应字节码文件,才注册bean到IOC容器
- @ConditionalOnMissingBean:判断环境中没有对应的bean(类型或名称),才注册bean到IOC容器
- @ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到IOC容器。
@Bean
@ConditionalOnMissingBean
public Gson gson(GsonBuilder gsonBuilder){
return gsonBuilder.create();
}